From f47581a43ee665f09b8c74994ba5ed76d7863356 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:32:42 +0800 Subject: [PATCH 001/162] test: reproduce Elixir quote live-hole and accounting gaps --- test/elixircheck.sh | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 76fb9e660..6bafcd10e 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -33,6 +33,66 @@ assert 'phantom' not in syms, 'quoted definition became an indexed symbol' assert 'nonexistent' not in calls('quoted'), 'quoted call site became a call edge' print(' PASS Elixir definitions, guards, macros, local/remote calls, pipes and negatives') PY + +# Issue #357: quoted syntax is inert, but unquote expressions are evaluated while +# constructing it. Keep this fixture separate from the golden corpus so the gate +# contrasts the call sites and the declined count directly. +mkdir "$TMP/inert" +cat > "$TMP/inert/quoted.ex" <<'EXINERT' +defmodule InertFixture do + def inert(x), do: x + def bar(x), do: x + def splice(x), do: [x] + def after_call(x), do: x + def quote(x), do: x + def comment(x), do: x + + def run(x) do + quote do + inert(x) + foo(unquote(bar(x))) + [unquote_splicing(splice(x))] + end + after_call(x) + InertFixture.quote(x) + InertFixture.comment(x) + end + + def options(x) do + quote bind_quoted: [item: bar(x)] do + inert(item) + end + end + + def nested(x) do + quote do + unquote(quote do + inert(x) + unquote(bar(x)) + end) + end + end +end +EXINERT +"$BIN" "$TMP/inert" --no-cache > "$TMP/inert.xml" +python3 - "$TMP/inert.xml" <<'PYINERT' +import re, sys, xml.etree.ElementTree as ET +data = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(data).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert 'inert/1' not in calls('run/1'), 'ordinary quoted call became an edge' +assert 'bar/1' in calls('run/1'), 'unquote(bar(x)) lost its live call edge' +assert 'splice/1' in calls('run/1'), 'unquote_splicing(splice(x)) lost its live call edge' +assert 'after_call/1' in calls('run/1'), 'call after quote was suppressed' +assert {'quote/1', 'comment/1'} <= calls('run/1'), 'ordinary named functions were treated as special forms' +assert 'bar/1' in calls('options/1'), 'bind_quoted option expression was suppressed' +assert 'inert/1' not in calls('options/1'), 'bind_quoted quote body became live' +assert 'bar/1' in calls('nested/1'), 'inner unquote in nested quote lost its live call edge' +assert 'inert/1' not in calls('nested/1'), 'nested quote body became live' +match = re.search(r'\bdeclined=(\d+)\b', data) +assert match and int(match.group(1)) >= 3, 'inert-region calls vanished from declined accounting' +print(' PASS inert quote bodies, live holes, near misses, boundaries, and declined count') +PYINERT for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From b5cacea5adcda5bf3505774e001285bfaa3b57ef Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:03:15 +0800 Subject: [PATCH 002/162] fix: preserve Elixir unquote calls and count quoted call sites --- docs/ARCHITECTURE.md | 10 ++++++---- queries/elixir/tags.scm | 4 +++- src/graph.h | 12 +++++++++++- src/graphlegend.h | 8 +++++++- src/ingest.cpp | 1 + src/ingest_cache.h | 4 +++- src/ingest_elixir.h | 33 ++++++++++++++++++++------------- src/ingest_inert.h | 32 ++++++++++++++++++++++++++++++++ src/ingest_model.h | 4 ++-- src/ingest_sidecap.h | 19 +++++++++++++++++++ src/main.cpp | 6 +++--- src/model.h | 8 +++++--- src/pincensus.h | 9 ++++----- src/quality.h | 2 +- src/serialize.h | 6 +++++- src/verbs_navigate.h | 5 +++-- test/elixircheck.sh | 25 +++++++++++++++++++++++++ 17 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 src/ingest_inert.h diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80c7fd288..189ab0fd5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -165,7 +165,11 @@ metadata do not become executable calls. Alias/import/require/use and behaviour module dependencies resolved through declared module identities, regardless of umbrella/file layout. `@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`. -**Static limits:** quoted AST and macro-generated definitions are not expanded. `use` records the +**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call +candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; +`--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and +`unquote_splicing(...)` are live, as are expressions in `bind_quoted:` options. Nested quotes +become inert again inside a live hole. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call @@ -196,9 +200,7 @@ that function — the underscore rule is for unused variables, and a bare `_seed (`src/ingest_elixir.h`); and a call that omits a defaulted argument reaches the bodyless head that evaluates the default beside the clauses, so `--path` and `--impact` see the default expression's calls from that caller, while a call that supplies the argument reaches the clauses alone -(`src/elixir_resolve.h`). One gap stays open: executable `unquote(...)` and `bind_quoted:` expressions -under `quote` are omitted with the rest of the quoted-AST filter, so a helper called only from inside -an `unquote` has no caller edge from its macro. +(`src/elixir_resolve.h`). `test/elixircheck.sh`, `test/eliximportcheck.sh` and `test/elixirsemanticcheck.sh` cover extraction, metrics, exact target selection against decoys, lexical boundaries, contracts, CLI/MCP use-site parity, diff --git a/queries/elixir/tags.scm b/queries/elixir/tags.scm index 03f8c89e6..26a90a60d 100644 --- a/queries/elixir/tags.scm +++ b/queries/elixir/tags.scm @@ -1,5 +1,7 @@ ; Elixir definitions are ordinary calls in the grammar. captureTagsFacts applies -; the keyword/head/quote filters in ingest_elixir.h; tags predicates are not evaluated. +; the keyword/head filters in ingest_elixir.h and the shared nearest-boundary inert +; walk in ingest_inert.h; tags predicates are not evaluated. Capturing every call as +; @inert/@live would still need C++ head checks and enlarge this hot capture stream. ; Anchors restrict definition names to the FIRST argument (never a call's body). (call target: (identifier) (arguments . [(alias) (dot) (atom)] @name)) @definition.module diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..a720ec728 100644 --- a/src/graph.h +++ b/src/graph.h @@ -129,6 +129,7 @@ struct Graph // stores what those calls could equally have meant ONCE PER DISTINCT candidate list (internDeclinedList): list k is declinedListCand[ off[k], off[k+1] ), // named by declinedListCallCount[k] calls — what the callers and impact answers read to count, once per call, the declines that could have meant THEIR symbols. std::vector declinedOut; + std::size_t inertDeclinedCalls = 0; // quoted call sites counted in declinedOut, with no candidate list std::vector declinedListOff{ 0u }; // the leading offset lives here, so a default Graph is already a valid zero-list CSR std::vector declinedListCand; std::vector declinedListCallCount; @@ -269,7 +270,7 @@ inline bool namespaceCompatible( RefRole role, SymKind kind ) noexcept return kind == SymKind::Macro; } case RefRole::Call: case RefRole::Read: case RefRole::Write: case RefRole::Import: - case RefRole::Value: case RefRole::Through: // never in the call loop; graph.h valueRefIndex narrows by kind itself + case RefRole::Value: case RefRole::Through: case RefRole::Inert: // Inert exits before the name ladder { return true; // un-narrowed — see the doctrine above; a NEW role is a -Werror=switch decision here } @@ -4056,6 +4057,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null disposition = CallDisposition::FileScope; // no caller node for an edge; the use-site index still lists the site continue; } + if( r.role == RefRole::Inert ) + { + // The syntax is a call candidate, but quoted AST cannot execute it. Count the declined + // site on its caller; do not intern target candidates, since it could reach none of them. + ++g.declinedOut[ r.fromSymbol ]; + ++g.inertDeclinedCalls; + disposition = CallDisposition::Declined; + continue; + } const auto it = byName.find( r.calleeName ); cand.clear(); diff --git a/src/graphlegend.h b/src/graphlegend.h index b35e988e2..e5110f774 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -598,6 +598,9 @@ inline constexpr const char* kDeclinedCallsLegend = // answer on a tree the gate never touched keeps its bytes. inline constexpr const char* kDeclinedCallsGateClause = "It also counts a call named like a builtin-type method (dict.get, list.append) whose bound definitions' classes the caller's file never names. "; +inline constexpr const char* kInertDeclinedCallsClause = + "In the callees form it also counts ordinary Elixir calls written inside quote AST outside unquote/unquote_splicing holes. " + "These sites cannot reach a target, so they contribute no caller or impact declined_calls= candidate list. "; inline const char* declinedCallsLegend( bool on ) noexcept { return on ? kDeclinedCallsLegend : ""; } inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDeclinedCallsGateClause : ""; } // --test-gate's own short form: the same attribute and unit, sized for a verb whose legend has an absolute byte budget @@ -605,7 +608,10 @@ inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDe inline constexpr const char* kDeclinedCallsTestGateLegend = "declined_calls=K (absent when 0): K call SITES the resolver declined to bind that could have reached the change or its radius (the map's declined=); a test behind one is in no row here. "; // The clause and, where the gate declined in this graph, its gate sentence — the one spelling every declined_calls= emitter uses. -inline std::string declinedCallsLegendWithGate( bool on, bool gateDeclined ) { return std::string( declinedCallsLegend( on ) ) + declinedCallsGateLegend( on && gateDeclined ); } +inline std::string declinedCallsLegendWithGate( bool on, bool gateDeclined, bool inertDeclined = false ) +{ + return std::string( declinedCallsLegend( on ) ) + declinedCallsGateLegend( on && gateDeclined ) + ( on && inertDeclined ? kInertDeclinedCallsClause : "" ); +} // ── reference-as-value round (src/valuerefs.h, src/ingest_valuerefs.h) ───────────────────────────────────────── // Each clause rides ONLY a document that carries the attribute or rows it defines (value_refs= / ), so an answer diff --git a/src/ingest.cpp b/src/ingest.cpp index 840d28e7f..ee0ef7005 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -192,6 +192,7 @@ extern "C" #include "ingest_docs.h" #include "ingest_names.h" #include "ingest_binds.h" +#include "ingest_inert.h" #include "ingest_elixir.h" #include "ingest_importcap.h" #include "ingest_valuerefs.h" diff --git a/src/ingest_cache.h b/src/ingest_cache.h index e13079f71..a5f6cebb3 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -302,7 +302,9 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 143; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change + // 145 = issue #357 lane: Elixir quote live holes and counted inert + // call candidates. 144 is main's rich-family tag; train renumbers. // 143 = 2026-10-04 (train 25 review fixes): two extraction changes — // a declaration named like the global object (`var self = this`, a // parameter `window`) is now a JsShadow binding, and a value-reference diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 028b5aefb..45aab8a65 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -156,13 +156,11 @@ bool elixirMetadataAttribute( std::string_view name ) noexcept /// references; retain executable defaults and attributes while excluding declaration heads and patterns. bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std::string_view src ) noexcept { - // Quoted syntax and module attributes (notably @spec/@type) are not runtime call sites. + // Module attributes (notably @spec/@type) are not runtime call sites. + // The shared inert-region decision runs after this capture's other syntax filters, + // so quoted definition heads and special forms never inflate declined=. for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { - if( elixirTarget( parent, src ) == "quote" ) - { - return false; - } if( std::strcmp( ts_node_type( parent ), "unary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "@" ) { if( elixirMetadataAttribute( elixirAttribute( parent, src ) ) @@ -215,8 +213,9 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: } } constexpr std::string_view special[] = { "defimpl", "defstruct", "defexception", "defoverridable", "alias", "import", "require", "use", - "quote", "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; + "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; if( elixirFunctionKeyword( target ) || elixirModuleKeyword( target ) + || ( target == "quote" && !ts_node_is_null( elixirBody( role, src ) ) ) || std::find( std::begin( special ), std::end( special ), target ) != std::end( special ) ) { return false; @@ -301,13 +300,21 @@ std::uint32_t elixirVisibilityEnd( TSNode scope, std::string_view src ) noexcept return end; } -bool elixirQuoted( TSNode node, std::string_view src ) noexcept +bool elixirInert( TSNode node, std::string_view src ) noexcept { - for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + return inInertRegion( node, [ & ]( TSNode ancestor, TSNode site ) noexcept { - if( elixirTarget( p, src ) == "quote" ) { return true; } - } - return false; + const auto target = elixirTarget( ancestor, src ); + if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) ) + { + return InertBoundary::Live; + } + if( target == "quote" && elixirContains( elixirBody( ancestor, src ), site ) ) + { + return InertBoundary::Inert; + } + return InertBoundary::None; + } ); } std::string elixirFunctionName( std::string_view name, std::uint32_t arity ) @@ -590,8 +597,8 @@ struct ElixirContext std::uint32_t size = 0; const char* name = ts_query_capture_name_for_id( query, capture.index, &size ); const std::string_view label( name, size ); - if( label == "elixir.context" && !elixirQuoted( capture.node, src ) ) { calls.push_back( capture.node ); } - if( label == "reference.bare" && !elixirQuoted( capture.node, src ) ) { identifiers.push_back( capture.node ); } + if( label == "elixir.context" && !elixirInert( capture.node, src ) ) { calls.push_back( capture.node ); } + if( label == "reference.bare" && !elixirInert( capture.node, src ) ) { identifiers.push_back( capture.node ); } } } std::sort( calls.begin(), calls.end(), []( TSNode a, TSNode b ) { return ts_node_start_byte( a ) < ts_node_start_byte( b ); } ); diff --git a/src/ingest_inert.h b/src/ingest_inert.h new file mode 100644 index 000000000..b2e8211c9 --- /dev/null +++ b/src/ingest_inert.h @@ -0,0 +1,32 @@ +#pragma once +#if !defined( RIPWIRE_INGEST_TU ) +#error "ingest_inert.h is a section of ingest.cpp; include it only there" +#endif + +namespace rw +{ +namespace +{ + +// The nearest boundary wins: a live hole re-enters an inert quote, and a nested +// quote inside that hole becomes inert again. Language syntax stays in the +// boundary callback; this walk is shared by every tags and context capture. +enum class InertBoundary : std::uint8_t { None, Inert, Live }; + +template +bool inInertRegion( TSNode node, BoundaryOf boundaryOf ) noexcept +{ + for( TSNode ancestor = ts_node_parent( node ); !ts_node_is_null( ancestor ); ancestor = ts_node_parent( ancestor ) ) + { + switch( boundaryOf( ancestor, node ) ) + { + case InertBoundary::Inert: return true; + case InertBoundary::Live: return false; + case InertBoundary::None: break; + } + } + return false; +} + +} // namespace +} // namespace rw diff --git a/src/ingest_model.h b/src/ingest_model.h index 13ad31101..daea95700 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -329,7 +329,7 @@ inline void mintModuleScopeOwners( IngestResult& result, std::vector& ra { continue; } - if( r.role != RefRole::Call && r.role != RefRole::Macro ) + if( r.role != RefRole::Call && r.role != RefRole::Macro && r.role != RefRole::Inert ) { continue; } @@ -928,7 +928,7 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, // one that does not, so one reference's spelling would depend on an unrelated fact about its file; // and --affected's import tier and graph.h's Binding tables both already read that kNoNode. const bool refTakesModuleScope = !r.isInherit && !r.isDocLink && !r.isCompose - && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); ref.fromSymbol = refTakesModuleScope ? refSweep.find( r.fileId, r.startByte ) : refSweep.findOwnedDef( r.fileId, r.startByte ); } diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 0b3ebb9a7..fa691e607 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1909,6 +1909,25 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t { continue; } + if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) + { + // Keep ordinary call candidates as declined facts, never edges. Definition heads and + // special forms have already failed elixirKeepCapture; bare names require lexical + // classification and are not counted as calls merely because they are identifiers. + if( isRef && refCapSv == "reference.call" ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( roleNode ); + r.line = nameRow + 1; + r.lang = Lang::Elixir; + r.role = RefRole::Inert; + const auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); + r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); + refs.push_back( std::move( r ) ); + } + continue; + } if( le.lang == Lang::Elixir && refCapSv == "reference.bare" ) { // Bare pipe targets have their own capture; every other bare name needs lexical variable exclusion. diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..304abc703 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1917,7 +1917,7 @@ int runDefaultMap( const MainDispatch& d ) { return {}; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); return measured.isWhole ? std::string( measured.bytes ) : std::string(); }; @@ -1930,7 +1930,7 @@ int runDefaultMap( const MainDispatch& d ) DISCLOSE( maxTokensFit, rw::MapAnnotations::MaxTokensFit::DisclosureWhy::ProbeUnmeasured, "runDefaultMap: open_memstream failed for the --max-tokens fit probe — the map is emitted unshaped and its ceiling unverified" ); return 0; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); if( !measured.isWhole ) { @@ -2686,7 +2686,7 @@ int runDefaultMap( const MainDispatch& d ) } else { - serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); } } else diff --git a/src/model.h b/src/model.h index cb58e3082..fcea0d0b7 100644 --- a/src/model.h +++ b/src/model.h @@ -370,16 +370,18 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a // may-call clue); never a use site, never in the CSR. -enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through }; +// Inert — an ordinary Elixir call candidate inside quote's AST body, outside an unquote live hole. +// It remains a site for declined/report accounting, but can never bind or mint a call edge. +enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Inert }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). -inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Through ) + 1; +inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Inert ) + 1; static_assert( enumCountIsExact(), "kRefRoleCount must name the LAST RefRole enumerator — move it with the append" ); static_assert( sizeof( RefRole ) == 1, "RefRole must be a single byte (SoA-friendly, smallest int that fits)" ); // the terse `role=` attribute string for the use-site index — a declarative table indexed by the enum, in enum // order. The static_assert is the guard a switch's -Werror=switch used to be: a NEW role without a spelling is a // build error, never a silent fallback. -inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through" }; +inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through", "inert" }; static_assert( std::size( kRefRoleTagTable ) == kRefRoleCount, "kRefRoleTagTable: one spelling per RefRole, in enum order" ); inline const char* refRoleTag( RefRole r ) noexcept { diff --git a/src/pincensus.h b/src/pincensus.h index 5add98daf..5b70acf1a 100644 --- a/src/pincensus.h +++ b/src/pincensus.h @@ -115,10 +115,9 @@ enum class CallDisposition : std::uint8_t OtherRoot = 5, // multi-root only: every compatible definition lives in ANOTHER root and no include/import // reaches it — external to this root, which is what that root's solo run would say QualifiedExternal = 6, // a Rust `Scope::name()` call no in-repo member of `Scope` can answer (the H4 W3 guard) - Declined = 7, // tier 3: two or more same-language candidates, none in the caller's file or directory, none - // pinned by a qualifier or a receiver rule; or the builtin-method name gate (graph.h - // BuiltinMethodGate) admitted none of a builtin-type method name's candidates — header - // declined=, answers' declined_calls= + Declined = 7, // tier 3: ambiguous calls with no safe binding; builtin-method name gate refusals; or + // inert quoted call candidates. No edge; all count in header declined=. An inert site + // contributes to its caller's declinedOut, but names no possible target list. FileScope = 8, // a call outside every symbol with no caller node to hang an edge on. Since issue #60 // ingest_model.h mintModuleScopeOwners gives every such call a module-scope owner over // exactly isResolvableCallReference's population below, so on a code corpus this bucket @@ -147,7 +146,7 @@ static_assert( std::string_view( kCallDispositionNames[ std::size_t( CallDisposi // re-derivation, so the two cannot disagree about what a call is — only about what happened to one. inline bool isResolvableCallReference( const Reference& r ) noexcept { - return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); } // Per-row provenance bits — every narrowing stage that FIRED on this site, not just the deciding one. A diff --git a/src/quality.h b/src/quality.h index 5961d1b00..17d56115f 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 143; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..3f83380e9 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2290,6 +2290,8 @@ inline constexpr const char* kDeclinedMapLegend = // only to a map where the gate declined at least one call, so a map the gate never touched keeps its bytes. inline constexpr const char* kDeclinedGateMapLegend = ""; +inline constexpr const char* kInertDeclinedMapLegend = + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged @@ -2658,7 +2660,8 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::vector* declinedOut = nullptr, // of those, the calls the builtin-method name gate declined (graph.h g.gateDeclinedCalls) → the // kDeclinedGateMapLegend clause, absent when zero. - std::size_t gateDeclinedCalls = 0 ) + std::size_t gateDeclinedCalls = 0, + std::size_t inertDeclinedCalls = 0 ) { const std::size_t* changedCount = ann.changedCount; const std::string* mapAtStamp = ann.atStamp; @@ -2888,6 +2891,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::size_t declinedTotal = counterTotal( declinedOut ); // calls tier 3 declined: no edge, and no guess legend += declinedTotal > 0 ? kDeclinedMapLegend : ""; // charged to the map that carries declined= legend += gateDeclinedCalls > 0 ? kDeclinedGateMapLegend : ""; // only where the builtin-method gate declined a call + legend += inertDeclinedCalls > 0 ? kInertDeclinedMapLegend : ""; // only where an Elixir quote declined a call candidate // C1 DRIFT FIX (Round C lane B, found by re-reading this header's own output). `precise=` means "how many // out-edges a SCIP index PINNED", and the emitter's own comment below says it is "emitted ONLY under // --scip". Both were true when outProv held only {0, 1}. A4-R5 then added value 2 (an FFI binding edge) diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 62d43e5d4..bbc7384d9 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -222,7 +222,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), - rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, + !wantCallers && g.inertDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( chRows.declinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + rw::crossKindLegend( !chRows.crossKind.empty() ) ).c_str(), // hono-07: likewise for cross_kind= @@ -2708,7 +2709,7 @@ std::optional runAround( const MainDispatch& d ) aroundAnn.seed = { ing.symbols[ focus ].name, cfg.aroundDepth, cfg.aroundFanout, definitionCountOfName( ing, focus ), eg.fanoutCut, eg.depthTruncated }; aroundAnn.seed.unprovenDefs = arUnprovenDefs; // H1: unproven_defs= beside defs= on the root, its clause in the map legend - serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut ); + serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, 0, g.inertDeclinedCalls ); if( !g.composeEdges.empty() ) { diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 6bafcd10e..2dc904bac 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -93,6 +93,31 @@ match = re.search(r'\bdeclined=(\d+)\b', data) assert match and int(match.group(1)) >= 3, 'inert-region calls vanished from declined accounting' print(' PASS inert quote bodies, live holes, near misses, boundaries, and declined count') PYINERT +mkdir "$TMP/inert-control" +python3 - "$TMP/inert/quoted.ex" "$TMP/inert-control/quoted.ex" <<'PYINERTCONTROL' +import pathlib, sys +original = pathlib.Path(sys.argv[1]).read_text() +lines = original.splitlines(keepends=True) +mutated = 0 +for i, line in enumerate(lines): + if line.strip() in {'inert(x)', 'inert(item)'}: + lines[i] = line[:len(line) - len(line.lstrip())] + 'x\n' + mutated += 1 + elif line.strip() == 'foo(unquote(bar(x)))': + lines[i] = line.replace('foo(unquote(bar(x)))', 'unquote(bar(x))') + mutated += 1 +assert mutated == 4, f'control did not remove all four inert call sites: {mutated}' +pathlib.Path(sys.argv[2]).write_text(''.join(lines)) +PYINERTCONTROL +"$BIN" "$TMP/inert-control" --no-cache > "$TMP/inert-control.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-control.xml" <<'PYINERTCOUNT' +import re, sys +def declined(path): + match = re.search(r'\bdeclined=(\d+)\b', open(path).read()) + return int(match.group(1)) if match else 0 +assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) +print(' PASS four inert call sites move declined accounting by four') +PYINERTCOUNT for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From d2870fa4763e7fc59539b9ff9d6b1e47ff19fb4c Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:03:35 +0800 Subject: [PATCH 003/162] test: re-pin extraction scheme tripwire for issue 357 --- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 2192e4759..1f78abd6d 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -32240f4552ee695a54f885409b67c60585d6fc7c1f3cffc362313f6c772dafcc +a4e37a094bcb61237281c6d9203daed614cfbef2e15e81b9c484c5bef4735858 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index eaf642263..bb5e04406 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-05, issue #357 lane: RE-DERIVED with UPDATE_GOLDEN=1. kParserVer 143 -> 145 for +# Elixir quote live holes and counted inert call candidates (144 is main's rich-family tag; +# train renumbers). quality.h's parser mirror moved with it. kQSnapCacheScheme stays 17: +# Snapshot meaning is unchanged and its key carries the extraction identity. # 2026-10-04, train 25 review fixes: RE-DERIVED with UPDATE_GOLDEN=1 (hash 32240f4552…2dafcc). kParserVer 141 -> 143 (two # extraction changes: a declaration named like the JS global object is a JsShadow binding; value-reference slot text # is cut on a UTF-8 boundary and JS string keys are capped) with quality.h's mirror; 143 because 141's full-use file From 0cb0a8269617a0968d7daf8866a3027cb35bc24b Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:12:27 +0800 Subject: [PATCH 004/162] test: cover Elixir quote options before enabling live holes --- test/elixircheck.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 2dc904bac..cdd81ea66 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -118,6 +118,43 @@ def declined(path): assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) print(' PASS four inert call sites move declined accounting by four') PYINERTCOUNT +# quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). +mkdir "$TMP/quote-options" +cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' +defmodule QuoteOptions do + def bar(x), do: x + def splice(x), do: [x] + def bound(x) do + quote bind_quoted: [x: x] do + unquote(bar(x)) + [unquote_splicing(splice(x))] + end + end + def disabled(x) do + quote unquote: false, do: unquote(bar(x)) + end + def enabled(x) do + quote bind_quoted: [x: x], unquote: true do + unquote(bar(x)) + end + end +end +EXOPTIONS +"$BIN" "$TMP/quote-options" --no-cache > "$TMP/options.xml" +python3 - "$TMP/options.xml" <<'PYOPTIONS' +import sys, xml.etree.ElementTree as ET +syms = {s.get('n'): s for s in ET.parse(sys.argv[1]).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert not calls('bound/1'), 'bind_quoted incorrectly activates unquote/splicing' +assert not calls('disabled/1'), 'unquote: false incorrectly activates unquote' +assert calls('enabled/1') == {'bar/1'}, 'explicit unquote: true failed to restore the live hole' +print(' PASS disabled unquote, bind_quoted default, and explicit re-enable') +PYOPTIONS +for n in a b c; do "$BIN" "$TMP/inert" > "$TMP/inert-$n.xml"; done +cmp "$TMP/inert.xml" "$TMP/inert-a.xml" +cmp "$TMP/inert-a.xml" "$TMP/inert-b.xml" +cmp "$TMP/inert-b.xml" "$TMP/inert-c.xml" +echo ' PASS inert count and live edges survive cold/warm round trips' for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From aa0361d63e462ed6e2df3865b8014596419c1783 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:14:00 +0800 Subject: [PATCH 005/162] test: expose inert declines in default map and callees output --- test/elixircheck.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index cdd81ea66..ccba85744 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -118,6 +118,21 @@ def declined(path): assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) print(' PASS four inert call sites move declined accounting by four') PYINERTCOUNT +"$BIN" "$TMP/inert" --callees=run/1 --no-cache > "$TMP/inert-callees.xml" +"$BIN" "$TMP/inert" --callers=inert/1 --no-cache > "$TMP/inert-callers.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-callees.xml" "$TMP/inert-callers.xml" <<'PYINERTVERBS' +import sys, xml.etree.ElementTree as ET +map_text, callees_text, callers_text = [open(p).read() for p in sys.argv[1:]] +assert 'including inert Elixir quote sites' in map_text, 'compact map misdescribes declined sites' +callees = ET.fromstring(callees_text) +assert callees.get('declined_calls') == '2', callees.attrib +assert 'including inert Elixir quote sites' in callees_text, 'compact callees misdescribes declined sites' +callers = ET.fromstring(callers_text) +assert callers.get('count') == '0', callers.attrib +assert 'declined_calls' not in callers.attrib, 'inert sites became possible callers' +print(' PASS declined map/callees accounting, compact readings, and no possible callers') +PYINERTVERBS + # quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). mkdir "$TMP/quote-options" cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' From f3f94d71a8f5a69981b11ed8ff1f43e754b63131 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:14:00 +0800 Subject: [PATCH 006/162] fix: honor quote options and explain inert declines in compact output --- docs/ARCHITECTURE.md | 6 ++++-- src/compactlegend.h | 14 +++++++++++++- src/graphlegend.h | 4 ++-- src/ingest_elixir.h | 23 ++++++++++++++++++++++- src/serialize.h | 2 +- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 189ab0fd5..afabf222b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -168,8 +168,10 @@ module dependencies resolved through declared module identities, regardless of u **Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and -`unquote_splicing(...)` are live, as are expressions in `bind_quoted:` options. Nested quotes -become inert again inside a live hole. `use` records the +`unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options. +`bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and +`unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. +Nested quotes become inert again inside a live hole. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..61e3bd156 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1788,7 +1788,19 @@ inline std::string compactLegendText( const CompactLegendSpec& spec, std::string for( const std::uint16_t i : compactPresentTerms( head, doc, spec.key ) ) { out += ' '; - out.append( compactReading( i ).reading ); + const auto& term = compactReading( i ); + if( term.attr == "declined_calls" && doc.find( "ordinary Elixir calls written inside quote AST" ) != std::string_view::npos ) + { + out += "declined_calls=K: K call sites without edges, including inert Elixir quote sites outside enabled unquote/unquote_splicing holes; inert sites have no target candidates or uses rows"; + } + else if( term.attr == "declined" && doc.find( "ordinary-Elixir-call-sites-inside-quote-AST" ) != std::string_view::npos ) + { + out += "declined=K: K call sites without edges, including inert Elixir quote sites outside enabled unquote/unquote_splicing holes"; + } + else + { + out.append( term.reading ); + } out += '.'; } // #60: the module-scope owner is the one kind that arrives under a dozen different spellings — diff --git a/src/graphlegend.h b/src/graphlegend.h index e5110f774..bc20789ae 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -599,8 +599,8 @@ inline constexpr const char* kDeclinedCallsLegend = inline constexpr const char* kDeclinedCallsGateClause = "It also counts a call named like a builtin-type method (dict.get, list.append) whose bound definitions' classes the caller's file never names. "; inline constexpr const char* kInertDeclinedCallsClause = - "In the callees form it also counts ordinary Elixir calls written inside quote AST outside unquote/unquote_splicing holes. " - "These sites cannot reach a target, so they contribute no caller or impact declined_calls= candidate list. "; + "In the callees form it also counts ordinary Elixir calls written inside quote AST outside enabled unquote/unquote_splicing holes. " + "These sites have no uses rows or target candidates, so they contribute no caller or impact declined_calls= candidate list. "; inline const char* declinedCallsLegend( bool on ) noexcept { return on ? kDeclinedCallsLegend : ""; } inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDeclinedCallsGateClause : ""; } // --test-gate's own short form: the same attribute and unit, sized for a verb whose legend has an absolute byte budget diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 45aab8a65..c331cd978 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -300,12 +300,33 @@ std::uint32_t elixirVisibilityEnd( TSNode scope, std::string_view src ) noexcept return end; } +// An unquote belongs to its nearest quote body. bind_quoted disables holes by +// default; an explicit literal unquote option takes precedence. Unknown options +// cannot prove a live hole, so retain the inert call candidate instead. +bool elixirLiveHole( TSNode hole, std::string_view src ) noexcept +{ + for( TSNode parent = ts_node_parent( hole ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) + { + if( elixirTarget( parent, src ) != "quote" || !elixirContains( elixirBody( parent, src ), hole ) ) + { + continue; + } + const TSNode option = elixirKeywordValue( parent, "unquote:", src ); + if( !ts_node_is_null( option ) ) + { + return nodeTextOf( option, src ) == "true"; + } + return ts_node_is_null( elixirKeywordValue( parent, "bind_quoted:", src ) ); + } + return false; +} + bool elixirInert( TSNode node, std::string_view src ) noexcept { return inInertRegion( node, [ & ]( TSNode ancestor, TSNode site ) noexcept { const auto target = elixirTarget( ancestor, src ); - if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) ) + if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) && elixirLiveHole( ancestor, src ) ) { return InertBoundary::Live; } diff --git a/src/serialize.h b/src/serialize.h index 3f83380e9..82743cf5a 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2291,7 +2291,7 @@ inline constexpr const char* kDeclinedMapLegend = inline constexpr const char* kDeclinedGateMapLegend = ""; inline constexpr const char* kInertDeclinedMapLegend = - ""; + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged From 474d9c0514028e6ee7127dc4d135dfbd80ec92c1 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:30:56 +0800 Subject: [PATCH 007/162] test: reproduce non-Elixir CDATA legend marker collision --- test/elixircheck.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index ccba85744..2ed67f34f 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -133,6 +133,28 @@ assert 'declined_calls' not in callers.attrib, 'inert sites became possible call print(' PASS declined map/callees accounting, compact readings, and no possible callers') PYINERTVERBS +# A non-Elixir body may quote our legend markers as source text. Only real +# comments outside CDATA may select the inert-decline reading. +mkdir -p "$TMP/marker/src" "$TMP/marker/a" "$TMP/marker/b" +cat > "$TMP/marker/src/run.cpp" <<'CPPMARKER' +int run() { + const char* marker = R"marker()marker"; + const char* clause = "ordinary Elixir calls written inside quote AST"; + return duplicate(); +} +CPPMARKER +printf 'int duplicate() { return 1; }\n' > "$TMP/marker/a/a.cpp" +printf 'int duplicate() { return 2; }\n' > "$TMP/marker/b/b.cpp" +"$BIN" "$TMP/marker" --pack-top-n=3 --no-cache > "$TMP/marker.xml" +python3 - "$TMP/marker.xml" <<'PYMARKER' +import sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +ET.fromstring(text) +assert 'declined=1' in text and ' "$TMP/quote-options/options.ex" <<'EXOPTIONS' From 3dca9ba47c03f79f39f11db067a47ca750b1f4c4 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:30:57 +0800 Subject: [PATCH 008/162] fix: select inert decline readings only from emitted XML comments --- src/compactlegend.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 61e3bd156..1eee1eee4 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1789,11 +1789,11 @@ inline std::string compactLegendText( const CompactLegendSpec& spec, std::string { out += ' '; const auto& term = compactReading( i ); - if( term.attr == "declined_calls" && doc.find( "ordinary Elixir calls written inside quote AST" ) != std::string_view::npos ) + if( term.attr == "declined_calls" && compactCommentOpenedBy( doc, ""; inline constexpr const char* kInertDeclinedMapLegend = - ""; + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 915e3ff7d..1b098c66d 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -185,8 +185,8 @@ PYINERTVERBS mkdir -p "$TMP/marker/src" "$TMP/marker/a" "$TMP/marker/b" cat > "$TMP/marker/src/run.cpp" <<'CPPMARKER' int run() { - const char* marker = R"marker()marker"; - const char* clause = "ordinary Elixir calls written inside quote AST"; + const char* marker = R"marker()marker"; + const char* clause = "ordinary Elixir call candidates in inert quote AST"; return duplicate(); } CPPMARKER From 04bdf4d4e62016450f9bd79d1ea80e1a58b01a1d Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:42:29 +0800 Subject: [PATCH 014/162] test(elixir): reproduce inert operator and capture accounting loss --- test/elixircheck.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 1b098c66d..d7794fb0e 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -202,6 +202,33 @@ assert 'including inert Elixir quote sites' not in text, 'C++ source text change print(' PASS non-Elixir CDATA markers cannot select the inert-decline reading') PYMARKER +# Operator calls and named function captures are reference candidates too. Bare +# variable-shaped AST names are not counted merely for being identifiers. +mkdir "$TMP/inert-kinds" +cat > "$TMP/inert-kinds/count.ex" <<'EXKINDS' +defmodule InertKinds do + def bar(x), do: x + def run(x) do + quote do + bar(x) + 1 + 2 + &bar/1 + x + end + end +end +EXKINDS +"$BIN" "$TMP/inert-kinds" --no-cache > "$TMP/inert-kinds.xml" +python3 - "$TMP/inert-kinds.xml" <<'PYKINDS' +import re, sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(text).iter('s')} +assert not list(syms['run/1'].iter('c')), 'inert operator/capture became an edge' +match = re.search(r'\bdeclined=(\d+)\b', text) +assert match and int(match.group(1)) == 3, 'inert operator/named capture references disappeared or variable AST inflated counting' +print(' PASS inert calls, operators and named captures counted; variable AST excluded') +PYKINDS + # quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). mkdir "$TMP/quote-options" cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' From 16284d0fa4686fcea8ee1167a38f81b960e83701 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:42:55 +0800 Subject: [PATCH 015/162] fix(ingest): count inert operator and named-capture references --- src/ingest_sidecap.h | 41 ++++++++++++++++++++++------------------- src/model.h | 2 +- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index fa691e607..8d3d09448 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1909,25 +1909,6 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t { continue; } - if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) - { - // Keep ordinary call candidates as declined facts, never edges. Definition heads and - // special forms have already failed elixirKeepCapture; bare names require lexical - // classification and are not counted as calls merely because they are identifiers. - if( isRef && refCapSv == "reference.call" ) - { - RawRef r; - r.fileId = fileId; - r.startByte = ts_node_start_byte( roleNode ); - r.line = nameRow + 1; - r.lang = Lang::Elixir; - r.role = RefRole::Inert; - const auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); - r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); - refs.push_back( std::move( r ) ); - } - continue; - } if( le.lang == Lang::Elixir && refCapSv == "reference.bare" ) { // Bare pipe targets have their own capture; every other bare name needs lexical variable exclusion. @@ -1943,6 +1924,28 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t if( nameTxt == "/" && elixirNodeIs( parent, "unary_operator" ) && nodeFieldText( parent, NodeField::Operator, src ) == "&" ) { continue; } } + if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) + { + // Keep call/operator/named-capture candidates as declined facts, never edges. Heads, + // special forms and non-call operators already failed their syntax filters. Bare AST + // names remain variable-shaped syntax unless they explicitly name a function capture. + if( isRef && ( refCapSv == "reference.call" || refCapSv == "reference.operator" + || ( refCapSv == "reference.bare" && elixir.namedCapture( nameNode ) ) ) ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( roleNode ); + r.line = nameRow + 1; + r.lang = Lang::Elixir; + r.role = RefRole::Inert; + auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); + if( refCapSv == "reference.operator" ) { count = elixirNodeIs( roleNode, "binary_operator" ) ? 2 : 1; known = true; } + r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); + refs.push_back( std::move( r ) ); + } + continue; + } + if( isDef ) { if( le.lang == Lang::Elixir && defCapSv == "definition.attribute" ) { kind = SymKind::Var; } diff --git a/src/model.h b/src/model.h index c173ad138..8c2084fe9 100644 --- a/src/model.h +++ b/src/model.h @@ -370,7 +370,7 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a // may-call clue); never a use site, never in the CSR. -// Inert — an ordinary Elixir call candidate whose enclosing quote AST remains inert after live-hole re-entry. +// Inert — an Elixir call/operator/named-capture candidate whose enclosing quote AST remains inert after live-hole re-entry. // It remains a site for declined/report accounting, but can never bind or mint a call edge. enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Inert }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). From fff9fa7f170060ff414a1943de10ef4178e6ccb7 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:43:25 +0800 Subject: [PATCH 016/162] docs: define inert reference counting coverage --- docs/ARCHITECTURE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 73bad2baf..05ea7fe74 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -165,9 +165,10 @@ metadata do not become executable calls. Alias/import/require/use and behaviour module dependencies resolved through declared module identities, regardless of umbrella/file layout. `@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`. -**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call -candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; -`--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and +**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call, +operator and named-function-capture candidates inside a `quote` body mint no edges and are counted +in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function. +Bare variable-shaped AST identifiers are not counted merely for being identifiers. Calls inside `unquote(...)` and `unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options. `bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and `unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. From 4bfb96d75011fdaa09a92cc8091763a34eeda562 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:47:36 +0800 Subject: [PATCH 017/162] test(elixir): reproduce quoted module stealing live-hole scope --- test/elixircheck.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index d7794fb0e..c1bb108be 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -81,6 +81,33 @@ assert not calls('disabled/1'), 'disabled declaration-head hole became live' print(' PASS live holes in quoted declaration heads/metadata, bare calls and variable negatives') PYHEADHOLES +# A module declaration which is itself quoted must not change the lexical +# module of an evaluated hole. A same-file namesake makes wrong-scope fallback visible. +mkdir "$TMP/module-hole" +cat > "$TMP/module-hole/modules.ex" <<'EXMODULEHOLE' +defmodule Real do + def bar(x), do: x + def run(x) do + quote do + defmodule Phantom do + unquote(bar(x)) + end + end + end +end +defmodule Other do + def bar(x), do: x +end +EXMODULEHOLE +"$BIN" "$TMP/module-hole" --no-cache > "$TMP/module-hole.xml" +python3 - "$TMP/module-hole.xml" <<'PYMODULEHOLE' +import sys, xml.etree.ElementTree as ET +syms = {(s.get('n'), s.get('sc', '')): s for s in ET.parse(sys.argv[1]).iter('s')} +assert 'bar/1' in {c.get('n') for c in syms['run/1', 'Real'].iter('c')}, 'inert module ancestor stole the live-hole lexical scope' +assert not any(n == 'Phantom' for n, sc in syms), 'inert module became a definition' +print(' PASS inert module declarations do not shadow live-hole lexical scope') +PYMODULEHOLE + # Issue #357: quoted syntax is inert, but unquote expressions are evaluated while # constructing it. Keep this fixture separate from the golden corpus so the gate # contrasts the call sites and the declined count directly. From 75a06708cbd516177b99d4ead7aafc456b0077dc Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:47:37 +0800 Subject: [PATCH 018/162] fix(elixir): skip inert declarations when finding live-hole module scope --- docs/ARCHITECTURE.md | 2 +- src/ingest_elixir.h | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 05ea7fe74..7c3bfcfac 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -173,7 +173,7 @@ Bare variable-shaped AST identifiers are not counted merely for being identifier `bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and `unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. Nested quotes become inert again inside a live hole; an inner quote that is itself inert -cannot execute its own holes in the current stage. Quoted declaration heads and metadata +cannot execute its own holes in the current stage. Quoted module declarations do not change the lexical module of evaluated holes. Quoted declaration heads and metadata do not suppress evaluated hole arguments; bound variables in those arguments remain variables. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 1ec9cef37..4ab76702e 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -378,8 +378,10 @@ struct ElixirContext } for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) { - if( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + if( ( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + && !elixirInert( p, src ) ) { + // Quoted module declarations are AST, not the lexical module of an evaluated hole. return moduleName( p, depth + 1 ); } } From 7e834936881134f50d9d9bab743e0630142fb80a Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:48:07 +0800 Subject: [PATCH 019/162] test(elixir): pin live-hole callee identity across quoted modules --- test/elixircheck.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index c1bb108be..4f424d177 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -107,6 +107,13 @@ assert 'bar/1' in {c.get('n') for c in syms['run/1', 'Real'].iter('c')}, 'inert assert not any(n == 'Phantom' for n, sc in syms), 'inert module became a definition' print(' PASS inert module declarations do not shadow live-hole lexical scope') PYMODULEHOLE +"$BIN" "$TMP/module-hole" --callees=run/1 --no-cache > "$TMP/module-hole-callees.xml" +python3 - "$TMP/module-hole-callees.xml" <<'PYMODULETARGET' +import sys, xml.etree.ElementTree as ET +rows = {(s.get('n'), s.get('p')) for s in ET.parse(sys.argv[1]).iter('s')} +assert rows == {('bar/1', 'modules.ex:2')}, f'live hole bound to the wrong module: {rows}' +print(' PASS live hole reaches Real.bar/1, not the same-file Other.bar/1') +PYMODULETARGET # Issue #357: quoted syntax is inert, but unquote expressions are evaluated while # constructing it. Keep this fixture separate from the golden corpus so the gate From 0167bdd85327a2332305478a321f8909b6e85a1e Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:04:52 +0800 Subject: [PATCH 020/162] chore: account for inert-region ingest section in optimization triage --- scripts/optremarks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/optremarks.py b/scripts/optremarks.py index 316ebac38..3785977e9 100755 --- a/scripts/optremarks.py +++ b/scripts/optremarks.py @@ -112,6 +112,8 @@ "per-node tree-sitter walk — is covered by ingest_binds.h and ingest_relations.h, which run for every grammar." ), ( "src/ingest_elixir.h", "Elixir-specific capture helpers: per node, but for one grammar with a small corpus share. Same argument as ingest_jsimports.h above." ), + ( "src/ingest_inert.h", + "The shared inert-boundary walk currently runs only for Elixir quote captures and context. Like ingest_elixir.h, its remarks cover one grammar, not the general corpus." ), # ── per-DIRECTIVE, where every hot ingest section above is per-node or per-file ──────────────────── ( "src/ingest_importcap.h", "the shared @import.path capture vocabulary (issue #358), carrying the C family for now. It is called from ONE place in ingest_sidecap.h and runs once per captured " From bc53df27f755d3934a7750bdc5ed770472180667 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:07:30 +0800 Subject: [PATCH 021/162] docs: regenerate shifted graph seed showcase blocks --- docs/captures/COMMANDS_showcase_2026-09-14.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..29f1a2f9b 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3921,29 +3921,29 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:5565` +## `./build/ripwire . --at=src/graph.h:5577 --legend=full` *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* ````` - - + + ````` -## `./build/ripwire . --callers=@src/graph.h:5565` +## `./build/ripwire . --callers=@src/graph.h:5577 --legend=full` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - + - - + + From b479b4f83d806f5aa65c37c3d62f846361b40335 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:14:44 +0800 Subject: [PATCH 022/162] docs: synchronize navigation captures and generated command reference --- docs/COMMANDS.md | 6 +++--- docs/captures/COMMANDS_showcase_2026-09-14.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..0e298cca2 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -3350,11 +3350,11 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:5565 +$ ./build/ripwire . --at=src/graph.h:5577 --legend=full - - + + ``` diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 29f1a2f9b..ff096c728 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3951,7 +3951,7 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 ````` -## `./build/ripwire . --at=src/graph.h:999999` +## `./build/ripwire . --at=src/graph.h:999999 --legend=full` *A seed past the end of the file — the refusal shape for a faulted location.* @@ -3964,7 +3964,7 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 stderr: ````` -ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 6763 lines — the seed asked for line 999999) +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 9428 lines — the seed asked for line 999999) ````` ## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` From d441cacd86de7df33928e1ce5ba0b99171445fca Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 09:54:23 -0400 Subject: [PATCH 023/162] docs(skills): --token-budget gates the map but shapes the task lenses map-before-you-read.md called --token-budget a "CI/hook budget gate, not a shaping flag". That holds for the default map, --query and --recall (exit 3, the map withheld). On --for, --pack-task and --from-trace the same flag shapes the answer and exits 0, as --help says. The paragraph now names both personalities. Co-Authored-By: Claude Opus 5.5 --- skills/ripwire-orient/map-before-you-read.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skills/ripwire-orient/map-before-you-read.md b/skills/ripwire-orient/map-before-you-read.md index 4ea33aa42..75783ce05 100644 --- a/skills/ripwire-orient/map-before-you-read.md +++ b/skills/ripwire-orient/map-before-you-read.md @@ -121,9 +121,11 @@ are deciding how much of them to read. - Budget a large map: `--max-tokens=8000` or `--top-k=50`. `est_tokens` is calibrated per language against real tokenizer output, not a flat chars/4 guess, and `--max-tokens` fits to 90%-of-budget headroom — a ceiling, never a target you pad against. -- **CI/hook budget gate, not a shaping flag:** `--token-budget=N[K|M|G]` asserts instead of fitting — exit 3 - if `est_tokens` exceeds N, map unchanged. Use it to fail a build/hook on drift; `--max-tokens` SHAPES - instead. Composable: set either, both, or neither. +- **`--token-budget=N[K|M|G]` has two personalities.** On the default map, `--query` and `--recall` it is a + CI/hook budget GATE: it asserts instead of fitting — exit 3 if `est_tokens` exceeds N, the map withheld, + never trimmed. Use it there to fail a build/hook on drift; `--max-tokens` SHAPES the map instead, and the + two compose (set either, both, or neither). On the task lenses (`--for`, `--pack-task`, `--from-trace`) + the same flag SHAPES the answer toward N and exits 0 — see `--help` for what it keeps. - **A high-cardinality verb paginates**: `--deps`/`--callers`/`--callees`/`--hotspots`/`--tree`/`--lint` take `--limit=N --offset=M` — sorted results, so `--offset=N` after `--limit=N` is the exact continuation, no drops/dupes. Default (no `--limit`) is the whole result; reach for pagination on a big monorepo instead. From 4b5a4dc04e89ab0932c2947fc4bd7221f0ad8c80 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 09:54:23 -0400 Subject: [PATCH 024/162] =?UTF-8?q?test:=20forsoftbudgetcheck=20=E2=80=94?= =?UTF-8?q?=20--for=20stops=20when=20complete;=20an=20explicit=20budget=20?= =?UTF-8?q?is=20a=20soft=20target=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate is written before the code it measures. Default --for: the ranked head () is the answer. The 7.5 KB payload budget becomes a target: the trim ladder may still shorten signatures and doc excerpts to approach it, but it drops no row below a runaway guard (kForRunawayGuardBytes). Rows served past the target are disclosed ( plus a legend clause). Hitting the guard is the only default cut, disclosed as over_ceiling="1" plus next=. Explicit --token-budget / MCP budget_tokens on --for and --pack-task: rows stop once the target is reached. The rank 1..4 floor, the header and every disclosure always ship. There is no byte promise, so no over_ceiling= and no legend clause dropped "(ceiling)". Served rows are a rank prefix, monotone in N, equal across the compact and full dialects, and a target wider than the answer serves exactly the default rows. Twins that must keep holding: the map's exit-3 gate (G1), and an answer complete under the target carries no new disclosure (D6). On base d4325a8c: 20 arms red (D1-D5, E1, E2 at N<=3000, E5, E6, P1); E3/E4 prefix and dialect-parity arms, D6 and G1 green. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/forsoftbudgetcheck.sh | 268 ++++++++++++++++++++++++++++ test/regression.sh | 2 +- 6 files changed, 278 insertions(+), 9 deletions(-) create mode 100755 test/forsoftbudgetcheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 9e06f3e73..d5cbafe01 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -200,6 +200,7 @@ "forrootlegendcheck.sh": 8.6, "forsectioncollapsecheck.sh": 5.2, "forsigspancheck.sh": 6.0, +"forsoftbudgetcheck.sh": 42.0, "freshclonecheck.sh": 170.8, "freshnesscheck.sh": 4.4, "g1configcheck.sh": 0.8, diff --git a/README.md b/README.md index 2432bb90f..bf380fff8 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-684 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **684 gate scripts** and is the authoritative list; +`test/regression.sh` names **685 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 234344482..11acf723b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 684 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **684 gate scripts**, all of which exist on disk. +naming **685 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 684. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e545797c6..db11d6c46 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["684 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "684", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["684 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh new file mode 100755 index 000000000..10c3394de --- /dev/null +++ b/test/forsoftbudgetcheck.sh @@ -0,0 +1,268 @@ +#!/usr/bin/env bash +# forsoftbudgetcheck.sh — the --for lens stops when its answer is complete; an explicit --token-budget on the +# answer-shaping lenses is a SOFT TARGET (owner decisions 2026-10-14). Written RED-FIRST, before the code. +# +# WHAT CHANGES, and the contract every arm below pins: +# +# DEFAULT --for (no budget). The ranked head the lens selected (, i.e. the relevance floor +# intersected with kForLensDefaultTopN or --pack-top-n) IS the answer. kForPayloadBudgetBytes stops being a +# ceiling and becomes a stair-step TARGET: the trim ladder's shrink steps (A..E: shorter signatures, doc +# excerpts) still run against it, but its row-drop step (F) runs only past a RUNAWAY GUARD +# (kForRunawayGuardBytes, far above a typical answer). So: +# (D1) a default answer serves every row of its head: shown == total; +# (D2) those rows, in rank order, are the rows an uncapped call (--signatures-only at a huge target) serves; +# (D3) rows served past the target are disclosed — and a legend clause defining it — +# and the legend no longer calls budget_bytes= a ceiling on that answer; no over_ceiling= (a target +# cannot be "over"); +# (D4) hitting the runaway guard is the only default-regime cut: over_ceiling="1" on the root, the +# cut carries next=, the legend names the guard, and the block stays within the guard; +# (D5) the MCP `for` twin serves its whole head too. +# +# EXPLICIT --token-budget=N / MCP budget_tokens on --for and --pack-task. Rows stop being added once the +# target is reached; the rank 1..4 floor, the header and every disclosure always ship; there is no byte +# promise, so nothing is labelled over a ceiling and nothing in the header is dropped to fit: +# (E1) no over_ceiling= at a tight target; the floor rows ship; the cut carries next=; +# (E2) no legend clause is dropped "(ceiling)" at any target; +# (E3) the served rows are a rank PREFIX that grows with N, and a prefix of the default answer's rows +# (a target never selects different rows, only fewer); +# (E4) the compact and full dialects serve the same rows at every target (rows are charged, not headers); +# (E5) stop when complete: a target wider than the answer serves exactly the default answer's rows — +# it neither pads nor caps the head below the default; +# (E6) MCP `for` at a tight budget_tokens: no over_ceiling=, next_budget_tokens= still names the continuation; +# (P1) --pack-task at a tight target: no over_ceiling=. +# +# UNCHANGED (twin, must hold on base and head): the exit-3 CI-gate personality of --token-budget on the +# default map (G1): over budget -> exit 3, the map withheld whole (a withheld="1" record, no rows). +# +# Usage: bash test/forsoftbudgetcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forsoftbudgetcheck.sh +# Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +cd "$ROOT" +echo "forsoftbudgetcheck: BIN=$BIN" + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +CORPUS="src" # short relative root (CHECKLIST 22): header bytes shift with the root path +Q="rank symbols by pagerank" # conceptual: a flat 40-row head whose default answer the 7.5 KB ladder cuts +CF="--cache=$TMP/c_for"; CP="--cache=$TMP/c_pack"; CM="--cache=$TMP/c_map" # one cache per verb class + +# the runaway guard, read from source (absent on a binary that predates it: D4's byte bound then FAILS, never skips) +GUARD="$( sed -n 's/^inline constexpr std::size_t kForRunawayGuardBytes *= *\([0-9]*\);.*/\1/p' src/serialize.h )" + +# ── helpers ────────────────────────────────────────────────────────────────────────────────────────────── +cat > "$TMP/rows.py" <<'PY' +# the served rows as "p:l:n", in RANK order (r=), one per line; exit 3 when there is no block +import re, sys +s = sys.stdin.read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( "\n".join( x[ 1 ] for x in rows ) ) +PY +cat > "$TMP/attr.py" <<'PY' +# attr.py ELEMENT NAME: the value of NAME on the first open tag ("" when absent; exit 3 when no such tag) +import re, sys +el, name = sys.argv[ 1 ], sys.argv[ 2 ] +s = sys.stdin.read() +m = re.search( r'<' + el + r'\b([^>]*)>', s ) +if not m: sys.exit( 3 ) +v = re.search( r'\b' + name + r'="([^"]*)"', m.group( 1 ) ) +print( v.group( 1 ) if v else "" ) +PY +cat > "$TMP/prefix.py" <<'PY' +# prefix.py A B: exit 0 when the lines of file A are a prefix of the lines of file B +import sys +a = open( sys.argv[ 1 ] ).read().split( "\n" ); b = open( sys.argv[ 2 ] ).read().split( "\n" ) +a = [ x for x in a if x ]; b = [ x for x in b if x ] +sys.exit( 0 if a == b[ :len( a ) ] else 1 ) +PY +cat > "$TMP/mcptext.py" <<'PY' +import sys, json +for line in sys.stdin: + line = line.strip() + if not line: continue + try: d = json.loads( line ) + except Exception: continue + c = d.get( "result", {} ).get( "content" ) + if c: print( c[ 0 ].get( "text", "" ) ) +PY +cli_for(){ "$BIN" "$CORPUS" --for="$Q" "$@" "$CF" 2>/dev/null; } +mcp_for(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s"%s}}}\n' \ + "$CORPUS" "$Q" "${1:-}" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +attr(){ python3 "$TMP/attr.py" "$1" "$2" < "$3"; } +rows(){ python3 "$TMP/rows.py" < "$1" > "$2"; } +isnum(){ case "${1:-}" in ''|*[!0-9]*) return 1;; esac; return 0; } + +# ── the default answer (premise for D1..D3, E3, E5) ────────────────────────────────────────────────────── +cli_for > "$TMP/def.xml"; rc=$? +if [ "$rc" -ne 0 ] || ! rows "$TMP/def.xml" "$TMP/def.rows"; then + no "premise: default --for on $CORPUS did not run (rc=$rc) or served no — every arm below would measure nothing" + echo "FAILURES ABOVE"; exit 1 +fi +DS="$( attr sigs shown "$TMP/def.xml" )"; DT="$( attr sigs total "$TMP/def.xml" )" +DN="$( grep -c . "$TMP/def.rows" )" +if ! isnum "$DT" || [ "$DT" -lt 30 ]; then + no "premise: the default head should be a wide one (total>=30) so the old 7.5 KB ladder cut it; total='$DT'" +fi + +# (D1) the whole head ships +if isnum "$DT" && [ "$DN" -eq "$DT" ] && { [ -z "$DS" ] || [ "$DS" = "$DT" ]; }; then + ok "(D1) default answer serves its whole head: $DN rows of total=$DT" +else + no "(D1) default answer served $DN rows (shown='$DS') of total='$DT' — rows of the ranked head were dropped below the runaway guard" +fi + +# (D2) the default rows ARE the uncapped rows, in rank order +cli_for --signatures-only --token-budget=1000000 > "$TMP/unc.xml" +if rows "$TMP/unc.xml" "$TMP/unc.rows" && cmp -s "$TMP/def.rows" "$TMP/unc.rows"; then + ok "(D2) default rows == uncapped rows ($( grep -c . "$TMP/unc.rows" ) rows, rank order)" +else + no "(D2) default rows ($DN) differ from the uncapped call's rows ($( grep -c . "$TMP/unc.rows" 2>/dev/null ))" +fi + +# (D3) past-target disclosure on that answer +PT="$( attr sigs past_target "$TMP/def.xml" )"; OC="$( attr ctx over_ceiling "$TMP/def.xml" )" +if isnum "$PT" && [ "$PT" -gt 0 ] && [ "$PT" -le "$DN" ]; then + ok "(D3) names the rows served past the default target" +else + no "(D3) carries no past_target=K (got '$PT') although the answer passes the 7.5 KB target" +fi +if grep -q 'past_target=' <( sed -n '1,/" +fi +if grep -q 'default BYTE ceiling this ranked payload' "$TMP/def.xml"; then + no "(D3) the legend still calls budget_bytes= a ceiling on an answer that passed it by design" +else + ok "(D3) the legend does not call budget_bytes= a ceiling on a past-target answer" +fi +if [ -z "$OC" ]; then ok "(D3) no over_ceiling= on a default answer under the guard" +else no "(D3) over_ceiling='$OC' on a default answer that is under the runaway guard (a target cannot be 'over')"; fi + +# (D4) the runaway guard: a 400-row head runs into it +cli_for --pack-top-n=400 > "$TMP/guard.xml" +GOC="$( attr ctx over_ceiling "$TMP/guard.xml" )"; GNX="$( attr sigs next "$TMP/guard.xml" )"; GCAP="$( attr sigs capped "$TMP/guard.xml" )" +if [ "$GOC" = "1" ] && [ "$GCAP" = "1" ] && [ -n "$GNX" ]; then + ok "(D4) --pack-top-n=400: the guard cut is disclosed (over_ceiling=1, )" +else + no "(D4) --pack-top-n=400: expected over_ceiling=1 + ; got over_ceiling='$GOC' capped='$GCAP' next='${GNX:0:40}'" +fi +if grep -qi 'runaway' <( sed 's/", sys.stdin.read(), re.S); print(len(m.group(0).encode()) if m else "")' < "$TMP/guard.xml" )" +if ! isnum "$GUARD"; then + no "(D4) kForRunawayGuardBytes not found in src/serialize.h — the guard has no single owner" +elif ! isnum "$SIGB"; then + no "(D4) no block on the guard answer" +elif [ "$SIGB" -le "$GUARD" ]; then + ok "(D4) block $SIGB B <= guard $GUARD B" +else + no "(D4) block $SIGB B exceeds the runaway guard $GUARD B" +fi + +# (D5) the MCP twin serves its whole head +mcp_for "" > "$TMP/mcp_def.xml" +MS="$( attr sigs shown "$TMP/mcp_def.xml" )"; MT="$( attr sigs total "$TMP/mcp_def.xml" )" +rows "$TMP/mcp_def.xml" "$TMP/mcp_def.rows"; MN="$( grep -c . "$TMP/mcp_def.rows" 2>/dev/null || echo 0 )" +if isnum "$MN" && [ "$MN" -gt 0 ] && { [ -z "$MT" ] || [ "$MN" = "$MT" ]; } && { [ -z "$MS" ] || [ "$MS" = "$MN" ]; }; then + ok "(D5) MCP for serves its whole head: $MN rows" +else + no "(D5) MCP for served $MN rows (shown='$MS') of total='$MT'" +fi + +# (D6) negative: an answer that is complete under the target carries no past-target or guard disclosure +# (a name-exact query: one anchor row set, well under 7.5 KB; on such answers the change is byte-identical) +"$BIN" "$CORPUS" --for="trimSigLadder" --signatures-only "$CF" > "$TMP/narrow.xml" 2>/dev/null; nrc=$? +NPT="$( attr sigs past_target "$TMP/narrow.xml" )"; NOC="$( attr ctx over_ceiling "$TMP/narrow.xml" )" +NB="$( wc -c < "$TMP/narrow.xml" | tr -d ' ' )" +if [ "$nrc" -ne 0 ] || ! isnum "$NB" || [ "$NB" -eq 0 ]; then no "(D6) the narrow name-exact answer did not run" +elif [ "$NB" -ge 7500 ]; then no "(D6) premise: the narrow answer ($NB B) is not under the 7.5 KB target — pick a narrower query" +elif [ -z "$NPT" ] && [ -z "$NOC" ] && ! grep -qi 'runaway\|past_target' "$TMP/narrow.xml"; then ok "(D6) a $NB B complete answer carries no past_target=/over_ceiling=/guard clause" +else no "(D6) a $NB B complete answer carries past_target='$NPT' over_ceiling='$NOC' or a guard/past-target clause"; fi + +# ── explicit targets ───────────────────────────────────────────────────────────────────────────────────── +SWEEP="300 1000 2000 3000 6000" +prev="" +for n in $SWEEP; do + for lg in full compact; do + cli_for --token-budget="$n" --legend="$lg" > "$TMP/t_${n}_$lg.xml" + rows "$TMP/t_${n}_$lg.xml" "$TMP/t_${n}_$lg.rows" || no "(E) N=$n $lg: no block" + done + f="$TMP/t_${n}_full.xml" + TOC="$( attr ctx over_ceiling "$f" )"; TN="$( grep -c . "$TMP/t_${n}_full.rows" )" + # (E1) at the tight end of the sweep + if [ "$n" -le 1000 ]; then + TNX="$( attr sigs next "$f" )" + if [ -z "$TOC" ] && [ "$TN" -ge 4 ] && [ -n "$TNX" ]; then + ok "(E1) N=$n: no over_ceiling=, $TN floor rows, the cut carries next=" + else + no "(E1) N=$n: over_ceiling='$TOC', rows=$TN (floor 4), sigs next='${TNX:0:40}' — a soft target is never 'over' and always names its continuation" + fi + fi + # (E2) the header is never shaped + for lg in full compact; do + if grep -q 'dropped (ceiling)' "$TMP/t_${n}_$lg.xml"; then + no "(E2) N=$n $lg: legend clauses dropped '(ceiling)' — the header must not be shaped by a soft target" + else + ok "(E2) N=$n $lg: no legend clause dropped" + fi + done + # (E3) rank prefix: of the default rows, and of the next wider target + if python3 "$TMP/prefix.py" "$TMP/t_${n}_full.rows" "$TMP/def.rows"; then ok "(E3) N=$n: rows ($TN) are a rank prefix of the default rows" + else no "(E3) N=$n: rows ($TN) are not a rank prefix of the default rows ($DN)"; fi + if [ -n "$prev" ]; then + if python3 "$TMP/prefix.py" "$TMP/t_${prev}_full.rows" "$TMP/t_${n}_full.rows"; then ok "(E3) rows(N=$prev) is a prefix of rows(N=$n)" + else no "(E3) rows(N=$prev) is not a prefix of rows(N=$n) — not monotone"; fi + fi + # (E4) dialects agree on rows + if cmp -s "$TMP/t_${n}_full.rows" "$TMP/t_${n}_compact.rows"; then ok "(E4) N=$n: compact rows == full rows ($TN)" + else no "(E4) N=$n: compact rows ($( grep -c . "$TMP/t_${n}_compact.rows" )) != full rows ($TN)"; fi + prev="$n" +done + +# (E5) stop when complete: a wide target serves exactly the default answer's rows +cli_for --token-budget=20000 > "$TMP/wide.xml" +rows "$TMP/wide.xml" "$TMP/wide.rows" +if cmp -s "$TMP/wide.rows" "$TMP/def.rows" && [ "$DN" = "$DT" ]; then + ok "(E5) --token-budget=20000 serves exactly the default (complete) rows: $DN" +else + no "(E5) --token-budget=20000 served $( grep -c . "$TMP/wide.rows" ) rows; the default served $DN of total=$DT — a target wider than the answer must serve the complete head" +fi + +# (E6) MCP for at a tight budget_tokens +mcp_for ',"budget_tokens":300' > "$TMP/mcp_300.xml" +M3OC="$( attr ctx over_ceiling "$TMP/mcp_300.xml" )"; M3NB="$( attr sigs next_budget_tokens "$TMP/mcp_300.xml" )" +if [ -z "$M3OC" ] && isnum "$M3NB"; then ok "(E6) MCP for budget_tokens=300: no over_ceiling=, next_budget_tokens=$M3NB" +else no "(E6) MCP for budget_tokens=300: over_ceiling='$M3OC' next_budget_tokens='$M3NB'"; fi + +# (P1) --pack-task at a tight target +"$BIN" "$CORPUS" --pack-task="$Q" --token-budget=300 "$CP" > "$TMP/pt.xml" 2>/dev/null; prc=$? +PTOC="$( attr ctx over_ceiling "$TMP/pt.xml" 2>/dev/null )"; pa=$? +if [ "$prc" -ne 0 ] || [ "$pa" -ne 0 ]; then no "(P1) --pack-task --token-budget=300 did not run (rc=$prc) or has no root" +elif [ -z "$PTOC" ]; then ok "(P1) --pack-task --token-budget=300: no over_ceiling= (soft target)" +else no "(P1) --pack-task --token-budget=300: over_ceiling='$PTOC' — a soft target is never 'over'"; fi + +# ── (G1) twin: the default map's CI-gate personality is unchanged ──────────────────────────────────────── +"$BIN" "$CORPUS" --token-budget=100 "$CM" > "$TMP/map.out" 2>/dev/null; grc=$? +GW="$( attr r withheld "$TMP/map.out" 2>/dev/null )" +if [ "$grc" -eq 3 ] && [ "$GW" = "1" ] && ! grep -q '= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 64c2e78befd47cbb831885ba5340fd6ea5526943 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 10:15:50 -0400 Subject: [PATCH 025/162] test(attribmultidef): gate per-row definition attribution on multi-definition answers A selector that matches two or more definitions answers --callers, --callees, --impact and --safe-delete with the union of their rows, and no row says which definition it belongs to; --path names a bound definition for an endpoint no path reached. This adds the gate for the fix (red on the current binary). test/attribmultidefcheck.sh (new, 82 arms; 60 fail on d4325a8c) over the in-repo corpus test/attribmultideffix: C++ (same-named methods of two classes, a declaration + definition calling an overload pair the resolver splits, overloads in one class, a nested namespace, a template specialisation), Go (three Get definitions of two kinds), Python / TypeScript / Java (two owners of one name in one file, a row with one proven and one by-name edge), Rust and ObjC (rows the resolver ties by name or by a split call). Covers XML, JSON, columnar, --legend=full, the impact file rollup and the MCP twins (find_referencing_symbols, find_symbol, impact, path_between). Registered in test/regression.sh and the shard weights; gate count 684 -> 685. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/attribmultidefcheck.sh | 379 ++++++++++++++++++++++ test/attribmultideffix/cpp/eval_env.h | 6 + test/attribmultideffix/cpp/eval_user.cc | 7 + test/attribmultideffix/cpp/fmt.cc | 11 + test/attribmultideffix/cpp/fmt.h | 19 ++ test/attribmultideffix/cpp/lexer.cc | 2 + test/attribmultideffix/cpp/lexer.h | 5 + test/attribmultideffix/cpp/parser.cc | 21 ++ test/attribmultideffix/cpp/parser.h | 6 + test/attribmultideffix/cpp/piece_user.cc | 7 + test/attribmultideffix/cpp/string_piece.h | 7 + test/attribmultideffix/cpp/util.cc | 8 + test/attribmultideffix/cpp/util.h | 4 + test/attribmultideffix/go/deb/deb.go | 16 + test/attribmultideffix/go/files/files.go | 4 + test/attribmultideffix/go/go.mod | 2 + test/attribmultideffix/go/modtime.go | 6 + test/attribmultideffix/java/Store.java | 11 + test/attribmultideffix/objc/Shapes.m | 15 + test/attribmultideffix/py/widgets.py | 25 ++ test/attribmultideffix/rs/Cargo.toml | 4 + test/attribmultideffix/rs/src/lib.rs | 10 + test/attribmultideffix/ts/shapes.ts | 19 ++ test/regression.sh | 2 +- 28 files changed, 604 insertions(+), 9 deletions(-) create mode 100755 test/attribmultidefcheck.sh create mode 100644 test/attribmultideffix/cpp/eval_env.h create mode 100644 test/attribmultideffix/cpp/eval_user.cc create mode 100644 test/attribmultideffix/cpp/fmt.cc create mode 100644 test/attribmultideffix/cpp/fmt.h create mode 100644 test/attribmultideffix/cpp/lexer.cc create mode 100644 test/attribmultideffix/cpp/lexer.h create mode 100644 test/attribmultideffix/cpp/parser.cc create mode 100644 test/attribmultideffix/cpp/parser.h create mode 100644 test/attribmultideffix/cpp/piece_user.cc create mode 100644 test/attribmultideffix/cpp/string_piece.h create mode 100644 test/attribmultideffix/cpp/util.cc create mode 100644 test/attribmultideffix/cpp/util.h create mode 100644 test/attribmultideffix/go/deb/deb.go create mode 100644 test/attribmultideffix/go/files/files.go create mode 100644 test/attribmultideffix/go/go.mod create mode 100644 test/attribmultideffix/go/modtime.go create mode 100644 test/attribmultideffix/java/Store.java create mode 100644 test/attribmultideffix/objc/Shapes.m create mode 100644 test/attribmultideffix/py/widgets.py create mode 100644 test/attribmultideffix/rs/Cargo.toml create mode 100644 test/attribmultideffix/rs/src/lib.rs create mode 100644 test/attribmultideffix/ts/shapes.ts diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 9e06f3e73..0940c7a74 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -25,6 +25,7 @@ "artifactcheck.sh": 5.4, "atcheck.sh": 12.1, "atomscheck.sh": 4.2, +"attribmultidefcheck.sh": 60.0, "attrvocabcheck.sh": 66.7, "baselinecheck.sh": 1.8, "baselinedirtycheck.sh": 9.8, diff --git a/README.md b/README.md index 2432bb90f..bf380fff8 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-684 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **684 gate scripts** and is the authoritative list; +`test/regression.sh` names **685 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 234344482..11acf723b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 684 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **684 gate scripts**, all of which exist on disk. +naming **685 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 684. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e545797c6..db11d6c46 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["684 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "684", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["684 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/attribmultidefcheck.sh b/test/attribmultidefcheck.sh new file mode 100755 index 000000000..e8b03222e --- /dev/null +++ b/test/attribmultidefcheck.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +# attribmultidefcheck.sh — per-row DEFINITION ATTRIBUTION on answers whose selector names several definitions. +# +# test/attribmultidefcheck.sh [BIN] # BIN defaults to $RIPWIRE_BIN, then build/ripwire +# +# The defect class (round 4, O1 false claims ninja-07 / nfpm-06 / ninja-26; kysely-06 / mdbook-06 complete only after a +# follow-on): a selector that matches two or more definitions (defs="2"+) answers with the UNION of their callers, +# callees or dependants, and every row reads as belonging to one definition. fix27-impact-multidef hedged the --impact +# file rollup only; this gate covers every verb that folds per-selector rows. +# +# The contract (docs: the lane design, reports/attrib-multidef.md): +# * DEFINITION GROUP: a bodied definition plus the bodyless declarations the parser pairs with it (same language family, +# scope, name and parameter count, one bodied partner); an unpaired declaration is its own group. An answer is +# MULTI-GROUP when its selector matched two or more groups. +# * A multi-group answer lists the groups as children of its root (CLI JSON +# "def_table":[{"ix","t","sc","p"}], MCP "def_table":[{"ix","kind","scope","file","line"}]); every row carries +# of_def="TOKENS" (JSON/MCP "of_def":[TOKENS]): space-separated K (a ) with a mark — K alone = a proven edge +# ties the row to group K, K~n = by name alone, K~s = only as one arm of a call the resolver split between +# same-named definitions. callers: row -> group; callees: group -> row; impact: a chain of calls from the row to the +# group (a by-name hop makes it ~n, else a split hop ~s); safe-delete rows as callers; the impact rollup +# row: the union of its symbols' tokens, the strongest mark per K. +# * A ONE-group answer (a single definition, or a definition with its own declarations) carries no table and no of_def: +# byte-identical. Its rows that are one arm of a split call carry prov="split" (the map's per-edge vocabulary) when +# the selector matched 2+ definitions (the ninja-26 shape: a declaration + its definition). +# * --path: an endpoint with several definitions that no path bound (reachable="0") carries no from_p=/to_p=. +# +# Corpus test/attribmultideffix (in-tree, outside test/fixture): +# cpp/ ninja-07 shape StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, RunPiece/RunEval at depth 2 +# in their own files), ninja-26 shape ManifestParser::ParseDefault (decl + def) calling the CanonicalizePath +# overload pair (a split call), near misses in fmt.{h,cc}: Fmt::put overloads in ONE class (split), outer::run vs +# outer::inner::run (nested namespace), Box::get vs the Box specialisation; ReadPath decl + def. +# go/ nfpm-06 shape: three Get (files.Get fn, modtime Get fn, (*Info).Get method), cross_kind fn:2,method:1. +# py/ts/java/ two same-named methods of different classes IN ONE FILE (entity identity: owner, not file + name); +# py/ also draw_mixed: a proven Widget.render call and a by-name thing.render call in one row. +# rs/ mdbook-06 shape: Book::iter vs Shelf::iter on param-typed receivers (by name + split today: hedged tokens). +# objc/ two -area methods, typed message receivers (split today: hedged tokens). +# +# Arm classes in the output: [P] positive — the defect shape, RED on a binary without per-row attribution; [N] near miss — +# a shape the new code could plausibly mis-handle (a one-group answer that must stay plain, an overload or a namesake that +# must not be over-attributed); it is red on such a binary only where it needs a hedge that binary cannot print; [C] control +# — holds on every binary. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/attribmultideffix" +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +fail=0 +# every verdict is an if/else: a failed write of a PASS line must never print FAIL for an arm that passed (gateexitcheck G2) +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +# say RC PASSMSG FAILMSG — the verdict of the test just run ($?), as an if/else (gateexitcheck G2) +say(){ if [ "$1" = 0 ]; then ok "$2"; else no "$3"; fi; } + +echo "attribmultidefcheck: BIN=$BIN" +[ -x "$BIN" ] || { echo " FAIL no ripwire binary at $BIN"; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo " FAIL python3 is required (the answers are parsed, not grepped)"; exit 1; } +[ -d "$FIX/cpp" ] || { echo " FAIL corpus $FIX missing"; exit 1; } + +# ── the answer reader: one canonical line per row, so every arm compares strings ───────────────────────────────────── +# amd.py FILE FORM ROOTTAG ROWKEY +# FORM xml : ROOTTAG = the root element (callers|callees|impact|safe-delete), ROWKEY = the row element (s|c|rf) +# FORM json: ROOTTAG unused, ROWKEY = the row array key (callers|callees|calledBy|calls|reach…); FILE may be an MCP +# JSON-RPC line (its content[0].text is read) +# prints: ROOT ok defs=N groups=G (G = rows; -1 when the root is absent) +# DEF k p bodyless sc +# ROW namecanonflags canon = the of_def tokens mapped to their group's p (sorted), "-" when the row has +# no of_def, "!K" for a token whose K names no ; flags = via=name / prov=split +cat >"$DIR/amd.py" <<'PY' +import json, re, sys +path, form, roottag, rowkey = sys.argv[1:5] +raw = open(path, encoding='utf-8', errors='replace').read() +def attrs(tag): + return dict(re.findall(r'\s([A-Za-z_]+)="([^"]*)"', tag)) +def canon(tokens, table): + out = [] + for t in tokens: + k, _, mark = t.partition('~') + p = table.get(k) + out.append(('!' + k) if p is None else (p + ('~' + mark if mark else ''))) + return ' '.join(sorted(out)) +if form == 'mcp': + try: + msg = json.loads(raw.strip().splitlines()[-1]) + raw = msg['result']['content'][0]['text'] + except Exception as e: + print('ROOT missing mcp-unreadable %s' % e); sys.exit(0) + form = 'json' if raw.lstrip().startswith('{') else 'xml' +if form == 'xml': + m = re.search(r'<%s\s[^>]*>' % re.escape(roottag), raw) + if not m: + print('ROOT missing'); sys.exit(0) + root = attrs(m.group(0)) + body = raw[m.end():] + end = body.find('' % roottag) + body = body if end < 0 else body[:end] + table = {} + defs = re.findall(r']*?/?>', body) + for d in defs: + a = attrs(d) + table[a.get('ix', '?')] = a.get('p', '?') + print('DEF\t%s\t%s\t%s\t%s' % (a.get('ix', '?'), a.get('p', '?'), a.get('bodyless', '0'), a.get('sc', ''))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s' % (root.get('defs', root.get('to_defs', '?')), len(defs), + root.get('to_p', '-'), root.get('from_p', '-'))) + for r in re.findall(r'<%s\s[^>]*?/?>' % re.escape(rowkey), body): + a = attrs(r) + name = a.get('n', a.get('p', '?')) + tok = a.get('of_def') + flags = ' '.join(x for x in (('via=name' if a.get('via') == 'name' else ''), ('prov=split' if a.get('prov') == 'split' else '')) if x) + print('ROW\t%s\t%s\t%s' % (name, '-' if tok is None else canon(tok.split(), table), flags)) +else: + try: + j = json.loads(raw) + except Exception as e: + print('ROOT missing json-unreadable %s' % e); sys.exit(0) + tab = j.get('def_table') or [] + table = {} + for d in tab: + p = d.get('p') or ('%s:%s' % (d.get('file', '?'), d.get('line', '?'))) + table[str(d.get('ix'))] = p + print('DEF\t%s\t%s\t%s\t%s' % (d.get('ix'), p, d.get('bodyless', 0), d.get('sc', d.get('scope', '')))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=-\tfrom_p=-' % (j.get('defs', '?'), len(tab))) + for r in j.get(rowkey) or []: + name = r.get('n', r.get('name', '?')) + tok = r.get('of_def') + flags = ' '.join(x for x in (('via=name' if r.get('via') == 'name' else ''), ('prov=split' if r.get('prov') == 'split' else '')) if x) + print('ROW\t%s\t%s\t%s' % (name, '-' if tok is None else canon([str(t) for t in tok], table), flags)) +PY + +# ans TAG LANG ROOTTAG ROWKEY FORM -- ARGS… : run BIN on the corpus, read the answer into $DIR/TAG.rows; FAIL (and return 1) +# when the run failed or printed no root, so no absence arm ever reads a crashed or empty run as "not attributed". +ans(){ + local tag="$1" lang="$2" roottag="$3" rowkey="$4" form="$5"; shift 6 + if ! ( cd "$FIX/$lang" && perl -e 'alarm 120; exec @ARGV' "$BIN" . --no-cache "$@" ) >"$DIR/$tag.out" 2>"$DIR/$tag.err"; then + no "$tag: '$*' on $lang exited non-zero: $( head -2 "$DIR/$tag.err" )"; : >"$DIR/$tag.rows"; return 1 + fi + python3 "$DIR/amd.py" "$DIR/$tag.out" "$form" "$roottag" "$rowkey" >"$DIR/$tag.rows" + if ! grep -q '^ROOT ok' "$DIR/$tag.rows"; then no "$tag: '$*' on $lang printed no <$roottag> root"; return 1; fi + return 0 +} +# mcp TAG LANG ROWKEY TOOL ARGSJSON : the same, through the MCP server (initialize + one tools/call) +mcp(){ + local tag="$1" lang="$2" rowkey="$3" tool="$4" args="$5" + printf '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s",%s}}}\n' \ + "$tool" "$FIX/$lang" "$args" | perl -e 'alarm 120; exec @ARGV' "$BIN" --mcp 2>"$DIR/$tag.err" | tail -1 >"$DIR/$tag.out" + python3 "$DIR/amd.py" "$DIR/$tag.out" mcp "$( [ "$tool" = impact ] && echo impact || echo path )" "$rowkey" >"$DIR/$tag.rows" + if ! grep -q '^ROOT ok' "$DIR/$tag.rows"; then no "$tag: MCP $tool on $lang gave no answer: $( head -c 200 "$DIR/$tag.out" )"; return 1; fi + return 0 +} +row(){ awk -F'\t' -v n="$2" '$1=="ROW" && $2==n { print $3; exit }' "$DIR/$1.rows"; } +rowflags(){ awk -F'\t' -v n="$2" '$1=="ROW" && $2==n { print $4; exit }' "$DIR/$1.rows"; } +groups(){ awk -F'\t' '$1=="ROOT" { sub("groups=","",$4); print $4; exit }' "$DIR/$1.rows"; } +rootf(){ awk -F'\t' -v f="$2" '$1=="ROOT" { for(i=3;i<=NF;i++) if(index($i,f"=")==1){ print substr($i,length(f)+2); exit } }' "$DIR/$1.rows"; } +nrows(){ grep -c '^ROW' "$DIR/$1.rows"; } + +# expect CLASS TAG NAME WANT : the row's canonical attribution equals WANT exactly +expect(){ + local cls="$1" tag="$2" n="$3" want="$4" got + got="$( row "$tag" "$n" )" + if [ -z "$got" ]; then no "[$cls] $tag: no row n=\"$n\" (rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2 | tr '\n' ' ' ))" + elif [ "$got" = "$want" ]; then ok "[$cls] $tag: $n -> $want" + else no "[$cls] $tag: $n attributed '$got', want '$want'"; fi +} +# hedged CLASS TAG NAME ALLOWED : the row has of_def, every token is marked (~n/~s) and names a group in ALLOWED +hedged(){ + local cls="$1" tag="$2" n="$3" allowed="$4" got t bad="" + got="$( row "$tag" "$n" )" + if [ -z "$got" ] || [ "$got" = "-" ]; then no "[$cls] $tag: $n carries no of_def ('$got'); a multi-definition row must name or hedge its definition"; return; fi + for t in $got; do + case "$t" in *~n|*~s) ;; *) bad="$bad $t(unmarked)" ;; esac + case " $allowed " in *" ${t%%~*} "*) ;; *) bad="$bad $t(not in: $allowed)" ;; esac + done + [ -z "$bad" ]; say $? "[$cls] $tag: $n hedged ($got)" \ + "[$cls] $tag: $n tokens '$got' —$bad" +} +# plain CLASS TAG : one-group answer — no table and no of_def on any row (a read of a successful run) +plain(){ + local cls="$1" tag="$2" + if [ "$( groups "$tag" )" = 0 ] && ! awk -F'\t' '$1=="ROW" && $3!="-" { f=1 } END { exit !f }' "$DIR/$tag.rows" \ + && [ "$( nrows "$tag" )" -gt 0 ]; then ok "[$cls] $tag: one definition group — no table, no of_def ($( nrows "$tag" ) rows)" + else no "[$cls] $tag: a one-group answer grew attribution: groups=$( groups "$tag" ), rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2,3 | tr '\t\n' '= ' )"; fi +} +# invariant CLASS TAG : every row has of_def, every K resolves to a , and a row with no unmarked token is hedged +invariant(){ + local cls="$1" tag="$2" bad + bad="$( awk -F'\t' '$1=="ROW" { if ($3=="-") print $2": no of_def"; else if ($3 ~ /!/) print $2": token names no ("$3")" }' "$DIR/$tag.rows" )" + if [ "$( nrows "$tag" )" -gt 0 ] && [ -z "$bad" ]; then ok "[$cls] $tag: invariant — all $( nrows "$tag" ) rows attributed or hedged, every K in the table" + else no "[$cls] $tag: invariant broken: $( printf '%s' "$bad" | tr '\n' ';' | cut -c1-300 )"; fi +} + +SP=string_piece.h:6; EV=eval_env.h:5 + +# ── (A) ninja-07 shape, C++ callers: the union names its definition per row ─────────────────────────────────────────── +echo "(A) C++ --callers=empty (StringPiece::empty vs EvalString::empty)" +if ans a_cpp cpp callers s xml -- --callers=empty; then + [ "$( groups a_cpp )" = 2 ]; say $? "[P] a_cpp: two groups listed" \ + "[P] a_cpp: rows=$( groups a_cpp ), want 2" + expect P a_cpp UsesPiece "$SP" + expect P a_cpp UsesEval "$EV" + expect P a_cpp UsesBoth "$EV $SP" + expect P a_cpp ParseDefault "$EV" + invariant P a_cpp + sed 's/-->.*//' "$DIR/a_cpp.out" | grep -q 'of_def='; say $? "[P] a_cpp: the legend defines of_def= (present-only clause)" \ + "[P] a_cpp: of_def= is not defined in the answer's legend" +fi +if ans a_cppj cpp - callers json -- --callers=empty --json; then + expect P a_cppj UsesPiece "$SP"; expect P a_cppj UsesBoth "$EV $SP" +fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty --format=columnar ) >"$DIR/a_col.out" 2>/dev/null && grep -q ' table and an of_def column" \ + "[P] a_col: columnar lacks rows or the of_def column: $( grep -oE 'fields="[^"]*"' "$DIR/a_col.out" )" +else no "a_col: columnar --callers=empty failed"; fi +if ans a_sd cpp safe-delete c xml -- --safe-delete=empty; then + expect P a_sd UsesPiece "$SP"; expect P a_sd UsesEval "$EV"; invariant P a_sd +fi +if mcp a_mcp cpp calledBy find_referencing_symbols '"symbol":"empty"'; then + expect P a_mcp UsesPiece "$SP"; expect P a_mcp UsesBoth "$EV $SP"; invariant P a_mcp +fi +if mcp a_mcps cpp calledBy find_symbol '"symbol":"empty"'; then + expect P a_mcps UsesEval "$EV" +fi + +# ── (B) ninja-26 shape, C++ callees: a declaration + its definition = ONE group; the split overload arms are hedged ── +echo "(B) C++ --callees=ParseDefault (decl + def; CanonicalizePath overload split)" +if ans b_cpp cpp callees s xml -- --callees=ParseDefault; then + plain N b_cpp + for n in CanonicalizePath; do + n_split="$( awk -F'\t' -v n="$n" '$1=="ROW" && $2==n && $4 ~ /prov=split/' "$DIR/b_cpp.rows" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] b_cpp: both CanonicalizePath arms carry prov=\"split\"" \ + "[P] b_cpp: $n_split of 2 CanonicalizePath rows carry prov=\"split\"" + done + for n in empty ReadPath; do + [ -z "$( rowflags b_cpp "$n" )" ]; say $? "[N] b_cpp: proven callee $n carries no hedge" \ + "[N] b_cpp: proven callee $n hedged '$( rowflags b_cpp "$n" )'" + done +fi +if ans b_cpp2 cpp callers s xml -- --callers=CanonicalizePath; then + hedged P b_cpp2 ParseDefault "util.cc:2 util.cc:6 util.h:3 util.h:4" + hedged P b_cpp2 CanonicalizePath "util.cc:2 util.cc:6 util.h:3 util.h:4" +fi +if ans b_rp cpp callers s xml -- --callers=ReadPath; then plain N b_rp; fi +if ans b_up cpp callers s xml -- --callers=UsesPiece; then plain N b_up; fi +if ans b_q cpp callers s xml -- --callers=string_piece.h:empty; then plain N b_q; fi + +# ── (C) near misses in one C++ file pair ────────────────────────────────────────────────────────────────────────────── +echo "(C) C++ near misses: overloads in one class, nested namespace, template specialisation" +if ans c_put cpp callers s xml -- --callers=put; then + hedged N c_put PutInt "fmt.cc:2 fmt.cc:3 fmt.h:3 fmt.h:4" + hedged N c_put PutStr "fmt.cc:2 fmt.cc:3 fmt.h:3 fmt.h:4" +fi +if ans c_run cpp callers s xml -- --callers=run; then + expect N c_run RunOuter fmt.cc:4 + expect N c_run RunInner fmt.cc:5 + sc4="$( awk -F'\t' '$1=="DEF" && $3=="fmt.cc:4" { print $5 }' "$DIR/c_run.rows" )" + sc5="$( awk -F'\t' '$1=="DEF" && $3=="fmt.cc:5" { print $5 }' "$DIR/c_run.rows" )" + { [ -n "$sc4" ] && [ -n "$sc5" ] && [ "$sc4" != "$sc5" ]; }; say $? "[N] c_run: the two run groups carry distinct sc= ($sc4 / $sc5)" \ + "[N] c_run: nested-namespace groups not told apart by sc= ('$sc4' / '$sc5')" +fi +if ans c_get cpp callers s xml -- --callers=get; then + # GetInt(const Box&) calls the SPECIALISATION's get (fmt.h:18). The resolver binds the primary (fmt.h:14) today: + # a proven token naming the primary alone is a false attribution; the specialisation, or a hedge, is correct. + g="$( row c_get GetInt )" + case "$g" in + "fmt.h:18") ok "[N] c_get: GetInt -> the Box specialisation" ;; + ""|"-"|"fmt.h:14") no "[N] c_get: GetInt attributed '$g' (the primary template, or nothing): must name fmt.h:18 or hedge" ;; + *) case "$g" in *~n|*~s) ok "[N] c_get: GetInt hedged ($g)" ;; *) no "[N] c_get: GetInt attributed '$g'" ;; esac ;; + esac + g="$( row c_get GetDouble )" + case "$g" in + "fmt.h:14") ok "[N] c_get: GetDouble -> the primary template" ;; + *~n|*~s) ok "[N] c_get: GetDouble hedged ($g)" ;; + *) no "[N] c_get: GetDouble attributed '$g' (want fmt.h:14 or a hedge)" ;; + esac +fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=empty ) >"$DIR/c_for.out" 2>/dev/null; then + n_d="$( grep -oE ']*n="empty"[^>]*>' "$DIR/c_for.out" | grep -c 'sc="' )" + [ "$n_d" -ge 2 ]; say $? "[C] c_for: --for=empty serves each definition as its own row with sc= ($n_d)" \ + "[C] c_for: --for=empty rows with sc=: $n_d" +else no "c_for: --for=empty failed"; fi + +# ── (D) impact: per-row attribution through the transitive reach, and the file rollup ─────────────────────────────── +echo "(D) C++ --impact=empty" +if ans d_imp cpp impact s xml -- --impact=empty; then + expect P d_imp UsesPiece "$SP"; expect P d_imp UsesEval "$EV"; expect P d_imp UsesBoth "$EV $SP" + expect P d_imp RunPiece "$SP"; expect P d_imp RunEval "$EV" + invariant P d_imp +fi +if ans d_rf cpp impact rf xml -- --impact=empty --limit=2; then + expect P d_rf parser.cc "$EV $SP" + expect P d_rf eval_user.cc "$EV" +fi +if ans d_impj cpp - reach json -- --impact=empty --json; then + # the row array key of the JSON dialect is read off the answer, never assumed + key="$( python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); print(next((k for k,v in j.items() if isinstance(v,list) and v and isinstance(v[0],dict) and "n" in v[0]), "-"))' "$DIR/d_impj.out" 2>/dev/null )" + python3 "$DIR/amd.py" "$DIR/d_impj.out" json - "$key" >"$DIR/d_impj.rows" + expect P d_impj RunPiece "$SP" +fi +if mcp d_mcp cpp s impact '"symbol":"empty"'; then expect P d_mcp RunEval "$EV"; fi +if ans d_one cpp impact s xml -- --impact=string_piece.h:empty; then plain N d_one; fi + +# ── (E) --path endpoints ────────────────────────────────────────────────────────────────────────────────────────────── +echo "(E) C++ --path endpoints" +if ans e_un cpp path s xml -- --path=Clear,empty; then + [ "$( rootf e_un to_p )" = "-" ]; say $? "[P] e_un: unreached multi-definition to= carries no to_p" \ + "[P] e_un: reachable=0 with to_defs=2 still claims to_p=\"$( rootf e_un to_p )\"" + [ "$( rootf e_un from_p )" = "eval_env.h:4" ]; say $? "[N] e_un: the single-definition from= keeps from_p" \ + "[N] e_un: from_p='$( rootf e_un from_p )'" +fi +if ans e_re cpp path s xml -- --path=RunPiece,empty; then + [ "$( rootf e_re to_p )" = "$SP" ]; say $? "[C] e_re: a reached multi-definition to= names the bound one ($SP)" \ + "[C] e_re: to_p='$( rootf e_re to_p )'" +fi +if ans e_sp cpp path s xml -- --path=PutInt,put; then + # the hop into put is one arm of a split call (Fmt::put(int) vs Fmt::put(const char*)): the row that ends the path says so + case "$( rowflags e_sp put )" in *prov=split*) ok "[P] e_sp: the split hop into a multi-definition to= carries prov=\"split\"" ;; + *) no "[P] e_sp: the hop into put is a split arm but carries '$( rowflags e_sp put )'" ;; esac +fi +if mcp e_mcp cpp s path_between '"from":"Clear","to":"empty"'; then + [ "$( rootf e_mcp to_p )" = "-" ]; say $? "[P] e_mcp: MCP path_between drops the unbound to_p too" \ + "[P] e_mcp: MCP to_p='$( rootf e_mcp to_p )'" +fi + +# ── (F) the other languages: nfpm-06 (Go), entity identity in one file (Python, TypeScript, Java), hedged (Rust, ObjC) ── +echo "(F) Go / Python / TypeScript / Java / Rust / ObjC" +if ans f_go go callers s xml -- --callers=Get; then + expect P f_go Package "deb/deb.go:7 files/files.go:4" + expect P f_go Describe "deb/deb.go:7" + expect P f_go ListOnly "files/files.go:4" + expect P f_go Stamp "modtime.go:4" + [ "$( groups f_go )" = 3 ]; say $? "[P] f_go: three groups" \ + "[P] f_go: rows=$( groups f_go ), want 3" +fi +if mcp f_gomcp go calledBy find_referencing_symbols '"symbol":"Get"'; then expect P f_gomcp Describe "deb/deb.go:7"; fi +if ans f_py py callers s xml -- --callers=render; then + expect P f_py draw_widget widgets.py:2 + expect P f_py draw_canvas widgets.py:6 + expect P f_py draw_both "widgets.py:2 widgets.py:6" + expect P f_py draw_mixed "widgets.py:2 widgets.py:6~n" +fi +if ans f_ts ts callers s xml -- --callers=area; then + expect P f_ts circleArea shapes.ts:2; expect P f_ts squareArea shapes.ts:5; expect P f_ts bothAreas "shapes.ts:2 shapes.ts:5" +fi +if ans f_java java callers s xml -- --callers=lookup; then + expect P f_java fromCache Store.java:2; expect P f_java fromRegistry Store.java:5; expect P f_java fromBoth "Store.java:2 Store.java:5" +fi +if ans f_rs rs callers s xml -- --callers=iter; then + hedged P f_rs count_items "src/lib.rs:3 src/lib.rs:7" + hedged P f_rs count_books "src/lib.rs:3 src/lib.rs:7" + case "$( rowflags f_rs count_items )" in *via=name*) ok "[N] f_rs: the existing via=\"name\" row hedge stays" ;; *) no "[N] f_rs: via=\"name\" lost on count_items" ;; esac +fi +if ans f_objc objc callers s xml -- --callers=area; then + hedged P f_objc circleArea "Shapes.m:6 Shapes.m:12" + hedged P f_objc squareArea "Shapes.m:6 Shapes.m:12" +fi +if ans f_py1 py callers s xml -- --callers=draw_widget; then + # a single definition with no caller: the root, no rows — read the table count only + [ "$( groups f_py1 )" = 0 ]; say $? "[N] f_py1: single definition, no table" \ + "[N] f_py1: rows on a single definition" +fi + +# ── (G) well-formedness and determinism ─────────────────────────────────────────────────────────────────────────────── +echo "(G) well-formedness, determinism" +if command -v xmllint >/dev/null 2>&1; then + for t in a_cpp a_col a_sd b_cpp d_imp d_rf e_un f_go; do + [ -s "$DIR/$t.out" ] || continue + xmllint --noout "$DIR/$t.out" 2>/dev/null; say $? "[C] $t: well-formed XML" \ + "[C] $t: malformed XML" + done + ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty --legend=full ) >"$DIR/g_full.out" 2>/dev/null && xmllint --noout "$DIR/g_full.out" 2>/dev/null; say $? "[C] g_full: --legend=full well-formed" \ + "[C] g_full: --legend=full malformed or failed" +else + printf ' SKIP xml well-formedness (no xmllint)\n' +fi +( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty ) >"$DIR/g_det.out" 2>/dev/null +cmp -s "$DIR/g_det.out" "$DIR/a_cpp.out"; say $? "[C] determinism: --callers=empty byte-identical run to run" \ + "[C] --callers=empty differs run to run" + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/attribmultideffix/cpp/eval_env.h b/test/attribmultideffix/cpp/eval_env.h new file mode 100644 index 000000000..72c2313da --- /dev/null +++ b/test/attribmultideffix/cpp/eval_env.h @@ -0,0 +1,6 @@ +#pragma once +struct EvalString { + int parsed_; + void Clear() { parsed_ = 0; } + bool empty() const { return parsed_ == 0; } +}; diff --git a/test/attribmultideffix/cpp/eval_user.cc b/test/attribmultideffix/cpp/eval_user.cc new file mode 100644 index 000000000..800f3ca63 --- /dev/null +++ b/test/attribmultideffix/cpp/eval_user.cc @@ -0,0 +1,7 @@ +// impact depth 2: reaches EvalString::empty only, through UsesEval +#include "eval_env.h" +bool UsesEval(EvalString ev); +bool RunEval() { + EvalString ev{0}; + return UsesEval(ev); +} diff --git a/test/attribmultideffix/cpp/fmt.cc b/test/attribmultideffix/cpp/fmt.cc new file mode 100644 index 000000000..74caa2c28 --- /dev/null +++ b/test/attribmultideffix/cpp/fmt.cc @@ -0,0 +1,11 @@ +#include "fmt.h" +void Fmt::put(int v) { (void)v; } +void Fmt::put(const char* s) { (void)s; } +void outer::run() {} +void outer::inner::run() {} +void PutInt(Fmt& f) { f.put(1); } +void PutStr(Fmt& f) { f.put("x"); } +void RunOuter() { outer::run(); } +void RunInner() { outer::inner::run(); } +int GetInt(const Box& b) { return b.get(); } +double GetDouble(const Box& b) { return b.get(); } diff --git a/test/attribmultideffix/cpp/fmt.h b/test/attribmultideffix/cpp/fmt.h new file mode 100644 index 000000000..89630d6aa --- /dev/null +++ b/test/attribmultideffix/cpp/fmt.h @@ -0,0 +1,19 @@ +#pragma once +struct Fmt { + void put(int v); + void put(const char* s); +}; +namespace outer { +void run(); +namespace inner { +void run(); +} +} +template struct Box { + T value; + T get() const { return value; } +}; +template <> struct Box { + int value; + int get() const { return value + 1; } +}; diff --git a/test/attribmultideffix/cpp/lexer.cc b/test/attribmultideffix/cpp/lexer.cc new file mode 100644 index 000000000..d390a0eab --- /dev/null +++ b/test/attribmultideffix/cpp/lexer.cc @@ -0,0 +1,2 @@ +#include "lexer.h" +bool Lexer::ReadPath(EvalString* out) { out->Clear(); return true; } diff --git a/test/attribmultideffix/cpp/lexer.h b/test/attribmultideffix/cpp/lexer.h new file mode 100644 index 000000000..b3d88953b --- /dev/null +++ b/test/attribmultideffix/cpp/lexer.h @@ -0,0 +1,5 @@ +#pragma once +#include "eval_env.h" +struct Lexer { + bool ReadPath(EvalString* out); +}; diff --git a/test/attribmultideffix/cpp/parser.cc b/test/attribmultideffix/cpp/parser.cc new file mode 100644 index 000000000..a2ac5f8f4 --- /dev/null +++ b/test/attribmultideffix/cpp/parser.cc @@ -0,0 +1,21 @@ +#include "parser.h" +#include "util.h" +#include "string_piece.h" +bool ManifestParser::ParseDefault(std::string* err) { + EvalString eval; + lexer_.ReadPath(&eval); + if (eval.empty()) return false; + std::string path = "x"; + unsigned long bits; + CanonicalizePath(&path, &bits); + return true; +} +bool UsesPiece(StringPiece sp) { + return sp.empty(); +} +bool UsesEval(EvalString ev) { + return ev.empty(); +} +bool UsesBoth(StringPiece sp, EvalString ev) { + return sp.empty() && ev.empty(); +} diff --git a/test/attribmultideffix/cpp/parser.h b/test/attribmultideffix/cpp/parser.h new file mode 100644 index 000000000..450629098 --- /dev/null +++ b/test/attribmultideffix/cpp/parser.h @@ -0,0 +1,6 @@ +#pragma once +#include "lexer.h" +struct ManifestParser { + Lexer lexer_; + bool ParseDefault(std::string* err); +}; diff --git a/test/attribmultideffix/cpp/piece_user.cc b/test/attribmultideffix/cpp/piece_user.cc new file mode 100644 index 000000000..4f687da8e --- /dev/null +++ b/test/attribmultideffix/cpp/piece_user.cc @@ -0,0 +1,7 @@ +// impact depth 2: reaches StringPiece::empty only, through UsesPiece +#include "string_piece.h" +bool UsesPiece(StringPiece sp); +bool RunPiece() { + StringPiece sp{"", 0}; + return UsesPiece(sp); +} diff --git a/test/attribmultideffix/cpp/string_piece.h b/test/attribmultideffix/cpp/string_piece.h new file mode 100644 index 000000000..3947a7326 --- /dev/null +++ b/test/attribmultideffix/cpp/string_piece.h @@ -0,0 +1,7 @@ +#pragma once +#include +struct StringPiece { + const char* str_; + std::size_t len_; + bool empty() const { return len_ == 0; } +}; diff --git a/test/attribmultideffix/cpp/util.cc b/test/attribmultideffix/cpp/util.cc new file mode 100644 index 000000000..b30389320 --- /dev/null +++ b/test/attribmultideffix/cpp/util.cc @@ -0,0 +1,8 @@ +#include "util.h" +void CanonicalizePath(std::string* path, unsigned long* bits) { + unsigned long len = path->size(); + CanonicalizePath(&(*path)[0], &len, bits); +} +void CanonicalizePath(char* path, unsigned long* len, unsigned long* bits) { + *len = 0; *bits = 0; (void)path; +} diff --git a/test/attribmultideffix/cpp/util.h b/test/attribmultideffix/cpp/util.h new file mode 100644 index 000000000..f23e64abd --- /dev/null +++ b/test/attribmultideffix/cpp/util.h @@ -0,0 +1,4 @@ +#pragma once +#include +void CanonicalizePath(std::string* path, unsigned long* bits); +void CanonicalizePath(char* path, unsigned long* len, unsigned long* bits); diff --git a/test/attribmultideffix/go/deb/deb.go b/test/attribmultideffix/go/deb/deb.go new file mode 100644 index 000000000..5863ce496 --- /dev/null +++ b/test/attribmultideffix/go/deb/deb.go @@ -0,0 +1,16 @@ +package deb + +import "example.com/pk/files" + +type Info struct{ Name string } + +func (i *Info) Get(key string) string { return i.Name + key } + +func Package(i *Info) []string { + _ = i.Get("x") + return files.Get(i.Name) +} + +func Describe(i *Info) string { return i.Get("d") } + +func ListOnly(n string) []string { return files.Get(n) } diff --git a/test/attribmultideffix/go/files/files.go b/test/attribmultideffix/go/files/files.go new file mode 100644 index 000000000..b0b22c9e4 --- /dev/null +++ b/test/attribmultideffix/go/files/files.go @@ -0,0 +1,4 @@ +package files + +// Get returns the content list. +func Get(name string) []string { return []string{name} } diff --git a/test/attribmultideffix/go/go.mod b/test/attribmultideffix/go/go.mod new file mode 100644 index 000000000..eba449962 --- /dev/null +++ b/test/attribmultideffix/go/go.mod @@ -0,0 +1,2 @@ +module example.com/pk +go 1.21 diff --git a/test/attribmultideffix/go/modtime.go b/test/attribmultideffix/go/modtime.go new file mode 100644 index 000000000..f7e4bf519 --- /dev/null +++ b/test/attribmultideffix/go/modtime.go @@ -0,0 +1,6 @@ +package pk + +// Get returns a timestamp. +func Get() int64 { return 0 } + +func Stamp() int64 { return Get() } diff --git a/test/attribmultideffix/java/Store.java b/test/attribmultideffix/java/Store.java new file mode 100644 index 000000000..e8eea7faa --- /dev/null +++ b/test/attribmultideffix/java/Store.java @@ -0,0 +1,11 @@ +class Cache { + String lookup(String k) { return k; } +} +class Registry { + String lookup(String k) { return k + "!"; } +} +class Store { + String fromCache(Cache c) { return c.lookup("a"); } + String fromRegistry(Registry r) { return r.lookup("b"); } + String fromBoth(Cache c, Registry r) { return c.lookup("a") + r.lookup("b"); } +} diff --git a/test/attribmultideffix/objc/Shapes.m b/test/attribmultideffix/objc/Shapes.m new file mode 100644 index 000000000..b712f5d39 --- /dev/null +++ b/test/attribmultideffix/objc/Shapes.m @@ -0,0 +1,15 @@ +#import +@interface Circle : NSObject +- (int)area; +@end +@implementation Circle +- (int)area { return 3; } +@end +@interface Square : NSObject +- (int)area; +@end +@implementation Square +- (int)area { return 4; } +@end +int circleArea(Circle* c) { return [c area]; } +int squareArea(Square* s) { return [s area]; } diff --git a/test/attribmultideffix/py/widgets.py b/test/attribmultideffix/py/widgets.py new file mode 100644 index 000000000..7cc038a26 --- /dev/null +++ b/test/attribmultideffix/py/widgets.py @@ -0,0 +1,25 @@ +class Widget: + def render(self): + return "w" + +class Canvas: + def render(self): + return "c" + +def draw_widget(): + w = Widget() + return w.render() + +def draw_canvas(): + c = Canvas() + return c.render() + +def draw_both(): + w = Widget() + c = Canvas() + return w.render() + c.render() + +def draw_mixed(thing): + w = Widget() + w.render() + return thing.render() diff --git a/test/attribmultideffix/rs/Cargo.toml b/test/attribmultideffix/rs/Cargo.toml new file mode 100644 index 000000000..89a1df393 --- /dev/null +++ b/test/attribmultideffix/rs/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "bk" +version = "0.1.0" +edition = "2021" diff --git a/test/attribmultideffix/rs/src/lib.rs b/test/attribmultideffix/rs/src/lib.rs new file mode 100644 index 000000000..bfcbc88e1 --- /dev/null +++ b/test/attribmultideffix/rs/src/lib.rs @@ -0,0 +1,10 @@ +pub struct Book { pub items: Vec } +impl Book { + pub fn iter(&self) -> std::slice::Iter<'_, u32> { self.items.iter() } +} +pub struct Shelf { pub books: Vec } +impl Shelf { + pub fn iter(&self) -> std::slice::Iter<'_, Book> { self.books.iter() } +} +pub fn count_items(b: &Book) -> usize { b.iter().count() } +pub fn count_books(s: &Shelf) -> usize { s.iter().count() } diff --git a/test/attribmultideffix/ts/shapes.ts b/test/attribmultideffix/ts/shapes.ts new file mode 100644 index 000000000..6ceac6cb1 --- /dev/null +++ b/test/attribmultideffix/ts/shapes.ts @@ -0,0 +1,19 @@ +export class Circle { + area(): number { return 3; } +} +export class Square { + area(): number { return 4; } +} +export function circleArea(): number { + const c = new Circle(); + return c.area(); +} +export function squareArea(): number { + const s = new Square(); + return s.area(); +} +export function bothAreas(): number { + const c = new Circle(); + const s = new Square(); + return c.area() + s.area(); +} diff --git a/test/regression.sh b/test/regression.sh index d5e21f5d4..4aef777ea 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From a44f30b9431a94d6f8a446e6ab56d7f261847464 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 10:45:26 -0400 Subject: [PATCH 026/162] =?UTF-8?q?test:=20forsoftbudgetcheck=20round=202?= =?UTF-8?q?=20=E2=80=94=20stop-at-target,=20continuations=20that=20run,=20?= =?UTF-8?q?legend=20sets,=20guard=20bounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Early gates review fixes (R1-R6, R9, S2-S7), still before any product code: - ST: in the default and --signatures-only modes, an explicit target's rows fit T (budgetBytesForTokens(N), charged against the bytes inside only) or are the rank 1..4 floor, and when rows were left out the next row of the complete answer would cross T. - NX1-NX3 execute the continuation of the tight cut, of the runaway guard cut and of the MCP next_budget_tokens re-call: each serves the whole head, and the cut's rows are its prefix. NX4 scans default answers: every capped element (or shown < total beyond noedge=) carries next=, so the (D-4) and (D-3) cuts are covered. - E2 compares the legend clause set with the wide answer's (present-only clauses aside), keeps task=/route= on the root and the task echo; E2b runs a 900-char task on --for and --pack-task. - D4 reads the guard value from the legend of the answer that hit it, bounds the cut from below (within 6000 B of the guard), and D4b checks over_ceiling against the uncut head size over --pack-top-n 40..400. - The head size is pinned to kForLensDefaultTopN; D5 compares the MCP rows with the CLI default rows; PT1 pins --pack-top-n x target. - P2: --pack-task --token-budget=6000 serves the default --pack-task's sections; P3: MCP explore at a tight budget_tokens; G2/G3: --query and --recall keep their exit-3 gate personality. On base d4325a8c: 40 FAIL / 45 PASS, every red the intended defect; the twins (D6, E3, E4, E2b --for, P2, G1-G3) are green. Probe loops use arrays. Shard weight re-measured under load. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 2 +- test/forsoftbudgetcheck.sh | 456 +++++++++++++++++++++++----- 2 files changed, 374 insertions(+), 84 deletions(-) diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index d5cbafe01..a013a8c25 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -200,7 +200,7 @@ "forrootlegendcheck.sh": 8.6, "forsectioncollapsecheck.sh": 5.2, "forsigspancheck.sh": 6.0, -"forsoftbudgetcheck.sh": 42.0, +"forsoftbudgetcheck.sh": 120.0, "freshclonecheck.sh": 170.8, "freshnesscheck.sh": 4.4, "g1configcheck.sh": 0.8, diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 10c3394de..2c7dbd676 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -1,38 +1,50 @@ #!/usr/bin/env bash # forsoftbudgetcheck.sh — the --for lens stops when its answer is complete; an explicit --token-budget on the -# answer-shaping lenses is a SOFT TARGET (owner decisions 2026-10-14). Written RED-FIRST, before the code. +# answer-shaping lenses is a SOFT TARGET (owner decisions 2026-10-14). Written RED-FIRST, before the code; round 2 +# (early gates review R1-R9) added the stop-at-target, continuation, legend-set, guard-bound and twin arms. # # WHAT CHANGES, and the contract every arm below pins: # -# DEFAULT --for (no budget). The ranked head the lens selected (, i.e. the relevance floor -# intersected with kForLensDefaultTopN or --pack-top-n) IS the answer. kForPayloadBudgetBytes stops being a -# ceiling and becomes a stair-step TARGET: the trim ladder's shrink steps (A..E: shorter signatures, doc -# excerpts) still run against it, but its row-drop step (F) runs only past a RUNAWAY GUARD -# (kForRunawayGuardBytes, far above a typical answer). So: -# (D1) a default answer serves every row of its head: shown == total; -# (D2) those rows, in rank order, are the rows an uncapped call (--signatures-only at a huge target) serves; +# DEFAULT --for (no budget). The ranked head the lens selected (: the relevance floor intersected with +# kForLensDefaultTopN or --pack-top-n) IS the answer. kForPayloadBudgetBytes stops being a ceiling and becomes a +# stair-step TARGET: the trim ladder's shrink steps (A..E: shorter signatures, doc excerpts) still run against it, +# but its row-drop step (F) runs only past a RUNAWAY GUARD (kForRunawayGuardBytes, far above a typical answer). So: +# (D1) a default answer serves every row of its head: shown == total, and total == kForLensDefaultTopN here; +# (D2) those rows, in rank order, are the rows an uncapped call serves; # (D3) rows served past the target are disclosed — and a legend clause defining it — -# and the legend no longer calls budget_bytes= a ceiling on that answer; no over_ceiling= (a target -# cannot be "over"); -# (D4) hitting the runaway guard is the only default-regime cut: over_ceiling="1" on the root, the -# cut carries next=, the legend names the guard, and the block stays within the guard; -# (D5) the MCP `for` twin serves its whole head too. +# and the legend calls budget_bytes= a TARGET on that answer; no over_ceiling= (a target cannot be "over"); +# (D3H) the compact route's serves its whole row set (decision D-3): shown + noedge == total, not capped; +# (D4) hitting the runaway guard is the only default-regime row cut: over_ceiling="1" on the root, the +# cut carries next=, the legend names the guard (its value read from that legend), and the block is +# within the guard yet close to it (a lower bound: the guard is not the old 7.5 KB step in disguise); +# (D4b) over a --pack-top-n sweep: over_ceiling="1" exactly when the uncut head passes the guard; +# (D5) the MCP `for` twin serves its whole head too: the CLI default's rows, as a set; +# (D6) negative: a complete answer under the target carries no past-target / guard disclosure; +# (NX4) every cut element of a default answer (capped="1", or shown < total beyond noedge=) carries next= (D-4). # -# EXPLICIT --token-budget=N / MCP budget_tokens on --for and --pack-task. Rows stop being added once the -# target is reached; the rank 1..4 floor, the header and every disclosure always ship; there is no byte +# EXPLICIT --token-budget=N / MCP budget_tokens on --for, --pack-task, MCP explore. T = N x kMinBytesPerToken x +# kBudgetHeadroom (the existing conversion) is charged against the ROWS only, never the header or a +# disclosure. Rows are added in rank order while they fit T; the rank 1..4 floor always ships; there is no byte # promise, so nothing is labelled over a ceiling and nothing in the header is dropped to fit: -# (E1) no over_ceiling= at a tight target; the floor rows ship; the cut carries next=; -# (E2) no legend clause is dropped "(ceiling)" at any target; -# (E3) the served rows are a rank PREFIX that grows with N, and a prefix of the default answer's rows -# (a target never selects different rows, only fewer); -# (E4) the compact and full dialects serve the same rows at every target (rows are charged, not headers); -# (E5) stop when complete: a target wider than the answer serves exactly the default answer's rows — -# it neither pads nor caps the head below the default; -# (E6) MCP `for` at a tight budget_tokens: no over_ceiling=, next_budget_tokens= still names the continuation; -# (P1) --pack-task at a tight target: no over_ceiling=. +# (ST) stop at target, in the default and --signatures-only modes: the rows' bytes fit T (or are only the +# floor), and when rows were left out the next row of the complete answer would cross T (fill is maximal); +# (E1) no over_ceiling= at a tight target; the floor rows ship; the cut carries next=; +# (E2) no legend clause is dropped: at every N the legend's clauses include the wide answer's (present-only +# clauses aside), the root keeps task= and route=, the full dialect keeps its task echo; and a 900-char +# task at N=300 keeps task= verbatim with no task_echo drop; +# (E3) the served rows are a rank PREFIX that grows with N, and a prefix of the default answer's rows; +# (E4) the compact and full dialects serve the same rows at every target (rows are charged, not headers); +# (E5) stop when complete: a target wider than the answer serves exactly the default answer's rows; +# (NX1) the cut's next= RUNS and serves the whole head, the cut's rows its prefix (NX2: the guard cut's; NX3: +# the MCP next_budget_tokens= re-call); +# (E6) MCP `for` at a tight budget_tokens: no over_ceiling=, next_budget_tokens= names the continuation; +# (PT1) --pack-top-n sets the head, the target fills it: a wide target serves exactly --pack-top-n rows; +# (P1) --pack-task at a tight target: no over_ceiling=, no "(ceiling)" drop, task= verbatim; +# (P2) --pack-task --token-budget=6000 serves the default --pack-task's sections (6000 is the default value); +# (P3) MCP explore at a tight budget_tokens: no over_ceiling=. # -# UNCHANGED (twin, must hold on base and head): the exit-3 CI-gate personality of --token-budget on the -# default map (G1): over budget -> exit 3, the map withheld whole (a withheld="1" record, no rows). +# UNCHANGED (twins, must hold on base and head): the exit-3 CI-gate personality of --token-budget on the default +# map (G1), --query (G2) and --recall (G3): over budget -> exit 3, the artifact withheld. # # Usage: bash test/forsoftbudgetcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forsoftbudgetcheck.sh # Exits non-zero on any failure. @@ -52,18 +64,23 @@ echo "forsoftbudgetcheck: BIN=$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT CORPUS="src" # short relative root (CHECKLIST 22): header bytes shift with the root path -Q="rank symbols by pagerank" # conceptual: a flat 40-row head whose default answer the 7.5 KB ladder cuts -CF="--cache=$TMP/c_for"; CP="--cache=$TMP/c_pack"; CM="--cache=$TMP/c_map" # one cache per verb class +Q="rank symbols by pagerank" # conceptual: a flat 40-row head whose default answer the 7.5 KB ladder cut +QH="parse command line flags" # conceptual: the compact route's was cut by its 1,000 B allowance on base +CF="--cache=$TMP/c_for"; CP="--cache=$TMP/c_pack"; CM="--cache=$TMP/c_map"; CD="--cache=$TMP/c_docs" # one cache per verb class -# the runaway guard, read from source (absent on a binary that predates it: D4's byte bound then FAILS, never skips) -GUARD="$( sed -n 's/^inline constexpr std::size_t kForRunawayGuardBytes *= *\([0-9]*\);.*/\1/p' src/serialize.h )" +# constants, read from source (a binary that predates the guard has none: the arms that need it FAIL, never skip) +srcnum(){ sed -n "s/^inline constexpr [a-z:_ ]*$1 *= *\([0-9.]*\);.*/\1/p" src/serialize.h | head -1; } +GUARD_SRC="$( srcnum kForRunawayGuardBytes )" +TOPN="$( srcnum kForLensDefaultTopN )" +MINBPT="$( srcnum kMinBytesPerToken )"; HEADROOM="$( srcnum kBudgetHeadroom )" +tbytes(){ python3 -c "import sys; print(int(float(sys.argv[1]) * float(sys.argv[2]) * float(sys.argv[3])))" "$1" "$MINBPT" "$HEADROOM"; } # ── helpers ────────────────────────────────────────────────────────────────────────────────────────────── cat > "$TMP/rows.py" <<'PY' # the served rows as "p:l:n", in RANK order (r=), one per line; exit 3 when there is no block import re, sys s = sys.stdin.read() -m = re.search( r']*>(.*?)', s, re.S ) +m = re.search( r']*>(.*?)', s, re.S ) if not m: sys.exit( 3 ) rows = [] for d in re.finditer( r']*)>', m.group( 1 ) ): @@ -74,6 +91,24 @@ for d in re.finditer( r']*)>', m.group( 1 ) ): rows.sort() print( "\n".join( x[ 1 ] for x in rows ) ) PY +cat > "$TMP/rowbytes.py" <<'PY' +# rowbytes.py FILE: " " then one line per row " p:l:n" in rank order. rows-bytes = the bytes +# inside the element (between its open tag and ): what a soft target is charged against. exit 3: no . +import re, sys +s = open( sys.argv[ 1 ] ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +inner = m.group( 1 ) +rows = [] +for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, len( d.group( 0 ).encode() ), g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( len( inner.encode() ), len( rows ) ) +for r in rows: print( r[ 1 ], r[ 2 ] ) +PY cat > "$TMP/attr.py" <<'PY' # attr.py ELEMENT NAME: the value of NAME on the first open tag ("" when absent; exit 3 when no such tag) import re, sys @@ -91,6 +126,44 @@ a = open( sys.argv[ 1 ] ).read().split( "\n" ); b = open( sys.argv[ 2 ] ).read() a = [ x for x in a if x ]; b = [ x for x in b if x ] sys.exit( 0 if a == b[ :len( a ) ] else 1 ) PY +cat > "$TMP/legend.py" <<'PY' +# legend.py NARROW WIDE: the legend clauses (header comments before , split at brackets / '; ' / '. ', digits +# normalised) of WIDE that NARROW lacks, one per line — present-only clauses (each rides only with its own attribute) +# excluded. Empty output = no clause was dropped. exit 3 when either file has no header comment. +import re, sys +PRESENT_ONLY = re.compile( r'dropped_positive|docs_dropped|capped=1 with shown|sigs next|next_offset|next_budget|past_target|' + r'budget_bytes|BYTE target|over_ceiling|runaway|docs_after_code|relevance floor|this budget left out|' + r'e= on a d row|d e= its last line|sc=scope|cut, recoverable|bodies shown|hops shown=0', re.I ) +def clauses( path ): + s = open( path ).read() + head = s.split( '', head, re.S ) + if not com: sys.exit( 3 ) + out = set() + for c in com: + for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s)\s*', c ): + seg = re.sub( r'\d+', '#', re.sub( r'\s+', ' ', seg ) ).strip( ' .:;' ) + if len( seg ) >= 12: out.add( seg ) + return out +n, w = clauses( sys.argv[ 1 ] ), clauses( sys.argv[ 2 ] ) +for c in sorted( w - n ): + if not PRESENT_ONLY.search( c ): print( c ) +PY +cat > "$TMP/cuts.py" <<'PY' +# cuts.py: every element of the answer that discloses a CUT — capped="1", or shown < total beyond what noedge= explains — +# and carries no continuation (next= / next_budget_tokens=), one per line. Empty = every cut is recoverable. +import re, sys +s = sys.stdin.read() +for m in re.finditer( r'<([a-z]+)\b([^>]*)>', s ): + el, a = m.group( 1 ), m.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "" ] )[ 1 ] + num = lambda k: int( g( k ) ) if g( k ).isdigit() else None + shown, total, noedge = num( "shown" ), num( "total" ), num( "noedge" ) or 0 + cut = g( "capped" ) == "1" or ( shown is not None and total is not None and shown + noedge < total ) + if el in ( "ctx", "files" ) or not cut: continue + if g( "next" ) or g( "next_budget_tokens" ): continue + print( el, a.strip()[ :160 ] ) +PY cat > "$TMP/mcptext.py" <<'PY' import sys, json for line in sys.stdin: @@ -102,22 +175,43 @@ for line in sys.stdin: if c: print( c[ 0 ].get( "text", "" ) ) PY cli_for(){ "$BIN" "$CORPUS" --for="$Q" "$@" "$CF" 2>/dev/null; } -mcp_for(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s"%s}}}\n' \ - "$CORPUS" "$Q" "${1:-}" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +mcp_call(){ # mcp_call TOOL TASK EXTRA-JSON + printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s","task":"%s"%s}}}\n' \ + "$1" "$CORPUS" "$2" "${3:-}" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } attr(){ python3 "$TMP/attr.py" "$1" "$2" < "$3"; } rows(){ python3 "$TMP/rows.py" < "$1" > "$2"; } isnum(){ case "${1:-}" in ''|*[!0-9]*) return 1;; esac; return 0; } +lines(){ local c; c="$( grep -c . "$1" 2>/dev/null )"; echo "${c:-0}"; } +# run a pasteable next= (XML-escaped argv fragment) against the same corpus; stdout to $2 +run_next(){ + python3 - "$1" > "$TMP/next.argv" <<'PY' || return 1 +import sys, shlex, html +print( "\0".join( shlex.split( html.unescape( sys.argv[ 1 ] ) ) ), end="" ) +PY + local -a a=() + while IFS= read -r -d '' x; do a+=( "$x" ); done < <( cat "$TMP/next.argv"; printf '\0' ) + [ "${#a[@]}" -gt 0 ] || return 1 + "$BIN" "$CORPUS" "${a[@]}" "$CF" > "$2" 2>/dev/null +} -# ── the default answer (premise for D1..D3, E3, E5) ────────────────────────────────────────────────────── +if ! isnum "$TOPN" || [ -z "$MINBPT" ] || [ -z "$HEADROOM" ]; then + no "premise: kForLensDefaultTopN / kMinBytesPerToken / kBudgetHeadroom not found in src/serialize.h" + echo "FAILURES ABOVE"; exit 1 +fi + +# ── the default answer (premise for D1..D3, ST, E3, E5) ────────────────────────────────────────────────── cli_for > "$TMP/def.xml"; rc=$? if [ "$rc" -ne 0 ] || ! rows "$TMP/def.xml" "$TMP/def.rows"; then no "premise: default --for on $CORPUS did not run (rc=$rc) or served no — every arm below would measure nothing" echo "FAILURES ABOVE"; exit 1 fi DS="$( attr sigs shown "$TMP/def.xml" )"; DT="$( attr sigs total "$TMP/def.xml" )" -DN="$( grep -c . "$TMP/def.rows" )" -if ! isnum "$DT" || [ "$DT" -lt 30 ]; then - no "premise: the default head should be a wide one (total>=30) so the old 7.5 KB ladder cut it; total='$DT'" +DN="$( lines "$TMP/def.rows" )" +# R5: the head size is pinned to the lens's own cap — a change that shrinks the head cannot pass D1 by shrinking total= +if isnum "$DT" && [ "$DT" -eq "$TOPN" ]; then + ok "premise: the default head is kForLensDefaultTopN wide: total=$DT" +else + no "premise: the default head total='$DT' != kForLensDefaultTopN=$TOPN — the head was narrowed, so 'complete' is relative to a smaller answer" fi # (D1) the whole head ships @@ -130,60 +224,122 @@ fi # (D2) the default rows ARE the uncapped rows, in rank order cli_for --signatures-only --token-budget=1000000 > "$TMP/unc.xml" if rows "$TMP/unc.xml" "$TMP/unc.rows" && cmp -s "$TMP/def.rows" "$TMP/unc.rows"; then - ok "(D2) default rows == uncapped rows ($( grep -c . "$TMP/unc.rows" ) rows, rank order)" + ok "(D2) default rows == uncapped rows ($( lines "$TMP/unc.rows" ) rows, rank order)" else - no "(D2) default rows ($DN) differ from the uncapped call's rows ($( grep -c . "$TMP/unc.rows" 2>/dev/null ))" + no "(D2) default rows ($DN) differ from the uncapped call's rows ($( lines "$TMP/unc.rows" ))" fi # (D3) past-target disclosure on that answer PT="$( attr sigs past_target "$TMP/def.xml" )"; OC="$( attr ctx over_ceiling "$TMP/def.xml" )" -if isnum "$PT" && [ "$PT" -gt 0 ] && [ "$PT" -le "$DN" ]; then - ok "(D3) names the rows served past the default target" -else - no "(D3) carries no past_target=K (got '$PT') although the answer passes the 7.5 KB target" -fi -if grep -q 'past_target=' <( sed -n '1,/ "$TMP/def.head" +if isnum "$PT" && [ "$PT" -gt 0 ] && [ "$PT" -le $(( DN - 4 )) ]; then + ok "(D3) names the rows served past the default target (<= shown-4: the floor is never past it)" else - no "(D3) no legend clause defines past_target= ahead of " + no "(D3) carries no past_target=K with 0 < K <= shown-4 (got '$PT', shown=$DN) although the answer passes the 7.5 KB target" fi -if grep -q 'default BYTE ceiling this ranked payload' "$TMP/def.xml"; then +if grep -q 'past_target=' "$TMP/def.head"; then ok "(D3) the header legend defines past_target=" +else no "(D3) no legend clause defines past_target= ahead of "; fi +if grep -q 'default BYTE ceiling this ranked payload' "$TMP/def.head"; then no "(D3) the legend still calls budget_bytes= a ceiling on an answer that passed it by design" +elif grep -q 'budget_bytes= is the default BYTE target' "$TMP/def.head"; then + ok "(D3) the legend calls budget_bytes= a TARGET on a past-target answer" else - ok "(D3) the legend does not call budget_bytes= a ceiling on a past-target answer" + no "(D3) the legend neither calls budget_bytes= a target nor a ceiling — its clause is missing on an answer that carries budget_bytes=" fi if [ -z "$OC" ]; then ok "(D3) no over_ceiling= on a default answer under the guard" else no "(D3) over_ceiling='$OC' on a default answer that is under the runaway guard (a target cannot be 'over')"; fi +# (NX4) every cut element of the default answer carries a continuation (rule 5; D-4: the ) +python3 "$TMP/cuts.py" < "$TMP/def.xml" > "$TMP/def.cuts" +if [ "$( lines "$TMP/def.cuts" )" -eq 0 ]; then ok "(NX4) every cut element of the default answer carries next=" +else no "(NX4) cut elements without a continuation: $( tr '\n' '|' < "$TMP/def.cuts" | cut -c1-300 )"; fi + +# (D3H) D-3: the compact route's serves its whole row set on the default answer +"$BIN" "$CORPUS" --for="$QH" "$CF" > "$TMP/hops.xml" 2>/dev/null; hrc=$? +HS="$( attr hops shown "$TMP/hops.xml" )"; HT="$( attr hops total "$TMP/hops.xml" )"; HE="$( attr hops noedge "$TMP/hops.xml" )"; HC="$( attr hops capped "$TMP/hops.xml" )" +HE="${HE:-0}" +if [ "$hrc" -ne 0 ] || ! isnum "$HS" || ! isnum "$HT"; then + no "(D3H) premise: --for='$QH' did not run (rc=$hrc) or served no (route changed?)" +elif [ $(( HS + HE )) -eq "$HT" ] && [ "$HC" != "1" ]; then + ok "(D3H) complete: shown=$HS + noedge=$HE == total=$HT, not capped" +else + no "(D3H) cut: shown=$HS noedge=$HE total=$HT capped='$HC' — the default answer must serve every hop row" +fi +python3 "$TMP/cuts.py" < "$TMP/hops.xml" > "$TMP/hops.cuts" +if [ "$hrc" -eq 0 ] && [ "$( lines "$TMP/hops.cuts" )" -eq 0 ]; then ok "(NX4) every cut element of the '$QH' answer carries next=" +else no "(NX4) '$QH': cut elements without a continuation: $( tr '\n' '|' < "$TMP/hops.cuts" | cut -c1-300 )"; fi + # (D4) the runaway guard: a 400-row head runs into it cli_for --pack-top-n=400 > "$TMP/guard.xml" GOC="$( attr ctx over_ceiling "$TMP/guard.xml" )"; GNX="$( attr sigs next "$TMP/guard.xml" )"; GCAP="$( attr sigs capped "$TMP/guard.xml" )" +GS="$( attr sigs shown "$TMP/guard.xml" )"; GT="$( attr sigs total "$TMP/guard.xml" )" if [ "$GOC" = "1" ] && [ "$GCAP" = "1" ] && [ -n "$GNX" ]; then ok "(D4) --pack-top-n=400: the guard cut is disclosed (over_ceiling=1, )" else no "(D4) --pack-top-n=400: expected over_ceiling=1 + ; got over_ceiling='$GOC' capped='$GCAP' next='${GNX:0:40}'" fi -if grep -qi 'runaway' <( sed 's/ "$TMP/guard.head" +# S3: the guard's value is read from the legend of the answer that hit it, and must agree with the source's one owner +GUARD="$( sed -n 's/.*runaway guard of \([0-9][0-9]*\) B.*/\1/p' "$TMP/guard.head" | head -1 )" +if isnum "$GUARD" && [ "$GUARD" = "$GUARD_SRC" ]; then ok "(D4) the legend names the runaway guard ($GUARD B, == kForRunawayGuardBytes)" +else no "(D4) the legend names no runaway guard value ('$GUARD') or it differs from src/serialize.h kForRunawayGuardBytes ('$GUARD_SRC')"; fi SIGB="$( python3 -c 'import re,sys; m=re.search(r"", sys.stdin.read(), re.S); print(len(m.group(0).encode()) if m else "")' < "$TMP/guard.xml" )" if ! isnum "$GUARD"; then - no "(D4) kForRunawayGuardBytes not found in src/serialize.h — the guard has no single owner" + no "(D4) no guard value to bound the block against" elif ! isnum "$SIGB"; then no "(D4) no block on the guard answer" -elif [ "$SIGB" -le "$GUARD" ]; then - ok "(D4) block $SIGB B <= guard $GUARD B" +elif [ "$SIGB" -le "$GUARD" ] && [ "$SIGB" -gt $(( GUARD - 6000 )) ]; then + ok "(D4) block $SIGB B within the guard $GUARD B and close to it (> guard - 6000: header + one row)" else - no "(D4) block $SIGB B exceeds the runaway guard $GUARD B" + no "(D4) block $SIGB B is not in (guard-6000, guard] = ($(( GUARD - 6000 )), $GUARD] — the cut is not at the guard" fi +# (NX2) the guard cut's next= runs and serves the whole 400-candidate head, the cut's rows its prefix +rows "$TMP/guard.xml" "$TMP/guard.rows" +if [ -n "$GNX" ] && run_next "$GNX" "$TMP/guard_next.xml" && rows "$TMP/guard_next.xml" "$TMP/guard_next.rows"; then + GXN="$( lines "$TMP/guard_next.rows" )" + if isnum "$GT" && [ "$GXN" -eq "$GT" ] && python3 "$TMP/prefix.py" "$TMP/guard.rows" "$TMP/guard_next.rows"; then + ok "(NX2) the guard cut's next= serves all $GXN rows of total=$GT; the cut's $( lines "$TMP/guard.rows" ) rows are its prefix" + else + no "(NX2) the guard cut's next= served $GXN rows (total=$GT) or the cut's rows are not its prefix" + fi +else + no "(NX2) the guard cut's next= ('${GNX:0:60}') is absent or did not run" +fi + +# (D4b) the guard biconditional over a --pack-top-n sweep: over_ceiling="1" <=> the uncut head passes the guard +for n in 40 100 200 400; do + cli_for --pack-top-n="$n" > "$TMP/pt_$n.xml" + cli_for --pack-top-n="$n" --signatures-only --token-budget=1000000 > "$TMP/pt_${n}_unc.xml" + o="$( attr ctx over_ceiling "$TMP/pt_$n.xml" )"; s="$( attr sigs shown "$TMP/pt_$n.xml" )"; t="$( attr sigs total "$TMP/pt_$n.xml" )" + rows "$TMP/pt_$n.xml" "$TMP/pt_$n.rows"; sn="$( lines "$TMP/pt_$n.rows" )" + # the uncut quantity the guard measures: this answer's bytes up to + the uncut block + q="$( python3 - "$TMP/pt_$n.xml" "$TMP/pt_${n}_unc.xml" <<'PY' +import re, sys +a = open( sys.argv[ 1 ] ).read(); u = open( sys.argv[ 2 ] ).read() +m = re.search( r'', u, re.S ) +print( len( a.split( ' guard -> over_ceiling=1, $sn of $t rows" + else no "(D4b) n=$n: uncut head $q B > guard $GUARD but over_ceiling='$o' rows=$sn total='$t'"; fi + else + if [ -z "$o" ] && isnum "$t" && [ "$sn" -eq "$t" ]; then ok "(D4b) n=$n: uncut head $q B <= guard -> complete ($sn of $t), no over_ceiling" + else no "(D4b) n=$n: uncut head $q B <= guard $GUARD but over_ceiling='$o' rows=$sn total='$t'"; fi + fi +done -# (D5) the MCP twin serves its whole head -mcp_for "" > "$TMP/mcp_def.xml" +# (D5) the MCP twin serves its whole head: the CLI default's rows (as a set; the dialects order nothing differently, but +# the set is the contract mcpforparitycheck already pins) +mcp_call for "$Q" "" > "$TMP/mcp_def.xml" MS="$( attr sigs shown "$TMP/mcp_def.xml" )"; MT="$( attr sigs total "$TMP/mcp_def.xml" )" -rows "$TMP/mcp_def.xml" "$TMP/mcp_def.rows"; MN="$( grep -c . "$TMP/mcp_def.rows" 2>/dev/null || echo 0 )" -if isnum "$MN" && [ "$MN" -gt 0 ] && { [ -z "$MT" ] || [ "$MN" = "$MT" ]; } && { [ -z "$MS" ] || [ "$MS" = "$MN" ]; }; then - ok "(D5) MCP for serves its whole head: $MN rows" +rows "$TMP/mcp_def.xml" "$TMP/mcp_def.rows"; MN="$( lines "$TMP/mcp_def.rows" )" +if isnum "$MT" && [ "$MT" = "$DT" ] && [ "$MN" = "$MT" ] && { [ -z "$MS" ] || [ "$MS" = "$MN" ]; } \ + && cmp -s <( sort "$TMP/mcp_def.rows" ) <( sort "$TMP/def.rows" ); then + ok "(D5) MCP for serves its whole head: $MN rows == the CLI default's rows (total=$MT)" else - no "(D5) MCP for served $MN rows (shown='$MS') of total='$MT'" + no "(D5) MCP for served $MN rows (shown='$MS') of total='$MT'; CLI default $DN of $DT — must be the same whole head" fi # (D6) negative: an answer that is complete under the target carries no past-target or guard disclosure @@ -197,15 +353,44 @@ elif [ -z "$NPT" ] && [ -z "$NOC" ] && ! grep -qi 'runaway\|past_target' "$TMP/n else no "(D6) a $NB B complete answer carries past_target='$NPT' over_ceiling='$NOC' or a guard/past-target clause"; fi # ── explicit targets ───────────────────────────────────────────────────────────────────────────────────── -SWEEP="300 1000 2000 3000 6000" +cli_for --token-budget=20000 > "$TMP/wide.xml" # the default dialect (compact) +cli_for --token-budget=20000 --legend=full > "$TMP/wide_full.xml" +cli_for --token-budget=20000 --legend=compact > "$TMP/wide_compact.xml" +rows "$TMP/wide.xml" "$TMP/wide.rows" +"$BIN" "$CORPUS" --for="$Q" --signatures-only "$CF" > "$TMP/so_def.xml" 2>/dev/null +SWEEP=( 300 1000 2000 3000 6000 ) prev="" -for n in $SWEEP; do +for n in "${SWEEP[@]}"; do + T="$( tbytes "$n" )" for lg in full compact; do - cli_for --token-budget="$n" --legend="$lg" > "$TMP/t_${n}_$lg.xml" - rows "$TMP/t_${n}_$lg.xml" "$TMP/t_${n}_$lg.rows" || no "(E) N=$n $lg: no block" + cli_for --token-budget="$n" --legend="$lg" > "$TMP/t_${n}_$lg.xml"; trc=$? + if [ "$trc" -ne 0 ] || ! rows "$TMP/t_${n}_$lg.xml" "$TMP/t_${n}_$lg.rows"; then + no "(E) N=$n $lg: the call failed (rc=$trc) or served no block — no arm below can read it" + : > "$TMP/t_${n}_$lg.rows"; continue + fi + # (E2) the header is never shaped: no "(ceiling)" drop, and the wide answer's clauses all ride (S4: read only + # off a call that ran and produced its rows, above) + wf="$TMP/wide_$lg.xml" + python3 "$TMP/legend.py" "$TMP/t_${n}_$lg.xml" "$wf" > "$TMP/t_${n}_$lg.lost"; lrc=$? + if grep -q 'dropped (ceiling)' "$TMP/t_${n}_$lg.xml"; then + no "(E2) N=$n $lg: legend clauses dropped '(ceiling)' — the header must not be shaped by a soft target" + elif [ "$lrc" -ne 0 ]; then + no "(E2) N=$n $lg: no header comment to compare" + elif [ "$( lines "$TMP/t_${n}_$lg.lost" )" -gt 0 ]; then + no "(E2) N=$n $lg: clauses of the wide answer missing: $( head -3 "$TMP/t_${n}_$lg.lost" | tr '\n' '|' | cut -c1-240 )" + else + ok "(E2) N=$n $lg: every clause of the wide answer rides (no clause dropped)" + fi + if [ "$( attr ctx task "$TMP/t_${n}_$lg.xml" )" = "$Q" ] && [ -n "$( attr ctx route "$TMP/t_${n}_$lg.xml" )" ]; then + ok "(E2) N=$n $lg: the root keeps task= verbatim and route=" + else + no "(E2) N=$n $lg: the root lost task= or route= at this target" + fi done f="$TMP/t_${n}_full.xml" - TOC="$( attr ctx over_ceiling "$f" )"; TN="$( grep -c . "$TMP/t_${n}_full.rows" )" + if [ -s "$TMP/t_${n}_full.rows" ] && grep -qF "lens for \"$Q\"" "$f"; then ok "(E2) N=$n full: the legend echoes the task" + else no "(E2) N=$n full: the task echo is gone from the legend (or the call failed)"; fi + TOC="$( attr ctx over_ceiling "$f" )"; TN="$( lines "$TMP/t_${n}_full.rows" )" # (E1) at the tight end of the sweep if [ "$n" -le 1000 ]; then TNX="$( attr sigs next "$f" )" @@ -215,14 +400,6 @@ for n in $SWEEP; do no "(E1) N=$n: over_ceiling='$TOC', rows=$TN (floor 4), sigs next='${TNX:0:40}' — a soft target is never 'over' and always names its continuation" fi fi - # (E2) the header is never shaped - for lg in full compact; do - if grep -q 'dropped (ceiling)' "$TMP/t_${n}_$lg.xml"; then - no "(E2) N=$n $lg: legend clauses dropped '(ceiling)' — the header must not be shaped by a soft target" - else - ok "(E2) N=$n $lg: no legend clause dropped" - fi - done # (E3) rank prefix: of the default rows, and of the next wider target if python3 "$TMP/prefix.py" "$TMP/t_${n}_full.rows" "$TMP/def.rows"; then ok "(E3) N=$n: rows ($TN) are a rank prefix of the default rows" else no "(E3) N=$n: rows ($TN) are not a rank prefix of the default rows ($DN)"; fi @@ -232,37 +409,150 @@ for n in $SWEEP; do fi # (E4) dialects agree on rows if cmp -s "$TMP/t_${n}_full.rows" "$TMP/t_${n}_compact.rows"; then ok "(E4) N=$n: compact rows == full rows ($TN)" - else no "(E4) N=$n: compact rows ($( grep -c . "$TMP/t_${n}_compact.rows" )) != full rows ($TN)"; fi + else no "(E4) N=$n: compact rows ($( lines "$TMP/t_${n}_compact.rows" )) != full rows ($TN)"; fi + # (ST) stop at target, default and --signatures-only modes: rows fit T (or are the floor); a cut is maximal + "$BIN" "$CORPUS" --for="$Q" --signatures-only --token-budget="$n" "$CF" > "$TMP/so_$n.xml" 2>/dev/null + for mode in def so; do + if [ "$mode" = def ]; then a="$f"; ref="$TMP/def.xml"; else a="$TMP/so_$n.xml"; ref="$TMP/so_def.xml"; fi + st="$( python3 - "$a" "$ref" "$T" <<'PY' +import subprocess, sys, re +def rb( p ): + s = open( p ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return None + inner = m.group( 1 ); rows = [] + for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + r = re.search( r'\br="(\d+)"', a ) + rows.append( ( int( r.group( 1 ) ) if r else 10**9, len( d.group( 0 ).encode() ) ) ) + rows.sort() + return len( inner.encode() ), rows +a, ref, T = rb( sys.argv[ 1 ] ), rb( sys.argv[ 2 ] ), int( sys.argv[ 3 ] ) +if a is None or ref is None: print( "ERR no " ); sys.exit( 0 ) +ab, ar = a; rr = ref[ 1 ] +k = len( ar ) +fits = ab <= T or k <= 4 +if k >= len( rr ): print( "OK fits=%d rows=%d/%d bytes=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) +nxt = rr[ k ][ 1 ] +maximal = ab + nxt > T - 160 +print( "%s fits=%d maximal=%d rows=%d/%d bytes=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T ) ) +PY +)" + case "$st" in + OK*) ok "(ST) N=$n $mode: $st" ;; + *) no "(ST) N=$n $mode: $st — the rows must fit the target and stop only where the next row would cross it" ;; + esac + done prev="$n" done +# (E2b) a 900-char task at a tight target: the task stays verbatim and its echo is never dropped +Q900="$Q $( printf 'with the symbol graph and the teleport vector over the whole index %.0s' $( seq 1 14 ) )" +Q900="${Q900:0:900}"; Q900="${Q900% }" +"$BIN" "$CORPUS" --for="$Q900" --token-budget=300 --legend=full "$CF" > "$TMP/q900.xml" 2>/dev/null; qrc=$? # the full dialect echoes the task +if [ "$qrc" -ne 0 ]; then no "(E2b) the 900-char task call failed (rc=$qrc)" +elif [ "$( attr ctx task "$TMP/q900.xml" )" = "$Q900" ] && ! grep -q 'task_echo' "$TMP/q900.xml" && grep -qF "lens for \"$Q900\"" "$TMP/q900.xml"; then + ok "(E2b) a ${#Q900}-char task at N=300: task= verbatim, the echo kept, no task_echo drop" +else + no "(E2b) a ${#Q900}-char task at N=300: task= changed, or the legend echo dropped ('task_echo' in the answer: $( grep -c task_echo "$TMP/q900.xml" ))" +fi +# …and --pack-task, whose ladder took the echo rung at 2000 on base (the rung w3fixbudgetcheck (7) pinned as reachable) +"$BIN" "$CORPUS" --pack-task="$Q900" --token-budget=2000 --legend=full "$CP" > "$TMP/q900p.xml" 2>/dev/null; qrc=$? +if [ "$qrc" -ne 0 ]; then no "(E2b) the 900-char --pack-task call failed (rc=$qrc)" +elif [ "$( attr ctx task "$TMP/q900p.xml" )" = "$Q900" ] && ! grep -q 'task_echo' "$TMP/q900p.xml"; then + ok "(E2b) a ${#Q900}-char --pack-task at N=2000: task= verbatim, no task_echo drop" +else + no "(E2b) a ${#Q900}-char --pack-task at N=2000: task= changed or the echo was dropped (a soft target never sheds the header)" +fi + +# (NX1) the tight cut's next= runs and serves the whole head; the cut's rows are its prefix +NX="$( attr sigs next "$TMP/t_300_full.xml" )" +if [ -n "$NX" ] && run_next "$NX" "$TMP/nx.xml" && rows "$TMP/nx.xml" "$TMP/nx.rows"; then + if cmp -s "$TMP/nx.rows" "$TMP/def.rows" && python3 "$TMP/prefix.py" "$TMP/t_300_full.rows" "$TMP/nx.rows"; then + ok "(NX1) N=300's next= serves the whole head ($( lines "$TMP/nx.rows" ) rows == the default's); the cut is its prefix" + else + no "(NX1) N=300's next= served $( lines "$TMP/nx.rows" ) rows, not the default's $DN in order (or the cut is not its prefix)" + fi +else + no "(NX1) N=300's sigs next= ('${NX:0:60}') is absent or did not run" +fi + # (E5) stop when complete: a wide target serves exactly the default answer's rows -cli_for --token-budget=20000 > "$TMP/wide.xml" -rows "$TMP/wide.xml" "$TMP/wide.rows" if cmp -s "$TMP/wide.rows" "$TMP/def.rows" && [ "$DN" = "$DT" ]; then ok "(E5) --token-budget=20000 serves exactly the default (complete) rows: $DN" else - no "(E5) --token-budget=20000 served $( grep -c . "$TMP/wide.rows" ) rows; the default served $DN of total=$DT — a target wider than the answer must serve the complete head" + no "(E5) --token-budget=20000 served $( lines "$TMP/wide.rows" ) rows; the default served $DN of total=$DT — a target wider than the answer must serve the complete head" fi -# (E6) MCP for at a tight budget_tokens -mcp_for ',"budget_tokens":300' > "$TMP/mcp_300.xml" +# (PT1) --pack-top-n sets the head; the target fills it (rule-4 table #4). An uncut block prints no total= (its rows are it). +cli_for --pack-top-n=10 --token-budget=20000 > "$TMP/ptn10.xml"; rows "$TMP/ptn10.xml" "$TMP/ptn10.rows" +P10T="$( attr sigs total "$TMP/ptn10.xml" )" +if { [ "$P10T" = "10" ] || [ -z "$P10T" ]; } && [ "$( lines "$TMP/ptn10.rows" )" -eq 10 ] && python3 "$TMP/prefix.py" "$TMP/ptn10.rows" "$TMP/def.rows"; then + ok "(PT1) --pack-top-n=10 --token-budget=20000: exactly the 10-row head, a prefix of the default rows" +else + no "(PT1) --pack-top-n=10 --token-budget=20000: total='$P10T' rows=$( lines "$TMP/ptn10.rows" ) — a wide target must serve exactly the --pack-top-n head" +fi + +# (E6) MCP for at a tight budget_tokens; (NX3) its continuation re-call serves the whole head +mcp_call for "$Q" ',"budget_tokens":300' > "$TMP/mcp_300.xml" M3OC="$( attr ctx over_ceiling "$TMP/mcp_300.xml" )"; M3NB="$( attr sigs next_budget_tokens "$TMP/mcp_300.xml" )" if [ -z "$M3OC" ] && isnum "$M3NB"; then ok "(E6) MCP for budget_tokens=300: no over_ceiling=, next_budget_tokens=$M3NB" else no "(E6) MCP for budget_tokens=300: over_ceiling='$M3OC' next_budget_tokens='$M3NB'"; fi +rows "$TMP/mcp_300.xml" "$TMP/mcp_300.rows" +if isnum "$M3NB"; then + mcp_call for "$Q" ",\"budget_tokens\":$M3NB" > "$TMP/mcp_nx.xml"; rows "$TMP/mcp_nx.xml" "$TMP/mcp_nx.rows" + if cmp -s "$TMP/mcp_nx.rows" "$TMP/mcp_def.rows" && python3 "$TMP/prefix.py" "$TMP/mcp_300.rows" "$TMP/mcp_nx.rows"; then + ok "(NX3) MCP re-call at next_budget_tokens=$M3NB serves the whole head ($( lines "$TMP/mcp_nx.rows" ) rows); the cut is its prefix" + else + no "(NX3) MCP re-call at next_budget_tokens=$M3NB served $( lines "$TMP/mcp_nx.rows" ) rows; the MCP default serves $MN" + fi +else + no "(NX3) no next_budget_tokens= to re-call with" +fi # (P1) --pack-task at a tight target "$BIN" "$CORPUS" --pack-task="$Q" --token-budget=300 "$CP" > "$TMP/pt.xml" 2>/dev/null; prc=$? PTOC="$( attr ctx over_ceiling "$TMP/pt.xml" 2>/dev/null )"; pa=$? if [ "$prc" -ne 0 ] || [ "$pa" -ne 0 ]; then no "(P1) --pack-task --token-budget=300 did not run (rc=$prc) or has no root" -elif [ -z "$PTOC" ]; then ok "(P1) --pack-task --token-budget=300: no over_ceiling= (soft target)" -else no "(P1) --pack-task --token-budget=300: over_ceiling='$PTOC' — a soft target is never 'over'"; fi +elif [ -z "$PTOC" ] && ! grep -q '(ceiling)\|header floor' "$TMP/pt.xml" && [ "$( attr ctx task "$TMP/pt.xml" )" = "$Q" ]; then + ok "(P1) --pack-task --token-budget=300: no over_ceiling=, no '(ceiling)' drop, task= verbatim (soft target)" +else + no "(P1) --pack-task --token-budget=300: over_ceiling='$PTOC', '(ceiling)'/'header floor' notes: $( grep -o '(ceiling)\|header floor' "$TMP/pt.xml" | wc -l | tr -d ' ' ) — a soft target is never 'over' and never sheds the header" +fi + +# (P2) R9: the explicit default value serves the default's sections — the quota cascade is the one shaping rule both run +"$BIN" "$CORPUS" --pack-task="$Q" "$CP" > "$TMP/pt_def.xml" 2>/dev/null; p2a=$? +"$BIN" "$CORPUS" --pack-task="$Q" --token-budget=6000 "$CP" > "$TMP/pt_6000.xml" 2>/dev/null; p2b=$? +if [ "$p2a" -ne 0 ] || [ "$p2b" -ne 0 ] || ! grep -q ' "$TMP/mcp_explore.xml" +EOC="$( attr ctx over_ceiling "$TMP/mcp_explore.xml" 2>/dev/null )"; ea=$? +if [ "$ea" -ne 0 ]; then no "(P3) MCP explore budget_tokens=300 produced no root" +elif [ -z "$EOC" ]; then ok "(P3) MCP explore budget_tokens=300: no over_ceiling= (soft target)" +else no "(P3) MCP explore budget_tokens=300: over_ceiling='$EOC'"; fi -# ── (G1) twin: the default map's CI-gate personality is unchanged ──────────────────────────────────────── +# ── twins: the CI-gate personality of --token-budget is unchanged on the map, --query and --recall ─────── "$BIN" "$CORPUS" --token-budget=100 "$CM" > "$TMP/map.out" 2>/dev/null; grc=$? GW="$( attr r withheld "$TMP/map.out" 2>/dev/null )" if [ "$grc" -eq 3 ] && [ "$GW" = "1" ] && ! grep -q ' "$TMP/query.out" 2>/dev/null; qrc=$? +QW="$( attr r withheld "$TMP/query.out" 2>/dev/null )" +if [ "$qrc" -eq 3 ] && [ "$QW" = "1" ] && ! grep -q ' "$TMP/recall.out" 2>/dev/null; rrc=$? +if [ "$rrc" -eq 3 ] && grep -q 'shown=0' "$TMP/recall.out" && grep -q 'withheld_est_tokens=' "$TMP/recall.out"; then + ok "(G3) --recall --token-budget=100: exit 3, shown=0, the withheld price disclosed" +else + no "(G3) --recall --token-budget=100: rc=$rrc — the gate personality must not move" +fi if [ "$fail" -ne 0 ]; then echo "FAILURES ABOVE"; exit 1; fi echo "forsoftbudgetcheck: all arms pass" From 271c6123b2accf6490cf096bd90241c2c70b6283 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 10:57:46 -0400 Subject: [PATCH 027/162] =?UTF-8?q?test(attribmultidef):=20round-2=20gates?= =?UTF-8?q?=20=E2=80=94=20foreign=20declarations,=20row=20pins,=20table=20?= =?UTF-8?q?cap,=20path=20root=20hedge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Early gates review (GO-WITH-CHANGES) required changes, gate side: - foreign-declaration near misses as C++ (a::detail::tally declared, b::detail::tally defined) and plain C (a static helper + a same-named prototype in another file): each declaration stays its own group (bodyless="1"); groups=2 on run/CanonicalizePath and the CanonicalizePath allowed set narrowed to the two definitions - a C prototype + definition and TS overload signatures + implementation stay one group - the template-specialisation arm checks per token: the primary only under its own ~t mark, never unmarked and never ~s; a premise arm pins today's resolver binding - plain one-group arms also assert no prov=; Q1 arm: a single-definition --callees answer marks its split arms prov="split" - row preservation: root count and the row-name multiset pinned from base on every multi-group answer (CLI XML/JSON, safe-delete, impact, rollup, MCP calledBy) - --path=PutInt,put and its MCP twin: no to_p for a to= bound only by a split hop - a generated 20-definition selector arms the 16-group table cap: defs_listed=, the invariant on page 1 and on the next_offset page - ninja-07 std::string::empty confounder; JS and Ruby corpus dirs; xmllint on the --legend=full answers of every touched verb Base d4325a8c: rc=1, 53 PASS / 87 FAIL; every FAIL is missing attribution or hedge. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 2 +- test/attribmultidefcheck.sh | 259 +++++++++++++++++++------ test/attribmultideffix/c/client.c | 2 + test/attribmultideffix/c/helper_a.c | 3 + test/attribmultideffix/c/helper_b.c | 3 + test/attribmultideffix/c/proto.c | 2 + test/attribmultideffix/c/proto.h | 4 + test/attribmultideffix/cpp/detail.cc | 4 + test/attribmultideffix/cpp/detail.h | 3 + test/attribmultideffix/cpp/std_user.cc | 3 + test/attribmultideffix/js/shapes.js | 5 + test/attribmultideffix/rb/shapes.rb | 18 ++ test/attribmultideffix/ts/overloads.ts | 5 + 13 files changed, 256 insertions(+), 57 deletions(-) create mode 100644 test/attribmultideffix/c/client.c create mode 100644 test/attribmultideffix/c/helper_a.c create mode 100644 test/attribmultideffix/c/helper_b.c create mode 100644 test/attribmultideffix/c/proto.c create mode 100644 test/attribmultideffix/c/proto.h create mode 100644 test/attribmultideffix/cpp/detail.cc create mode 100644 test/attribmultideffix/cpp/detail.h create mode 100644 test/attribmultideffix/cpp/std_user.cc create mode 100644 test/attribmultideffix/js/shapes.js create mode 100644 test/attribmultideffix/rb/shapes.rb create mode 100644 test/attribmultideffix/ts/overloads.ts diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 0940c7a74..fc6a54872 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -25,7 +25,7 @@ "artifactcheck.sh": 5.4, "atcheck.sh": 12.1, "atomscheck.sh": 4.2, -"attribmultidefcheck.sh": 60.0, +"attribmultidefcheck.sh": 75.0, "attrvocabcheck.sh": 66.7, "baselinecheck.sh": 1.8, "baselinedirtycheck.sh": 9.8, diff --git a/test/attribmultidefcheck.sh b/test/attribmultidefcheck.sh index e8b03222e..4d178e1a4 100755 --- a/test/attribmultidefcheck.sh +++ b/test/attribmultidefcheck.sh @@ -9,31 +9,42 @@ # file rollup only; this gate covers every verb that folds per-selector rows. # # The contract (docs: the lane design, reports/attrib-multidef.md): -# * DEFINITION GROUP: a bodied definition plus the bodyless declarations the parser pairs with it (same language family, -# scope, name and parameter count, one bodied partner); an unpaired declaration is its own group. An answer is -# MULTI-GROUP when its selector matched two or more groups. +# * DEFINITION GROUP: a bodied definition plus the bodyless declarations that DECLARE it — for C/C++/ObjC the +# --edit-check identity rule (src/editcheckdecl.h editCheckTieDeclaration: same full scope chain, qualifiers, +# parameter-type list, include proof, linkage), for TypeScript the overload signatures beside their implementation; +# a declaration that declares no listed definition (a foreign one: another scope chain, a static definition in +# another file) is its own group. An answer is MULTI-GROUP when its selector matched two or more groups. # * A multi-group answer lists the groups as children of its root (CLI JSON # "def_table":[{"ix","t","sc","p"}], MCP "def_table":[{"ix","kind","scope","file","line"}]); every row carries # of_def="TOKENS" (JSON/MCP "of_def":[TOKENS]): space-separated K (a ) with a mark — K alone = a proven edge # ties the row to group K, K~n = by name alone, K~s = only as one arm of a call the resolver split between -# same-named definitions. callers: row -> group; callees: group -> row; impact: a chain of calls from the row to the -# group (a by-name hop makes it ~n, else a split hop ~s); safe-delete rows as callers; the impact rollup -# row: the union of its symbols' tokens, the strongest mark per K. -# * A ONE-group answer (a single definition, or a definition with its own declarations) carries no table and no of_def: -# byte-identical. Its rows that are one arm of a split call carry prov="split" (the map's per-edge vocabulary) when -# the selector matched 2+ definitions (the ninja-26 shape: a declaration + its definition). -# * --path: an endpoint with several definitions that no path bound (reachable="0") carries no from_p=/to_p=. -# -# Corpus test/attribmultideffix (in-tree, outside test/fixture): -# cpp/ ninja-07 shape StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, RunPiece/RunEval at depth 2 -# in their own files), ninja-26 shape ManifestParser::ParseDefault (decl + def) calling the CanonicalizePath -# overload pair (a split call), near misses in fmt.{h,cc}: Fmt::put overloads in ONE class (split), outer::run vs -# outer::inner::run (nested namespace), Box::get vs the Box specialisation; ReadPath decl + def. -# go/ nfpm-06 shape: three Get (files.Get fn, modtime Get fn, (*Info).Get method), cross_kind fn:2,method:1. +# same-named definitions, K~t = bound to a primary template whose specialisation is also a listed group (which of +# them the call reaches is unproven). callers: row -> group; callees: group -> row; impact: a chain of calls from +# the row to the group (a by-name hop makes it ~n, else a split hop ~s); safe-delete rows as callers; the +# impact rollup row: the union of its symbols' tokens, the strongest mark per K. Above 16 groups the table +# lists the groups the page's rows name and the root says defs_listed=N. +# * Every other answer (one definition, or one definition with its own declarations) carries no table and no of_def; +# its rows that are one arm of a split call carry prov="split" (the map's per-edge vocabulary), whatever defs= is. +# * --path: a to= with several definitions carries no to_p= unless a proven hop bound it (unreached, or the hop into +# it is a split arm or by name: dropped); a from= with several definitions carries no from_p= when unreached. + +# Corpus test/attribmultideffix (in-tree, outside test/fixture); the stand-in dir per known-items row in [brackets]: +# cpp/ [ninja-07] StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, RunPiece/RunEval at depth 2 +# in their own files; UsesStd, a std::string::empty confounder), [ninja-26] ManifestParser::ParseDefault (decl + +# def) calling the CanonicalizePath overload pair (a split call), near misses in fmt.{h,cc}: Fmt::put overloads in +# ONE class (split), outer::run vs outer::inner::run (nested namespace), Box::get vs the Box +# specialisation; ReadPath decl + def; detail.{h,cc}: a::detail::tally declared, b::detail::tally defined (same +# leaf scope, another chain: a foreign declaration). +# c/ plain C: a static helper in helper_a.c + a same-named prototype in helper_b.c (foreign declaration); proto.h's +# lost + its definition in proto.c (the tmux prototype + definition shape: one group). +# go/ [nfpm-06] three Get (files.Get fn, modtime Get fn, (*Info).Get method), cross_kind fn:2,method:1. # py/ts/java/ two same-named methods of different classes IN ONE FILE (entity identity: owner, not file + name); -# py/ also draw_mixed: a proven Widget.render call and a by-name thing.render call in one row. -# rs/ mdbook-06 shape: Book::iter vs Shelf::iter on param-typed receivers (by name + split today: hedged tokens). +# py/ also draw_mixed: a proven Widget.render call and a by-name thing.render call in one row. [kysely-06] ts/; +# ts/overloads.ts: overload signatures of differing arity + one implementation (one group). +# js/rb/ two same-named methods of two classes, constructor-typed receivers. +# rs/ [mdbook-06] Book::iter vs Shelf::iter on param-typed receivers (by name + split today: hedged tokens). # objc/ two -area methods, typed message receivers (split today: hedged tokens). +# (generated at run time) 20 Python classes with one same-named method each: the definition-table cap (16). # # Arm classes in the output: [P] positive — the defect shape, RED on a binary without per-row attribution; [N] near miss — # a shape the new code could plausibly mis-handle (a one-group answer that must stay plain, an overload or a namesake that @@ -100,8 +111,9 @@ if form == 'xml': a = attrs(d) table[a.get('ix', '?')] = a.get('p', '?') print('DEF\t%s\t%s\t%s\t%s' % (a.get('ix', '?'), a.get('p', '?'), a.get('bodyless', '0'), a.get('sc', ''))) - print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s' % (root.get('defs', root.get('to_defs', '?')), len(defs), - root.get('to_p', '-'), root.get('from_p', '-'))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s\tcount=%s\tdefs_listed=%s\tnext_offset=%s' % ( + root.get('defs', root.get('to_defs', '?')), len(defs), root.get('to_p', '-'), root.get('from_p', '-'), + root.get('count', root.get('reaches', root.get('callers', '?'))), root.get('defs_listed', '-'), root.get('next_offset', '-'))) for r in re.findall(r'<%s\s[^>]*?/?>' % re.escape(rowkey), body): a = attrs(r) name = a.get('n', a.get('p', '?')) @@ -119,7 +131,9 @@ else: p = d.get('p') or ('%s:%s' % (d.get('file', '?'), d.get('line', '?'))) table[str(d.get('ix'))] = p print('DEF\t%s\t%s\t%s\t%s' % (d.get('ix'), p, d.get('bodyless', 0), d.get('sc', d.get('scope', '')))) - print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=-\tfrom_p=-' % (j.get('defs', '?'), len(tab))) + print('ROOT\tok\tdefs=%s\tgroups=%d\tto_p=%s\tfrom_p=%s\tcount=%s\tdefs_listed=%s\tnext_offset=%s' % ( + j.get('defs', j.get('to_defs', '?')), len(tab), j.get('to_p', '-'), j.get('from_p', '-'), + j.get('count', j.get('reaches', '?')), j.get('defs_listed', '-'), j.get('next_offset', '-'))) for r in j.get(rowkey) or []: name = r.get('n', r.get('name', '?')) tok = r.get('of_def') @@ -173,12 +187,31 @@ hedged(){ [ -z "$bad" ]; say $? "[$cls] $tag: $n hedged ($got)" \ "[$cls] $tag: $n tokens '$got' —$bad" } -# plain CLASS TAG : one-group answer — no table and no of_def on any row (a read of a successful run) +# plain CLASS TAG [split] : one-group answer — no table and no of_def on any row (a read of a successful run); and, +# unless `split` is given (an answer whose split arms must carry it), no prov="split" on any row either plain(){ - local cls="$1" tag="$2" + local cls="$1" tag="$2" allow="${3:-}" if [ "$( groups "$tag" )" = 0 ] && ! awk -F'\t' '$1=="ROW" && $3!="-" { f=1 } END { exit !f }' "$DIR/$tag.rows" \ - && [ "$( nrows "$tag" )" -gt 0 ]; then ok "[$cls] $tag: one definition group — no table, no of_def ($( nrows "$tag" ) rows)" - else no "[$cls] $tag: a one-group answer grew attribution: groups=$( groups "$tag" ), rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2,3 | tr '\t\n' '= ' )"; fi + && { [ -n "$allow" ] || ! awk -F'\t' '$1=="ROW" && $4 ~ /prov=split/ { f=1 } END { exit !f }' "$DIR/$tag.rows"; } \ + && [ "$( nrows "$tag" )" -gt 0 ]; then ok "[$cls] $tag: one definition group — no table, no of_def$( [ -n "$allow" ] || printf ', no prov' ) ($( nrows "$tag" ) rows)" + else no "[$cls] $tag: a one-group answer grew attribution: groups=$( groups "$tag" ), rows: $( grep '^ROW' "$DIR/$tag.rows" | cut -f2,3,4 | tr '\t\n' '= ' )"; fi +} +# rowset CLASS TAG COUNT NAMES : attribution adds attributes and never removes a row — the root count and the sorted row-name +# multiset equal the base binary's (pinned from base d4325a8c) +rowset(){ + local cls="$1" tag="$2" want_n="$3" want="$4" got got_n + got="$( awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/$tag.rows" | sort | tr '\n' ' ' | sed 's/ $//' )" + got_n="$( rootf "$tag" count )" + { [ "$got" = "$want" ] && [ "$got_n" = "$want_n" ]; }; say $? "[C] $tag: rows preserved (count=$got_n; $got)" \ + "[C] $tag: rows changed: count='$got_n' want '$want_n'; rows '$got' want '$want'" +} +# defbody CLASS TAG P BODYLESS : the row with p=P exists and carries bodyless=BODYLESS (1 = a declaration tied to no +# listed definition, its own group) +defbody(){ + local cls="$1" tag="$2" p="$3" want="$4" got + got="$( awk -F'\t' -v p="$p" '$1=="DEF" && $3==p { print $4; exit }' "$DIR/$tag.rows" )" + [ "$got" = "$want" ]; say $? "[$cls] $tag: bodyless=$want" \ + "[$cls] $tag: bodyless='$got', want '$want' (defs: $( awk -F'\t' '$1=="DEF" { printf "%s/%s ", $3, $4 }' "$DIR/$tag.rows" ))" } # invariant CLASS TAG : every row has of_def, every K resolves to a , and a row with no unmarked token is hedged invariant(){ @@ -200,30 +233,44 @@ if ans a_cpp cpp callers s xml -- --callers=empty; then expect P a_cpp UsesBoth "$EV $SP" expect P a_cpp ParseDefault "$EV" invariant P a_cpp + rowset C a_cpp 4 "ParseDefault UsesBoth UsesEval UsesPiece" + # the std::string::empty confounder: the C++ standard-member gate declines it (no row), or the row names no group proven + g="$( row a_cpp UsesStd )" + case "$g" in + "") ok "[N] a_cpp: UsesStd (std::string::empty) is no row" ;; + *) bad=""; for t in $g; do case "$t" in *~n|*~s|*~t) ;; *) bad="$bad $t" ;; esac; done + [ -z "$bad" ]; say $? "[N] a_cpp: UsesStd row hedged ($g)" "[N] a_cpp: UsesStd (a std::string member call) carries proven token(s):$bad" ;; + esac sed 's/-->.*//' "$DIR/a_cpp.out" | grep -q 'of_def='; say $? "[P] a_cpp: the legend defines of_def= (present-only clause)" \ "[P] a_cpp: of_def= is not defined in the answer's legend" fi if ans a_cppj cpp - callers json -- --callers=empty --json; then expect P a_cppj UsesPiece "$SP"; expect P a_cppj UsesBoth "$EV $SP" + rowset C a_cppj 4 "ParseDefault UsesBoth UsesEval UsesPiece" fi if ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty --format=columnar ) >"$DIR/a_col.out" 2>/dev/null && grep -q ' table and an of_def column" \ "[P] a_col: columnar lacks rows or the of_def column: $( grep -oE 'fields="[^"]*"' "$DIR/a_col.out" )" else no "a_col: columnar --callers=empty failed"; fi if ans a_sd cpp safe-delete c xml -- --safe-delete=empty; then - expect P a_sd UsesPiece "$SP"; expect P a_sd UsesEval "$EV"; invariant P a_sd + expect P a_sd UsesPiece "$SP"; expect P a_sd UsesEval "$EV"; expect P a_sd UsesBoth "$EV $SP"; expect P a_sd ParseDefault "$EV" + invariant P a_sd + rowset C a_sd 4 "ParseDefault UsesBoth UsesEval UsesPiece" fi if mcp a_mcp cpp calledBy find_referencing_symbols '"symbol":"empty"'; then expect P a_mcp UsesPiece "$SP"; expect P a_mcp UsesBoth "$EV $SP"; invariant P a_mcp + rowset C a_mcp 4 "ParseDefault UsesBoth UsesEval UsesPiece" fi if mcp a_mcps cpp calledBy find_symbol '"symbol":"empty"'; then expect P a_mcps UsesEval "$EV" + [ "$( groups a_mcps )" = 2 ]; say $? "[P] a_mcps: find_symbol's root symbol object rides beside a def_table of 2" \ + "[P] a_mcps: def_table rows=$( groups a_mcps ), want 2" fi # ── (B) ninja-26 shape, C++ callees: a declaration + its definition = ONE group; the split overload arms are hedged ── echo "(B) C++ --callees=ParseDefault (decl + def; CanonicalizePath overload split)" if ans b_cpp cpp callees s xml -- --callees=ParseDefault; then - plain N b_cpp + plain N b_cpp split for n in CanonicalizePath; do n_split="$( awk -F'\t' -v n="$n" '$1=="ROW" && $2==n && $4 ~ /prov=split/' "$DIR/b_cpp.rows" | wc -l | tr -d ' ' )" [ "$n_split" = 2 ]; say $? "[P] b_cpp: both CanonicalizePath arms carry prov=\"split\"" \ @@ -235,20 +282,32 @@ if ans b_cpp cpp callees s xml -- --callees=ParseDefault; then done fi if ans b_cpp2 cpp callers s xml -- --callers=CanonicalizePath; then - hedged P b_cpp2 ParseDefault "util.cc:2 util.cc:6 util.h:3 util.h:4" - hedged P b_cpp2 CanonicalizePath "util.cc:2 util.cc:6 util.h:3 util.h:4" + # two overloads, each declared in util.h and defined in util.cc: the declarations fold into their definitions + [ "$( groups b_cpp2 )" = 2 ]; say $? "[N] b_cpp2: two groups (each util.h declaration folds into its util.cc definition)" \ + "[N] b_cpp2: rows=$( groups b_cpp2 ), want 2" + hedged P b_cpp2 ParseDefault "util.cc:2 util.cc:6" + hedged P b_cpp2 CanonicalizePath "util.cc:2 util.cc:6" fi if ans b_rp cpp callers s xml -- --callers=ReadPath; then plain N b_rp; fi if ans b_up cpp callers s xml -- --callers=UsesPiece; then plain N b_up; fi if ans b_q cpp callers s xml -- --callers=string_piece.h:empty; then plain N b_q; fi +# Q1 (orchestrator ruling): the split-arm hedge rides single-definition answers too (by-design byte change on them) +if ans b_one cpp callees s xml -- --callees=PutInt; then + plain N b_one split + n_split="$( awk -F'\t' '$1=="ROW" && $2=="put" && $4 ~ /prov=split/' "$DIR/b_one.rows" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] b_one: a single-definition --callees=PutInt marks both put arms prov=\"split\"" \ + "[P] b_one: $n_split of 2 put rows carry prov=\"split\" on the single-definition answer" +fi -# ── (C) near misses in one C++ file pair ────────────────────────────────────────────────────────────────────────────── -echo "(C) C++ near misses: overloads in one class, nested namespace, template specialisation" +# ── (C) near misses in one C++ file pair, the foreign declarations (C++ and C), the TS overload signatures ────────────── +echo "(C) near misses: overloads in one class, nested namespace, template specialisation, foreign declarations, TS overloads" if ans c_put cpp callers s xml -- --callers=put; then - hedged N c_put PutInt "fmt.cc:2 fmt.cc:3 fmt.h:3 fmt.h:4" - hedged N c_put PutStr "fmt.cc:2 fmt.cc:3 fmt.h:3 fmt.h:4" + hedged N c_put PutInt "fmt.cc:2 fmt.cc:3" + hedged N c_put PutStr "fmt.cc:2 fmt.cc:3" fi if ans c_run cpp callers s xml -- --callers=run; then + [ "$( groups c_run )" = 2 ]; say $? "[N] c_run: two groups (each fmt.h declaration folds into its own fmt.cc definition)" \ + "[N] c_run: rows=$( groups c_run ), want 2" expect N c_run RunOuter fmt.cc:4 expect N c_run RunInner fmt.cc:5 sc4="$( awk -F'\t' '$1=="DEF" && $3=="fmt.cc:4" { print $5 }' "$DIR/c_run.rows" )" @@ -256,22 +315,47 @@ if ans c_run cpp callers s xml -- --callers=run; then { [ -n "$sc4" ] && [ -n "$sc5" ] && [ "$sc4" != "$sc5" ]; }; say $? "[N] c_run: the two run groups carry distinct sc= ($sc4 / $sc5)" \ "[N] c_run: nested-namespace groups not told apart by sc= ('$sc4' / '$sc5')" fi +# Box::get (fmt.h:14) vs the Box specialisation's get (fmt.h:18). The resolver binds GetInt(const Box&) to the +# PRIMARY today (premise arm below), so a token naming the primary is a claim no proof backs: it must carry the ~t mark +# (bound to a primary template whose specialisation is listed), never stand unmarked, and never pose as a split (~s: the +# resolver split nothing). The specialisation itself, proven, is accepted (a future resolver fix). +gettok(){ + local tag="$1" n="$2" g t bad="" + g="$( row "$tag" "$n" )" + if [ -z "$g" ] || [ "$g" = "-" ]; then no "[N] $tag: $n carries no of_def ('$g')"; return; fi + for t in $g; do case "$t" in "fmt.h:14~t"|"fmt.h:18") ;; *) bad="$bad $t" ;; esac; done + [ -z "$bad" ]; say $? "[N] $tag: $n tokens '$g' (the primary only as ~t)" \ + "[N] $tag: $n tokens '$g' — not allowed:$bad (want fmt.h:14~t or fmt.h:18)" +} if ans c_get cpp callers s xml -- --callers=get; then - # GetInt(const Box&) calls the SPECIALISATION's get (fmt.h:18). The resolver binds the primary (fmt.h:14) today: - # a proven token naming the primary alone is a false attribution; the specialisation, or a hedge, is correct. - g="$( row c_get GetInt )" - case "$g" in - "fmt.h:18") ok "[N] c_get: GetInt -> the Box specialisation" ;; - ""|"-"|"fmt.h:14") no "[N] c_get: GetInt attributed '$g' (the primary template, or nothing): must name fmt.h:18 or hedge" ;; - *) case "$g" in *~n|*~s) ok "[N] c_get: GetInt hedged ($g)" ;; *) no "[N] c_get: GetInt attributed '$g'" ;; esac ;; - esac - g="$( row c_get GetDouble )" - case "$g" in - "fmt.h:14") ok "[N] c_get: GetDouble -> the primary template" ;; - *~n|*~s) ok "[N] c_get: GetDouble hedged ($g)" ;; - *) no "[N] c_get: GetDouble attributed '$g' (want fmt.h:14 or a hedge)" ;; - esac + gettok c_get GetInt + gettok c_get GetDouble fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=get --legend=full ) >"$DIR/c_getl.out" 2>/dev/null; then + sed 's/-->.*//' "$DIR/c_getl.out" | grep -q 'K~t'; say $? "[P] c_getl: the legend reads the ~t mark (K~t)" \ + "[P] c_getl: no K~t reading in the --legend=full answer" +else no "c_getl: --callers=get --legend=full failed"; fi +# S4 premise: the resolver's binding this lane hedges and does not fix (the resolver card flips this arm when it lands) +if ans c_geti cpp callees s xml -- --callees=GetInt; then + grep -q 'n="get" p="fmt.h:14"' "$DIR/c_geti.out"; say $? "[C] c_geti: premise — the resolver binds GetInt's b.get() to the primary (fmt.h:14)" \ + "[C] c_geti: premise moved — GetInt's get is no longer fmt.h:14 (resolver card landed? re-anchor c_get): $( grep -o ']*>' "$DIR/c_geti.out" | head -2 | tr '\n' ' ' )" +fi +if ans c_tal cpp callers s xml -- --callers=tally; then + [ "$( groups c_tal )" = 2 ]; say $? "[N] c_tal: a::detail::tally (declared) and b::detail::tally (defined) are two groups" \ + "[N] c_tal: rows=$( groups c_tal ), want 2 (same leaf scope, another chain: never one group)" + defbody N c_tal detail.h:3 1 + defbody N c_tal detail.cc:2 0 + invariant N c_tal +fi +if ans c_hlp c callers s xml -- --callers=helper; then + [ "$( groups c_hlp )" = 2 ]; say $? "[N] c_hlp: C static helper (helper_a.c) and the prototype in helper_b.c are two groups" \ + "[N] c_hlp: rows=$( groups c_hlp ), want 2 (internal linkage in another file: never one group)" + defbody N c_hlp helper_b.c:2 1 + defbody N c_hlp helper_a.c:2 0 + invariant N c_hlp +fi +if ans c_lost c callers s xml -- --callers=lost; then plain N c_lost; fi # C prototype + definition: one group +if ans c_pick ts callers s xml -- --callers=pick; then plain N c_pick; fi # TS overload signatures + implementation if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=empty ) >"$DIR/c_for.out" 2>/dev/null; then n_d="$( grep -oE ']*n="empty"[^>]*>' "$DIR/c_for.out" | grep -c 'sc="' )" [ "$n_d" -ge 2 ]; say $? "[C] c_for: --for=empty serves each definition as its own row with sc= ($n_d)" \ @@ -282,12 +366,14 @@ else no "c_for: --for=empty failed"; fi echo "(D) C++ --impact=empty" if ans d_imp cpp impact s xml -- --impact=empty; then expect P d_imp UsesPiece "$SP"; expect P d_imp UsesEval "$EV"; expect P d_imp UsesBoth "$EV $SP" - expect P d_imp RunPiece "$SP"; expect P d_imp RunEval "$EV" + expect P d_imp RunPiece "$SP"; expect P d_imp RunEval "$EV"; expect P d_imp ParseDefault "$EV" invariant P d_imp + rowset C d_imp 6 "ParseDefault RunEval RunPiece UsesBoth UsesEval UsesPiece" fi if ans d_rf cpp impact rf xml -- --impact=empty --limit=2; then expect P d_rf parser.cc "$EV $SP" expect P d_rf eval_user.cc "$EV" + rowset C d_rf 6 "eval_user.cc parser.cc" fi if ans d_impj cpp - reach json -- --impact=empty --json; then # the row array key of the JSON dialect is read off the answer, never assumed @@ -311,17 +397,26 @@ if ans e_re cpp path s xml -- --path=RunPiece,empty; then "[C] e_re: to_p='$( rootf e_re to_p )'" fi if ans e_sp cpp path s xml -- --path=PutInt,put; then - # the hop into put is one arm of a split call (Fmt::put(int) vs Fmt::put(const char*)): the row that ends the path says so + # the hop into put is one arm of a split call (Fmt::put(int) vs Fmt::put(const char*)): the row that ends the path says + # so, and the root does not claim the arm the BFS happened to take as the definition put was bound to case "$( rowflags e_sp put )" in *prov=split*) ok "[P] e_sp: the split hop into a multi-definition to= carries prov=\"split\"" ;; *) no "[P] e_sp: the hop into put is a split arm but carries '$( rowflags e_sp put )'" ;; esac + [ "$( rootf e_sp to_p )" = "-" ]; say $? "[P] e_sp: a to= bound only by a split hop carries no to_p" \ + "[P] e_sp: to_p=\"$( rootf e_sp to_p )\" claimed for a to= the path reached by a split arm" + [ "$( rootf e_sp from_p )" = "fmt.cc:6" ]; say $? "[N] e_sp: the single-definition from= keeps from_p" \ + "[N] e_sp: from_p='$( rootf e_sp from_p )'" fi if mcp e_mcp cpp s path_between '"from":"Clear","to":"empty"'; then [ "$( rootf e_mcp to_p )" = "-" ]; say $? "[P] e_mcp: MCP path_between drops the unbound to_p too" \ "[P] e_mcp: MCP to_p='$( rootf e_mcp to_p )'" fi +if mcp e_mcps cpp s path_between '"from":"PutInt","to":"put"'; then + [ "$( rootf e_mcps to_p )" = "-" ]; say $? "[P] e_mcps: MCP path_between drops a to_p bound by a split hop" \ + "[P] e_mcps: MCP to_p='$( rootf e_mcps to_p )'" +fi -# ── (F) the other languages: nfpm-06 (Go), entity identity in one file (Python, TypeScript, Java), hedged (Rust, ObjC) ── -echo "(F) Go / Python / TypeScript / Java / Rust / ObjC" +# ── (F) the other languages: nfpm-06 (Go), entity identity in one file (Python, TypeScript, Java), JS, Ruby, hedged (Rust, ObjC) +echo "(F) Go / Python / TypeScript / Java / JavaScript / Ruby / Rust / ObjC" if ans f_go go callers s xml -- --callers=Get; then expect P f_go Package "deb/deb.go:7 files/files.go:4" expect P f_go Describe "deb/deb.go:7" @@ -329,28 +424,45 @@ if ans f_go go callers s xml -- --callers=Get; then expect P f_go Stamp "modtime.go:4" [ "$( groups f_go )" = 3 ]; say $? "[P] f_go: three groups" \ "[P] f_go: rows=$( groups f_go ), want 3" + rowset C f_go 4 "Describe ListOnly Package Stamp" +fi +if mcp f_gomcp go calledBy find_referencing_symbols '"symbol":"Get"'; then + expect P f_gomcp Describe "deb/deb.go:7" + rowset C f_gomcp 4 "Describe ListOnly Package Stamp" fi -if mcp f_gomcp go calledBy find_referencing_symbols '"symbol":"Get"'; then expect P f_gomcp Describe "deb/deb.go:7"; fi if ans f_py py callers s xml -- --callers=render; then expect P f_py draw_widget widgets.py:2 expect P f_py draw_canvas widgets.py:6 expect P f_py draw_both "widgets.py:2 widgets.py:6" expect P f_py draw_mixed "widgets.py:2 widgets.py:6~n" + rowset C f_py 4 "draw_both draw_canvas draw_mixed draw_widget" fi if ans f_ts ts callers s xml -- --callers=area; then expect P f_ts circleArea shapes.ts:2; expect P f_ts squareArea shapes.ts:5; expect P f_ts bothAreas "shapes.ts:2 shapes.ts:5" + rowset C f_ts 3 "bothAreas circleArea squareArea" fi if ans f_java java callers s xml -- --callers=lookup; then expect P f_java fromCache Store.java:2; expect P f_java fromRegistry Store.java:5; expect P f_java fromBoth "Store.java:2 Store.java:5" + rowset C f_java 3 "fromBoth fromCache fromRegistry" +fi +if ans f_js js callers s xml -- --callers=area; then + expect P f_js circleArea shapes.js:1; expect P f_js squareArea shapes.js:2 + rowset C f_js 2 "circleArea squareArea" +fi +if ans f_rb rb callers s xml -- --callers=area; then + expect P f_rb circle_area shapes.rb:2; expect P f_rb square_area shapes.rb:7 + rowset C f_rb 2 "circle_area square_area" fi if ans f_rs rs callers s xml -- --callers=iter; then hedged P f_rs count_items "src/lib.rs:3 src/lib.rs:7" hedged P f_rs count_books "src/lib.rs:3 src/lib.rs:7" case "$( rowflags f_rs count_items )" in *via=name*) ok "[N] f_rs: the existing via=\"name\" row hedge stays" ;; *) no "[N] f_rs: via=\"name\" lost on count_items" ;; esac + rowset C f_rs 2 "count_books count_items" fi if ans f_objc objc callers s xml -- --callers=area; then hedged P f_objc circleArea "Shapes.m:6 Shapes.m:12" hedged P f_objc squareArea "Shapes.m:6 Shapes.m:12" + rowset C f_objc 2 "circleArea squareArea" fi if ans f_py1 py callers s xml -- --callers=draw_widget; then # a single definition with no caller: the root, no rows — read the table count only @@ -358,16 +470,51 @@ if ans f_py1 py callers s xml -- --callers=draw_widget; then "[N] f_py1: rows on a single definition" fi +# ── (H) the definition-table cap: 20 same-named definitions, 12 callers — the table lists what the page names ───────── +echo "(H) the definition-table cap (16): a generated 20-definition selector" +mkdir -p "$DIR/capfix" +python3 - "$DIR/capfix/caps.py" <<'PY' +import sys +out = [] +for k in range(20): + out.append('class C%02d:\n def go(self):\n return %d\n' % (k, k)) +for k in range(12): + out.append('def use_%02d():\n x = C%02d()\n return x.go()\n' % (k, k)) +open(sys.argv[1], 'w').write('\n'.join(out)) +PY +FIXSAVE="$FIX"; FIX="$DIR" +if ans h_all capfix callers s xml -- --callers=go; then + [ "$( groups h_all )" = 12 ] && [ "$( rootf h_all defs_listed )" = 12 ]; say $? "[P] h_all: 20 groups, 12 rows: the table lists the 12 the rows name, defs_listed=\"12\"" \ + "[P] h_all: rows=$( groups h_all ) defs_listed='$( rootf h_all defs_listed )', want 12 / 12" + invariant P h_all +fi +if ans h_p1 capfix callers s xml -- --callers=go --limit=5; then + [ "$( groups h_p1 )" = 5 ] && [ "$( rootf h_p1 defs_listed )" = 5 ]; say $? "[P] h_p1: page 1 (--limit=5) lists its 5 groups, defs_listed=\"5\"" \ + "[P] h_p1: rows=$( groups h_p1 ) defs_listed='$( rootf h_p1 defs_listed )', want 5 / 5" + invariant P h_p1 + nxt="$( rootf h_p1 next_offset )" + if [ "$nxt" = 5 ] && ans h_p2 capfix callers s xml -- --callers=go --limit=5 --offset="$nxt"; then + invariant P h_p2 + both="$( { awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/h_p1.rows"; awk -F'\t' '$1=="ROW" { print $2 }' "$DIR/h_p2.rows"; } | sort | uniq -d | tr '\n' ' ' )" + [ -z "$both" ] && [ "$( nrows h_p2 )" = 5 ]; say $? "[C] h_p2: next_offset=$nxt pages on to 5 new rows, each attributed in its own table" \ + "[C] h_p2: page 2 rows=$( nrows h_p2 ), repeated from page 1: '$both'" + else no "[C] h_p1: next_offset='$nxt' (want 5) or the page-2 call failed"; fi +fi +FIX="$FIXSAVE" + # ── (G) well-formedness and determinism ─────────────────────────────────────────────────────────────────────────────── echo "(G) well-formedness, determinism" if command -v xmllint >/dev/null 2>&1; then - for t in a_cpp a_col a_sd b_cpp d_imp d_rf e_un f_go; do + for t in a_cpp a_col a_sd b_cpp c_get d_imp d_rf e_un e_sp f_go h_p1; do [ -s "$DIR/$t.out" ] || continue xmllint --noout "$DIR/$t.out" 2>/dev/null; say $? "[C] $t: well-formed XML" \ "[C] $t: malformed XML" done - ( cd "$FIX/cpp" && "$BIN" . --no-cache --callers=empty --legend=full ) >"$DIR/g_full.out" 2>/dev/null && xmllint --noout "$DIR/g_full.out" 2>/dev/null; say $? "[C] g_full: --legend=full well-formed" \ - "[C] g_full: --legend=full malformed or failed" + for v in "--callers=empty" "--callees=ParseDefault" "--impact=empty" "--safe-delete=empty" "--path=PutInt,put" "--callers=get" "--callers=empty --format=columnar"; do + # shellcheck disable=SC2086 + ( cd "$FIX/cpp" && "$BIN" . --no-cache $v --legend=full ) >"$DIR/g_full.out" 2>/dev/null && xmllint --noout "$DIR/g_full.out" 2>/dev/null; say $? "[C] g_full: $v --legend=full well-formed" \ + "[C] g_full: $v --legend=full malformed or failed" + done else printf ' SKIP xml well-formedness (no xmllint)\n' fi diff --git a/test/attribmultideffix/c/client.c b/test/attribmultideffix/c/client.c new file mode 100644 index 000000000..c4edb6bf8 --- /dev/null +++ b/test/attribmultideffix/c/client.c @@ -0,0 +1,2 @@ +#include "proto.h" +int LoseIt(void) { return lost(3); } diff --git a/test/attribmultideffix/c/helper_a.c b/test/attribmultideffix/c/helper_a.c new file mode 100644 index 000000000..134e76eec --- /dev/null +++ b/test/attribmultideffix/c/helper_a.c @@ -0,0 +1,3 @@ +/* a static (internal-linkage) definition: no prototype in another file declares it */ +static int helper(int x) { return x + 1; } +int UseA(void) { return helper(1); } diff --git a/test/attribmultideffix/c/helper_b.c b/test/attribmultideffix/c/helper_b.c new file mode 100644 index 000000000..17332e1bb --- /dev/null +++ b/test/attribmultideffix/c/helper_b.c @@ -0,0 +1,3 @@ +/* a same-named prototype in another file: a foreign declaration of the static helper in helper_a.c */ +int helper(int x); +int UseB(void) { return helper(2); } diff --git a/test/attribmultideffix/c/proto.c b/test/attribmultideffix/c/proto.c new file mode 100644 index 000000000..bbbbb01a6 --- /dev/null +++ b/test/attribmultideffix/c/proto.c @@ -0,0 +1,2 @@ +#include "proto.h" +int lost(int c) { return c - 1; } diff --git a/test/attribmultideffix/c/proto.h b/test/attribmultideffix/c/proto.h new file mode 100644 index 000000000..e27722f87 --- /dev/null +++ b/test/attribmultideffix/c/proto.h @@ -0,0 +1,4 @@ +#ifndef PROTO_H +#define PROTO_H +int lost(int c); +#endif diff --git a/test/attribmultideffix/cpp/detail.cc b/test/attribmultideffix/cpp/detail.cc new file mode 100644 index 000000000..3f26700be --- /dev/null +++ b/test/attribmultideffix/cpp/detail.cc @@ -0,0 +1,4 @@ +#include "detail.h" +namespace b { namespace detail { int tally(int x) { return x + 1; } } } +int CountA() { return a::detail::tally(1); } +int CountB() { return b::detail::tally(2); } diff --git a/test/attribmultideffix/cpp/detail.h b/test/attribmultideffix/cpp/detail.h new file mode 100644 index 000000000..4d64413d0 --- /dev/null +++ b/test/attribmultideffix/cpp/detail.h @@ -0,0 +1,3 @@ +#pragma once +// a foreign declaration: same name, same leaf scope `detail`, same parameter types — another scope chain +namespace a { namespace detail { int tally(int x); } } diff --git a/test/attribmultideffix/cpp/std_user.cc b/test/attribmultideffix/cpp/std_user.cc new file mode 100644 index 000000000..6bc1aea2c --- /dev/null +++ b/test/attribmultideffix/cpp/std_user.cc @@ -0,0 +1,3 @@ +// ninja-07 confounder: a std::string member call named like the selector — never a proven row of either empty +#include +bool UsesStd(const std::string& s) { return s.empty(); } diff --git a/test/attribmultideffix/js/shapes.js b/test/attribmultideffix/js/shapes.js new file mode 100644 index 000000000..ceec76ba9 --- /dev/null +++ b/test/attribmultideffix/js/shapes.js @@ -0,0 +1,5 @@ +class Circle { area() { return 3; } } +class Square { area() { return 4; } } +function circleArea() { const c = new Circle(); return c.area(); } +function squareArea() { const s = new Square(); return s.area(); } +module.exports = { circleArea, squareArea }; diff --git a/test/attribmultideffix/rb/shapes.rb b/test/attribmultideffix/rb/shapes.rb new file mode 100644 index 000000000..36e240a96 --- /dev/null +++ b/test/attribmultideffix/rb/shapes.rb @@ -0,0 +1,18 @@ +class Circle + def area + 3 + end +end +class Square + def area + 4 + end +end +def circle_area + c = Circle.new + c.area +end +def square_area + s = Square.new + s.area +end diff --git a/test/attribmultideffix/ts/overloads.ts b/test/attribmultideffix/ts/overloads.ts new file mode 100644 index 000000000..a6385fc0b --- /dev/null +++ b/test/attribmultideffix/ts/overloads.ts @@ -0,0 +1,5 @@ +// overload signatures of DIFFERING arity before one implementation: one function, one definition group +export function pick(a: string): string; +export function pick(a: string, b: number): string; +export function pick(a: string, b?: number): string { return b === undefined ? a : a + b; } +export function usePick(): string { return pick("x") + pick("y", 2); } From d099ec21e08552261bcc50dee88224e5a25bc291 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 12:31:02 -0400 Subject: [PATCH 028/162] feat(for): stop when complete; an explicit --token-budget is a soft target Default --for: the ranked head () is the answer and every row ships. kForPayloadBudgetBytes (7.5 KB) becomes a stair-step target: the trim ladder's text-shrink steps still aim at it, but its row-drop step runs only past a runaway guard (kForRunawayGuardBytes, measured on the same quantity). Rows past the target are disclosed as with a legend clause; budget_bytes= then reads as a target. Hitting the guard is the one default row cut: over_ceiling="1", and a clause naming the guard. Trailing sections are funded as if the rows stopped at the target. The compact route's serves every hop row (D-3), and a capped carries next= in every regime (D-4). Explicit --token-budget / MCP budget_tokens on --for: a soft target over the rows only (never the header), so compact and full serve the same rows: rows in rank order while they fit, rank 1..4 always, the cut continued (next=, next_offset=, next_budget_tokens=). No over_ceiling for it, no header ladder (no legend clause, echo or route= dropped), no paid/unpaid re-renders: the charged-continuation regime is retired. --pack-task, MCP explore and --from-trace take an explicit budget as a soft target too (section cascades unchanged; no ladder, no over_ceiling); their defaults and --partition keep their hard ceilings. --max-tokens, the map, --query and --recall gates are unchanged. Gates: forsoftbudgetcheck green; rule-4 replacements in tokenbudgetcheck #17, budgetpolicycheck (D), forrankordercheck (3)/(9)/(10)/(10c)/(11); estcalib for-budgeted re-pinned with pin.py. Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 59 +-- src/compactlegend.h | 11 +- src/graphlegend.h | 16 + src/main.cpp | 5 +- src/mcp.h | 12 +- src/mcpverbs.h | 91 ++-- src/packtask.h | 42 +- src/serialize.h | 408 +++++++++------- src/tracelocus.h | 27 +- src/verbs_for.h | 955 +++++++------------------------------ test/budgetpolicycheck.sh | 44 +- test/estcalib.manifest | 4 +- test/forrankordercheck.sh | 196 ++++---- test/forsoftbudgetcheck.sh | 23 +- test/tokenbudgetcheck.sh | 54 ++- 15 files changed, 747 insertions(+), 1200 deletions(-) diff --git a/src/cli.h b/src/cli.h index cb6c9acbc..4f8ff80d9 100644 --- a/src/cli.h +++ b/src/cli.h @@ -869,7 +869,7 @@ inline constexpr char kHelpHead[] = " answer is not worth cutting to fit — so the root DISCLOSES the overshoot instead,\n" " carrying over_ceiling=\"1\" beside max_tokens=N whenever est_tokens exceeds N. Reach\n" " for --token-budget=N when the whole DOCUMENT must be bounded; this flag SHAPES.\n" - " --token-budget=N[K|M|G] cap the whole document at N tokens — a hard gate on the map, a trim-to-fit on the task lenses\n" + " --token-budget=N[K|M|G] N tokens: a hard gate on the map, a soft row target on the task lenses\n" " two personalities depending on the verb:\n" " - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's\n" " est_tokens exceeds N. That is the map PLUS every block appended after it\n" @@ -895,17 +895,17 @@ inline constexpr char kHelpHead[] = " 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the\n" " same N can pass or fail differently under --json depending on dialect AND\n" " size — never assume one direction, measure the request you actually gate.\n" - " - --for / --pack-task / --from-trace / --run-trace: SHAPES instead of gating — overrides that\n" - " lens's own default payload budget and trims to fit, always exit 0. --for's\n" - " header reports est_tokens=\"N\" so its fit is checkable; --pack-task/--from-trace\n" - " report their budget ledger in the header report line instead. On --for's auto\n" - " bundle the ceiling is SPLIT, not handed to the signatures first: the sig side's\n" - " claim caps at the default sig budget and the rest flows to the inline bodies,\n" - " so a wider ceiling never serves fewer of them (see --for below).\n" - " Its VERBATIM task echo is bytes no trim can shrink, so past some task length the\n" - " header floor alone exceeds the ceiling: the lens drops the comment's DUPLICATE echo\n" - " first (task_echo: dropped (ceiling); task= keeps the verbatim copy), then labels it\n" - " over_ceiling (--recall: over_ceiling=1) — never a trim it did not actually do.\n" + " - --for / --pack-task / --from-trace / --run-trace: a SOFT TARGET, always exit 0 — rows\n" + " stop once it is reached, never a promise of bytes. --for charges N against its\n" + " ranked rows only (never the header or a disclosure, so compact and full\n" + " serve the same rows): rows are added in rank order while they fit, the rank 1..4\n" + " rows always ship, and the cut names its continuation (). The bodies,\n" + " hops and tail fill from what the rows left. A target wider than the answer serves\n" + " the default answer's rows. --pack-task keeps its section shares (an explicit 6000\n" + " serves the default's sections); --from-trace its section pricing. No lens drops a\n" + " legend clause or the task echo for it, and none labels itself over_ceiling for it:\n" + " est_tokens= may exceed N (budget_tokens= says it is the target asked). --pack-task\n" + " without the flag keeps its hard 6K default (over_ceiling when it cannot fit).\n" " --help-task=TASK describe a task, get ONE recommended command back — or an honest abstention\n" " deterministic enhanced help: recommend ONE executable Ripwire CLI command for this\n" " repository and task, or abstain when evidence/applicability is insufficient. Reports\n" @@ -919,24 +919,28 @@ inline constexpr char kHelpHead[] = " Wrap it in backticks (task text: `F`) or write it in call form (F()) to route on\n" " it by name.\n" " --for=TASK the task lens: say what you are doing, get the signatures and bodies to read first\n" - " the task lens: ranked signatures + metrics framed for reuse. The bundle enforces a\n" + " the task lens: ranked signatures + metrics framed for reuse. STOPS WHEN COMPLETE: the\n" + " ranked head (: the relevance floor within the top 40, or --pack-top-n) is\n" + " the answer and every row of it ships. A ~7.5KB payload TARGET shortens row text toward\n" + " it (tail signatures and doc excerpts first; with S=T\n" + " marks a shrink); rows served past it carry past_target=\"K\" (the last K rows). Only a\n" + " runaway guard (32KB, a bug signal) cuts rows: over_ceiling=\"1\" and serving\n" + " them all (S printed of the T rows handed to the trim). The r=1 (top-ranked)\n" // §B7.5 (CA4): this said — the STRING ENUM the §P8 vocabulary migration // replaced with the boolean capped="1" every other truncating element spells (serialize.h's own // comment records the rename). No output the tool can emit has contained payload= since; --help was // the last place the dead spelling survived, and a reader grepping their bundle for it finds nothing. - " ~7.5KB default payload budget (tail entries trim first; marks it: T rows handed to the trim, S printed). The r=1 (top-ranked)\n" " row carries next=\"--expand=FILE:NAME\" — the one pasteable follow-up, the body\n" " that ends the search (defined here, not in the bundle's own header, which the\n" " token ladder does not charge). An explicit\n" - " --token-budget=N overrides the default at the conservative byte rate\n" - " (SHAPES, exit 0; see --token-budget above) and the header reports the delivered est_tokens.\n" + " --token-budget=N is a soft row target at the conservative byte rate\n" + " (exit 0; see --token-budget above) and the header reports the delivered est_tokens.\n" " TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride\n" " inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance —\n" " whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The \n" " root discloses it: bundle=\"auto\" bodies=\"N\" (bodies=\"0\" reason=\"budget\" when none fit) —\n" - " on EVERY auto-mode run: a ceiling the signatures alone exhaust still carries the attribute\n" - " (legend and empty shell dropped there; only the attribute has reserved bytes), and\n" + " on EVERY auto-mode run: a target the signatures alone exhaust still carries the attribute\n" + " (with its legend and the empty shell), and\n" " --for --json, which serves no bodies by design, says so with \"bundle\":\"sigs\". Only the\n" " caller-chosen postures (--signatures-only, --detail=N) are attribute-free.\n" " ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own\n" @@ -950,16 +954,13 @@ inline constexpr char kHelpHead[] = " A ranked function or method whose whole name is a word of the query (by name, not\n" " resolved) gets the first rows, past that head: up to 3, each listing its\n" " callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=.\n" - " --auto-bodies restores the body walk there. That shape discloses on every run too: a\n" - " ceiling the signatures alone exhaust carries bundle=\"compact\" bodies=\"0\"\n" + " --auto-bodies restores the body walk there. The default serves every hop row. That shape\n" + " discloses on every run too: a target the signatures alone exhaust carries bundle=\"compact\" bodies=\"0\"\n" " reason=\"budget\" — three distinct reasons, never collapsed (compact-route = the route\n" " chose edges, no_candidates = nothing scored, budget = the ceiling was spent).\n" - " An explicit --token-budget=N is a hard ceiling, split so a wider ceiling never buys less:\n" - " the signature side's claim is capped at the DEFAULT ~7.5KB sig budget and every byte beyond\n" - " it flows to the enrichment — at any ceiling at or above the default's effective total the\n" - " block is byte-identical to the default run's, so every body (or hop row) the default\n" - " serves still fits. An explicit --pack-top-n is an explicit SIG posture and keeps the\n" - " whole-ceiling sig claim. --compress composes: the served bodies (auto/anchor and\n" + " An explicit --token-budget=N is a soft target over the rows: rows in rank order\n" + " while they fit N, the bodies or hop rows from what they left; --pack-top-n sets the head\n" + " the target fills. --compress composes: the served bodies (auto/anchor and\n" " --detail=N alike) go through the same comment-strip --expand uses, disclosed as\n" " compress=\"1\" on the element (nothing to strip on the compact route).\n" " RANKING CONFIDENCE, disclosed not scored: the root always carries\n" @@ -1002,8 +1003,8 @@ inline constexpr char kHelpHead[] = " a counted stub by default — this IS the stub's own next= spelling, so pasting it back\n" " unchanged is enough. A comma-separated, order-insensitive, closed set (lego and/or\n" " compose, each named at most once); every other section of the bundle is unaffected —\n" - " the budget is the stubbed answer's. Under an explicit --token-budget\n" - " the restored sections are charged in full, so can carry fewer rows there.\n" + " the budget is the stubbed answer's, under an explicit --token-budget too\n" + " (a soft target charges the rows only).\n" " --signatures-only (with --for) signatures only: no automatic bodies in the bundle\n" " (with --for) opt out of the terminal-by-default bundle: no auto bodies, no bundle=\"auto\"\n" " attribute — the signatures-only lens exactly as before. Contradicts --detail=N (refused\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index bfcb2316b..b6f55246c 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -2333,7 +2333,10 @@ inline void eraseLedgerOverCeilingClause( std::string& doc ) } // Returns true when `original`'s root label was corrected (the caller compacts it again). -inline bool settleOverCeilingLabel( std::string& original, std::string_view compacted ) +// forbudget-soft: `budgetIsSoft` — the caller knows (out of band, never from the document's text, which echoes the task +// verbatim) that this answer's budget_tokens= is a SOFT target the caller passed (the task lenses' explicit budget, owner +// decision 2026-10-14): est_tokens may exceed it by contract, so only max_tokens= is judged here. +inline bool settleOverCeilingLabel( std::string& original, std::string_view compacted, bool budgetIsSoft = false ) { constexpr std::string_view kLabel = " over_ceiling=\"1\""; const CompactRootInfo outRoot = findCompactRoot( compacted ); @@ -2343,7 +2346,7 @@ inline bool settleOverCeilingLabel( std::string& original, std::string_view comp return false; } const std::string_view outOpen = compacted.substr( outRoot.openBegin, outRoot.openEnd - outRoot.openBegin ); - const std::size_t budget = rootUnsignedAttr( outOpen, "budget_tokens" ); + const std::size_t budget = budgetIsSoft ? 0u : rootUnsignedAttr( outOpen, "budget_tokens" ); const std::size_t maxTok = rootUnsignedAttr( outOpen, "max_tokens" ); const std::size_t est = rootUnsignedAttr( outOpen, "est_tokens" ); if( ( budget == 0 && maxTok == 0 ) || est == 0 ) @@ -2400,12 +2403,12 @@ inline bool settleOverCeilingLabel( std::string& original, std::string_view comp return true; } -inline CompactOutcome applyCompactDialect( std::string& doc, std::string_view hint ) +inline CompactOutcome applyCompactDialect( std::string& doc, std::string_view hint, bool budgetIsSoft = false ) { std::string original = doc; const std::size_t pricedBytes = doc.size(); // what the emitter's est_tokens= describes CompactOutcome outcome = applyCompactDialectOnce( doc, hint ); - if( outcome == CompactOutcome::Rewritten && settleOverCeilingLabel( original, doc ) ) + if( outcome == CompactOutcome::Rewritten && settleOverCeilingLabel( original, doc, budgetIsSoft ) ) { doc = original; outcome = applyCompactDialectOnce( doc, hint, pricedBytes ); diff --git a/src/graphlegend.h b/src/graphlegend.h index d2cbddbfc..3be632c73 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -381,6 +381,22 @@ inline constexpr std::string_view kMcpForLensColumnsLegend = // dialects (verbs_for.h, mcpverbs.h) spell the same sentence. inline constexpr std::string_view kForBudgetBytesNote = " [budget_bytes= is the default BYTE ceiling this ranked payload was shaped against; it bounds that payload, not the whole document est_tokens prices]"; +// forbudget-soft: the same attribute on an answer whose ranked head ran PAST it (sigs past_target=). There the number shaped +// the row TEXT (signatures and doc excerpts shortened toward it) and removed no row, so calling it a ceiling would be false. +// Says what it does not mean: not a bound on the rows. No "--" (it rides inside an XML comment, G4). +inline constexpr std::string_view kForBudgetBytesTargetNote = + " [budget_bytes= is the default BYTE target this ranked payload was shaped toward: row text shortens toward it, no row of" + " the ranked head is removed for it (sigs past_target=)]"; +// forbudget-soft: , present only on a block whose ranked head passed the default target. The rows are +// a rank prefix, so "the last K" names them exactly. Not a cut: nothing is missing. +inline constexpr std::string_view kForPastTargetNote = + " [sigs past_target=K: the last K rows are past the budget_bytes target, served because they are rows of the ranked head;" + " none is cut]"; +// forbudget-soft: an EXPLICIT token budget on a task lens is a SOFT target (owner decision 2026-10-14) — the full dialect's +// reading of budget_tokens= (the compact one rides its est_tokens clause). Says what it does NOT promise: a byte bound. +inline constexpr std::string_view kForSoftBudgetNote = + " [budget_tokens=: the soft token target; rows stop once it is reached, the rank 1..4 rows and every disclosure always ship," + " so est_tokens may exceed it]"; // The prose of the two root-relative comments above, as the dictionary quotes it. Asserted to sit inside them, so the // wrapper constants and these cannot drift apart. inline constexpr std::string_view kForRootRelProse = diff --git a/src/main.cpp b/src/main.cpp index 55bab907c..0ab8b9a01 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4074,7 +4074,10 @@ static int finishCompactCapture( const rw::Config& cfg, std::string& doc, int rc }; // the DEFAULT posture shapes what it can and leaves the rest exactly as it was emitted (cfg.legendDefaulted); // only an ASKED --legend=compact refuses an answer the dialect cannot shape - switch( rw::applyCompactDialect( doc, compactLegendHint( cfg, doc ) ) ) + // forbudget-soft: an explicit --token-budget on a task lens is a SOFT target — the settle never labels it "over" + const bool budgetIsSoft = cfg.tokenBudget > 0 + && ( !cfg.forTask.empty() || ( !cfg.packTask.empty() && cfg.partitionCount == 0 ) || !cfg.fromTrace.empty() || !cfg.runTrace.empty() ); + switch( rw::applyCompactDialect( doc, compactLegendHint( cfg, doc ), budgetIsSoft ) ) { case rw::CompactOutcome::Rewritten: case rw::CompactOutcome::AlreadyCompact: diff --git a/src/mcp.h b/src/mcp.h index 481af44a2..87d388968 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1325,7 +1325,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "situational_awareness", pathIsRequired ) + "}," "{\"name\":\"mentions\",\"description\":\"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. " + std::string( kAtSeedRebindClause ) + "\"," + mcprefuse::toolMetadataFor( "mentions", pathIsRequired ) + "}," - "{\"name\":\"for\",\"description\":\"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional ceiling (the CLI --for --token-budget). The header carries route= (which ranker answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + "{\"name\":\"for\",\"description\":\"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional SOFT target over the ranked rows (the CLI --for --token-budget): rows stop once it is reached, never a byte promise; without it the whole ranked head ships. The header carries route= (which ranker answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + mcprefuse::toolMetadataFor( "for", pathIsRequired ) + "}," "{\"name\":\"lego\",\"description\":\"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base name, or file:name to disambiguate; " + std::string( kAtSeedShortClause ) + "\"," + mcprefuse::toolMetadataFor( "lego", pathIsRequired ) + "}," @@ -1368,9 +1368,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // L4 — the one-call orientation front door + B11 verb parity. `explore` is // the SAME handler as the CLI --pack-task; the older name `pack_task` still dispatches (tools/call // name=="pack_task" works) but is not separately advertised here — see mcp.h's kMcpVerbTable comment. - "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional (default 6000); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional soft target (sections stop at their shares; the default 6000 stays a hard ceiling); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + mcprefuse::toolMetadataFor( "explore", pathIsRequired ) + "}," - "{\"name\":\"from_trace\",\"description\":\"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional.\"," + "{\"name\":\"from_trace\",\"description\":\"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional (a soft target).\"," + mcprefuse::toolMetadataFor( "from_trace", pathIsRequired ) + "}," "{\"name\":\"edit_check\",\"description\":\"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not a proof: call edges are matched by NAME, so a same-named callee this tool does not index can flag a caller that never touches the edited symbol at all, and a clean tree can carry a nonzero incompatible= with nothing edited. status is one of unchanged / new-symbol / contract-change, and callers= is itself a FLOOR — 'no incompatible caller' is not proof of safety either. Fast and targeted; for the same question over a WHOLE diff use quality_delta. symbol = the def name (file:name to disambiguate); at= names the commit compared against. " + std::string( kAtSeedShortClause ) + " limit/offset page the unflagged caller rows only. PRE-APPLY PREVIEW: pass new_body to ask the same question about a replacement that has NOT been written — it is spliced in memory, re-parsed, and the answer carries preview=1. Nothing is written; a payload that does not parse, or does not define the symbol, is refused.\"," + mcprefuse::toolMetadataFor( "edit_check", pathIsRequired ) + "}," @@ -1673,7 +1673,11 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo if( legendCompactPosture ) { compacted = text; - rw::applyCompactDialect( compacted, mcpCompactLegendHint( name ) ); + // forbudget-soft: a caller's budget_tokens on a task lens is a SOFT target — never labelled "over" by the settle + const bool budgetIsSoft = budgetArg.isPresent + && ( name == "for" || name == "from_trace" + || ( ( name == "explore" || name == "pack_task" ) && partitionCount == 0 ) ); + rw::applyCompactDialect( compacted, mcpCompactLegendHint( name ), budgetIsSoft ); body = &compacted; } if( legendRefPosture ) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f3e720b51..188c1520c 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1857,7 +1857,10 @@ inline void spliceForLegoCountLegend( std::string& doc, bool carries ) } } -inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) +// forbudget-soft: budget_tokens= is a SOFT target here too (the CLI twin's rule) — est_tokens may exceed it and nothing is +// labelled "over" for it. The one over_ceiling this surface can carry is the runaway guard's (`guardOver`: the default head +// passed kForRunawayGuardBytes); its reading (forRunawayGuardNote) was spliced with the clauses by the assembly. +inline void priceForTaskRoot( std::string& doc, bool guardOver ) { if( doc.empty() ) { @@ -1865,14 +1868,9 @@ inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) } std::size_t estTokens = 0; std::string rootAttrs = rw::pricedRootAttr( doc.size(), rw::kBytesPerTokenDefault, 0, &estTokens ); - if( budgetTokens > 0 && estTokens > budgetTokens ) + if( guardOver ) { - const std::size_t legendEnd = doc.find( " -->" ); - if( legendEnd != std::string::npos ) - { - doc.insert( legendEnd, rw::kOverCeilingLegend ); - rootAttrs = rw::pricedRootAttr( doc.size(), rw::kBytesPerTokenDefault, 0, &estTokens ); - } + rootAttrs = rw::pricedRootAttr( doc.size() + sizeof( " over_ceiling=\"1\"" ) - 1, rw::kBytesPerTokenDefault, 0, &estTokens ); rootAttrs += " over_ceiling=\"1\""; } rw::spliceRootAttrs( doc, rootAttrs ); @@ -1898,6 +1896,7 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref const std::vector& impure; RedactCounts* redact; std::size_t sigsBudget; + std::size_t rowDropBudget; // forbudget-soft: where step F aims (the guard / the soft target) std::size_t fixedBytes; std::size_t budgetTokens; const notes::NoteIndex& noteIndex; @@ -1913,7 +1912,7 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref // decided once in forTaskText (the noRoute/task/budget verdicts live there) const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render }; -inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFromRows, std::string headerStr, std::string legoStr, +inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::string headerStr, std::string legoStr, std::string composeStr, std::vector> legoScoped, std::size_t legoPreCapCount ) { const IngestResult& ing = in.ing; @@ -1942,9 +1941,9 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr { return McpForAssembly{}; // the answer buffer could not be opened: an internal error, never "not found" } + // forbudget-soft: exempt in every regime (no byte promise to pay into: a target + the runaway guard, or a soft target) const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, - /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false, - /*ledgerGapBytes=*/0u, payFromRows }; + /*json=*/false, /*pasteHandle=*/false }; // A2 (survey card, 2026-09-03): sigs render into their OWN buffer (rather than streaming straight into // `mem` the way this call used to) so droppedPositive is known BEFORE headerStr is written — headerStr, // once flushed to `mem`, cannot be edited retroactively (the same reason the CLI twin's degrade path @@ -1953,6 +1952,8 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation + mcpSigsCut.rowDropBudgetBytes = in.rowDropBudget; // forbudget-soft: IN — the CLI twin's two budgets + mcpSigsCut.pastTargetDisclosed = budgetTokens == 0; std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) { @@ -2004,7 +2005,18 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr const std::size_t closeAt = headerStr.rfind( " -->" ); if( closeAt != std::string::npos ) { - headerStr.insert( closeAt, rw::kForBudgetBytesNote ); + // forbudget-soft: "target" where the head ran past it (the CLI twin's choice) + headerStr.insert( closeAt, mcpSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ); + } + } + // forbudget-soft: the runaway guard's reading, where it cut the default head (the CLI twin's clause; priceForTaskRoot puts + // over_ceiling="1" on the root) + if( mcpSigsCut.rowDropCut && budgetTokens == 0 ) + { + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, rw::forRunawayGuardNote() ); } } // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped, docs_after_code=, the continuation's) — @@ -2098,7 +2110,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render // above may have changed the section's size since forTaskText charged the budget, and the invariant below is the // two planners' agreement on one input: the served stub never exceeds the stub the charge plan collapses it to. - const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, budgetTokens > 0, task, mcpLegoServedIn, mcpComposeServedIn ); + const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, /*explicitCeiling=*/false, task, mcpLegoServedIn, mcpComposeServedIn ); const bool mcpLegoWillStub = plan.legoWillStub; const bool mcpComposeWillStub = plan.composeWillStub; // postcondition of the rule itself, checked BEFORE the swap below discards the original size: @@ -2132,7 +2144,8 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFr { std::vector tailEsc; const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), - kForFileTailShownCap, tailEsc ); + kForFileTailShownCap, tailEsc, + nextAttrXml( forWidenNext( task ) ) ); // forbudget-soft D-4: the cap's next= std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } rw::emitRaw( mem, "" ); @@ -2517,6 +2530,7 @@ inline std::optional forTaskText( const std::string& root, const st + std::string( mcpIdRouteParts.route ) + std::string( rw::kMcpForBundleSigsLegend ) + std::string( mcpForConf.note ) + + ( budgetTokens > 0 ? std::string( rw::kForSoftBudgetNote ) : std::string() ) // forbudget-soft: budget_tokens= is a soft target // No "--" anywhere in this clause: it rides inside an XML comment, where a double // hyphen is ill-formed (G4), so the CLI verb is named without its dashes. + std::string( rw::kMcpForLensColumnsLegend ) @@ -2578,12 +2592,15 @@ inline std::optional forTaskText( const std::string& root, const st .renderedBytes = legoStr.size() }; const ForStubbableSection mcpComposeChargeIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, .renderedBytes = composeStr.size() }; - const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, budgetTokens > 0, task, mcpLegoChargeIn, mcpComposeChargeIn ); + const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, /*explicitCeiling=*/false, task, mcpLegoChargeIn, mcpComposeChargeIn ); const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - mcpEndLineExemptBytes - rw::forZeroNoteBytes( headerStr ) // lean-answers: the zero reading never costs a row + mcpSigChargePlan.servedBytes( mcpLegoChargeIn, mcpComposeChargeIn ) + routeStr.size() + 6; // + "" - const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" + // forbudget-soft: the CLI twin's two budgets (rw::forSigsBudgets) — the default target + the runaway guard, or a soft target + // over the rows (one dialect here, so the soft tier's header is this one) + const ForSigsBudgets mcpSigsBudgets = forSigsBudgets( budgetTokens > 0 ? forBudgetBytes : 0u, fixedBytes, fixedBytes, /*tierAtDefault=*/false ); // no bodies ride here + const std::size_t sigsBudget = mcpSigsBudgets.target; // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO CLI argv here — an // MCP client continues by re-calling the tool: the machine form next_budget_tokens=T (re-call `for` with budget_tokens=T) @@ -2595,54 +2612,21 @@ inline std::optional forTaskText( const std::string& root, const st const notes::NoteIndex* const notesPtr = noteIndex.empty() ? nullptr : ¬eIndex; // knob-honesty-068 (orchestrator ruling C3): the assembly is one pass (assembleMcpFor above), run once or twice below. - const McpForAssemblyInputs assemblyIn{ ing, lensRank, forTopN, fanIn, impure, redact, sigsBudget, fixedBytes, budgetTokens, + const McpForAssemblyInputs assemblyIn{ ing, lensRank, forTopN, fanIn, impure, redact, sigsBudget, mcpSigsBudgets.rowDrop, fixedBytes, budgetTokens, noteIndex, notesPtr, flRootArg, task, mcpTopRowNext, mcpForHdrRows, routeStr, composePreCapCount, sections, rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) && rw::docsAfterCodeFitsCeiling( budgetTokens, /*bodyCeiling=*/false ), rw::endLinesFitCeiling( budgetTokens, /*bodyCeiling=*/false ) ), // LB-A, e=, docs reorder ix.g.implementors }; - // C3 (completed 2026-10-08) — drop a row to pay for the handle ONLY where dropping it is what makes the answer fit (the CLI - // twin's rule, runForLens). The UNPAID pass runs first: every row the cut leaves + the handle; when it fits (or nothing is - // capped under a budget) it is served and nothing is paid. Only when it lands past budget_tokens does the PAID pass run, and - // it is served only when IT fits — paying is what made it fit. Otherwise (this surface overshoots on its own: the header - // bytes its ledger exempts) the unpaid answer is served: the cut's rows, the handle riding unpaid, over_ceiling="1" - // (priceForTaskRoot). Both passes share the one ranking above; only the assembly re-runs. - const auto estOf = []( std::size_t docBytes ) - { - std::size_t est = 0; - (void)rw::pricedRootAttr( docBytes, rw::kBytesPerTokenDefault, 0, &est ); // priceForTaskRoot's own reading - return est; - }; - const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; - McpForAssembly chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + // forbudget-soft: ONE assembly (the C3 unpaid/paid passes went with the hard ceiling they priced against) + McpForAssembly chosen = assembleMcpFor( assemblyIn, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); if( !chosen.doc ) { return std::nullopt; } - if( budgetTokens > 0 && chosen.cut.hasContinuation && estOf( chosen.doc->size() ) > budgetTokens ) - { - const std::optional tallyUnpaid = redact != nullptr ? std::optional( *redact ) : std::nullopt; - if( tallyAtEntry ) - { - *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served - } - McpForAssembly paid = assembleMcpFor( assemblyIn, /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); - if( !paid.doc ) - { - return std::nullopt; - } - if( estOf( paid.doc->size() ) <= budgetTokens ) - { - chosen = std::move( paid ); - } - else if( tallyUnpaid ) - { - *redact = *tallyUnpaid; // the unpaid answer is served: its tally - } - } std::string out = std::move( *chosen.doc ); - priceForTaskRoot( out, budgetTokens ); // R1: est_tokens=, and over_ceiling="1" when it exceeds budgetTokens + priceForTaskRoot( out, chosen.cut.rowDropCut && budgetTokens == 0 ); // R1: est_tokens=; over_ceiling="1" only past the runaway guard return out; } @@ -4542,6 +4526,7 @@ inline std::string packTaskText( const std::string& root, const std::string& tas PackTaskInputs in; in.budgetTokens = budgetTokens; + in.softTarget = budgetTokens > 0 && !( partitionCount >= packpartition::kMinPartitions && partitionCount <= packpartition::kMaxPartitions ); // forbudget-soft: the CLI twin's rule in.fanIn = &fanIn; in.impure = &impure; in.redact = redact; diff --git a/src/packtask.h b/src/packtask.h index edf6e1757..527ca49e9 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -177,6 +177,7 @@ struct PackTaskHeaderParts // must carry the SAME root as the pre-built rootOpenStr did. std::string_view runClause; // M21(b)/E1: testmap.h's run=/run_unknown=/ clause — ROWS-GATED (empty when // the tests section kept no row), so a bundle that omits tests pays nothing + bool softBudget = false; // forbudget-soft: the caller's own budget — a soft target (kPackTaskSoftBudgetSentence) }; // P10 (L7): the bundle legend's BODY, one constant, spliced by the standalone header (packTaskHeaderText) and @@ -242,6 +243,17 @@ inline constexpr const char* kPackTaskBundleLegendBody = // F2: over_ceiling= is now the property those two numbers state, on every rung — not only the // last-rung case the old wording described. Same length class, one clause, unconditional as before. "budget_tokens= is the token target; over_ceiling= is 1 when est_tokens exceeds it (the bundle is then complete, not trimmed). "; +// forbudget-soft (owner decision 2026-10-14): the reading of budget_tokens= on a bundle whose budget the CALLER passed — a SOFT +// target. The section shares are the default's own cascade (an explicit 6000 serves the default's sections byte for byte), but +// nothing is labelled over_ceiling and the header (task echo, route=) is never dropped for it. Replaces the legend body's last +// sentence on such a bundle (packTaskHeaderText); the default (hard) bundle and the partitioned document keep that sentence. +inline constexpr std::string_view kPackTaskHardBudgetSentence = + "budget_tokens= is the token target; over_ceiling= is 1 when est_tokens exceeds it (the bundle is then complete, not trimmed). "; +inline constexpr std::string_view kPackTaskSoftBudgetSentence = + "budget_tokens= is the soft token target asked: each section stops once its share is reached; est_tokens may exceed it " + "(the header always ships; no over_ceiling). "; +static_assert( std::string_view( kPackTaskBundleLegendBody ).ends_with( kPackTaskHardBudgetSentence ), + "the soft bundle swaps exactly the legend body's last sentence" ); // r4/r6 (2026-09-10 lift-disclosure round): append the two experimental lift notes' JSON keys — same // absent-unless-present convention as the mention/boost/doc_mention keys beside them. Factored out (unlike @@ -293,7 +305,18 @@ inline std::string packTaskHeaderText( const PackTaskHeaderParts& p, bool withRo h.append( p.sibliftNote ); h.append( p.expandNote ); h.append( p.docMentionNote ); - h += kPackTaskBundleLegendBody; // P10 (L7): the body is ONE constant — the partitioned document states it once for all slices + // P10 (L7): the body is ONE constant — the partitioned document states it once for all slices. forbudget-soft: a soft + // target's bundle swaps the body's last sentence (the hard budget's reading) for the soft one. + const std::string_view body( kPackTaskBundleLegendBody ); + if( p.softBudget ) + { + h.append( body.substr( 0, body.size() - kPackTaskHardBudgetSentence.size() ) ); + h.append( kPackTaskSoftBudgetSentence ); + } + else + { + h.append( body ); + } h.append( p.runClause ); // M21(b)/E1: the row's run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated h.append( p.report ); h.append( extraNotes ); @@ -549,6 +572,12 @@ struct PackTaskInputs // its own local; nullptr ⇒ a render failure is still returned as empty text, just not recorded. PackTaskRenderFaults* renderFaults = nullptr; + // forbudget-soft (owner decision 2026-10-14): the caller passed this budget (CLI --pack-task --token-budget, MCP explore + // budget_tokens) — a SOFT target. The section cascade is the default's, unchanged; the ceiling ladder does not run (the + // task echo and route= always ship) and nothing is labelled over_ceiling. false: the default budget (kPackTaskDefaultTokens) + // and the --partition slices, which keep the hard ceiling (packtaskcheck 4b, partitioncheck 5). + bool softTarget = false; + // L1 fix round (rv-r1-L1 MED-4): the run's legend posture is compact, so the ceiling ladder's rungs (drop the task echo, // then route=, then label over_ceiling) are judged on the bytes the compact layer DELIVERS — the task echo lives in the // prose the layer strips, and dropping it for a full-dialect price cut a fact the compact answer had room for. The @@ -1998,7 +2027,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // ceiling (the silence-means-nothing-happened convention route/mention/boost use above), never "not // measured". Measured on the FINISHED document, so the key's own bytes are part of what it reports on. constexpr std::size_t kOverCeilingKeyBytes = 22; // `,"over_ceiling":true` + the closing brace - if( j.size() + kOverCeilingKeyBytes > rw::ceilingAllowanceBytes( budgetTokens ) ) + if( !in.softTarget && j.size() + kOverCeilingKeyBytes > rw::ceilingAllowanceBytes( budgetTokens ) ) // forbudget-soft: never on a soft target { j += ",\"over_ceiling\":true"; } @@ -2082,7 +2111,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, const std::string runClauseStr = rw::runHintClauseIfRows( tests.kept, rw::runsAreRootRelative( ing, in.rootArg ) ); // the ONE gate: the section's own kept count const PackTaskHeaderParts headerParts{ task, rootOpenStr, taskNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, report, droppedPositiveAttr, in.rootArg, - runClauseStr }; + runClauseStr, /*softBudget=*/in.softTarget && !in.innerBundle }; const auto buildHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { if( in.innerBundle ) // P10 (L7): a partition slice — the outer legend speaks once for all of them @@ -2146,7 +2175,8 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, std::size_t estTokens = 0; std::string attrs = rw::pricedRootAttr( markupBytes, rw::kBytesPerTokenDefault, bodiesStr.size(), &estTokens ); attrs += " budget_tokens=\"" + std::to_string( budgetTokens ) + "\""; - if( lastRungFired || ( budgetTokens > 0 && estTokens > budgetTokens ) ) { attrs += " over_ceiling=\"1\""; } + // forbudget-soft: a soft target (the caller's own budget) is never "over"; the default budget keeps the label + if( !in.softTarget && ( lastRungFired || ( budgetTokens > 0 && estTokens > budgetTokens ) ) ) { attrs += " over_ceiling=\"1\""; } if( in.isTrailingUncharged ) { attrs += " est_measured=\"0\""; } // defined by the comment spliced in below return attrs; }; @@ -2168,7 +2198,9 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // judging the as-built header at the exact ceiling would take a rung whose note claims a drop the reader cannot // see. Under the compact posture the as-built answer stands when it fits the allowance; route= (a delivered root // attribute) and the over_ceiling rung keep their meaning (compactlegendcheck (FX4)). - const rw::CeilingLadderChoice chosen = in.compactLegend && !in.innerBundle + // forbudget-soft: a soft target climbs no ladder — the header as built ships (echo, route= and every clause) + const rw::CeilingLadderChoice chosen = in.softTarget ? rw::CeilingLadderChoice{ headerStr, rw::CeilingRung::AsBuilt } + : in.compactLegend && !in.innerBundle ? rw::climbCeilingLadderBy( buildHeader, headerStr, deliveredFits( rw::ceilingAllowanceBytes( budgetTokens ) ), deliveredFits( rw::ceilingAllowanceBytes( budgetTokens ) ), /*hasRouteAttr=*/!lr.routeNote.empty(), kNotes ) : climbCeilingLadder( buildHeader, headerStr, diff --git a/src/serialize.h b/src/serialize.h index 830ed426e..c3840a358 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -36,7 +36,6 @@ #include #include // §H7 degrade seam: std::getenv for the non-release fault switch #include -#include // knob-honesty-068: the resume index's widest digit count (sigsCutContinuationReserve) #include #include #include // back_inserter (reorderDocsAfterCode) @@ -889,18 +888,6 @@ inline constexpr std::string_view kForSigsNextBudgetNote = // (rv-knob-honesty-068 N2). No consumer is added in this lane. inline constexpr std::string_view kForSigsNextOffsetNote = " [sigs next_offset=: the candidate index the cut starts at; no consumer yet (the for offset pages files; next= re-runs the list)]"; -// knob-honesty-068 (orchestrator rulings 2026-10-07, PROCESS rule 5: the recovery handle ALWAYS ships): a charged pays -// for its next= in rows ONLY where that makes the answer fit. Where it cannot — the rank 1..4 floor has nothing left to give, -// or the answer lands past its ceiling paid or not (dropping rows does not make it fit) — no row is dropped for it: the -// handle rides unpaid on the rows the cut alone left, and when the document lands past its ceiling over_ceiling="1" says so -// and this clause says why. Not "est_tokens exceeds budget_tokens": on a --detail bundle the bodies price at the body rate, -// so the byte allowance (budget_tokens x 2.36 x 1.15) can be the ceiling crossed. -// NO ANGLE BRACKETS (CHECKLIST 17): the clause rides the header legend, BEFORE the real tag, so a literal sigs tag spelled -// here satisfied every "the sigs block is intact" grep and moved sigsblock()'s span start into this comment on exactly the -// documents that carry it (rv-knob-honesty-068 C2). -inline constexpr std::string_view kForSigsUnpaidOverCeilingNote = - " [over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]"; - // The clauses a cut report owes, concatenated in a fixed order ("" when it owes none) — defined below SigsCutReport // (sigsCutReportLegend), because the continuation's clauses read the report itself. @@ -923,13 +910,84 @@ inline constexpr std::string_view kForSigsUnpaidOverCeilingNote = // deterministic, query-independent, and self-announcing (capped="1" on ). An EXPLICIT // --token-budget=N overrides the default (N tokens × the conservative byte rate), so a caller who asks // for a bigger (or smaller) bundle beats the default — but in AUTO-BUNDLE mode the sig side's claim on -// that wider ceiling is capped at THIS default budget (main.cpp sigSideCeiling; classb-bytes-memo §2's +// that wider ceiling is capped at THIS default budget (main.cpp sigSideCeiling, retired by forbudget-soft; classb-bytes-memo §2's // N=8000 trap: an uncapped claim re-inflated the trimmed tail and crowded the auto bodies out), so the // excess buys bodies, not sig tail. An explicit --pack-top-n keeps the whole-ceiling sig claim, and // --signatures-only / --detail=N / --json (no auto bodies to reserve for) are likewise uncapped. +// +// STOP WHEN COMPLETE (forbudget-soft, owner decisions 2026-10-14; PROCESS rule 5). The default bundle's ranked head (: the relevance floor intersected with kForLensDefaultTopN or --pack-top-n) IS the answer, so this budget is a +// STAIR-STEP TARGET, not a ceiling: steps A..E above still shrink row TEXT against it (an answer that fit before is +// byte-identical), but step F — the only step that removes a row — runs against kForRunawayGuardBytes below. The rows served +// past the target are disclosed on the block (: the last K rows are the ones F would have dropped +// here) and the budget_bytes= clause calls this number a target on such an answer. An EXPLICIT --token-budget is a SOFT +// target over the rows alone (forSigsBudgets below); the text above about its frozen sig share describes the +// legacy hard-ceiling regime it replaced. inline constexpr std::size_t kForPayloadBudgetBytes = 7500; inline constexpr std::size_t kForCapTailSigBytes = 96; +// ── THE RUNAWAY GUARD (forbudget-soft; PROCESS rule 5's role (a)) ──────────────────────────────────────────────── +// The one default-regime row cut. Measured on the same quantity the 7.5 KB target measures (the charged header + the charged +// sections + ), far above a typical answer: hitting it is a BUG SIGNAL (a recursion, a dump, a head far wider than any +// question needs), never a budget. A default answer that hits it discloses over_ceiling="1" on the root, (the continuation serves every row: an explicit target is never cut by this guard, see max(guard, T)) and +// kForRunawayGuardNote. The value is set by the pre-registered rule (forbudget-soft PLAN §G): at least 4 x the target and +// 2 x the largest complete default payload measured on the round-2 --for argv, verified with zero hits on the round-4 and +// capsweep argv. Gate: test/forsoftbudgetcheck.sh (D4, D4b, NX2). +inline constexpr std::size_t kForRunawayGuardBytes = 32000; +static_assert( kForRunawayGuardBytes >= 4 * kForPayloadBudgetBytes, "the runaway guard sits far above the default target" ); +// The reading spliced on an answer that hit the guard (and only there). It names the value so a reader (and the gate) reads the +// guard from the answer itself. "A bug signal" is true of a default-size head; an explicitly wide --pack-top-n head is named +// as the other way to reach it. No "--" (it rides inside an XML comment, G4). +inline std::string forRunawayGuardNote() +{ + return " [over_ceiling=1: the ranked head passed the runaway guard of " + std::to_string( kForRunawayGuardBytes ) + + " B (header plus ranked payload), so the rows past it are cut and sigs next= serves them all; on a default-size head" + " this is a bug signal, not a budget (a wide pack-top-n head can reach it too)]"; +} + +// ── THE TWO BUDGETS (forbudget-soft, owner decisions 2026-10-14; gate: forsoftbudgetcheck) ────────────────────── +// `target`: what the ladder's text-shrink steps A..E aim at (serialize.h trimSigLadder's effectiveBudget). `rowDrop`: where +// its row-drop step F aims (SigsCutReport::rowDropBudgetBytes). Both in the block's own bytes. +// • DEFAULT (no --token-budget, softRowBytes == 0): the target is kForPayloadBudgetBytes' sig share (the bundle's charged +// header + sections subtracted), exactly as before, so an answer that fit is byte-identical; rows drop only past the +// runaway guard's share (kForRunawayGuardBytes, the same quantity) — the whole ranked head is the answer. +// • SOFT TARGET (an explicit --token-budget / MCP budget_tokens: T = softRowBytes): T is charged against the ROWS inside +// only — never the header or a disclosure — so the compact and full dialects serve the same rows. Rows drop, +// tail-first, until they fit T (+ the "" wrapper); rank 1..4 always ship. Rows are therefore a rank prefix, +// monotone in T, and the next row would cross T. The text tier (what A..E aim at) is T itself — a wide target serves the +// rows uncut, which is what the cut's own next= (--signatures-only at a wide target) promises — EXCEPT on the default +// auto bundle (`tierAtDefault`: bodies or hops ride after the rows and no --pack-top-n names a sig posture), where it is +// the DEFAULT target's share as the full dialect's header prices it (`fullFixedBytes`) when T is wider: there a wider +// target must not re-inflate row text at the bodies' expense (classb-bytes-memo §2's N=8000 trap), so the rows shrink +// as the default's do and a target wider than the complete answer serves the default answer's rows at its tier. +// This replaced the auto bundle's frozen sig share and the trailing-section reserve of the hard-ceiling regime (classb-bytes- +// memo §2, lego-charge fix round 4): with no byte promise there is nothing for either to protect — the trailing sections take +// what the rows leave of T, in the default answer's priority order. +struct ForSigsBudgets +{ + std::size_t target = 1; // >= 1: 0 means "no budget" to packSignatures + std::size_t rowDrop = 0; // > target whenever a soft target or the guard is in force +}; +inline ForSigsBudgets forSigsBudgets( std::size_t softRowBytes, std::size_t fixedBytes, std::size_t fullFixedBytes, bool tierAtDefault ) noexcept +{ + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "": the block's own frame, not a row + const auto share = []( std::size_t budget, std::size_t fixed ) { return budget > fixed ? budget - fixed : std::size_t( 1 ); }; + ForSigsBudgets b; + if( softRowBytes > 0 ) + { + b.target = tierAtDefault ? std::min( softRowBytes, share( kForPayloadBudgetBytes, fullFixedBytes ) ) : softRowBytes; + b.rowDrop = softRowBytes + kSigsWrapperBytes; + } + else + { + b.target = share( kForPayloadBudgetBytes, fixedBytes ); + b.rowDrop = std::max( share( kForRunawayGuardBytes, fixedBytes ), b.target + 1 ); + } + ENSURES( b.target >= 1 && b.rowDrop > b.target, "forSigsBudgets: the row-drop budget sits above the text target" ); + return b; +} + // ── THE --for LENS'S RANKED-HEAD CAP, OWNED ONCE (round-4 finding F-03) ────────────────────────────── // How many ranked symbols the --for lens considers before the relevance floor narrows it and the ladder // above trims it. The CLI lens (`--pack-top-n`, else this) and the MCP `for` verb MUST read the same @@ -950,12 +1008,11 @@ inline constexpr int kForLensDefaultTopN = 40; // the DEFAULT gets richer rather than a richer verb staying unreachable. This allowance is the body // budget the DEFAULT bundle gains ON TOP of kForPayloadBudgetBytes (~1.6K tokens at kBytesPerTokenBody): // the signature budget above is untouched, so the ranked map is byte-identical to the signatures-only -// run and the bodies ride only on this disclosed extra. An EXPLICIT --token-budget=N is a hard ceiling — -// no allowance; bodies take the budget the signature bundle left over, where the signature side's claim -// is capped at kForPayloadBudgetBytes (main.cpp sigSideCeiling — the forbudgetmonotoncheck invariant: a -// wider ceiling never serves less decisive content; at any ceiling >= kForPayloadBudgetBytes + -// kForAutoBodyBudgetBytes the sig render equals the default regime's, so the bodies' leftover is -// provably >= the default's and every body the default serves still fits). Disclosure is on +// run and the bodies ride only on this disclosed extra. An EXPLICIT --token-budget=N is a soft target over +// the rows (forbudget-soft) — no allowance; bodies take what the rows left of N, the rows shrunk at +// the default's tier on this bundle (forSigsBudgets tierAtDefault), so a wide N never re-inflates row text at +// the bodies' expense (the forbudgetmonotoncheck invariant: a wider target never serves less decisive +// content). Disclosure is on // the root (bundle="auto" bodies="N", reason="budget" when none fit) plus a legend sentence; // --signatures-only opts out byte-identically; --detail=N (the explicit knob) supersedes the automatic // selection. Candidate cap: kPackTaskBodyCandidates — the default --for converges on the pack-task shape. @@ -1101,12 +1158,14 @@ inline std::string renderFileTailXml( const FileTail& t, std::size_t shownCap, s return x; } -// The JSON twin: `"tail":{"total":T,"shown":K,"files":[…]}` (the caller supplies the leading comma). -inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap ) +// The JSON twin: `"tail":{"total":T,"shown":K,"files":[…]}` (the caller supplies the leading comma). forbudget-soft (D-4): a +// capped tail carries `"next":"…"` — the XML twin's next= (the file page that lists what it left out) — before its files. +inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap, std::string_view cutNextInvocation = {} ) { const std::size_t shown = std::min( shownCap, t.paths.size() ); std::string j = "\"tail\":{\"total\":" + std::to_string( t.total ) - + ",\"shown\":" + std::to_string( shown ) + ",\"files\":["; + + ",\"shown\":" + std::to_string( shown ) + + ( shown < t.total ? nextFieldJson( cutNextInvocation ) : std::string() ) + ",\"files\":["; for( std::size_t i = 0; i < shown; ++i ) { if( i > 0 ) @@ -4912,10 +4971,24 @@ inline std::size_t droppedPositiveCount( std::size_t candidatePositives, std::si // so subtract the old cost first and add the new one — never form `before - after` (it can be negative, // i.e. unsigned-overflow UB under G1's -fsanitize=integer). `total >= before` always holds (before is a // summand of total). +// +// THE TARGET AND THE ROW-DROP BUDGET (forbudget-soft). Steps A..E aim at `effectiveBudget` (the TARGET: they shorten row text, +// no row goes). Step F — the only step that removes a row — runs against `dropBudget` (>= the target; equal to it in the +// classic hard-ceiling call below). When it is wider, the rows F WOULD have dropped at the target but keeps are counted +// (a dry run over the same tail walk, so they are exactly the last rows kept: pastTargetRows) and `targetCutTotal` is the +// block total that cut would have left — the caller's trailing sections are funded as if the block stopped there. +struct SigLadderOutcome +{ + std::size_t pastTargetRows = 0; // rows F would have dropped at the target and kept (the --for default's past_target=) + std::size_t droppedRows = 0; // rows F dropped against dropBudget + std::size_t targetCutTotal = 0; // the block total after F at the target (== total when nothing ran past it) +}; template -inline void trimSigLadder( std::vector& entries, std::vector& files, - std::size_t& total, std::size_t effectiveBudget, CostFn entryCost ) +inline SigLadderOutcome trimSigLadder( std::vector& entries, std::vector& files, + std::size_t& total, std::size_t effectiveBudget, CostFn entryCost, std::size_t dropBudget ) { + EXPECTS( dropBudget >= effectiveBudget ); // the row-drop budget is never tighter than the target the shrink steps aim at + SigLadderOutcome out; const auto fits = [ & ] { return total <= effectiveBudget; }; const auto shrinkSig = [ & ]( EntryT& e, std::size_t cap ) { total -= entryCost( e ); truncateUtf8WithEllipsis( e.sig, cap ); total += entryCost( e ); }; @@ -4963,7 +5036,34 @@ inline void trimSigLadder( std::vector& entries, std::vector& fil // file-major form ("tail file first, source order inside it") could drop a rank-5 row in the last file // while a rank-39 row in the first file survived. A file's wrapBytes (its file notes now — the // wrapper is gone) go with the file's last live row, exactly as before. - for( std::size_t k = entries.size(); k > 0 && !fits(); ) + // F's dry run at the TARGET (only when the row-drop budget is wider): the same walk, on a copy of the live counts, so the + // rows it names are exactly the tail rows the real walk below keeps past the target. + std::size_t wouldDrop = 0; + out.targetCutTotal = total; + if( dropBudget > effectiveBudget && !fits() ) + { + std::vector live( files.size() ); + for( std::size_t i = 0; i < files.size(); ++i ) + { + live[ i ] = files[ i ].liveCount; + } + for( std::size_t k = entries.size(); k > 0 && out.targetCutTotal > effectiveBudget; ) + { + const EntryT& e = entries[ --k ]; + if( e.dropped || e.globalRank <= 4 ) + { + continue; + } + out.targetCutTotal -= entryCost( e ); + ++wouldDrop; + if( --live[ e.fileSlot ] == 0 ) + { + out.targetCutTotal -= files[ e.fileSlot ].wrapBytes; + } + } + } + const auto fitsDrop = [ & ] { return total <= dropBudget; }; + for( std::size_t k = entries.size(); k > 0 && !fitsDrop(); ) { EntryT& e = entries[ --k ]; if( e.dropped || e.globalRank <= 4 ) @@ -4972,12 +5072,21 @@ inline void trimSigLadder( std::vector& entries, std::vector& fil } total -= entryCost( e ); e.dropped = true; + ++out.droppedRows; FileT& sf = files[ e.fileSlot ]; if( --sf.liveCount == 0 ) { total -= sf.wrapBytes; // the file's notes go with its last live row } } + if( dropBudget == effectiveBudget ) + { + out.targetCutTotal = total; + } + // the guard's cut is a tail of the target's (same walk, wider stop), so it never drops a row the target kept + ASSUME( out.droppedRows <= wouldDrop || dropBudget == effectiveBudget ); + out.pastTargetRows = wouldDrop > out.droppedRows ? wouldDrop - out.droppedRows : 0u; + return out; } // ── THE ROW GATE (`budgetBytes`, --pack-budget-bytes), RANK FIRST (cut-fix lane A, 2026-09-23) ───────────── @@ -5050,6 +5159,16 @@ struct SigsCutReport // the report — carried here rather than as one more defaulted trailing parameter, so a train merge that adds another // defaulted parameter to these 20+-parameter signatures cannot silently bind a value to the wrong one. nullptr ⇒ none. const SigsCutContinuation* continuationRequest = nullptr; + // IN (forbudget-soft): where the ladder's row-drop step F aims. 0 = at the payload budget, the steps A..E target (the hard + // ceiling every caller had before). Nonzero = F runs against THIS budget (>= the payload budget; same units, the block's + // own bytes): the payload budget stays the TARGET the text-shrink steps aim at, and rows past it are kept until this one. + // The --for default passes the runaway guard's share here; an explicit soft target passes its row bytes T. + std::size_t rowDropBudgetBytes = 0; + bool pastTargetDisclosed = false; // IN: print past_target= (the default regime's target); the soft regime does not + std::size_t pastTarget = 0; // OUT: rows step F would have dropped at the payload budget and kept (past_target=) + std::size_t pastTargetBytes = 0; // OUT: the block bytes those rows add over the target cut (rows, their file notes, + // the tag's attribute bytes) — what the caller's trailing sections must not pay for + bool rowDropCut = false; // OUT: step F dropped rows against rowDropBudgetBytes (the guard / the soft target) std::size_t shown = 0; std::size_t total = 0; std::size_t docsDropped = 0; @@ -5062,13 +5181,6 @@ struct SigsCutReport std::size_t lastShownRank = 0; // the 1-based candidate rank of the last row printed (rows are a rank PREFIX: the ladder // drops tail-first) — the resume index; next_offset= when rows were dropped bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) - bool terseContinuation = false; // a CHARGED pasteable one under a hard ceiling: next= alone (no next_offset=, no - // clause — the bundle's own next= reading defines it); see SigsCutContinuation - std::size_t continuationPaidSurplus = 0; // train 27: what the rows gave up past what they owed (a whole row dropped for a - // smaller handle) — the caller adds it to a measured overrun (verbs_for.h runForLens) - std::size_t continuationUnpaidBytes = 0; // a CHARGED one's bytes no row paid for — the rank 1..4 floor could not give them up, - // or the caller forbade the payment (payFromRows=false): it rides anyway (the recovery - // handle always ships) and the caller labels any overshoot }; // ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── @@ -5076,30 +5188,15 @@ struct SigsCutReport // r=1 row's (a body, or the file-grain widening page), so the trimmed tail was a dead end. The block now carries its own // next= — the SAME ranked lens re-run with the sig side widened until nothing is cut: // --for=TASK --signatures-only [RANKING FLAGS] [--pack-top-n=N] --token-budget=T [--json] -// --signatures-only gives the signatures the WHOLE explicit ceiling (verbs_for.h forSigSideCeiling: no auto-body share), -// and T is sized from THIS block's untrimmed bytes plus the bytes the caller says ride beside it (`fixedBytes`), at the -// budget conversion's own rate, rounded up to a 100-token step — a ceiling, so over-sizing it costs nothing (the re-run is -// as big as its content, bounded by the same candidate pool). --pack-top-n rides only when the caller set it (it sets -// total=). The value depends on the task and the untrimmed rows only — never on the ceiling that cut them — so the block -// stays byte-identical at the default and at any explicit ceiling above it (forbudgetmonotoncheck's invariant). -// WHO PAYS FOR ITS BYTES — two regimes, one predicate per caller (`charged`): -// • EXEMPT (the default ceiling; an explicit one wide enough that the sig side is frozen at the default's): the plan never -// reserves it — the e= / docs_after_code convention, a disclosure never costs a row the ceiling did not already cut — so -// the ranked set is the one it was and forbudgetmonotoncheck's byte identity holds. Downstream sections pay (the body -// leftover, the tail funded last); est_tokens measures it. -// • CHARGED (an explicit ceiling whose sig side is ALL of it: a --token-budget at or under the default sig share, any -// explicit one beside --signatures-only, MCP budget_tokens, --json --token-budget): nothing downstream can pay there, and -// an explicit --token-budget is a HARD ceiling (est_tokens <= budget, fornotesbudgetcheck), so the attribute is reserved -// inside the block like shown=/total=, and it rides TERSE there: next= alone — no next_offset=, no bracket clause; the -// bundle's own next= reading defines it ("the follow-up to paste"). Measured why (knob-honesty-068 report): with the -// clauses, the compact dialect — whose sig ledger leans on the header rungs — landed past the ceiling where the full -// one fitted (compactlegendcheck P4), and the rungs cannot shed a clause that is the cut's only disclosure. The e= -// ruling's shape: under a tight ceiling the extras go, the one thing that must stay (here the call) stays and is paid -// for. MCP keeps its clause (next_budget_tokens= has no other reading). -// …PAID ONLY WHERE PAYING IS WHAT MAKES THE ANSWER FIT (orchestrator ruling C3, 2026-10-07/08, CLI and MCP alike): the -// caller renders with payFromRows=false first (the rows the cut alone leaves, the whole handle unpaid) and serves that -// when it fits; it pays only when that answer lands past its ceiling and the paid one does not. Past it either way, the -// unpaid answer ships with over_ceiling="1" on the root (kForSigsUnpaidOverCeilingNote). +// --signatures-only serves the rows uncut at a wide soft target (forSigsBudgets: the text tier is the target itself off the +// default auto bundle), and T is sized from THIS block's untrimmed bytes plus the bytes the caller says ride beside it +// (`fixedBytes`), at the budget conversion's own rate, rounded up to a 100-token step — a soft target, so over-sizing it costs +// nothing (the re-run is as big as its content, bounded by the same candidate pool). --pack-top-n rides only when the caller +// set it (it sets total=). The value depends on the task and the untrimmed rows only — never on the target that cut them. +// WHO PAYS FOR ITS BYTES: nobody — the continuation is EXEMPT (the e= / docs_after_code convention: a disclosure never costs +// a row). forbudget-soft retired the CHARGED regime (knob-honesty-068: a hard explicit ceiling paid the handle from rows, +// terse, and re-rendered up to three times under ruling C3): an explicit budget is a soft target over the rows now, with no +// byte promise for a handle to be paid into. // THE RESUME INDEX, next_offset= (present when rows were DROPPED, not only shrunk): the candidate index the cut starts at — // the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no // row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation @@ -5116,22 +5213,10 @@ struct SigsCutContinuation int packTopN = 0; // the caller's explicit --pack-top-n (0 = the default pool): it sets total=, so it is echoed std::string_view rankFlags = {}; // the caller's RANKING flags, pre-spelled (" --no-route --adaptive"…): the re-run must rank // the same set, or "the rows this block cut" are rows of a different list - std::size_t fixedBytes = 0; // what the re-run's ceiling must carry besides the block: the charged header + sibling blocks - bool charged = false; // reserve it (and its legend clause) INSIDE the block's budget — see the regime rule above + std::size_t fixedBytes = 0; // what the re-run's target must carry besides the block: the charged header + sibling blocks bool json = false; // the --json dialect: the re-run is --json too bool pasteHandle = true; // false on MCP: the client re-calls the tool, so no CLI argv — the machine form, // next_budget_tokens=, is the argument a re-call of the same tool needs - std::size_t ledgerGapBytes = 0; // a CHARGED one's plan also reserves this: the sig room the caller's dialect was given beyond - // what its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes), - // so a capped compact block is cut to the rows a PAID full one keeps - bool payFromRows = true; // a CHARGED one: pay for it in rows (the plan's ledger gap + paySigsContinuationFromRows). - // false = the caller found that paying cannot make its answer fit (over its ceiling either - // way; orchestrator ruling C3, 2026-10-07): the block keeps the rows the cut alone leaves — - // no ledger gap, no payment — and the whole continuation rides unpaid (continuationUnpaidBytes) - std::size_t overrunBytes = 0; // train 27 (compactlegendcheck P1-B): a CHARGED one paying from rows also pays these bytes - // from the rows, byte for byte beside the handle — the overrun past the exact budget the - // caller MEASURED on its paid render (verbs_for.h runForLens, the compact dialect only); - // 0 everywhere else, so every other caller's rows are untouched }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -5191,6 +5276,7 @@ struct SigsTrimPlan bool capped = false; std::size_t effectiveBudget = 0; // the ladder's target: the budget minus the tag's reserved bytes std::size_t docsDroppable = 0; // the docs_dropped= upper bound + std::size_t reserveBytes = 0; // the tag's reserved bytes (forbudget-soft: the row-drop budget subtracts them too) }; template inline SigsTrimPlan planSigsTrim( const std::vector& entries, std::size_t totalRows, std::size_t gateCut, @@ -5207,9 +5293,42 @@ inline SigsTrimPlan planSigsTrim( const std::vector& entries, std::size_ && blockBytes + docsReserve + ( gateCut > 0 ? markerBytes : 0u ) > payloadBudgetBytes; plan.capped = plan.ladderFires || gateCut > 0; plan.effectiveBudget = payloadBudgetBytes > reserve ? payloadBudgetBytes - reserve : 0u; + plan.reserveBytes = reserve; return plan; } +// forbudget-soft: step F's effective budget for a caller that asked for a wider row-drop budget (SigsCutReport:: +// rowDropBudgetBytes): that budget less the tag's reserve and the past_target= attribute the block may then carry — never +// below the target's own effective budget (0 asked, or one at or under the payload budget = the classic hard ceiling). +inline std::size_t sigsRowDropEffective( const SigsTrimPlan& plan, std::size_t payloadBudgetBytes, std::size_t rowDropBudgetBytes, + std::size_t totalRows ) noexcept +{ + if( rowDropBudgetBytes <= payloadBudgetBytes ) + { + return plan.effectiveBudget; + } + const std::size_t reserve = plan.reserveBytes + sizeof( " past_target=\"\"" ) - 1 + sigsDecimalDigits( totalRows ); + return std::max( plan.effectiveBudget, rowDropBudgetBytes > reserve ? rowDropBudgetBytes - reserve : 0u ); +} + +// forbudget-soft: what the ladder's two budgets left on the report — the rows past the target, the bytes they add over the +// target cut (rows, file notes and, when it is printed, the attribute), and whether the wider budget itself cut rows. +inline void sigsCutRecordLadder( SigsCutReport& cut, const SigLadderOutcome& ladder, std::size_t totalAfterLadder, + std::size_t rowDropBudgetBytes, bool pastTargetDisclosed ) +{ + cut.rowDropBudgetBytes = rowDropBudgetBytes; + cut.pastTargetDisclosed = pastTargetDisclosed; + cut.pastTarget = ladder.pastTargetRows; + cut.rowDropCut = rowDropBudgetBytes > 0 && ladder.droppedRows > 0; + if( ladder.pastTargetRows > 0 ) + { + ASSUME( totalAfterLadder >= ladder.targetCutTotal ); // the target cut is a superset of the guard's: fewer bytes + cut.pastTargetBytes = totalAfterLadder - ladder.targetCutTotal + + ( pastTargetDisclosed ? sizeof( " past_target=\"\"" ) - 1 + sigsDecimalDigits( ladder.pastTargetRows ) : 0u ); + } + ENSURES( cut.pastTarget <= cut.shown, "sigsCutRecordLadder: more rows past the target than rows shown" ); // they are shown rows +} + // What the finished block shows, from its rows after the gate and the ladder: shown rows, and the shown rows whose doc // comment was removed (hadDoc, and no doc at emission) — the docs_dropped= value. template @@ -5237,8 +5356,7 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua { return; } - cut.hasContinuation = true; - cut.terseContinuation = req->charged && req->pasteHandle; + cut.hasContinuation = true; if( req->pasteHandle ) { cut.next = invocation; @@ -5250,7 +5368,7 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua } inline bool sigsCutHasNextOffset( const SigsCutReport& cut ) noexcept { - return cut.hasContinuation && !cut.terseContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; + return cut.hasContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; } // the legend clauses a cut report owes, in a fixed order: shrunk-not-dropped, the continuation's (knob-honesty-068), docs_dropped, // docs_after_code (train 26b: lean's reorder reading joins the knob's report legend) @@ -5261,7 +5379,11 @@ inline std::string sigsCutReportLegend( const SigsCutReport& cut ) { notes += kForSigsShrunkNote; } - if( cut.isCapped && !cut.next.empty() && !cut.terseContinuation ) + if( cut.pastTargetDisclosed && cut.pastTarget > 0 ) + { + notes += kForPastTargetNote; + } + if( cut.isCapped && !cut.next.empty() ) { notes += kForSigsNextNote; } @@ -5293,67 +5415,6 @@ inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::si cut.docsDropped = docsDropped; return sigsCutReportLegend( cut ); } -// The bytes a CHARGED continuation adds, an upper bound fixed before the plan: the pasteable form is terse (the call alone); -// the MCP machine form keeps its attribute, the resume index at its widest digits, and the clauses that define them. -inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, std::size_t valueAttrBytes, std::size_t offsetAttrBytes, - bool withClauses ) -{ - if( req == nullptr || !req->charged ) - { - return 0; - } - if( req->pasteHandle ) - { - return valueAttrBytes; // terse: next= alone (sigsCutAttachContinuation) - } - const std::size_t clauses = withClauses ? kForSigsNextBudgetNote.size() + kForSigsNextOffsetNote.size() : 0u; - return valueAttrBytes + offsetAttrBytes + clauses; -} -// The plan-side share of a CHARGED continuation: the dialect's ledger gap (SigsCutContinuation::ledgerGapBytes). It rides the -// marker's reservation, so it is spent only when the ladder fires — an uncapped block is untouched. -inline bool sigsCutPaysFromRows( const SigsCutContinuation* req ) noexcept -{ - return req != nullptr && req->charged && req->payFromRows; -} -inline std::size_t sigsCutPlanReserve( const SigsCutContinuation* req ) noexcept -{ - return sigsCutPaysFromRows( req ) ? req->ledgerGapBytes : 0u; -} -// train 27: what a payment from the rows gave up PAST what it owed — the ladder drops whole rows, so a 90 B handle can cost a -// 150 B row. A caller that measures its paid document still over its budget owes that overrun ON TOP of what was given up. -inline std::size_t sigsCutPaidSurplus( std::size_t totalBefore, std::size_t totalAfter, std::size_t owed, std::size_t unpaid ) noexcept -{ - const std::size_t given = totalBefore > totalAfter ? totalBefore - totalAfter : 0u; - const std::size_t paid = owed > unpaid ? owed - unpaid : 0u; - const std::size_t surplus = given > paid ? given - paid : 0u; - ENSURES( surplus <= given, "sigsCutPaidSurplus: the surplus exceeds what the rows gave up" ); - return surplus; -} -// train 27: the row-side share beside the handle — the caller's measured overrun (SigsCutContinuation::overrunBytes), owed only by -// a capped block whose caller lets rows pay; what the rank 1..4 floor cannot give up is reported with the handle's unpaid bytes -inline std::size_t sigsCutRowOverrun( const SigsCutContinuation* req ) noexcept -{ - return sigsCutPaysFromRows( req ) ? req->overrunBytes : 0u; -} -// A CHARGED continuation is paid FROM THE ROWS, byte for byte: the same ladder runs on past the cut until the block has given -// up `cost` bytes, so block + continuation never exceeds the block the cut alone left — in either dialect, whatever the plan's -// slack (reserving it in the plan let that slack absorb it and the bundle grow). Returns the bytes the rank 1..4 floor could -// NOT give up (0 = paid in full); the continuation rides regardless (orchestrator ruling 2026-10-07) and the caller labels it. -// Called only where the caller lets rows pay (sigsCutPaysFromRows): its callers render that way only after the unpaid answer -// landed past its ceiling, and serve the result only when it fits (ruling C3), so no row is dropped unless that buys the fit. -template -inline std::size_t paySigsContinuationFromRows( std::vector& entries, std::vector& files, std::size_t& total, - std::size_t cost, CostFn entryCost ) -{ - if( cost == 0 ) - { - return 0; - } - const std::size_t target = total > cost ? total - cost : 0u; - trimSigLadder( entries, files, total, target, entryCost ); - return total > target ? total - target : 0u; // not bounded by `cost`: a ladder action may append an ellipsis (trimSigLadder) -} - // The open tag: `` untrimmed; ` shown= total= capped="1"` when rows were cut or shrunk; ` docs_dropped=` // when a shown row lost its doc comment. The bytes it adds are the ones planSigsTrim reserved. inline std::string sigsOpenTag( const SigsCutReport& cut ) @@ -5365,6 +5426,11 @@ inline std::string sigsOpenTag( const SigsCutReport& cut ) rw::formatTo( nb, sizeof( nb ), " shown=\"{}\" total=\"{}\" capped=\"1\"", cut.shown, cut.total ); tag += nb; } + if( cut.pastTargetDisclosed && cut.pastTarget > 0 ) // forbudget-soft: the rows served past the default target + { + rw::formatTo( nb, sizeof( nb ), " past_target=\"{}\"", cut.pastTarget ); + tag += nb; + } if( cut.docsDropped > 0 ) { rw::formatTo( nb, sizeof( nb ), " docs_dropped=\"{}\"", cut.docsDropped ); @@ -5490,10 +5556,16 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // on GLOBAL rank), so pages merge without duplicate r= { const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; + // forbudget-soft: the two other INPUTS the report carries (the row-drop budget and whether past_target= is printed), read + // before the reset like the continuation request + const std::size_t rowDropBudgetIn = cutOut != nullptr ? cutOut->rowDropBudgetBytes : 0u; + const bool pastTargetDisclosed = cutOut != nullptr && cutOut->pastTargetDisclosed; if( cutOut ) { *cutOut = SigsCutReport {}; cutOut->continuationRequest = cutNext; + cutOut->rowDropBudgetBytes = rowDropBudgetIn; + cutOut->pastTargetDisclosed = pastTargetDisclosed; } if( droppedPositiveOut ) { @@ -5794,34 +5866,25 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // the tag's own attributes cost bytes — budget the trimmed state INCLUDING them (planSigsTrim; capture-audit // 2026-09-04 for the shown=/total= marker, cut-fix lane A for docs_dropped=). - // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block, known before the plan, so - // a CHARGED one rides the marker's reservation (attribute + its legend clause) and an exempt one costs the plan nothing + // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block; exempt (a disclosure never + // costs a row), so the plan reserves nothing for it const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); const std::size_t cutNextTokens = cutNext != nullptr ? sigsCutNextTokens( total, cutNext->fixedBytes ) : 0u; - const std::size_t cutNextCost = sigsCutContinuationReserve( - cutNext, - ( cutNext != nullptr && cutNext->pasteHandle ) ? nextAttrXml( cutNextInv ).size() - : sizeof( " next_budget_tokens=\"\"" ) - 1 + sigsDecimalDigits( cutNextTokens ), - sizeof( " next_offset=\"\"" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/true ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1 + sigsCutPlanReserve( cutNext ), + sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); + SigLadderOutcome ladder; if( plan.ladderFires ) { // one ladder ACTION on one entry, tail-first; every action re-checks the budget so the ladder // stops at the first fitting state. Pure function of (global rank, the kFor* constants) — the // ladder itself is trimSigLadder() above, shared verbatim with the JSON sibling (§A4a). - trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, entryCost ); - } - // knob-honesty-068: a CHARGED continuation is paid from the rows (paySigsContinuationFromRows); what the floor - // cannot pay rides anyway and is reported for the caller's over_ceiling label - // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows - const std::size_t cutNextOwed = plan.capped ? cutNextCost + sigsCutRowOverrun( cutNext ) : 0u; - const std::size_t totalBeforePay = total; - const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) - ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, entryCost ) - : cutNextOwed; - const std::size_t cutNextSurplus = sigsCutPaysFromRows( cutNext ) ? sigsCutPaidSurplus( totalBeforePay, total, cutNextOwed, cutNextUnpaid ) : 0u; + // forbudget-soft: step F aims at the caller's row-drop budget when it asked for a wider one (the --for default's + // runaway guard, an explicit soft target); the shrink steps keep aiming at payloadBudgetBytes + ladder = trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, entryCost, + sigsRowDropEffective( plan, payloadBudgetBytes, rowDropBudgetIn, totalRows ) ); + } + const std::size_t totalAfterLadder = total; // A2: the exact count, computed AFTER the ladder has made its final drop decisions (droppedPositiveCount // above — the shared arithmetic with the JSON sibling). Pure bookkeeping, no output bytes either way. @@ -5869,9 +5932,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext, spelling.elideZeroMetrics }, esc, rootArg ); } ); } - cut.continuationRequest = cutNext; - cut.continuationUnpaidBytes = cutNextUnpaid; - cut.continuationPaidSurplus = cutNextSurplus; + cut.continuationRequest = cutNext; + sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -10820,6 +10882,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // knob-honesty-068: continuationRequest is an INPUT { const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; + const std::size_t rowDropBudgetIn = cutOut != nullptr ? cutOut->rowDropBudgetBytes : 0u; // forbudget-soft: the XML twin's inputs + const bool pastTargetDisclosed = cutOut != nullptr && cutOut->pastTargetDisclosed; const bool rankAdaptivePayload = lens.rankAdaptivePayload; if( outCapped ) { @@ -10829,6 +10893,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s { *cutOut = SigsCutReport {}; cutOut->continuationRequest = cutNext; // knob-honesty-068: the request survives the reset + cutOut->rowDropBudgetBytes = rowDropBudgetIn; + cutOut->pastTargetDisclosed = pastTargetDisclosed; } if( droppedPositiveOut ) { @@ -10911,28 +10977,19 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // reserved INSIDE this array's budget exactly as the XML tag's attributes are inside its block's — they used to be // absent, and a JSON consumer could not tell 24 rows of 40 from 24 of 24. // (Without the rank-adaptive payload every globalRank is 0: no row is droppable and no doc is ever removed.) - // knob-honesty-068: the "sigs_next" key, charged or exempt by the XML twin's rule (SigsCutContinuation; no legend here) + // knob-honesty-068: the "sigs_next" key, exempt by the XML twin's rule (SigsCutContinuation; no legend here) const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); const std::size_t cutNextTokens = cutNext != nullptr ? sigsCutNextTokens( total, cutNext->fixedBytes ) : 0u; - const std::size_t cutNextCost = sigsCutContinuationReserve( cutNext, nextFieldJson( cutNextInv, "sigs_next" ).size(), - sizeof( ",\"sigs_next_offset\":" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), - /*withClauses=*/false ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1 + sigsCutPlanReserve( cutNext ), + sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); + SigLadderOutcome ladder; if( plan.ladderFires ) { - trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost ); - } - // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule); the floor's unpaid rest rides anyway - // and this dialect's own predicate (forLensJsonOverCeiling: bytes past the allowance) labels any overshoot - // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows - const std::size_t cutNextOwed = plan.capped ? cutNextCost + sigsCutRowOverrun( cutNext ) : 0u; - const std::size_t totalBeforePay = total; - const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) - ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, jsonSigEntryCost ) - : cutNextOwed; - const std::size_t cutNextSurplus = sigsCutPaysFromRows( cutNext ) ? sigsCutPaidSurplus( totalBeforePay, total, cutNextOwed, cutNextUnpaid ) : 0u; + ladder = trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost, + sigsRowDropEffective( plan, payloadBudgetBytes, rowDropBudgetIn, totalRows ) ); // forbudget-soft: the XML twin's split + } + const std::size_t totalAfterLadder = total; if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped @@ -10946,9 +11003,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s [ & ]( JsonSigEntry& e, std::uint32_t newRank ) { e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); } ); } - cut.continuationRequest = cutNext; - cut.continuationUnpaidBytes = cutNextUnpaid; - cut.continuationPaidSurplus = cutNextSurplus; + cut.continuationRequest = cutNext; + sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for if( cutOut ) { diff --git a/src/tracelocus.h b/src/tracelocus.h index 74d04c899..fe8d66702 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -1169,9 +1169,21 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph // M11: the root's machine-readable price, defined beside the ledger prose. // F5: "it" used to have three possible referents on this root. over_ceiling= names the ONE comparison // it is: est_tokens against the smallest ceiling this root prints. - h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the token target you passed (absent " - "when none), max_tokens= is the body ceiling you passed via the max_tokens flag (absent when none); over_ceiling= is 1 when " - "est_tokens exceeds the smallest ceiling named here (the bundle is then complete, not trimmed)."; + // forbudget-soft (decision D-5): the caller's budget_tokens= is a SOFT target — sections are shaped toward it, nothing is + // labelled over it and the header is never dropped for it; max_tokens= stays the hard body ceiling. A bundle without a + // budget of the caller's keeps its reading byte for byte. + if( in.budgetTokens > 0 ) + { + h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the soft token target you passed " + "(est_tokens may exceed it, the header always ships), max_tokens= is the body ceiling you passed via the max_tokens " + "flag (absent when none); over_ceiling= is 1 when est_tokens exceeds max_tokens (the bundle is then complete, not trimmed)."; + } + else + { + h += "On the root: est_tokens= prices the delivered bundle in tokens, budget_tokens= is the token target you passed (absent " + "when none), max_tokens= is the body ceiling you passed via the max_tokens flag (absent when none); over_ceiling= is 1 when " + "est_tokens exceeds the smallest ceiling named here (the bundle is then complete, not trimmed)."; + } h += extraNotes; if( in.notesDegraded ) { @@ -1292,9 +1304,8 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph // nothing else, so the attribute and the numbers beside it can always be reconciled by subtraction. // 0 = the caller named none (the working budget is the lens's own default, stated in the ledger prose // in bytes) and the label is then withheld rather than pointed at a number the root does not carry. - std::size_t namedCeiling = 0; - if( in.budgetTokens > 0 ) { namedCeiling = in.budgetTokens; } - if( in.maxTokens > 0 ) { namedCeiling = namedCeiling > 0 ? std::min( namedCeiling, in.maxTokens ) : in.maxTokens; } + // forbudget-soft (D-5): budget_tokens= is a soft target, never a ceiling this root labels against — max_tokens= is + const std::size_t namedCeiling = in.maxTokens; const auto rootAttrsFor = [ & ]( const std::string& doc, bool widestSpelling ) -> std::string { std::size_t est = 0; @@ -1322,7 +1333,9 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph spliceRootAttrs( candidate, rootAttrsFor( candidate, /*widestSpelling=*/true ) ); return pricedBytesOf( traceDeliveredBytes( candidate ) ); }; - const CeilingLadderChoice chosen = in.compactLegend + // forbudget-soft (D-5): under the caller's soft budget no ladder runs — the src echo always ships + const CeilingLadderChoice chosen = in.budgetTokens > 0 ? CeilingLadderChoice{ headerStr, CeilingRung::AsBuilt } + : in.compactLegend ? climbDeliveredLadder( buildRung, headerStr, whole.substr( headerStr.size() ), deliveredOf, ceilingAllowanceFromBudgetBytes( bundleBudget ), kNotes ) : climbCeilingLadder( buildRung, diff --git a/src/verbs_for.h b/src/verbs_for.h index a14ab77a9..f2f4e5327 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -429,6 +429,7 @@ struct ForLensJsonInputs std::string_view task; int packTopN; std::string_view rankFlags; // forRankShapingFlags( cfg ), owned by the caller for the call's duration + std::string_view tailCutNext; // forbudget-soft (D-4): the capped tail's continuation (rw::forWidenNext), REQUIRED }; // The lens bundle's opening keys. Every note is absent-unless-present — the same silence-means-nothing- @@ -512,19 +513,11 @@ struct ForLensHeaderParts bool autoBundle = false; // T3: auto mode is on (cfg.detail==0, no --signatures-only) — appends the bundle=auto legend bool compactBundle = false; // COMPACT conceptual serving: appends the bundle=compact legend INSTEAD of the auto one — never both, because only one of the two sections can be emitted bool compactLegend = false; // --legend=compact: versioned, shorter explanatory dialect - bool tailLegend = true; // deep-tail: the r=/ legend clause. Cleared ONLY by the - // ceiling ladder's rung zero (with the confidence clause): - // the facts (r= attrs, the element) survive, only the - // explainer goes — the L1 "first rung that costs no unique - // information" ordering. - bool idRouteLegend = true; // row 6 (2026-09-12): the sc=/route= readings (graphlegend.h - // kForIdRouteLegend) — ceiling-droppable exactly like the two - // above; rung zero clears all three and the note names them - bool legendDropped = false; // …and rung zero SAYS SO: set with the two clears above, it splices - // kForLegendDroppedNote. A field rather than a note the rung - // appends once, because the ladder rebuilds this header up to - // three more times and a disclosure that survives only the - // as-built shape is a disclosure the tight budgets never see. + bool tailLegend = true; // deep-tail: the r=/ legend clause. forbudget-soft: never dropped any more + // (the ceiling ladder's rung zero went with the hard ceiling); kept as the + // one switch the header writers read + bool idRouteLegend = true; // row 6 (2026-09-12): the sc=/route= readings (graphlegend.h kForIdRouteLegend); + // never dropped either, for the same reason std::string_view rootArg; // R-E (2026-08-17): the single-root run's own root= — the ladder's // route-dropped rebuild below calls ctxRootOpen a second time and // must carry the SAME root as the pre-built rootOpenStr did. @@ -561,35 +554,6 @@ struct ForLensHeaderParts bool viaByOwnerOnly = false; // owner-hop: viaPresent holds ONLY because the owner rows joined the via question // (the six ranked hop ids alone answer no) — the via clause is then the owner // rows' disclosure, exempt from the sig trim exactly as the qword clause is - - // ── THE DROPPABLE LEGEND, as ONE bit ────────────────────────────────────────────────────────────── - // confidenceNote / tailLegend / idRouteLegend moved in lock step at every read and every write, and the - // guard that used to spell all three was true in the only state rung zero can be reached in. One question, - // asked once; one clear, spelled once. legendDropped is the disclosure and is set by the same call, so a - // future clear cannot forget it (which is exactly how a silent drop ships). - bool legendDroppable() const noexcept - { - return !confidenceNote.empty() || tailLegend || idRouteLegend; - } - void dropDroppableLegend() noexcept - { - confidenceNote = {}; - tailLegend = false; // deep-tail: the explainer falls with the confidence clause — the r= attrs - // and the element (the facts) survive - idRouteLegend = false; // row 6: the sc=/route= readings fall with them — the attributes stay - legendDropped = true; // …and this is the sentence that says so, naming exactly what fell - } - // train 27 (compactlegendcheck P1-B): the compact (compose) / (lego) readings are decided before the - // payload, on an over-approximation; a section served as its counted stub, or not at all, prints none of those rows. - // Clears the reading of each section the caller says is rowless; true when a clause the header carried went (the header - // must be rebuilt). No note: no printed attribute loses its reading. - bool dropRowlessSectionReadings( bool composeRowless, bool legoRowless ) noexcept - { - const bool changed = ( composePresent && composeRowless ) || ( legoPresent && legoRowless ); - composePresent = composePresent && !composeRowless; - legoPresent = legoPresent && !legoRowless; - return changed; - } }; // Splice a pre-formatted fragment in front of a structural boundary, or leave the document exactly as it @@ -649,106 +613,6 @@ inline std::string rootOpenWithSchema( std::string rootOpen, std::string_view sc using rw::ForConfidence; using rw::deriveForConfidence; -// L1 — RUNG ZERO'S OWN DISCLOSURE. The ceiling ladder's rung zero (runForLens, below) buys header bytes back by -// dropping the two clauses whose loss costs no UNIQUE information: the confidence=/margin_pct= + -// budget_tokens=/max_tokens= sentence and the r=/tail sentence. Every fact they describe stays on the document; -// only the explanation goes. THE TRADE IS RIGHT and stays — METHODOLOGY §9 takes the smaller document inside the -// budget over the larger one past it, and on src/ at --token-budget=1300 that is 2 890 B against 3 654 B, the -// two clauses being ~750 B of it. What was wrong is that it was SILENT: rungs (b) and (c) each splice a "[… dropped (ceiling)]" -// sentence of their own, and rung zero spliced nothing, so `--for --token-budget=1300` shipped confidence= -// margin_pct= budget_tokens= r= and the counts with no legend defining any of them and no word that a -// definition had been removed — the reader cannot tell a budget cut from a missing feature. This is that word. -// It names the attributes VERBATIM, the way the cap clause and dropped_positive= do, so a reader who meets one -// on the root meets its name here and is told where its definition went. No "--" anywhere: it rides inside an -// XML comment, where a double hyphen is ill-formed (G4). Gate: test/legendcoveragecheck.sh, the for-budgeted row. -// -// ITS LENGTH IS THE SIBLING RUNGS', not a paragraph. Rungs (b) and (c) say "[task_echo: dropped (ceiling) - the -// verbatim copy is the task= attribute above]" in 31-80 B: the thing dropped, the cause, and where the survivor -// is. This says the same four things in 181 B, and the extra bytes are ENTIRELY the attribute names, which are -// the disclosure — a shorter sentence that said "some clauses were dropped" would leave the reader exactly where -// the silence did. The bytes are not free and must not be: this note rides ONLY on a run whose header did not -// fit, so every one of them competes with the sig row the rung just bought back. Gates that pin a budget against -// this floor (fornotesbudgetcheck's tight rung, estcalib's for-budgeted pin) are re-anchored with arithmetic when -// it moves — the note is never re-worded to fit a pin. -// -// WHERE IT SENDS THE READER, and why not to the help text. The first spelling of this note ended "the ripwire -// help text defines them", which was FALSE of half of what it names, measured on the binary that shipped it: -// `budget_tokens` appears ZERO times in --help (19 376 B), in --help=--for (5 027 B) and in --help=all -// (202 636 B); `max_tokens=` appears only in --help=all, never in the --for entry a reader of THIS document -// would open. Only confidence= and margin_pct= are genuinely defined there. A disclosure whose whole job is to -// stop a reader mistaking a budget cut for a missing feature must not then send them somewhere the definition -// is missing — they would conclude the feature is. So it points at the one place that provably has all of them: -// the SAME query at a wider ceiling. Every clause rung zero drops is unconditional at a budget where no rung -// fires, so re-running wider both recovers the definitions AND demonstrates that the absence was a cut. Gate: -// test/legendcoveragecheck.sh arm (E) re-asks every name this note spells against that wider run. -// -// THE CORRECTION IS ONE BYTE SHORTER THAN THE SENTENCE IT REPLACES, and that is a measurement, not a coincidence -// worth ignoring. A first draft of it ran to 191 B, and forrootlegendcheck's arm 2 went red: its --token-budget=800 -// fixture sat at est_tokens=799 with ONE token of headroom, the nine bytes took it to 802, and crossing the budget -// bought the 70 B of over_ceiling="1" plus its legend clause — so nine bytes of prose landed as 31 tokens and put -// a bundle that had fitted 3.9% past the ceiling it names. METHODOLOGY §9 is the tie-breaker: the smaller document -// inside the budget beats the larger one past it. Two honest spellings were available and the shorter was taken; -// that is not the same as trimming a disclosure until a pin goes green, and the day it is, the pin moves instead. -// RUNG ZERO'S OWN DISCLOSURE, ASSEMBLED FROM WHAT ACTUALLY FELL (PR #215 review item 2). -// -// WHAT WAS WRONG. This was four constants — full/thin x default/compact — chosen by one `coverage=` lookup, and -// three of the four were wrong about the document they described: -// * the THIN spellings named neither sc= nor route=, although rung zero clears idRouteLegend on a thin answer -// exactly as it does on any other. MEASURED on the pre-fix binary: `test/cppqualfix --for='mutex and pick -// overloads' --token-budget=300` served two rows, mentioned sc= nowhere in its legend, and its note -// named neither — an attribute with no reading and no disclosure that it had lost one. -// * the non-thin default spelling named route= unconditionally, so a --no-route run was told a definition had -// been "dropped (ceiling)" for an attribute that never rode. That is the error this note exists to prevent, -// pointed the other way: a feature that is absent, reported as a cut. -// * the compact spellings named sc=, which that dialect defines in its always-on rows clause and rung zero -// does not take down. -// A note about a cut is worth nothing if it is not about THIS document, so it is built from this document: the -// four booleans are the four facts that decide what the clauses defined. -// -// ASSEMBLED AT RUNTIME, deliberately. The older comment here argued for constants because "the byte ledgers that -// exempt and charge these strings read their sizes"; no ledger does — this note is spliced into the header and -// priced by finishForLensHeader's own fixpoint, like every other late splice. Nothing reads its size at compile -// time, and the four constants bought only the drift above. -// -// No "--" anywhere: it rides inside an XML comment, where a double hyphen is ill-formed (G4). -// -// compact the compact dialect, whose confidence clause defines confidence=/margin_pct= and NOTHING else -// (budget_tokens= has never been defined there at any budget, so naming it as dropped would be -// the same lie in miniature), and whose sc= reading rides the un-droppable rows clause. So do its -// total= shown= capped= readings ("total= shown= capped=1 if cut"), so the compact note does not -// name them as dropped either (train 27: it did, the same lie, and 24 B a spent ceiling paid for). -// thin the root carries coverage= (L-W present-only), whose clause rode the confidence sentence. -// routeWasOn route= rode this answer, so its reading was there to lose. -// scWasOn a served row carries sc=, so the sc= reading was there to lose (default dialect only). -inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, bool scWasOn ) -{ - std::string note = " [legend clauses: confidence=/margin_pct="; - if( thin ) - { - note += "/coverage="; - } - if( !compact ) - { - note += ", budget_tokens=/max_tokens="; - } - std::string identity; // the sc= / route= readings, named only where they actually rode - if( !compact && scWasOn ) - { - identity = "sc="; - } - if( routeWasOn ) - { - identity += identity.empty() ? "route=" : "/route="; - } - if( !identity.empty() ) - { - note += ", " + identity; - } - note += compact ? std::string_view( " and r=/tail dropped (ceiling) - the attributes stay; a wider token-budget defines them]" ) - : std::string_view( " and r=/tail (total= shown= capped=) dropped (ceiling) - the attributes stay; a wider token-budget defines them]" ); - return note; -} - // A1′ (owner decision 2026-09-12): --for's COMPACT legend, pinned like every other verb's (test/compactlegendcheck.sh, the // ripwire.for/v1 row) and PRESENT-ONLY — every clause below is emitted only when the document carries the // attribute it defines, and every reading is the shortest honest one. It used to spend 1,177–1,216 B here @@ -762,8 +626,7 @@ inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, // clause; the lens columns one clause (cx/ccx/in/churn/amp/clone/tested are always computed on this // surface, clone=/tested= appear only where 1); sc= and next= ride the clause; route= only when the // root carries it (the ladder's rung (c) drops the attribute, and then its reading goes with it); the -// confidence, sc=/route= and tail clauses are the ceiling-droppable trio (rung zero, kForLegendDroppedNoteCompact -// names them); the compact bundle's hops/calls/noedge clause and the auto bundle's bodies clause are +// confidence, sc=/route= and tail clauses always ride (forbudget-soft: no ceiling ladder sheds them); the compact bundle's hops/calls/noedge clause and the auto bundle's bodies clause are // mutually exclusive by construction (planForEnrichment). inline constexpr std::string_view kForCompactLegendRoot = "bundle=/bodies=/reason= the body posture; "; @@ -968,14 +831,6 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, h += clause; } } - if( p.legendDropped ) - { - // L1: the droppable clauses went to the ceiling — the attributes stay, and the note names exactly the - // ones THIS document had readings for. scWasOn is false here on purpose: this dialect defines sc= in its - // rows clause, which rung zero does not take down. - h += legendDroppedNote( /*compact=*/true, forCoverageAttrPresent( p ), - forRouteAttrPresent( p, withRouteAttr ), /*scWasOn=*/false ); - } // the data notes, numbers kept — see compactForNote h += compactForNote( p.adaptiveNote ); h += compactForNote( p.mentionNote ); @@ -1118,14 +973,6 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout { h.append( rw::forViaNameClause() ); // FE-B: present-only, on the same bit the compact strip reads } - if( p.legendDropped ) - { - // L1: the droppable clauses went to the ceiling — say so, and name exactly the ones THIS document had - // readings for (PR #215 review item 2: a thin answer used to be told about neither sc= nor route=, and a - // no-route answer used to be told route= had been dropped when it never rode). - h.append( legendDroppedNote( /*compact=*/false, forCoverageAttrPresent( p ), - forRouteAttrPresent( p, withRouteAttr ), p.scPresent ) ); - } h.append( extraNotes ); h += " -->"; // W3-S item 5 (2026-08-19): the --for lens carries root= on its with nothing defining it — the @@ -1272,27 +1119,6 @@ inline std::string forLensJsonBudgetStanza( std::size_t tokenBudget ) return ",\"budget_tokens\":" + std::to_string( tokenBudget ); } -// R1 — the OVER-CEILING PREDICATE for the JSON dialect: ONE unit, and the same one the XML twin uses. -// This side used to compare BYTES against ceilingAllowanceBytes (N x kMinBytesPerToken(2.36) x -// kCeilingFirstEntryTolerance(1.15) = 2.714 N) while the number it prints, est_tokens, prices those same -// bytes at kBytesPerTokenDefault (2.50). So a document in the band 2.50 N < bytes <= 2.714 N printed -// est_tokens > budget_tokens and said nothing — an 8.6%-wide silent band that any bundle saturating the -// ladder passes straight through (MEASURED on 4b722433: six rungs of fornotesbudgetcheck's own fixture). -// The XML twin (verbs_for.h F2) labels on estTokens > cfg.tokenBudget, and the legend both dialects answer -// to says "over_ceiling=1 says est_tokens exceeds budget_tokens": one sentence, so one predicate. -// The byte clause is KEPT rather than replaced — it is the header-floor case emitForLensJson's W3FIX H2 -// note describes, where a user-length task echo cannot be trimmed under the ceiling at all. Bytes past the -// allowance always price past the budget (2.714 / 2.50 = 1.086), so keeping it costs nothing, and dropping -// it would quietly narrow what this key means. -// A FREE FUNCTION rather than a lambda so the emitter's fixpoint reservation and its emitted key are -// literally the same expression — the pre-2026-09-05 shape wrote those as two separate comparisons, which -// is exactly how both halves of §C2's own bug got in. -inline bool forLensJsonOverCeiling( std::size_t tokenBudget, std::size_t ceilingAllowance, - std::size_t bundleBytes, std::size_t estTokens ) -{ - return tokenBudget > 0 && ( bundleBytes > ceilingAllowance || estTokens > tokenBudget ); -} - // #61 (redhat-et/ripwire#61, YogevKr, 2026-09-08) — THE OVER-CEILING PREDICATE for the XML dialect, and the // SECOND ceiling a `--for` root can name. Written as a free function for the reason its JSON sibling above // is: runForLens is one of the largest bodies in this file, and the rule this expresses is a contract of its @@ -1317,16 +1143,12 @@ inline bool forLensJsonOverCeiling( std::size_t tokenBudget, std::size_t ceiling // // CONVERGENCE is unchanged and for the unchanged reason: the attribute and its legend clause only ADD bytes, // so a document over EITHER ceiling stays over it and the caller's est_tokens fixpoint never oscillates. -// ladderFired is the W3FIX ceiling ladder's last rung (a --token-budget state); it is kept as its own input -// rather than folded into the budget comparison because the rung fires on BYTES against the allowance, which -// is a strictly wider condition than the token comparison beside it — dropping it would narrow what the -// attribute means. It arrives as the VALUE the ladder returned (serialize.h CeilingRung), never as a search -// of the emitted text — see ForLensRootFinish::lastRungFired. Gate: test/formaxtokenscheck.sh. -inline bool forLensOverCeiling( bool ladderFired, std::size_t tokenBudget, int maxTokens, bool hasBodyCeiling, - std::size_t estTokens ) noexcept +// forbudget-soft: the W3FIX ceiling ladder's last rung (ladderFired) went with the hard --token-budget ceiling, and the +// --for callers pass tokenBudget 0 (budget_tokens= names a soft target) — max_tokens= is the ceiling this predicate labels. +// Gate: test/formaxtokenscheck.sh. +inline bool forLensOverCeiling( std::size_t tokenBudget, int maxTokens, bool hasBodyCeiling, std::size_t estTokens ) noexcept { - return ladderFired - || ( tokenBudget > 0 && estTokens > tokenBudget ) + return ( tokenBudget > 0 && estTokens > tokenBudget ) || ( hasBodyCeiling && maxTokens > 0 && estTokens > std::size_t( maxTokens ) ); } @@ -1369,24 +1191,15 @@ struct ForLensRootFinish std::string_view notesDegradedAttr, notesDegradedNote; bool measured = false; std::string_view estTokensLegend, overCeilingLegend; - // M3: the ceiling ladder's VERDICT, carried as a value. False ⇒ the ladder did not run, or stopped above its - // last rung. It used to be the rung's note text, which finishForLensHeader searched the finished header for, - // and the header contains the caller's task echoed verbatim — so a task quoting the note forged the verdict - // and put over_ceiling="1" on a 9.8 KB bundle under a 100 000-token budget. climbCeilingLadderBy returns the - // rung it took (serialize.h CeilingRung); the fit predicate below sets this per candidate, the call site sets - // it from the chosen rung, and no code path derives it from text. Gate: test/ceilingverdictcheck.sh. - bool lastRungFired = false; std::size_t nonHeaderMarkupBytes = 0; // every markup byte outside the header, "" included std::size_t bodyTokens = 0; // the body-rate sections, already priced std::size_t tokenBudget = 0; int maxTokens = 0; bool bodyCeiling = false; - // knob-honesty-068 (orchestrator rulings 2026-10-07): a charged continuation rode unpaid — the rank 1..4 floor could - // not give its bytes up, or no row was dropped for it because the answer lands past its ceiling paid or not (ruling C3) — - // (SigsCutReport::continuationUnpaidBytes) AND the document lands past its ceiling: byte allowance or budget_tokens. - // Decided by the caller on the finished document (a value, never a search of the text); labels it over_ceiling="1" - // with kForSigsUnpaidOverCeilingNote, which is true where kOverCeilingLegend's est_tokens reading may not be. - bool sigsUnpaidOver = false; + // forbudget-soft: the default answer's ranked head passed the runaway guard (SigsCutReport::rowDropCut): over_ceiling="1". + // Its reading (forRunawayGuardNote) rides the clauses (sigsCeilingNote) already — never kOverCeilingLegend, whose + // est_tokens-versus-a-named-ceiling reading is not what happened. + bool guardOver = false; }; // PR #215 review: the finished header AND the number it prints, handed back together. finishForLensHeader @@ -1470,13 +1283,12 @@ inline ForLensPricedHeader finishForLensHeaderPriced( std::string header, const // clause unconditionally would charge every budgeted bundle for an attribute it does not carry. Monotone — // adding bytes only RAISES est_tokens — so one extra stage is exact and the flag never oscillates. The // definition rides the legend of the document that carries the attribute. - const bool namedCeilingOver = forLensOverCeiling( f.lastRungFired, f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); - if( namedCeilingOver || f.sigsUnpaidOver ) + const bool namedCeilingOver = forLensOverCeiling( f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); + if( namedCeilingOver || f.guardOver ) { - // each clause rides only where its reading is true: the est_tokens/last-rung one when that predicate holds, the - // unpaid one when the recovery handle rode unpaid on an answer past its ceiling (knob-honesty-068) — both when both hold + // each clause rides only where its reading is true: the est_tokens one when that predicate holds; the runaway guard's + // reading rode the clauses already (sigsCeilingNote) — both when both hold spliceBefore( header, " -->", /*fromEnd=*/true, namedCeilingOver ? f.overCeilingLegend : std::string_view() ); - spliceBefore( header, " -->", /*fromEnd=*/true, f.sigsUnpaidOver ? rw::kForSigsUnpaidOverCeilingNote : std::string_view() ); overAttr = " over_ceiling=\"1\""; priced = priceFixpoint( header.size() + f.nonHeaderMarkupBytes, overAttr.size() ); } @@ -1492,50 +1304,25 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin return finishForLensHeaderPriced( std::move( header ), f ).header; } -// train 27 (compactlegendcheck P1-B): how far a paid render's document runs past the exact budget, in the bytes its rows must -// give up to buy it back. Priced on the header AS BUILT (the ladder's input: no rung note, no over_ceiling label — the shape a -// fitting answer would carry) with every late splice, markup at the rate est_tokens prices it, plus one token for est_tokens' -// rounding, plus what the rows already gave up past the handle (`paidSurplus`: the next render's payment starts from the same -// cut). 0 when that unlabelled document is inside the budget. -inline std::size_t forUnlabelledOverrunBytes( const std::string& asBuiltHeader, const ForLensRootFinish& f, std::size_t tokenBudget, - std::size_t paidSurplus ) -{ - ForLensRootFinish unlabelled = f; - unlabelled.lastRungFired = false; - unlabelled.sigsUnpaidOver = false; - unlabelled.tokenBudget = 0; // no named budget ⇒ no over_ceiling label in the price - unlabelled.bodyCeiling = false; - const std::size_t est = finishForLensHeaderPriced( asBuiltHeader, unlabelled ).estTokens; - return est > tokenBudget ? std::size_t( double( est - tokenBudget + 1 ) * rw::kBytesPerTokenDefault + 0.999 ) + paidSurplus : 0u; -} - -// train 27: the compact dialect never echoes the task in its comment (appendCompactForLegend), so its rung (b) drops nothing -// and claims nothing (the --pack-task FX4 rule) and its rung (c) names only the route attribute. The full dialect's notes -// are `base`'s. `lastRung` is the last rung's reason, chosen by the caller on what is true. -inline constexpr std::string_view kCompactRouteDroppedNote = " [route_attr: dropped (ceiling)]"; -inline rw::CeilingLadderNotes forLadderNotes( bool compact, const rw::CeilingLadderNotes& base, std::string_view lastRung ) noexcept -{ - return { compact ? std::string_view() : base.echoDropped, compact ? kCompactRouteDroppedNote : base.echoAndRouteDropped, lastRung }; -} - // DEEP-TAIL d2, JSON dialect — the tail stanza and its explicit-regime fit, as a free function over -// emitForLensJson's locals (the forSigSideCeiling/ForLensHeaderParts precedent: that emitter is already +// emitForLensJson's locals (the ForLensHeaderParts precedent: that emitter is already // carrying the whole envelope fixpoint). Default regime: the full row cap. Explicit regime: the hard // ceiling stays hard — rows trim one by one into whatever the rendered bundle left, down to the honest // empty shell (the always-present key is the disclosure; an absent key would read as "no tail exists"). +// forbudget-soft: a soft target funds the tail from what the ROWS left of it (`rowsAllowance` = T minus the rows served before +// it); the header is never charged. `cutNext` (D-4) rides a capped tail in every regime. inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t ceilingAllowance, std::size_t baseNoTailBytes ) + std::size_t rowsAllowance, std::string_view cutNext ) { - std::string stanza = "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap ); + std::string stanza = "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap, cutNext ); if( tokenBudget == 0 ) { return stanza; } - const std::size_t tailAllowed = ceilingAllowance > baseNoTailBytes ? ceilingAllowance - baseNoTailBytes : 0; for( std::size_t shown = tail.paths.size(); ; --shown ) { - stanza = "," + rw::renderFileTailJson( tail, shown ); - if( stanza.size() <= tailAllowed || shown == 0 ) + stanza = "," + rw::renderFileTailJson( tail, shown, cutNext ); + if( stanza.size() <= rowsAllowance || shown == 0 ) { break; } @@ -1543,11 +1330,8 @@ inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t return stanza; } -// knob-honesty-068 (orchestrator ruling C3, completed 2026-10-08): one RENDER of the --json bundle. `sigsNextPayFromRows` is -// the charged "sigs_next" payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextTryOther` set, a render -// whose capped, charged bundle lands past its ceiling writes NOTHING and asks for the other mode (emitForLensJson below). -inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in, bool sigsNextPayFromRows, - bool* sigsNextTryOther ) +// One RENDER of the --json bundle (emitForLensJson below). +inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) { using namespace rw; @@ -1589,11 +1373,13 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const const std::size_t fixedBytes = header.size() - std::min( header.size(), rw::forZeroNoteBytes( header ) ) // lean-answers: exempt + kJsonEnvelopeBytes + kJsonSurfaceCountsBytes + ( in.noteIndex ? kJsonNotesStanzaBytes : 0 ); - const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; - // knob-honesty-068: the cut's continuation. This dialect serves no bodies, so its sig side IS the ceiling: charged under - // an explicit --token-budget, exempt at the default (the XML twin's rule, SigsCutContinuation) - const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*charged=*/in.tokenBudget > 0, /*json=*/true, - /*pasteHandle=*/true, /*ledgerGapBytes=*/0u, sigsNextPayFromRows }; + // forbudget-soft: the XML twin's two budgets (forSigsBudgets): the default target + the runaway guard, or a soft target + // over the rows. One dialect here, so the soft tier's header is this one. + const ForSigsBudgets sigsBudgets = forSigsBudgets( in.tokenBudget > 0 ? bundleBudget : 0u, fixedBytes, fixedBytes, /*tierAtDefault=*/false ); // no bodies ride here + const std::size_t sigsBudget = sigsBudgets.target; + // knob-honesty-068: the cut's continuation — exempt in every regime now (the XML twin's rule: no byte promise to pay into) + const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*json=*/true, + /*pasteHandle=*/true }; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, @@ -1630,11 +1416,13 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // row cap here (the degrade path below needs a stanza before any budget is knowable); the normal path // re-fits it after the sigs render (forLensJsonTailStanza above) — residual-funded, sigs untouched: // the reservation feeding sigsBudget above deliberately does NOT include these bytes. - std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap ); + std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap, in.tailCutNext ); bool sigsCapped = false; // the LADDER's verdict: decides the budget_bytes= stanza rw::SigsCutReport sigsCut; // cut-fix lane A: what the array cut (gate or ladder) — "capped" and the sigs_* keys sigsCut.continuationRequest = &cutNext; // knob-honesty-068: IN — "sigs_next" / "sigs_next_offset" on a capped array + sigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: IN — the guard / the soft target's rows + sigsCut.pastTargetDisclosed = in.tokenBudget == 0; std::size_t sigsDroppedPositive = 0; // A2: set only by the memstream-buffered render below (nullptr on the ENOMEM degrade path) std::vector jsonShownIds; // lane 2: the sigs rows actually emitted (the XML twin's shownSigIds) std::string sigsJson; @@ -1704,6 +1492,10 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const } sigsCutStanza += rw::nextFieldJson( sigsCut.next, "sigs_next" ); } + if( sigsCut.pastTargetDisclosed && sigsCut.pastTarget > 0 ) // forbudget-soft: the XML tag's past_target= + { + sigsCutStanza += ",\"sigs_past_target\":" + std::to_string( sigsCut.pastTarget ); + } if( sigsCut.docsDropped > 0 ) { sigsCutStanza += ",\"docs_dropped\":" + std::to_string( sigsCut.docsDropped ); @@ -1730,7 +1522,8 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // over, and adding its 20 bytes can only keep it over, never bring it back under. const std::size_t cappedClauseBytes = sigsCappedKey ? 14u : 15u; // ,"capped":true / ,"capped":false const std::size_t envelopeTextBytes = cappedClauseBytes + 14u + 8u + 1u + kJsonBundleSigsKey.size(); // + ,"est_tokens": + ,"sigs": + } + ,"bundle":"sigs" - const std::size_t ceilingAllowance = in.tokenBudget > 0 ? ceilingAllowanceBytes( in.tokenBudget ) : 0; + // forbudget-soft: the one over-ceiling this dialect can carry is the runaway guard's (a soft target is never "over") + const bool guardOver = sigsCut.rowDropCut && in.tokenBudget == 0; const std::string budgetStanza = forLensJsonBudgetStanza( in.tokenBudget ); // R1 // The XML root's budget_bytes= twin (runForLens, below its own long note): the DEFAULT byte ceiling, @@ -1746,12 +1539,8 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // DEEP-TAIL, explicit-regime fit (forLensJsonTailStanza above): residual-funded, sigs untouched. // lane 2: same rule as the XML twin — the tail excludes the files of the rows actually emitted, not the whole surface const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, jsonShownIds, in.rootArg ); - tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, ceilingAllowance, - header.size() + sigsJson.size() + notesStanza.size() - + surfaceCountsStanza.size() + envelopeTextBytes - + budgetStanza.size() - + sigsCutStanza.size() // cut-fix lane A: the sigs_* keys ride the root too - + sigsCeilingStanza.size() ); // the residual the tail is funded from must see this disclosure's bytes + tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, bundleBudget > sigsJson.size() ? bundleBudget - sigsJson.size() : 0u, + in.tailCutNext ); // soft: the rows' leftover funds the tail; D-4: its cut's continuation const std::size_t bundleBytesBase = header.size() + sigsJson.size() + notesStanza.size() + surfaceCountsStanza.size() + tailStanza.size() + envelopeTextBytes + droppedPositiveStanza.size() // A2: 0 bytes on the (overwhelming) no-drop path @@ -1768,8 +1557,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // R1: the same predicate on the same pair the post-loop emission reads — when isOver is false, // withKey == baseBytes and baseTokens == the settled estTokens, so the reservation and the key can // never disagree; when it is true the 20 bytes only make the document more over, never less. - const std::size_t baseTokens = std::size_t( double( baseBytes ) / kBytesPerTokenDefault + 0.5 ); - const bool isOver = forLensJsonOverCeiling( in.tokenBudget, ceilingAllowance, baseBytes, baseTokens ); + const bool isOver = guardOver; const std::size_t withKey = baseBytes + ( isOver ? 20u : 0u ); // ,"over_ceiling":true const std::size_t nextTokens = std::size_t( double( withKey ) / kBytesPerTokenDefault + 0.5 ); bundleBytes = withKey; @@ -1788,19 +1576,8 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // §C2: the SAME verdict the charge above solved for — read from the solved bundleBytes so the key that is // written and the bytes that were charged can never disagree (they used to be two separate comparisons, // one of which did not count the key it was deciding to emit). - std::string overCeiling; - if( forLensJsonOverCeiling( in.tokenBudget, ceilingAllowance, bundleBytes, estTokens ) ) - { - overCeiling = ",\"over_ceiling\":true"; - } + const std::string overCeiling = guardOver ? std::string( ",\"over_ceiling\":true" ) : std::string(); ASSUME( overCeiling.size() == 0 || overCeiling.size() == 20 ); // the 20 the charge above reserved for it - // knob-honesty-068 (ruling C3, completed): a capped, charged bundle past its ceiling in THIS payment mode is not served from - // this render. Write nothing; the caller tries the other mode (emitForLensJson: unpaid, then paid, then unpaid served). - if( sigsNextTryOther != nullptr && cutNext.charged && sigsCut.hasContinuation && !overCeiling.empty() ) - { - *sigsNextTryOther = true; - return 0; - } std::fputs( header.c_str(), out ); std::fwrite( kJsonBundleSigsKey.data(), 1, kJsonBundleSigsKey.size(), out ); std::fwrite( surfaceCountsStanza.data(), 1, surfaceCountsStanza.size(), out ); @@ -1817,40 +1594,10 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const return 0; } -// The --json bundle (orchestrator ruling C3, completed 2026-10-08): a charged "sigs_next" is paid for in rows ONLY where that is -// what makes the bundle fit — runForLens's three modes over one ranking: unpaid (served when it fits), paid (served when IT -// fits), unpaid again (served over its ceiling, over_ceiling disclosed: no row dropped for a handle that cannot make it fit). -// A render that hands back wrote nothing; the redaction tally is put back to its entry value before each later render, so the -// rows the summary counts are the ones written. Rendering only — the ranking is the caller's. +// The --json bundle: one render (forbudget-soft: no charged "sigs_next" to pay for, so no payment-mode re-renders). inline int emitForLensJson( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) { - if( in.tokenBudget == 0 ) // only an explicit --token-budget charges the handle, so only then can a render hand back - { - return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, nullptr ); - } - const std::optional tallyAtEntry = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; - const auto restoreTally = [ & ] - { - if( tallyAtEntry ) - { - *in.redact = *tallyAtEntry; - } - }; - bool unpaidOver = false; - const int unpaidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, &unpaidOver ); - if( !unpaidOver ) - { - return unpaidRc; - } - restoreTally(); - bool paidOver = false; - const int paidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, &paidOver ); - if( !paidOver ) - { - return paidRc; - } - restoreTally(); - return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, nullptr ); + return emitForLensJsonPass( out, header, in ); } // ── T3: the terminal-by-default auto section (pre-registered: docs/EVALS.md §4) ───────────────── @@ -1888,10 +1635,10 @@ struct ForAutoBodiesResult rw::ChargedSection section; // rendered auto bytes; empty when nothing is emitted std::string attr; // the root disclosure; empty ⇒ no attribute (surface off / degrade) bool surfaceOff = false; // true ⇒ the caller rebuilds the header WITHOUT the bundle=auto legend - bool legendOff = false; // exhausted explicit ceiling: attr KEPT (its bytes are the reserve), legend + section dropped — - // the D10 fit contract and the T3 disclosure contract met at once (fordisclosurecheck #2). - // BOTH enrichment shapes set it: the body walk and the compact walk exhaust the same way, - // and a disclosure that survives only one serving path is not a disclosure. + bool hopsCutMarker = false; // forbudget-soft: a soft target left no row room for the compact : the + // counted marker (shown="0" … next=) is served and its present-only clause rides. + // The bundle legend always stays (a soft target never sheds the header); this + // replaced legendOff, the hard ceiling's attribute-only shape (fordisclosurecheck #2). // THE est_tokens SPLIT, filled by buildForEnrichment: exactly one of these is non-zero, because a // section is either markup (compact : summed with the rest of the markup and rounded ONCE) or // body text (T3 : charged at the body rate, which is why the sum splits by kind at all). @@ -1977,22 +1724,6 @@ void restrictBodiesToRouteAnchor( const rw::IngestResult& ing, std::vector 1,782 B -> 9,483 B, 8 rows -> 40) and crowded the auto body walk -// from 6 served bodies down to 2: a bundle both BIGGER than the natural default and WORSE on the axis -// the r10 judging credited (full bodies of the decisive symbols). The invariant now enforced: a wider -// ceiling never buys less decisive content. In auto-bundle mode with no explicit --pack-top-n, the sig -// side's claim is capped at the DEFAULT sig budget — a no-op in the default regime (bundleBudget IS -// kForPayloadBudgetBytes there, so the default bundle stays byte-identical), and at any explicit ceiling -// >= kForPayloadBudgetBytes + kForAutoBodyBudgetBytes the sig render equals the default regime's byte -// for byte, so the bodies' leftover is provably >= the default's: every body the default serves still -// fits, and everything beyond the frozen share flows to bodies. An explicit --pack-top-n is an explicit -// SIG posture and keeps the legacy whole-ceiling claim — the frozen share would trim the rows the caller -// literally asked for down to the ladder floor. A free function over runForLens' locals (the -// ForLensHeaderParts precedent) — runForLens is already one of the largest functions in this file. // knob-honesty-068: the flags that change WHICH rows the lens ranks (and so total=/the order cuts), pre-spelled for the // capped block's continuation (serialize.h SigsCutContinuation::rankFlags) — the re-run must rank the same list. inline std::string forRankShapingFlags( const rw::Config& cfg ) @@ -2007,41 +1738,6 @@ inline std::string forRankShapingFlags( const rw::Config& cfg ) return f; } -std::size_t forSigSideCeiling( bool autoBundleMode, int packTopN, std::size_t bundleBudget ) -{ - if( autoBundleMode && packTopN == 0 ) - { - return std::min( bundleBudget, rw::kForPayloadBudgetBytes ); - } - return bundleBudget; -} - -// ── THE TRAILING-SECTION RESERVE under an explicit ceiling (lego-charge fix round 4, estchargecheck #11 A7) ── -// §F1 charged --with-graph's block and the --detail bodies, but only AFTER had already claimed the whole -// sig-side ceiling: the graph block (a FIXED cost, no budget knob) and the first --detail body (packBodies serves -// it whole or head-cut even at a 1-byte budget, with its section wrapper and the omitted-bodies comment) then rode -// past the stated budget. MEASURED on base with `src --for --token-budget=2000 --detail=20 --with-graph`: 6177 B -// ("parse the command line flags") and 6167 B ("emit the legend") against a 5428 B allowance, both over_ceiling="1" -// with the ladder's last-rung note claiming the HEADER floor left no payload to trim — the trimmable payload was -// , sized as if the two trailing sections cost nothing. The fix is the forSigSideCeiling idea applied to these -// two sections: under an explicit --token-budget the sig side's claim is capped so the graph block and a first-body -// floor fit inside the bundle budget. The default regime (no --token-budget) and a budgeted run with neither flag -// pass reserveBytes=0, so the claim is the unchanged sigSideCeiling (byte-identical by construction). The cap only -// binds when the bundle cannot hold sigSideCeiling AND the reserve, so a wide explicit ceiling in auto-bundle mode -// (sig side already frozen at the default share) is untouched too — forbudgetmonotoncheck's invariant holds. -std::size_t forSigClaimWithTrailingReserve( std::size_t sigSideCeiling, std::size_t bundleBudget, std::size_t reserveBytes ) noexcept -{ - if( reserveBytes == 0 ) - { - return sigSideCeiling; - } - const std::size_t room = bundleBudget > reserveBytes ? bundleBudget - reserveBytes : 0; - const std::size_t claim = std::min( sigSideCeiling, room ); - ENSURES( claim <= sigSideCeiling && claim + reserveBytes <= std::max( bundleBudget, reserveBytes ), - "forSigClaimWithTrailingReserve: the sig claim grew past its ceiling or left the reserve no room" ); - return claim; -} - // The FIRST-BODY FLOOR's byte budget: the first --detail body served whole when it is at most a quarter of the // bundle budget, else head-cut at that quarter (packBodies' truncateOversizedFirst path, which carries lines= and // the next= that serves the rest — a disclosed, recoverable cut). Only the FIRST body is floored: the rest of @@ -2165,12 +1861,8 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR // for why nothing but the attribute appears. This branch used to turn the WHOLE surface off with // no attribute at all — the silent shape the 2026-08-22 Lane-AA mine caught on 5 of 26 real // --for calls (see the BUDGET paragraph above); the T3 registration makes no exception for it. - if( cfg.tokenBudget > 0 && leftBytes == 0 ) - { - out.attr = " bundle=\"auto\" bodies=\"0\" reason=\"budget\""; - out.legendOff = true; - return out; - } + // forbudget-soft: under a soft target an exhausted allowance no longer drops the legend — the walk below renders the + // honest empty shell () at a 1-byte budget and the root says reason="budget". if( autoBodyIds.empty() ) { // R9 fix (W3-S, 2026-08-19): this used to return with `out.section` untouched (default-constructed, @@ -2531,7 +2223,7 @@ inline std::vector forOwnerHopNodes( const ForOwnerPlan& plan, const // packHops' owner description of a plan. Default regime: the owner rows ride ON TOP of the hop budget (completeness first — // every ranked row keeps the budget it had, and the name guard is the only stop). Explicit ceiling: the owners are funded // first from what the ceiling left, minus the widest qword_cut/next= disclosure the walk could write (the envelope is the -// ranked rows' own reserve, kCompactFixedBytes, and is not charged twice). +// ranked rows' own reserve, the attribute and envelope reserves, and is not charged twice). inline rw::HopOwners forHopOwners( const rw::Config& cfg, const rw::IngestResult& ing, const ForOwnerPlan& plan, std::size_t leftBytes, std::string_view rootArg ) { @@ -2591,26 +2283,16 @@ inline constexpr std::string_view kForHopsBudgetCutLegend = "; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them"; // …and the compact dialect's spelling (train 27, compactlegendcheck P1-B): that dialect defines total= shown= capped=1 and next= // in its un-droppable clauses (kForCompactLegendRows / kForCompactLegendFacts), so its hops clause says only what the element -// is — every reading is the shortest honest one. The reserve below stays the longer spelling's (the widest, like -// kCompactAttrReserve), so the compact sig room is the full dialect's to the byte and rows(compact) ⊇ rows(full) holds. +// is — every reading is the shortest honest one. inline constexpr std::string_view kForHopsBudgetCutLegendCompact = "; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)"; static_assert( kForHopsBudgetCutLegendCompact.size() < kForHopsBudgetCutLegend.size(), - "forHopsCutReserve reserves the full spelling: it must be the wider one" ); -// the hops cut's continuation (` next="--for=TASK"`) — one spelling for the marker, the partial-cut splice and the reserve + "the compact hops clause is the shorter honest spelling of the full one" ); +// the hops cut's continuation (` next="--for=TASK"`) — one spelling for the marker and the partial-cut splice inline std::string forHopsCutNextAttr( const rw::Config& cfg ) { return rw::nextAttrXml( rw::nextFlag( "--for=", cfg.forTask ) ); } -// the bytes an explicit ceiling reserves out of the sig claim so the compact hops' cut disclosure always fits: the widest -// marker (``, total <= kPackTaskBodyCandidates, two digits) plus its -// present-only legend clause — the kCompactAttrReserve / kForFileTailShellReserve pattern. -inline std::size_t forHopsCutReserve( const rw::Config& cfg ) -{ - constexpr std::size_t kMarkerShell = std::string_view( "" ).size(); - return kMarkerShell + forHopsCutNextAttr( cfg ).size() + kForHopsBudgetCutLegend.size(); -} - rw::ChargedSection forHopsBudgetCutMarker( const rw::Config& cfg, const rw::IngestResult& ing, const std::vector& hopIds ) { rw::ChargedSection marker; @@ -2687,24 +2369,24 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest // as another. Gate: test/fordisclosurecheck.sh (compact arm). if( cfg.tokenBudget > 0 && leftBytes == 0 ) { - out.attr = " bundle=\"compact\" bodies=\"0\" reason=\"budget\""; - out.legendOff = true; + out.attr = " bundle=\"compact\" bodies=\"0\" reason=\"budget\""; // fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): the hop rows this budget left out are NAMED, not dropped // silently — an empty carrying the count and the call that serves them (the same question without the - // budget, whose compact allowance is its own). The bytes are this section's and the ladder prices them. - out.section = forHopsBudgetCutMarker( cfg, ing, hopIds ); + // budget, which serves every hop row). forbudget-soft: the legend stays; the marker's clause rides (hopsCutMarker). + out.section = forHopsBudgetCutMarker( cfg, ing, hopIds ); + out.hopsCutMarker = !out.section.xml.empty(); return out; } - // the surface allowance MINUS this surface's own fixed disclosure — see kForCompactSurfaceBudgetBytes. - constexpr std::size_t kCompactFixedBytes = kForCompactBundleLegend.size() + kCompactAttrReserve + kCompactWrapReserve; // fix round 4: under an explicit ceiling a cut carries next= (forHopsCutNextAttr), so its bytes come out of the // rows' budget first; the default regime is unchanged (cutNext empty) const std::string cutNext = cfg.tokenBudget > 0 ? forHopsCutNextAttr( cfg ) : std::string(); const std::size_t rowRoom = leftBytes > cutNext.size() ? leftBytes - cutNext.size() : 1u; - const std::size_t hopBudget = std::min( rowRoom, rw::kForCompactSurfaceBudgetBytes > kCompactFixedBytes - ? rw::kForCompactSurfaceBudgetBytes - kCompactFixedBytes - : std::size_t( 1 ) ); + // forbudget-soft (D-3): the DEFAULT answer serves every hop row — the section is bounded by its own structure (at most + // kPackTaskBodyCandidates rows of at most 16 callees each), so the 1,000 B allowance no longer cuts it; the runaway guard's + // byte value is the only bound. An explicit soft target fills the rows from what the target left after (rowRoom), + // with no allowance of its own: a target wide enough for the whole section serves it whole, as the default does. + const std::size_t hopBudget = cfg.tokenBudget == 0 ? rw::kForRunawayGuardBytes : rowRoom; // THE QUESTION'S OWNER (forOwnerHopPlan): owner rows first, the candidates they were not, the edgeless owners last; // an empty plan leaves both exactly as they were (forOwnerHopNodes / forHopOwners). @@ -2766,7 +2448,7 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR } // DEEP-TAIL d2 — the XML tail render + its explicit-regime fit, a free function over runForLens' locals -// (that function is already one of the largest in this file — the forSigSideCeiling precedent). Default +// (that function is already one of the largest in this file — the ForLensHeaderParts precedent). Default // regime: the full row cap, bytes riding on top (est_tokens measures them). Explicit regime: rows fit the // residual the rendered bundle actually left; the shell's bytes were reserved ahead of the body walk // (kForFileTailShellReserve inside the enrichment's committed sum), so the disclosure always fits. @@ -2779,19 +2461,20 @@ inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t t std::vector esc; if( tokenBudget == 0 ) { - return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc ); + return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc, cutNextAttr ); // forbudget-soft D-4: the cap's next= too } const std::size_t tailAllowed = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, rw::kForFileTailShellReserve + cutNextAttr.size() ); return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc, cutNextAttr ), esc, cutNextAttr ); } -// The explicit-budget tail's cut continuation: the --for file page (one row per positive-score file, pageable), the -// one call that lists the files a budget left out of . Empty without --token-budget: the default regime's tail is -// unchanged (a named deferral — adding it there moves every default --for answer). +// The tail's cut continuation: the --for file page (one row per positive-score file, pageable), the one call that lists the +// files the left out. forbudget-soft (decision D-4, PROCESS rule 5: every cut recoverable): in EVERY regime now — the +// default's 24-row cap was the one default cut with no next=; it moves every default answer whose tail is capped by this +// attribute alone (rendered only on a capped tail, renderFileTailXml). inline std::string forTailCutNextAttr( const rw::Config& cfg ) { - return cfg.tokenBudget > 0 ? rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ) : std::string(); + return rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ); } // R2-AF (round 2, S4): render the task's named-file/decl-impl-partner rows (rw::forNamedHeaderRows, @@ -2865,29 +2548,16 @@ inline bool candidatePageWindowed( const rw::Config& cfg ) return cfg.tokenBudget != 0 && ( cfg.pageLimit > 0 || cfg.pageOffset > 0 ); } -// knob-honesty-068 (orchestrator ruling C3, completed 2026-10-08): one RENDER of the --for lens over the ranking runForLens -// computed once (`lr`). `sigsNextPayFromRows` is the charged continuation's payment mode (serialize.h -// SigsCutContinuation::payFromRows); with `sigsNextTryOther` set, a run whose capped, charged document lands past its ceiling -// writes NOTHING to stdout and asks for the run in the other payment mode (runForLens below decides which answer ships). -// `forAtStamp` (train 26c, CodeRabbit on #383): the at= stamp, read ONCE per run by runForLens — every render shares it. +// The --for lens's RENDER over the ranking runForLens computed once (`lr`). `forAtStamp` (train 26c, CodeRabbit on #383): the +// at= stamp, read ONCE per run by runForLens. forbudget-soft: one render per run — the C3 payment modes (unpaid / paid / +// overrun re-renders of a charged continuation) went with the hard ceiling they priced against. struct ForLensPassMode { - bool sigsNextPayFromRows = false; - bool* sigsNextTryOther = nullptr; std::string_view forAtStamp; - // train 27 (compactlegendcheck P1-B): the compact dialect's OVERRUN payment. A paid compact render that lands past its - // ceiling reports, through `overrunBytesOut`, the bytes its unlabelled document runs past the exact budget (plus what its - // rows already gave up past the handle); the next render pays `overrunReserveBytes` from the rows beside the handle - // (serialize.h SigsCutContinuation::overrunBytes) — see runForLens. - std::size_t overrunReserveBytes = 0; - std::size_t* overrunBytesOut = nullptr; }; std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, const ForLensPassMode& mode ) { using namespace rw; - EXPECTS( mode.overrunReserveBytes == 0 || mode.sigsNextPayFromRows, "runForLensPass: an overrun is paid from the rows only in the paying mode" ); - const bool sigsNextPayFromRows = mode.sigsNextPayFromRows; - bool* const sigsNextTryOther = mode.sigsNextTryOther; const std::string forAtStamp( mode.forAtStamp ); const Config& cfg = d.cfg; const IngestResult& ing = d.ing; @@ -3176,11 +2846,17 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { forConf.attrs += " max_tokens=\"" + std::to_string( cfg.maxTokens ) + "\""; } - if( forGateBudget || forBodyCeiling ) + // forbudget-soft (owner decision 2026-10-14): budget_tokens= is a SOFT target now — rows stop once it is reached and + // nothing else is shaped by it — so its reading says so (kForSoftBudgetNote); max_tokens= stays the hard body ceiling. + // No "--" in these strings: they ride inside an XML comment, where a double hyphen is ill-formed (G4). The flags are + // named without their dashes for exactly that reason. + if( forGateBudget ) { - // No "--" in this string: it rides inside an XML comment, where a double hyphen is ill-formed - // (G4). The flags are named without their dashes for exactly that reason. - forConf.note += " [budget_tokens=/max_tokens=: the token ceiling this bundle was shaped against]"; + forConf.note += rw::kForSoftBudgetNote; + } + if( forBodyCeiling ) + { + forConf.note += " [max_tokens=: the token ceiling the detail bodies were shaped against]"; } // §L10b: mention_anchored=/doc_mentions= — the machine form of mentionNote/docMentionNote, EACH @@ -3361,7 +3037,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, forConf.attrs, forConf.note, forAtAttrStr, mentionDocAttrsStr, cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", - /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg, + /*tailLegend=*/true, /*idRouteLegend=*/true, flRootArg, /*hdrLegend=*/!forHdrRows.empty(), forScPresent, forComposePresent, forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent, forViaPresent, ownerPlan.any(), forViaByOwnerOnly }; @@ -3486,7 +3162,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co cfg.packBudgetBytes, cfg.tokenBudget, notesPtr, legoTotal, composeTotal, routesTotal, flRootArg, &forFileTail, d.notesDegraded, forDocsAfterCode, cfg.forTask, - cfg.packTopN, forRankShapingFlags( cfg ) } ); + cfg.packTopN, forRankShapingFlags( cfg ), rw::forWidenNext( cfg.forTask ) } ); // §B0: this early return skipped the end-of-function tally below, so a --for --json run redacted // SILENTLY — the one stderr line that tells the user a secret was in their tree never appeared. reportRedactions( stderr, redactCounts ); @@ -3601,7 +3277,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // The disclosure is exempt from the SIG-TRIM allowance in BOTH regimes. Charging it under an // explicit ceiling (the first cut of this change) shrank the sig section BELOW the default // regime's — an explicit ceiling wider than the default re-trimming sigs is exactly the - // inversion forSigSideCeiling exists to forbid (gate: forbudgetmonotoncheck, which caught it). + // inversion the frozen sig share existed to forbid (gate: forbudgetmonotoncheck, which caught it). // The est_tokens<=N promise is kept by the BODY side, not by this trim: committedBytes below // counts the emitted header verbatim, so leftBytes hands the bodies ~240 B less and the bundle // total never crosses the ceiling — charging the sig side as well was double-counting one cost. @@ -3655,7 +3331,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // tokens: 995 vs 816). So the compact charge is capped at the FULL dialect's charge for the same header, computed by the // same rule: rows(default) ⊇ rows(full) by construction, and a default that pays more header than full never gets more // room for rows than full had. - std::size_t compactLedgerGapBytes = 0; // knob-honesty-068: how much MORE sig room this compact header was given than full's + // forbudget-soft: the FULL dialect's charge for this header, in both dialects — a soft target's text tier is the default + // target's sig share as THIS number prices it, so the compact and full answers shrink their rows alike (forSigsBudgets) + std::size_t fullChargedHeaderBytes = chargedHeaderBytes; if( compactLegendOn ) { ForLensHeaderParts fullParts = headerParts; @@ -3669,29 +3347,25 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co + ownerLegendExemptBytes( headerParts, /*compact=*/false ); std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; fullCharged -= std::min( fullCharged, rw::forZeroNoteBytes( fullHeader ) ); // lean-answers: exempt, as above - chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); - // knob-honesty-068, train 26b: the gap is read AFTER both exemptions (the e= clause and lean's zero note) on - // BOTH headers, so it measures only the sig room the compact dialect was given beyond its honest cost - compactLedgerGapBytes = fullCharged - chargedHeaderBytes; // >= 0 by the min above + chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + fullChargedHeaderBytes = fullCharged; } // lego-charge: / cost what they are SERVED as — a collapsing section its stub, not its full render // (rw::forSectionsSigChargePlan states both rules and why --sections= keeps the default's charge without a ceiling). // A section the degrade path will stream unmeasured has no bytes here (legoStr/composeStr empty): blockCharge discloses it. - const bool explicitForCeiling = cfg.tokenBudget > 0; // the lens's only ceiling (--max-tokens bounds --detail bodies) + // forbudget-soft: the DEFAULT charge in both regimes — a soft target charges the rows alone, and the default + // target's share it tiers its text by is the default answer's (forSigsBudgets), sections priced as the default prices them. + const bool forSoftTarget = cfg.tokenBudget > 0; // an explicit --token-budget: a SOFT target over the rows const rw::ForStubbableSection legoChargeIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = legoPreRendered, .renderedBytes = legoStr.size() }; const rw::ForStubbableSection composeChargeIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = composePreRendered, .renderedBytes = composeStr.size() }; - const rw::ForSectionStubPlan sigChargePlan = rw::forSectionsSigChargePlan( cfg.sections, explicitForCeiling, cfg.forTask, legoChargeIn, composeChargeIn ); - const std::size_t fixedBytes = chargedHeaderBytes + sigChargePlan.servedBytes( legoChargeIn, composeChargeIn ) + routeStr.size() + 6; // + "" - // the auto bundle's SECTION SPLIT — the sig side's claim is capped so an explicit ceiling wider - // than the default cannot re-inflate the trimmed sig tail at the bodies' expense (the rule, its - // measured defect and the invariant: forSigSideCeiling above; gate: forbudgetmonotoncheck). - const std::size_t sigSideCeiling = forSigSideCeiling( autoBundleMode, cfg.packTopN, bundleBudget ); + const rw::ForSectionStubPlan sigChargePlan = rw::forSectionsSigChargePlan( cfg.sections, /*explicitCeiling=*/false, cfg.forTask, legoChargeIn, composeChargeIn ); + const std::size_t sectionsChargedBytes = sigChargePlan.servedBytes( legoChargeIn, composeChargeIn ) + routeStr.size() + 6; // + "" + const std::size_t fixedBytes = chargedHeaderBytes + sectionsChargedBytes; // §F1's graph block, rendered HERE (before ) rather than beside the bodies: it reads only lensRank, so the - // bytes are the same wherever it renders, and an explicit ceiling must know them before the sig side claims its - // share (fix round 4: forSigClaimWithTrailingReserve). Emission order is unchanged (detail, then graph). + // bytes are the same wherever it renders. Emission order is unchanged (detail, then graph). rw::ChargedSection graphSection, detailSection; if( cfg.withGraph ) { @@ -3703,42 +3377,17 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co DISCLOSE( blockCharge, ForLensBlockCharge::DisclosureWhy::SiblingBlockUnmeasured, "runForLens: the graph block streams uncharged — est_tokens omitted" ); } } - // fix round 4 (estchargecheck #11 A7): under an EXPLICIT ceiling the graph block and a first --detail body floor are - // reserved out of the sig side's claim, so cannot spend the bytes the two trailing sections will ship. The - // floor is MEASURED — the --detail section rendered at the floor budget (wrapper, the first body, the omitted-bodies - // comment) — not estimated. No explicit ceiling, or neither flag: reserve 0, claim unchanged (byte-identical). const std::vector detailIds = cfg.detail > 0 ? forDetailIds( ing, lensRank, cfg.detail, forTopN ) : std::vector{}; // (score desc, id asc) — same order as the sigs - // - // THE PRE-REGISTERED KILL (fix round 4, reports/lego-charge.md): with BOTH --detail and --with-graph the reserve is OFF. - // Measured on the 32 round-1 questions at --token-budget=2000 --detail=3 --with-graph, reserving both sections cut the - // gold-relevant owner Reactive._set (textual-12, r=8) out of ; header + graph + rows through r=8 + any real first - // body exceed the allowance there, so no floor keeps the row AND meets the budget. That shape keeps the ranked rows' - // claim, the first body still gets its floor (head-cut with next=, recoverable), and the overshoot is disclosed by the - // root's over_ceiling="1" — every cut named, nothing reserved away. Each section alone keeps its reserve (no gold row lost). - const bool reserveTrailing = explicitForCeiling && !( cfg.detail > 0 && cfg.withGraph ); - const std::size_t detailFloorBudget = explicitForCeiling ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; - std::size_t trailingReserve = reserveTrailing ? graphSection.xml.size() : 0u; - if( reserveTrailing && detailFloorBudget > 0 ) - { - // a MEASURE, not a rendering the reader gets: it redacts the same text into a scratch tally, so a secret the - // real render redacts is still counted once - std::optional floorRedact = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; - trailingReserve += rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailFloorBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, floorRedact ? &*floorRedact : nullptr, - /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); }, - rw::kBytesPerTokenBody ).xml.size(); // a degraded measure is 0 bytes: the reserve falls back to the graph alone - } - const std::string forTailNext = forTailCutNextAttr( cfg ); // fix round 4: the explicit-budget tail's cut next= (empty otherwise) - if( explicitForCeiling ) - { - // fix round 4 (rule 5): the two cut disclosures an explicit ceiling owes — the tail's next= and, on the compact - // route, the hops cut (marker or spliced next= + its clause) — are reserved like the trailing sections, so - // disclosing a cut never pushes the answer over the budget it was cut for - trailingReserve += forTailNext.size() + ( plan.compact ? forHopsCutReserve( cfg ) : 0u ); - } - const std::size_t sigClaim = forSigClaimWithTrailingReserve( sigSideCeiling, bundleBudget, trailingReserve ); - const std::size_t sigsBudget = sigClaim > fixedBytes ? sigClaim - fixedBytes : 1; // ≥1: 0 would mean "no budget" + // the first --detail body's floor under a soft target (head-cut with next= when it is over a quarter of the target): the + // target fills the rows first, and the floor keeps the one body the caller asked for from being cut to nothing + const std::size_t detailFloorBudget = forSoftTarget ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; + const std::string forTailNext = forTailCutNextAttr( cfg ); // D-4: a capped 's next= (the file page), every regime + // THE TWO BUDGETS (forbudget-soft; serialize.h kForPayloadBudgetBytes / kForRunawayGuardBytes): the target the + // text-shrink steps aim at, and where the row-drop step aims — the runaway guard (default), or the soft target's rows. + const ForSigsBudgets sigsBudgets = forSigsBudgets( forSoftTarget ? bundleBudget : 0u, fixedBytes, + fullChargedHeaderBytes + sectionsChargedBytes, + /*tierAtDefault=*/autoBundleMode && cfg.packTopN == 0 ); + const std::size_t sigsBudget = sigsBudgets.target; // The two attributes SPLICED into the header AFTER the ceiling ladder has chosen a rung — est_tokens // (" est_tokens=\"NNNNNNNN\"", bounded well under 24 B: 8 digits covers ~100M tokens) and, when the @@ -3761,8 +3410,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const bool compactEstLegend = cfg.legend == "compact"; const std::string_view kForEstTokensLegend = !compactEstLegend ? std::string_view( " est_tokens= prices this bundle in tokens" ) - : forGateBudget && forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens=/max_tokens= the token budget/body ceiling asked" ) - : forGateBudget ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens= the token budget asked" ) + : forGateBudget && forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens=/max_tokens= the soft row target (rows stop once it is reached)/body ceiling asked" ) + : forGateBudget ? std::string_view( " est_tokens= prices this bundle in tokens; budget_tokens= the soft row target asked (rows stop once it is reached)" ) : forBodyCeiling ? std::string_view( " est_tokens= prices this bundle in tokens; max_tokens= the body ceiling asked" ) : std::string_view( " est_tokens= prices this bundle in tokens" ); // F2: spliced ONLY onto a document that actually carries over_ceiling="1" (the attribute is now @@ -3775,7 +3424,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // settled far above at the H9 splice), so the binding is const rather than constexpr. Keyed on the // attributes present, not on which ceiling was exceeded — see overCeilingLegendFor. A budget-only run // still selects kOverCeilingLegend, so nothing about --for --token-budget moves by a byte. - const std::string_view kForOverCeilingLegend = rw::overCeilingLegendFor( forGateBudget, forBodyCeiling ); + // forbudget-soft: budget_tokens= is a soft target, never a ceiling this root is "over" — only max_tokens= is + const std::string_view kForOverCeilingLegend = rw::overCeilingLegendFor( /*namesBudgetTokens=*/false, forBodyCeiling ); const std::size_t headerSpliceReserve = kEstTokensAttrReserve + kForEstTokensLegend.size() + ( forWeak ? kWeakAttrBytes : 0u ); // D10: --token-budget SHAPES this bundle (exit-0 trim) rather than gating it (exit-3 like the @@ -3791,30 +3441,18 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // headerStr is already flushed to stdout by the time that path runs and cannot be edited retroactively // (the same reason est_tokens is "omitted", not "wrong", on that path — see its DISCLOSE). std::size_t forDroppedPositive = 0; - // knob-honesty-068: a capped names the call that serves it uncut (serialize.h SigsCutContinuation, where the - // two regimes are argued): CHARGED exactly when an explicit ceiling's sig side is the whole ceiling (nothing - // downstream can pay, and the ceiling is hard); exempt at the default and when the sig side is frozen at the - // default's share (forSigSideCeiling), so those ranked sets — and forbudgetmonotoncheck's identity — are untouched. - // A charged one is paid from the rows byte for byte (serialize.h paySigsContinuationFromRows), so a capped bundle never - // grows past the bytes the cut alone left it. THE COMPACT DIALECT ALSO PAYS FROM FULL'S ROOM: its sig ledger charges - // less header than it emits and leans on the header rungs to land inside the ceiling, so a charged one also reserves - // the room the compact charge was given beyond full's (compactLedgerGapBytes) — in the plan, so only a block the ladder - // already trims pays it: a PAID capped compact answer serves the rows the full one does and lands no further over its - // ceiling (compactlegendcheck P4). (Rows a PAID full answer would serve: where full's UNPAID answer fits and the compact - // one's does not — a compact header larger than full's, round 3 — full keeps a row the compact answer pays; that is the - // one exemption compactlegendcheck (P1-B) rules, orchestrator option B.) An uncapped answer is untouched. AT THE FLOOR (rank 1..4, nothing left to shed) the - // handle still ships and an overshoot is labelled (rootFinish.sigsUnpaidOver below; orchestrator ruling 2026-10-07). - // AND ONLY WHERE PAYING IS WHAT MAKES IT FIT (ruling C3, completed 2026-10-08): runForLens renders unpaid first - // (sigsNextPayFromRows=false: no ledger gap, no payment — the rows the cut alone leaves) and pays only when that - // answer lands past its ceiling and the paid one does not. - const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; + // knob-honesty-068: a capped names the call that serves it uncut (serialize.h SigsCutContinuation). forbudget-soft: + // it is EXEMPT in every regime now — the default has no byte promise (a target and a runaway guard), and an explicit + // budget is a soft target with none either — so no row is ever dropped to pay for the handle and the lens renders ONCE + // (the C3 unpaid/paid/overrun re-renders are gone with the hard ceiling they priced against). const std::string forRankFlags = forRankShapingFlags( cfg ); - const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, - /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u, - sigsNextPayFromRows, /*overrunBytes=*/forSigsNextCharged ? mode.overrunReserveBytes : 0u }; + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, /*json=*/false, + /*pasteHandle=*/true }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation + forSigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: IN — where step F aims (guard / soft target) + forSigsCut.pastTargetDisclosed = !forSoftTarget; // …and the default's rows past its target are disclosed sigsPreRendered = preRender( [ & ]( std::FILE* sm ) { packSignatures( sm, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, @@ -3903,13 +3541,19 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // forbudgetmonotoncheck pins exactly that identity across ceilings. No "--" in the clause: it rides // inside an XML comment, where a double hyphen is ill-formed (G4). const bool forDefaultCeiling = forSigsCapped && cfg.tokenBudget == 0; + // forbudget-soft: the default answer's ONLY row cut — the runaway guard (a soft target's row stop is no "over") + const bool forGuardCut = forSigsCut.rowDropCut && !forSoftTarget; const std::string sigsCeilingAttr = forDefaultCeiling ? " budget_bytes=\"" + std::to_string( rw::kForPayloadBudgetBytes ) + "\"" : std::string(); // cut-fix lane A: the tag's two cut readings (docs_dropped=, shrunk-not-dropped) ride the same splice, present // only when the tag carries the case (serialize.h sigsCutLegendNotes), so the reserve below already covers them. - const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( rw::kForBudgetBytesNote ) : std::string() ) - + rw::sigsCutReportLegend( forSigsCut ); // + the continuation's and the docs reorder's readings (docs_after_code=) + // forbudget-soft: on an answer whose ranked head ran PAST the target (past_target=), the number shaped row text and + // removed no row — its reading says "target" there; elsewhere the payload fit it and the ceiling reading stays true + const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( forSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ) + : std::string() ) + + rw::sigsCutReportLegend( forSigsCut ) // + the continuation's and the docs reorder's readings (docs_after_code=) + + ( forGuardCut ? rw::forRunawayGuardNote() : std::string() ); // forbudget-soft: the runaway guard's reading, where it cut // ── the INDEXING-cap disclosure (mention.h CapDisclosure), at the same splice point and for the // same reason: a --for header is charged against the payload ceiling, so a disclosure folded into // the notes above is paid for in ranked rows. Measured on sixteen real invocations, that cost three @@ -4021,6 +3665,14 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // both kept alive past the render so the isRendered=false degrade path below re-emits the SAME set at // the SAME budget (the map path's emitSection lambda has the identical contract); detailIds is computed above std::size_t detailBodyBudget = 0; + // forbudget-soft: WHAT THE TRAILING SECTIONS ARE FUNDED FROM (bodies, hops, tail), in the default answer's priority + // order. DEFAULT: the bundle as it stood at the 7.5 KB target — the rows served past it (past_target=) are not charged + // to the sections after them, so no section shrinks because the head was kept whole. SOFT TARGET: the rows already + // served (, the header rows, the fixed sections); the header and every disclosure are never charged. + const std::size_t sigsFundedBytes = sigsStr.size() - std::min( sigsStr.size(), forSoftTarget ? 0u : forSigsCut.pastTargetBytes ); + const std::size_t rowsSpentBytes = forHdrXml.size() + sigsFundedBytes + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size(); + const std::size_t headerSpentBytes = forSoftTarget ? 0u + : headerStr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve; // --detail=N (lever 3): importance-weighted detail — spend FULL bodies on only // the top-N ranked symbols (the head the rank identifies), leaving the rest as the signatures emitted @@ -4039,14 +3691,11 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co detailBodyBudget = cfg.maxTokens > 0 ? rw::budgetBytesForTokens( std::size_t( cfg.maxTokens ) ) : cfg.packBudgetBytes; - if( cfg.tokenBudget > 0 ) + if( forSoftTarget ) { - const std::size_t spentBytes = headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve - + rw::kForFileTailShellReserve + forTailNext.size(); // deep-tail: the shell's reserved bytes + its cut next= (explicit regime only — this branch) - const std::size_t leftBytes = bundleBudget > spentBytes ? bundleBudget - spentBytes : 1; - // fix round 4: never below the first-body floor the sig side reserved (forDetailFirstBodyFloor) — the header's - // exempt disclosures are paid by this side, and they must not cut the one body the reserve was taken for + // the soft target's rows left this much; never below the first body's floor (forDetailFirstBodyFloor: head-cut + // with next= when oversized), so the one body the caller asked for always ships + const std::size_t leftBytes = bundleBudget > rowsSpentBytes ? bundleBudget - rowsSpentBytes : 1; detailBodyBudget = std::min( detailBodyBudget, std::max( leftBytes, detailFloorBudget ) ); } detailSection = rw::chargeSection( [ & ]( std::FILE* f ) @@ -4071,16 +3720,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co ForAutoBodiesResult enrich; // .attr is spliced onto the root after the ladder; its bytes are priced there if( autoBundleMode && sigsPreRendered ) { + // the DEFAULT regime funds the bodies from the bundle at its target (sigsFundedBytes) — the tail rides on top there and + // the bodies stay what they were; a soft target funds them from the rows' leftover (headerSpentBytes is 0) enrich = buildForEnrichment( cfg, ing, g, lensSurfaceIds, lensRank, plan, routeAnchorDefs, redactPtr, - headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve - + ( cfg.tokenBudget > 0 ? rw::kForFileTailShellReserve + forTailNext.size() : 0u ), - // deep-tail: under an explicit ceiling the tail SHELL's bytes are - // reserved ahead of the body walk (the kAutoAttrReserve pattern) so - // the disclosure always fits; the DEFAULT regime reserves nothing — - // the tail rides on top there and the bodies stay byte-identical. + headerSpentBytes + rowsSpentBytes, bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan } ); - if( enrich.surfaceOff || enrich.legendOff ) + if( enrich.surfaceOff ) { headerParts.autoBundle = headerParts.compactBundle = false; headerParts.viaPresent = false; // FE-B: the section that could carry it is gone (autoBundleMode excludes --detail) @@ -4099,10 +3744,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co } const rw::ChargedSection& autoSection = enrich.section; const std::string& autoAttr = enrich.attr; - if( enrich.legendOff && !autoSection.xml.empty() ) + if( enrich.hopsCutMarker ) { - // fix round 4: the hops budget-cut marker rides a header whose compact legend was just dropped — its one - // present-only clause says what the empty element is (priced by the ladder through rootFinish) + // fix round 4: the hops budget-cut marker's present-only clause says what the empty element is sectionsStubNote += headerParts.compactLegend ? kForHopsBudgetCutLegendCompact : kForHopsBudgetCutLegend; } @@ -4114,46 +3758,11 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // ladder rung can only SHRINK the header, so the fit stays conservative and deterministic). const FileTail forFileTailShown = sigsPreRendered ? computeFileTail( ing, lensRank, shownSigIds, flRootArg ) : forFileTail; const std::string tailStr = renderForFileTailXml( forFileTailShown, cfg.tokenBudget, bundleBudget, - headerStr.size() + forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() - + routeStr.size() + graphSection.xml.size() + detailSection.xml.size() - + autoSection.xml.size() + autoAttr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve, - forTailNext ); - - // N1: the last rung's note DEFINES the root attribute it accompanies (over_ceiling= …), so the attribute is - // never on a document whose legend does not explain it; the bracket spelling stays. It is PROSE ONLY now — - // M3 took the verdict off it. finishForLensHeader used to recover "the last rung fired" by finding this - // exact text in the finished header, which the verbatim task echo could also supply. - static constexpr rw::CeilingLadderNotes kNotes{ - " [task_echo: dropped (ceiling)]", " [task_echo + route_attr: dropped (ceiling)]", - " [over_ceiling= is 1 on the root: the header floor (verbatim task echo + fixed legend) exceeds this budget" - " - no payload left to trim]" }; - // fix round 5 (owner ruling 2026-10-09, option B; reports/lego-charge.md): the both-trailing-sections shape under an - // explicit ceiling KEEPS its ranked rows (reserveTrailing above is off) and goes over the budget on purpose — answers - // first, a budget never silently removes an answer. There the header floor is NOT what overshoots (measured: a ~1.8 KB - // header under a 5.4 KB allowance), so blaming it was false. This note names what really rides past the budget. It - // is chosen only when the header as built fits the allowance by itself; a header floor that alone exceeds it keeps - // the note above (true for this shape: estchargecheck #11 A7B-N). Single-flag shapes still get the note above even - // where their header fits — pre-existing, a named follow-up. Spelled without double hyphens: it rides inside the - // header XML comment. - static constexpr rw::CeilingLadderNotes kNotesKeptRows{ - kNotes.echoDropped, kNotes.echoAndRouteDropped, - " [over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections, and with the" - " with-graph block and the first detail body at its floor they exceed this budget - kept, not trimmed]" }; - // train 27 (compactlegendcheck P1-B): every other over-ceiling shape whose header fits the allowance by itself. The header - // floor note above was served there too (lego-charge round 5's named follow-up), and it was false: the overshoot is the - // rows and sections the header carries, each cut of which names its next=. Chosen only when the header as built fits - // the allowance alone (headerAloneFits below). No double hyphens: it rides inside the header XML comment. - static constexpr rw::CeilingLadderNotes kNotesPayloadOver{ - kNotes.echoDropped, kNotes.echoAndRouteDropped, - " [over_ceiling= is 1 on the root: the header alone fits this budget; the rows and sections served with it exceed" - " it]" }; + rowsSpentBytes + detailSection.xml.size() + autoSection.xml.size(), forTailNext ); // PR #135: the priced root this header will carry — every late splice and the est_tokens fixpoint, in - // finishForLensHeader (above runForLens, with each splice's rationale). Assembled BEFORE the ladder so the - // ladder prices the header it will EMIT. NOT const (M3): .lastRungFired is the ladder's answer, written - // from the rung climbCeilingLadderBy returns — it starts false, which is exactly the state of a run whose - // ladder never ran. - ForLensRootFinish rootFinish{ + // finishForLensHeader (above runForLens, with each splice's rationale). + const ForLensRootFinish rootFinish{ .autoAttr = autoAttr, .sigsCeilingAttr = sigsCeilingAttr, .capAttrs = capAttrsStr, @@ -4173,189 +3782,16 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co + enrich.markupBytes + tailStr.size() + 6, // + "" (deep-tail: the tail at the markup rate) // T3: the body-rate sections; enrich.bodyTokens is zero on the compact route (markup, above) .bodyTokens = detailSection.tokens + enrich.bodyTokens, - .tokenBudget = cfg.tokenBudget, + // forbudget-soft: a soft target is never "over" — budget_tokens= names a target, so it is not a ceiling this root + // labels against; max_tokens= (the detail bodies' hard ceiling) still is, and the runaway guard's cut is the default's + .tokenBudget = 0, .maxTokens = cfg.maxTokens, - .bodyCeiling = forBodyCeiling }; - - // W3FIX H2 — the ceiling ladder (rungs + rationale: serialize.h climbCeilingLadder), same rungs in the - // same order --pack-task climbs. The header IS charged to the budget above, but charging is not FITTING: at - // an explicit --token-budget the header floor (fixed legend + the task echoed twice) can exceed the stated - // ceiling by itself, sigsBudget then clamps to 1, and packSignatures' first-entry-whole floor still emits - // ~1.5 KB — a silent 5.3x overrun on a 900-char task, against a --help that promises "trims to fit". Runs - // BEFORE est_tokens so the estimate covers the disclosure; inert without an explicit --token-budget. - if( cfg.tokenBudget > 0 && sigsPreRendered ) - { - // §F1: the ladder prices what will actually be EMITTED, so the two sections charged above are in - // this sum. headerSpliceReserve covers the est_tokens (and weak="1") attributes spliced in below — - // see its definition for why a reserve rather than a measurement. - const std::size_t ladderPayloadBytes = forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() + routeStr.size() - + detailSection.xml.size() + autoSection.xml.size() + graphSection.xml.size() - + tailStr.size() // deep-tail: the tail is priced like every other section - + autoAttr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve; // + "" + the header splices below (autoAttr exact-counted; A2's own reserve is exact too) - const std::size_t ladderCeiling = rw::ceilingAllowanceBytes( cfg.tokenBudget ); - // PR #135: the ALLOWANCE shape FITS when the reserve-priced header above does AND the header it would - // emit does — finishForLensHeader's output plus every other byte stdout receives (the body section is - // --detail's, else the rendered auto one). The first half is the pre-#135 test verbatim, so a bundle - // that was inside its allowance picks the same shape byte for byte. The second half is what the reserve - // cannot see: over_ceiling="1" and its legend clause (70 B), owed whenever est_tokens exceeds - // budget_tokens. Both halves are BYTES, because the allowance is a byte bound; the exact ceiling below - // is a token comparison and shares neither sum. - const std::size_t emittedNonHeaderBytes = forHdrXml.size() + sigsStr.size() + legoStr.size() + composeStr.size() + routeStr.size() + tailStr.size() - + detailSection.xml.size() + ( autoSection.isRendered ? autoSection.xml.size() : 0u ) - + graphSection.xml.size() + 6; // + "" - // The 70 B of over_ceiling="1" + the clause defining it are priced by finishForLensHeader itself, from - // the TOKEN comparison (est_tokens > budget_tokens) it makes on every candidate — so a candidate that - // will carry the label is measured carrying it. Nothing here decides that from text: the ladder's own - // verdict used to be recovered inside finishForLensHeader by looking for the rung's note in the - // candidate, which the verbatim task echo could supply, and injected text then added those bytes to - // EVERY rung's price and could push an honest bundle down the ladder for no reason. - // `rootFinish.lastRungFired` is FALSE here and stays false, because it is the ladder's answer and the - // ladder has not answered yet. M3's first version passed the candidate's rung in and set the flag from - // `rung == OverCeiling`, which no call could ever satisfy — the terminal rung is the branch that never - // asks `fits` — so the flag was invariantly false while a comment claimed it was doing the pricing. - // THE ALLOWANCE test — raw bytes, which is its contract: ceilingAllowanceBytes is a BYTE bound - // (budget x 2.36 x 1.15), the widest thing this lens may deliver, and rungs (b)/(c)/(d) are judged - // by it (serialize.h climbCeilingLadderBy). Both halves: the reserve-priced sum above AND the header - // that will actually be emitted, because the reserve cannot see over_ceiling="1" and its clause (70 B), - // owed whenever est_tokens exceeds budget_tokens and priced by finishForLensHeader's own fixpoint. - const auto fitsCeiling = [ & ]( std::string_view candidate ) - { - return candidate.size() + ladderPayloadBytes <= ladderCeiling - && finishForLensHeader( std::string( candidate ), rootFinish ).size() + emittedNonHeaderBytes <= ladderCeiling; - }; - // …AND THE EXACT CEILING IS NOT A BYTE TEST AT ALL (PR #215 review: "use mixed-rate accounting for the - // exact-ceiling test"). What the root PROMISES is `est_tokens <= budget_tokens`, and est_tokens is not - // bytes/2.50: finishForLensHeader prices MARKUP at kBytesPerTokenDefault and the --detail / auto BODIES - // at kBytesPerTokenBody (3.80), one rate per kind. Comparing the raw byte total against - // ceilingBytes( budget ) charges every body byte at 2.50, i.e. 1.52x what the root charges it, and it - // also re-prices the candidate through `ladderPayloadBytes` — a sum built from RESERVES and from the - // auto section whether or not that section is rendered, so it is not the document stdout receives. - // Both errors point the same way: a document whose root says it fits was judged not to. MEASURED on a - // git-less 5-symbol fixture at --detail=1 (test/estchargecheck.sh #18): at every budget in 1069..1099 - // the kept document priced at est_tokens=1069 with no over_ceiling=, and rung zero dropped all three - // clauses anyway and delivered est_tokens=715 — 354 tokens of headroom spent to buy nothing, and three - // definitions the reader no longer has. At 1090..1099 even the raw byte total fitted (2 736 B of 2 737) - // and only the reserve-priced half vetoed. - // ONE QUESTION, ASKED ONCE: build the candidate's FINAL document and read the number it will print. - // No second fixpoint, no second rate — the disagreement is only possible while two expressions exist. - // The maxTokens/--detail body ceiling is deliberately NOT folded in: this rung has always been about - // the --token-budget ceiling, forLensOverCeiling answers the wider question on the finished document, - // and widening a rung is a change to what it drops, not a fix to how it prices. - const auto fitsExactCeiling = [ & ]( std::string_view candidate ) - { - return finishForLensHeaderPriced( std::string( candidate ), rootFinish ).estTokens <= cfg.tokenBudget; - }; - // RUNG ZERO — the confidence LEGEND clause, before any of the ladder's own rungs: it is the one - // header string whose loss costs NO unique information (confidence=/margin_pct= stay on the root - // as facts; only their explanation goes), so it must fall before the verbatim task echo does — - // the L1 "first rung that costs unique information" ordering. Same silent-legend-drop shape as - // the enrichment legendOff above (the ATTRIBUTE is the disclosure that survives a spent - // ceiling). Cleared on headerParts itself so every later ladder rebuild stays note-free. - // Measured need: fornotesbudgetcheck's 850-ceiling fixture holds 35 tokens of headroom and the - // clause is ~55 — charged-not-exempt (the explicit-regime split above) still cannot fit it, - // because the floor there is notes + first-entry-whole, neither of which may trim. - // L1: and it DISCLOSES itself now, like rungs (b) and (c) do — legendDropped splices - // kForLegendDroppedNote, naming the attributes whose definitions just went. Silence here was the - // reader seeing confidence= margin_pct= budget_tokens= r= and the counts with nothing in the - // legend about any of them and no way to tell a budget cut from a feature that does not exist. - // RUNG ZERO TRIGGERS ON THE EXACT CEILING, not on the 1.15 allowance rungs (c)/(d) are judged by - // (2026-09-13, PR #215). kCeilingFirstEntryTolerance exists for the residual a lens cannot trim — a first - // signature that is not divisible — and it was also gating this rung, so a document 1..15% over its budget - // that STILL carried three droppable clauses shipped over_ceiling=1 with all three riding - // (fornotesbudgetcheck's 1640 rung at est_tokens=1755, forrootlegendcheck's 800 rung at 831, both CI). - // …AND "EXACT" IS THE ROOT'S OWN RATE, not the densest-language one (review item 1). This test priced at - // kMinBytesPerToken(2.36) while est_tokens=/over_ceiling= price at kBytesPerTokenDefault(2.50), so the rung - // fired on documents 6% INSIDE the budget their root reports: `test/cppqualfix --for="widget ping make box" - // --token-budget=1200` printed est_tokens="778", no over_ceiling=, and had dropped all three clauses. - // …AND THE RATE WAS ONLY HALF OF IT: a BYTE ceiling cannot express this root's promise at all, because - // est_tokens is two rates (markup 2.50, bodies 3.80) over a document the byte sum also mis-assembles - // from reserves. fitsExactCeiling above is the token comparison itself now; see its own note. - // - // ONE DROPPABLE BIT. These three fields moved in lock step at every read and write, and the guard that used - // to test all three was true whenever any clause remained, which is the only state this branch is reached - // in — an always-true condition documenting itself as a check. legendDroppable() asks the one question. - // - // AND THE DROP MUST PAY (review item 3). Rung zero removes readings and splices the note that names - // them, and in the COMPACT dialect the note is longer than the clauses: measured on the pre-fix binary, - // 110-164 B of clauses went out and a 161 B note came in — +51 B net on a route-less answer, so the rung - // made the very document it was trying to shrink BIGGER while costing the reader three definitions. The - // candidate is built and compared; a drop that does not pay is not taken, and the readings simply ride. - // This is the assertion, not a comment about one: there is no shape in which the byte-negative branch - // can be reached, because the branch is the comparison. - // train 27 (compactlegendcheck P1-B): THE ROWLESS-SECTION CLAUSES go before any rung that costs information. The - // compact dialect decides its (compose) and (lego) readings BEFORE the payload, on an over-approximation - // (composePresent / legoPresent: a ranked symbol with a compose edge / implementors). By now the stub decision is made: - // a section served as its counted stub, or not at all, prints none of those rows, so the clause defines nothing in this - // document. Under a spent exact ceiling it goes and no note is owed — no printed attribute loses its reading. A document - // inside its ceiling keeps every byte; a section streamed unmeasured (the degrade path) keeps its clause. - if( headerParts.compactLegend && !fitsExactCeiling( headerStr ) - && headerParts.dropRowlessSectionReadings( composePreRendered && ( composeWillStub || composeStr.empty() ), - legoPreRendered && ( legoWillStub || legoStr.empty() ) ) ) - { - headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); - } - if( headerParts.legendDroppable() && !fitsExactCeiling( headerStr ) ) - { - const ForLensHeaderParts keptParts = headerParts; - headerParts.dropDroppableLegend(); - std::string droppedHeader = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); - if( droppedHeader.size() < headerStr.size() ) - { - headerStr = std::move( droppedHeader ); - } - else - { - headerParts = keptParts; // byte-negative: keep the readings and say nothing - } - } - // M3: the ladder hands back the rung it took. That value — never a search of the emitted text — is - // what puts over_ceiling="1" on the root below. - // fix round 5: the last rung's reason must be TRUE — the header floor only when the header alone overruns. - // train 27 (compactlegendcheck P1-B): …in every shape, not only the both-trailing-sections one: a header that fits - // its allowance by itself gets the reason that IS true (kNotesPayloadOver), never the header floor. And the compact - // dialect never echoes the task in its comment (appendCompactForLegend), so its echo rung drops nothing and claims - // nothing (the --pack-task FX4 rule): rung (b) is its header as built, rung (c) names only the route attribute. - // "Alone" is measured on the header AS THE LAST RUNG WOULD SERVE IT: this note, every late splice, the over_ceiling - // label and (when the handle rides unpaid) its clause — the bytes a reader counts up to , never a pre-splice draft. - ForLensRootFinish lastRungFinish = rootFinish; - lastRungFinish.lastRungFired = true; - lastRungFinish.sigsUnpaidOver = forSigsCut.continuationUnpaidBytes > 0; - const bool keptShape = explicitForCeiling && !reserveTrailing; - const std::string_view fitsNote = keptShape ? kNotesKeptRows.overCeiling : kNotesPayloadOver.overCeiling; - const bool headerAloneFits = - finishForLensHeader( buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, fitsNote ), lastRungFinish ).size() + 6 - <= ladderCeiling; - const rw::CeilingLadderNotes ladderNotes = forLadderNotes( headerParts.compactLegend, kNotes, headerAloneFits ? fitsNote : kNotes.overCeiling ); - const std::string ladderInputHeader = headerStr; // train 27: the as-built shape the overrun is measured on - rw::CeilingLadderChoice chosen = rw::climbCeilingLadderBy( buildForHeader, headerStr, fitsExactCeiling, fitsCeiling, - /*hasRouteAttr=*/!routeNoteRaw.empty(), ladderNotes ); - headerStr = std::move( chosen.header ); - rootFinish.lastRungFired = ( chosen.rung == rw::CeilingRung::OverCeiling ); - // knob-honesty-068 (orchestrator rulings 2026-10-07): the recovery handle ALWAYS ships. Read off the FINISHED - // document (the header this ladder chose + every byte stdout receives): does it land past its ceiling — the byte - // allowance, or the token budget the root names? The label only adds bytes, so a document over before it is over - // after it. - const ForLensPricedHeader asChosen = finishForLensHeaderPriced( headerStr, rootFinish ); - const bool landsOver = asChosen.header.size() + emittedNonHeaderBytes > ladderCeiling - || asChosen.estTokens > cfg.tokenBudget; - // C3 (completed): a capped, charged answer that lands past its ceiling in THIS payment mode is not served from this - // run — nothing has reached stdout yet: hand back and let runForLens try the other mode (unpaid → paid: does paying - // make it fit? paid → unpaid: it did not, so no row is dropped for the handle). - if( sigsNextTryOther != nullptr && forSigsNextCharged && forSigsCut.hasContinuation && ( landsOver || rootFinish.lastRungFired ) ) - { - *sigsNextTryOther = true; - // …but never on the both-trailing-sections shape: there the ranked rows are kept and the answer goes over on - // purpose (owner ruling 2026-10-09, option B; kNotesKeptRows), so no row is dropped to pay an overrun - if( mode.overrunBytesOut != nullptr && !keptShape ) - { - *mode.overrunBytesOut = forUnlabelledOverrunBytes( ladderInputHeader, rootFinish, cfg.tokenBudget, forSigsCut.continuationPaidSurplus ); - } - return std::nullopt; - } - // the handle rode unpaid (the floor could not give its bytes up, or this is the unpaid run) and the document lands - // past its ceiling: over_ceiling="1" + kForSigsUnpaidOverCeilingNote - rootFinish.sigsUnpaidOver = forSigsCut.continuationUnpaidBytes > 0 && landsOver; - } + .bodyCeiling = forBodyCeiling, + .guardOver = forGuardCut }; + + // forbudget-soft: no header ladder. The W3FIX ceiling ladder (task echo / route= / legend clauses dropped "(ceiling)", + // over_ceiling's header-floor note) existed to land a HARD ceiling; an explicit --token-budget is a soft target over the + // rows now (owner decision 2026-10-14), so the header — every legend clause and the verbatim task echo — always ships. headerStr = finishForLensHeader( std::move( headerStr ), rootFinish ); std::fwrite( headerStr.data(), 1, headerStr.size(), stdout ); @@ -4370,6 +3806,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { rw::SigsCutReport degradeSigsCut; // knob-honesty-068: carries the continuation request IN, as on the buffered path degradeSigsCut.continuationRequest = &forSigsNext; + degradeSigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: the same two budgets + degradeSigsCut.pastTargetDisclosed = !forSoftTarget; packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, rw::forLensRules( forDocsAfterCode, forEndLines ), nullptr, nullptr, nullptr, // the degrade path selects identically @@ -4448,15 +3886,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co return std::nullopt; } -// The --for lens (orchestrator ruling C3, completed 2026-10-08: drop a row to pay for a charged next= ONLY when dropping -// it is what makes the answer fit). The ranking runs once; the render runs in up to three payment modes over it, each deciding -// on its FINISHED document before the first byte reaches stdout: -// 1. unpaid: every row the cut leaves + the handle. Fits (or nothing is capped and charged): served, no row dropped. -// 2. paid: rows dropped to pay for the handle. Fits: served, because paying is what made it fit. -// 3. unpaid again, served whatever its size: it cannot fit either way, so it keeps the cut's rows, the handle rides unpaid, -// over_ceiling="1" + kForSigsUnpaidOverCeilingNote. -// So the served answer never has fewer rows than an alternative that also fits. The redaction tally is put back to its entry -// value before every render, so the summary counts the rows written once. A render that hands back wrote nothing. +// The --for lens: the ranking runs once, the render once (forbudget-soft: no payment-mode re-renders — the lens has no hard +// ceiling left to land a charged continuation under). std::optional runForLens( const MainDispatch& d ) { const rw::Config& cfg = d.cfg; @@ -4469,52 +3900,7 @@ std::optional runForLens( const MainDispatch& d ) // the at= stamp (gitstamp.h): one read per run — every payment-mode render below shares it, like the ranking const bool forSingleRoot = d.ing.realPaths.empty() && cfg.roots.size() == 1; const std::string forAtStamp = forSingleRoot ? rw::gitstamp::stampAt( d.root ) : std::string(); - // only an explicit --token-budget charges the handle (forSigsNextCharged), so only then can a render hand back - if( cfg.tokenBudget == 0 ) - { - return runForLensPass( d, std::move( lr ), ForLensPassMode{ .sigsNextPayFromRows = true, .forAtStamp = forAtStamp } ); - } - const rw::RedactCounts tallyAtEntry = d.redactCounts; - bool unpaidOver = false; - std::optional rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = false, .sigsNextTryOther = &unpaidOver, .forAtStamp = forAtStamp } ); - if( !unpaidOver ) - { - return rc; - } - ASSUME( !rc ); // the unpaid render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - bool paidOver = false; - std::size_t overrunBytes = 0; - const bool compactPays = cfg.legend == "compact"; - rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = true, .sigsNextTryOther = &paidOver, .forAtStamp = forAtStamp, - .overrunBytesOut = compactPays ? &overrunBytes : nullptr } ); - if( !paidOver ) - { - return rc; - } - ASSUME( !rc ); // the paid render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - // train 27 (compactlegendcheck P1-B): THE COMPACT DIALECT ALSO PAYS ITS HEADER. Its sig ledger is capped at the full - // dialect's header charge (runForLensPass, the L1 fix round: rows(default) ⊇ rows(full)), so the bytes its header emits - // beyond that charge are paid by nobody; lego-charge charged the stubbed / as served, which removed the - // slack that used to absorb them, and the paid compact answer landed past budgets the full answer fits. Where paying the - // handle is not enough, pay the MEASURED overrun from the rows too — once, and serve it only when it then fits (C3: no row is - // dropped unless that buys the fit; the rows it drops are the ruled exemption, compactlegendcheck P1-B). The full dialect's - // ledger is not capped — it charges its own header, less the disclosures both dialects exempt — so this payment is the - // compact dialect's only, and a full answer past its ceiling after paying keeps C3's unpaid fallback below, unchanged. - if( compactPays && overrunBytes > 0 ) - { - bool overrunOver = false; - rc = runForLensPass( d, lr, ForLensPassMode{ .sigsNextPayFromRows = true, .sigsNextTryOther = &overrunOver, .forAtStamp = forAtStamp, - .overrunReserveBytes = overrunBytes } ); - if( !overrunOver ) - { - return rc; - } - ASSUME( !rc ); // the overrun-paying render that handed back wrote nothing - d.redactCounts = tallyAtEntry; - } - return runForLensPass( d, std::move( lr ), ForLensPassMode{ .sigsNextPayFromRows = false, .forAtStamp = forAtStamp } ); + return runForLensPass( d, std::move( lr ), ForLensPassMode{ .forAtStamp = forAtStamp } ); } std::optional runTargetedViews( const MainDispatch& d ) @@ -4773,6 +4159,7 @@ std::optional runPackTask( const MainDispatch& d ) PackTaskInputs in; in.budgetTokens = cfg.tokenBudget; // F5: stays std::size_t end-to-end (0 ⇒ the shared default) + in.softTarget = cfg.tokenBudget > 0 && cfg.partitionCount == 0; // forbudget-soft: the caller's budget is a soft target in.compactLegend = cfg.legend == "compact" && !cfg.json; // L1 fix round: the ladder prices what the compact layer delivers in.sigLadderBudgetBytes = cfg.packBudgetBytes; in.compress = cfg.compress; diff --git a/test/budgetpolicycheck.sh b/test/budgetpolicycheck.sh index 81c0c46af..8efa5a752 100755 --- a/test/budgetpolicycheck.sh +++ b/test/budgetpolicycheck.sh @@ -274,7 +274,7 @@ echo "=== (D) over_ceiling= is 1 exactly when est_tokens exceeds a ceiling named # named on that root, and whether it says over_ceiling. Comments are stripped: a legend DEFINES these names # and a definition is not a measurement. Prints nothing when the document names no ceiling at all. root_nums() { - python3 - "$1" <<'PY_EOF' + python3 - "$1" "${2:-}" <<'PY_EOF' import re, sys t = re.sub( r"", "", open( sys.argv[1], encoding = "utf-8", errors = "replace" ).read(), flags = re.S ) # XML dialects put every root attribute in the first start-tag; --recall's is a PROSE header line, and its @@ -289,24 +289,40 @@ def num( name ): g = re.search( r'\b%s="?(\d+)"?' % name, root ) return int( g.group( 1 ) ) if g else None est = num( "est_tokens" ) -ceils = [ c for c in ( num( "max_tokens" ), num( "budget_tokens" ), num( "budget" ) ) if c ] +# forbudget-soft: on a SOFT lens (argv[2] == "soft": an explicit budget on --for / --pack-task / --from-trace) budget_tokens= is a +# target, not a ceiling — only max_tokens= is one there. The target must still be NAMED (arm (B)'s echo): a missing one is a miss. +soft = len( sys.argv ) > 2 and sys.argv[ 2 ] == "soft" +if soft and num( "budget_tokens" ) is None: + print( "NOBUDGET" ); raise SystemExit +ceils = [ c for c in ( num( "max_tokens" ), None if soft else num( "budget_tokens" ), None if soft else num( "budget" ) ) if c ] +if soft and est is not None and not ceils: + print( "%d 0 %d" % ( est, 1 if re.search( r'\bover_ceiling="?1"?', root ) else 0 ) ); raise SystemExit over = 1 if re.search( r'\bover_ceiling="?1"?', root ) else 0 if est is None or not ceils: raise SystemExit print( "%d %d %d" % ( est, min( ceils ), over ) ) PY_EOF } -label_probe() { # label_probe LABEL ARGS... +label_probe() { # label_probe LABEL ARGS... (LABEL starting "soft:" = a soft-target lens, see root_nums) local label="$1"; shift + local mode="" + case "$label" in soft:*) mode="soft"; label="${label#soft:}";; esac "$BIN" . "$@" >"$TMP/lab.out" 2>/dev/null - local nums; nums="$( root_nums "$TMP/lab.out" )" - if [ -z "$nums" ]; then - no "(D) $label: the root names no ceiling and no price — arm (B)'s subject vanished, fix this probe" + local nums; nums="$( root_nums "$TMP/lab.out" $mode )" + if [ -z "$nums" ] || [ "$nums" = "NOBUDGET" ]; then + no "(D) $label: the root names no ceiling/target and no price — arm (B)'s subject vanished, fix this probe" return fi # shellcheck disable=SC2086 set -- $nums local est="$1" ceil="$2" over="$3" + if [ "$mode" = soft ] && [ "$ceil" = 0 ]; then + # forbudget-soft (rule 4, the replacement for the by-design break): a soft target is never "over" — no label, + # whatever the price; budget_tokens= still names it (checked in root_nums) + if [ "$over" = 0 ]; then ok "(D) $label: soft target, est=$est — no over_ceiling (budget_tokens= names a target, not a ceiling)" + else no "(D) $label: over_ceiling=\"1\" on a soft target (est=$est)"; fi + return + fi if [ "$est" -gt "$ceil" ] && [ "$over" = 0 ]; then no "(D) $label: root prices $est tokens against a ceiling of $ceil and says NOTHING (over_ceiling absent)" elif [ "$est" -le "$ceil" ] && [ "$over" = 1 ]; then @@ -317,13 +333,15 @@ label_probe() { # label_probe LABEL ARGS... } # Every budgeted verb, through BOTH front doors where it has two, at a ceiling that BINDS and one that does # not — the pair is the point: a verb that labels everything passes the first half and fails the second. -label_probe "--for --token-budget=600" '--for=pagerank power iteration' --token-budget=600 -label_probe "--for --token-budget=1600" '--for=pagerank power iteration' --token-budget=1600 -label_probe "--for --token-budget=8000" '--for=pagerank power iteration' --token-budget=8000 -label_probe "--pack-task --token-budget=600" '--pack-task=pagerank power iteration' --token-budget=600 -label_probe "--pack-task --token-budget=8000" '--pack-task=pagerank power iteration' --token-budget=8000 -label_probe "--from-trace --token-budget=200" "--from-trace=$TMP/trace.txt" --token-budget=200 -label_probe "--from-trace --token-budget=8000" "--from-trace=$TMP/trace.txt" --token-budget=8000 +# forbudget-soft (owner decision 2026-10-14): the task lenses' explicit --token-budget is a SOFT target ("soft:" probes: +# never labelled, still named); the family law above holds for every hard ceiling (max_tokens=, --handoff, --recall). +label_probe "soft:--for --token-budget=600" '--for=pagerank power iteration' --token-budget=600 +label_probe "soft:--for --token-budget=1600" '--for=pagerank power iteration' --token-budget=1600 +label_probe "soft:--for --token-budget=8000" '--for=pagerank power iteration' --token-budget=8000 +label_probe "soft:--pack-task --token-budget=600" '--pack-task=pagerank power iteration' --token-budget=600 +label_probe "soft:--pack-task --token-budget=8000" '--pack-task=pagerank power iteration' --token-budget=8000 +label_probe "soft:--from-trace --token-budget=200" "--from-trace=$TMP/trace.txt" --token-budget=200 +label_probe "soft:--from-trace --token-budget=8000" "--from-trace=$TMP/trace.txt" --token-budget=8000 label_probe "--from-trace --max-tokens=200" "--from-trace=$TMP/trace.txt" --max-tokens=200 label_probe "--from-trace --max-tokens=8000" "--from-trace=$TMP/trace.txt" --max-tokens=8000 label_probe "--handoff --token-budget=100" --handoff --token-budget=100 diff --git a/test/estcalib.manifest b/test/estcalib.manifest index 36099e7d0..85dcd64e1 100644 --- a/test/estcalib.manifest +++ b/test/estcalib.manifest @@ -11,6 +11,6 @@ expand 541 542 455 --expand=billableTotal # for-named-noe is its e=-free twin (RIPWIRE_FOR_ENDLINES=never, byte-identical to the pre-e= answer). Other rows: 2026-09-12 pins. for-named 962 958 1290 --for=billableTotal for-named-noe 898 893 1195 RIPWIRE_FOR_ENDLINES=never --for=billableTotal -# for-budgeted re-pinned 2026-10-08 on the train 26b tree (knob-honesty's next= + floor clause, measured with pin.py; other rows kept). -for-budgeted 491 486 698 --for=dedupe --token-budget=400 +# for-budgeted re-pinned 2026-10-14 (forbudget-soft: an explicit budget is a soft row target — the header is no longer shed to fit, measured with pin.py; other rows kept). +for-budgeted 855 849 1201 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 9f0faf355..a3b6556b2 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -187,10 +187,23 @@ for q in "${REPO_Q[@]}"; do out="$( "$BIN" . --for="$q" 2>/dev/null )" now="$( printf '%s' "$out" | wc -c | tr -d ' ' )" marker="$( printf '%s' "$out" | grep -o ']*>' | head -1 )" - pct="$( python3 -c "print( '%+.2f' % ( ( $now - $base ) * 100.0 / $base ) )" )" - printf ' q%-2d %-28s %5d → %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$pct" "$marker" - if [ "$now" -gt $(( base * 104 / 100 )) ]; then - no "(3) repo q$i '$q': $now B > 1.04 × $base B"; growth_fail=1 + # forbudget-soft (stop when complete; PROCESS rule 5's stair-step target): rows served past the 7.5 KB target are disclosed + # (: the last K rows) and are what the answer needs, so the band applies to the answer WITHOUT them — + # every byte over 4% must be a disclosed past-target row, or a row past the old 1,000 B allowance (decision D-3: + # the default serves every hop row), never anything else + past="$( printf '%s' "$out" | python3 -c ' +import re, sys +s = sys.stdin.read(); m = re.search( r"]*)>(.*?)", s, re.S ) +k = re.search( r"past_target=\"([0-9]+)\"", m.group( 1 ) ) if m else None +rows = sorted( ( int( re.search( r"\br=\"([0-9]+)\"", d.group( 1 ) ).group( 1 ) ), len( d.group( 0 ).encode() ) ) + for d in re.finditer( r"]*)>.*?", m.group( 2 ) if m else "", re.S ) ) +past = sum( b for _, b in rows[ len( rows ) - int( k.group( 1 ) ): ] ) if k else 0 +h = re.search( r"", s, re.S ) # D-3: the compact is no longer cut at its 1,000 B allowance +print( past + max( 0, len( h.group( 0 ).encode() ) - 1000 ) if h else past )' )" + pct="$( python3 -c "print( '%+.2f' % ( ( $now - $past - $base ) * 100.0 / $base ) )" )" + printf ' q%-2d %-28s %5d → %5d - past-target/hops %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$past" "$pct" "$marker" + if [ $(( now - past )) -gt $(( base * 104 / 100 )) ]; then + no "(3) repo q$i '$q': $now B - $past B past-target/hop rows > 1.04 × $base B"; growth_fail=1 fi done # nine fixture bundles measured on the pre-fix binary (git-less copies; d5ac29a7) @@ -423,11 +436,14 @@ esac # ── (9) A CAPPED NAMES THE CALL THAT SERVES IT UNCUT (knob-honesty-068; PROCESS rule 5) ─────────────────── # The ladder disclosed its cut (shown= total= capped="1") and named no call that serves the cut rows: the bundle's only # next= was the r=1 row's (a body, or the widening page). Fixture: 40 matching C functions with long docs and long -# parameter lists, so the DEFAULT ceiling drops rows. The tag must carry next= (+ its legend clause), pasting it must -# serve all total= rows uncut in the same rank order, the shown rows must be its prefix. Under a tight explicit (hard) -# ceiling it is CHARGED and TERSE: next= alone, and est_tokens stays within budget_tokens. At an explicit ceiling wide enough that -# the sig side is frozen at the default's it is EXEMPT: the block is byte-identical to the default's. JSON + MCP. -# Negatives: an uncapped tag (arm 8's fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). +# parameter lists, past the default target. The tag must carry next= (+ its legend clause), pasting it must serve all total= +# rows uncut in the same rank order, the shown rows must be its prefix. JSON + MCP. Negatives: an uncapped tag (arm 8's +# fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). +# forbudget-soft (owner decisions 2026-10-14; rule 4 replacements): the DEFAULT ships the whole head — the target only shrinks +# row text (capped="1" with shown == total, past_target= the rows past it), so its next= serves the rows UNSHRUNK and it has no +# resume index (nothing was dropped). The row CUT this arm's resume-index and paste arms need is an explicit soft target's: +# --token-budget=1500 is exempt now (next= with its clause and next_offset=, no over_ceiling — a soft target is never "over"), +# and a wide target (8000) serves the default's rows rather than a frozen byte-identical block. mkdir -p "$TMP/fxcut" python3 - "$TMP/fxcut" <<'PY' import sys @@ -454,6 +470,7 @@ out = { "capped": "capped=\"1\"" in tag, "shown": int( sh.group( 1 ) ) if sh els "clause": "[sigs next=:" in s[ : s.find( "]*>", s ); r = root.group( 0 ) if root else "" for k in ( "est_tokens", "budget_tokens" ): @@ -462,10 +479,10 @@ out[ "over" ] = "over_ceiling=\"1\"" in r print( json.dumps( out ) )'; } cfield(){ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); v=d.get(sys.argv[2]); print(json.dumps(v) if isinstance(v,(list,dict,bool)) or v is None else v)' "$1" "$2"; } C1="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-cache 2>/dev/null | cut_arm )" -if [ "$( cfield "$C1" capped )" = true ] && [ "$( cfield "$C1" shown )" -lt "$( cfield "$C1" total )" ]; then - ok "(9) presence guard: the default ceiling drops rows ($( cfield "$C1" shown ) of $( cfield "$C1" total ))" +if [ "$( cfield "$C1" capped )" = true ] && [ "$( cfield "$C1" shown )" = "$( cfield "$C1" total )" ] && [ "$( cfield "$C1" past_target )" -gt 0 ]; then + ok "(9) presence guard: the default ships the whole head past its target, shrunk ($( cfield "$C1" shown ) of $( cfield "$C1" total ), past_target=$( cfield "$C1" past_target ))" else - no "(9) presence guard: the fixture is not cut at the default ceiling: $C1" + no "(9) presence guard: the fixture's default is not a whole head shrunk past its target: $C1" fi NX1="$( cfield "$C1" next )" case "$NX1" in @@ -473,12 +490,11 @@ case "$NX1" in *) no "(9) the capped carries next='$NX1' (want --for='gadget assembler' --signatures-only --token-budget=N)" ;; esac if [ "$( cfield "$C1" clause )" = true ]; then ok "(9) the [sigs next=: …] clause defines it ahead of "; else no "(9) rides with no legend clause"; fi -# the resume index (the #362 candidate page's next_offset): the last printed row's candidate rank, defined by its own clause -if [ "$( cfield "$C1" next_offset )" = "$( cfield "$C1" max_r )" ] && [ "$( cfield "$C1" next_offset )" = "$( cfield "$C1" shown )" ] \ - && [ "$( cfield "$C1" offset_clause )" = true ]; then - ok "(9) next_offset=\"$( cfield "$C1" next_offset )\" is the resume index: the last printed row's rank (= shown, no pseudo slot here), defined" +# nothing dropped, so no resume index (the explicit cut below carries one) +if [ "$( cfield "$C1" next_offset )" = null ] && [ "$( cfield "$C1" offset_clause )" = false ]; then + ok "(9) the default's complete head carries no next_offset= (no row was dropped)" else - no "(9) next_offset='$( cfield "$C1" next_offset )' vs last printed r=$( cfield "$C1" max_r ), shown=$( cfield "$C1" shown ), clause=$( cfield "$C1" offset_clause )" + no "(9) the default's complete head carries next_offset='$( cfield "$C1" next_offset )' (clause $( cfield "$C1" offset_clause ))" fi # paste it (shlex-split, as an agent would) pasted(){ python3 -c 'import shlex,sys; print( "\0".join( shlex.split( sys.argv[1] ) ), end = "" )' "$1" > "$TMP/nx.argv"; ( cd "$TMP" && xargs -0 "$BIN" "$2" --no-cache < "$TMP/nx.argv" 2>/dev/null ); } @@ -491,20 +507,25 @@ fi python3 -c 'import json,sys; a=json.loads(sys.argv[1])["names"]; b=json.loads(sys.argv[2])["names"]; sys.exit(0 if a and b[:len(a)]==a else 1)' "$C1" "${C2:-{\"names\":[]\}}" 2>/dev/null \ && ok "(9) the cut block's rows are a prefix of the continuation's (same ranked list, same order)" \ || no "(9) the continuation does not extend the cut block's rows in order" -# tight explicit ceiling: CHARGED — still named, and est_tokens stays within the hard ceiling the root names +# a tight explicit (SOFT) target: the row cut — named with its clause, the resume index defined, never "over" C3="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=1500 --no-cache 2>/dev/null | cut_arm )" NX3="$( cfield "$C3" next )" -{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] && [ "$( cfield "$C3" next_offset )" = null ] \ - && [ "$( cfield "$C3" clause )" = false ] && [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ]; } \ - && ok "(9) --token-budget=1500: charged and terse — next= alone (no next_offset=, no clause), est_tokens $( cfield "$C3" est_tokens ) <= budget_tokens 1500" \ +{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] && [ "$( cfield "$C3" clause )" = true ] \ + && [ "$( cfield "$C3" shown )" -lt "$( cfield "$C3" total )" ]; } \ + && ok "(9) --token-budget=1500: the soft target's cut ($( cfield "$C3" shown ) of $( cfield "$C3" total )) carries next= and its clause, no over_ceiling" \ || no "(9) --token-budget=1500: $C3" -# wide explicit ceiling (sig side frozen at the default's): EXEMPT — the block, next= included, is the default's byte for byte -sigsblk(){ python3 -c 'import re,sys; m=re.search(r"]*>.*?",sys.stdin.read(),re.S); print(m.group(0) if m else "")'; } -if [ "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-cache 2>/dev/null | sigsblk )" = "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=8000 --no-cache 2>/dev/null | sigsblk )" ]; then - ok "(9) --token-budget=8000: exempt — the block (its next= included) is the default's byte for byte" +# the resume index (the #362 candidate page's next_offset): the last printed row's candidate rank, defined by its own clause +if [ "$( cfield "$C3" next_offset )" = "$( cfield "$C3" max_r )" ] && [ "$( cfield "$C3" next_offset )" = "$( cfield "$C3" shown )" ] \ + && [ "$( cfield "$C3" offset_clause )" = true ]; then + ok "(9) next_offset=\"$( cfield "$C3" next_offset )\" is the resume index: the last printed row's rank (= shown, no pseudo slot here), defined" else - no "(9) --token-budget=8000: the block differs from the default's (the exempt regime moved rows or its next=)" + no "(9) next_offset='$( cfield "$C3" next_offset )' vs last printed r=$( cfield "$C3" max_r ), shown=$( cfield "$C3" shown ), clause=$( cfield "$C3" offset_clause )" fi +# a wide explicit target serves the default's rows (stop when complete: no pad, no cap below the default) +C8="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=8000 --no-cache 2>/dev/null | cut_arm )" +python3 -c 'import json,sys; a=json.loads(sys.argv[1]); b=json.loads(sys.argv[2]); sys.exit(0 if a["names"] and a["names"]==b["names"] and not a["over"] else 1)' "$C8" "$C1" \ + && ok "(9) --token-budget=8000: serves exactly the default's rows ($( cfield "$C8" shown )), no over_ceiling" \ + || no "(9) --token-budget=8000: rows differ from the default's (or over_ceiling): $C8" C4="$( [ -n "$NX3" ] && pasted "$NX3" fxcut | cut_arm )" [ -n "$C4" ] && [ "$( cfield "$C4" capped )" = false ] && [ "$( cfield "$C4" shown )" = "$( cfield "$C3" total )" ] \ && ok "(9) --token-budget=1500: pasting its next= serves the block uncut" || no "(9) --token-budget=1500: the pasted next= left it cut: ${C4:-}" @@ -523,8 +544,9 @@ case "$JNX" in *' --signatures-only '*' --json') ok "(9) JSON twin: \"sigs_next\" names the --json call" ;; *) no "(9) JSON twin: sigs_next='$JNX'" ;; esac -JNO="$( printf '%s' "$J1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("sigs_next_offset"), d.get("sigs_shown"))' 2>/dev/null )" -case "$JNO" in "None"*|"") no "(9) JSON twin: no sigs_next_offset ('$JNO')" ;; *) set -- $JNO; if [ "$1" = "$2" ]; then ok "(9) JSON twin: sigs_next_offset=$1 (the resume index)"; else no "(9) JSON twin: sigs_next_offset=$1 vs sigs_shown=$2"; fi ;; esac +# the resume index rides a row CUT: the explicit soft target's (the default's head is complete) +JNO="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --json --token-budget=1500 --no-cache 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("sigs_next_offset"), d.get("sigs_shown"))' 2>/dev/null )" +case "$JNO" in "None"*|"") no "(9) JSON twin: no sigs_next_offset on the soft target's cut ('$JNO')" ;; *) set -- $JNO; if [ "$1" = "$2" ]; then ok "(9) JSON twin: sigs_next_offset=$1 (the resume index of the soft target's cut)"; else no "(9) JSON twin: sigs_next_offset=$1 vs sigs_shown=$2"; fi ;; esac J2="$( [ -n "$JNX" ] && pasted "$JNX" fxcut | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("capped"), len(d["sigs"]), "sigs_next" in d)' 2>/dev/null )" [ "$J2" = "False $( printf '%s' "$J1" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sigs_total"))' 2>/dev/null ) False" ] \ && ok "(9) JSON twin: pasting sigs_next serves every row uncut" || no "(9) JSON twin: pasted sigs_next gave '$J2'" @@ -540,11 +562,17 @@ for line in sys.stdin: M1="$( mcpfor '' )" MNB="$( cfield "${M1:-{\}}" next_budget_tokens 2>/dev/null )" if [ -n "$M1" ] && [ "$( cfield "$M1" capped )" = true ] && [ -z "$( cfield "$M1" next )" ] && [ "$MNB" != null ] && [ "$( cfield "$M1" budget_clause )" = true ] \ - && [ "$( cfield "$M1" next_offset )" = "$( cfield "$M1" max_r )" ]; then - ok "(9) MCP twin: no CLI argv on ; next_budget_tokens=\"$MNB\" and next_offset=\"$( cfield "$M1" next_offset )\", both defined" + && [ "$( cfield "$M1" shown )" = "$( cfield "$M1" total )" ] && [ "$( cfield "$M1" next_offset )" = null ]; then + ok "(9) MCP twin: the whole head, shrunk; no CLI argv on ; next_budget_tokens=\"$MNB\" defined; no next_offset= (nothing dropped)" else no "(9) MCP twin: ${M1:-}" fi +M1B="$( mcpfor ',"budget_tokens":1500' )" +if [ -n "$M1B" ] && [ "$( cfield "$M1B" next_offset )" = "$( cfield "$M1B" max_r )" ] && [ "$( cfield "$M1B" next_budget_tokens )" != null ] && [ "$( cfield "$M1B" over )" = false ]; then + ok "(9) MCP twin, budget_tokens=1500: the soft cut's next_offset=\"$( cfield "$M1B" next_offset )\" and next_budget_tokens=, no over_ceiling" +else + no "(9) MCP twin, budget_tokens=1500: ${M1B:-}" +fi M2="$( [ "$MNB" != null ] && mcpfor ",\"budget_tokens\":$MNB" )" if [ -n "$M2" ] && [ "$( cfield "$M2" capped )" = false ] && [ "$( cfield "$M2" shown )" = "$( cfield "$M1" total )" ]; then ok "(9) MCP twin: re-calling for with budget_tokens=$MNB serves all $( cfield "$M2" shown ) rows uncut" @@ -552,24 +580,11 @@ else no "(9) MCP twin: the re-call with budget_tokens=$MNB did not serve the block uncut: ${M2:-}" fi -# ── (10) C3: NO ROW IS DROPPED TO PAY FOR A HANDLE THAT CANNOT MAKE THE ANSWER FIT (orchestrator ruling 2026-10-07) ────────── -# The MCP `for` answer overshoots budget_tokens on its own (its sig ledger exempts header bytes), so under a budget that caps -# it is often past its ceiling paid or not; paying next_budget_tokens= from the rows (dd6e4c8e: ~245 B, 1-2 rows) -# bought nothing there. Over its ceiling either way → the rows the cut alone leaves, the handle unpaid, over_ceiling="1". -# Where paying IS what makes it fit, it pays (unchanged). Fixture: (9)'s 40 gadget functions, called by a RELATIVE path from -# $TMP so no checkout or temp-dir path rides in the header bytes the rows are budgeted against (root="fxcut" everywhere). -# over-anyway @2000 / @3000: 255dc199 serves 14 / 25 of 40 (est 2270 / 3125, over); dd6e4c8e paid 2 rows (12 / 23) and was -# still over (est 2206 / 3071) — RED there. -# train 26b re-pin (2026-10-08): lean-answers' zero elision made every row smaller, so @3000 is no longer over either way -# on the merged tree — the pre-continuation train binary (d82e4cf1) serves 25 rows at est 2945, FITTING; the unpaid -# answer + handle lands over and the paid one (23 rows, est 2918) fits, i.e. @3000 moved into the payable regime and is -# served paid, as ruled. The over-either-way budget is now @2600: d82e4cf1 serves 21 rows at est 2684 (over before any -# handle); the merged binary serves those 21 + the unpaid handle, est 2793, over_ceiling="1" (paying ~2 rows, ~90 tokens, -# cannot reach 2600). @2000 is unchanged (d82e4cf1 14 rows est 2178 over). Rows >= 255dc199's (re-pin from a pre-continuation binary if row bytes or the -# header change; smaller rows only raise it), the handle rides, over_ceiling="1". (Budgets with a margin on both sides of -# the ceiling: near one, a few bytes of legend decide whether paying fits, and the arm would pin that, not the rule.) -# payable @3800: 255dc199 serves 35 rows at est 3824 — OVER; paying the handle from rows (33, est 3773) is what makes it fit: -# the answer carries next_budget_tokens= and NO over_ceiling= (a binary that never pays serves 35 rows at est 3934, over). +# ── (10) MCP budget_tokens IS A SOFT TARGET (forbudget-soft, owner decisions 2026-10-14; rule 4: replaces the C3 rulings' arm) ── +# C3 (2026-10-07) ruled when a charged handle is paid from rows under a HARD budget; an explicit budget is a soft target now, so +# nothing is paid and nothing is labelled "over". What must hold instead, at 2000 / 2600 / 3800 on (9)'s fixture: the rows are +# a rank PREFIX of the MCP default's (whole head) and monotone in the budget, a cut carries next_budget_tokens=, and no +# over_ceiling= rides. Same relative path from $TMP as before (root="fxcut"). mcprel(){ ( cd "$TMP" && printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"fxcut","task":"gadget assembler","budget_tokens":%s}}}\n' "$1" \ | "$BIN" --mcp 2>/dev/null ) | python3 -c 'import json,sys for line in sys.stdin: @@ -577,30 +592,28 @@ for line in sys.stdin: if not line: continue d=json.loads(line) if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm; } -c3row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["shown"], d["capped"], d["next_budget_tokens"] is not None, d["over"], d["est_tokens"])' 2>/dev/null; } -for spec in 2000:14 2600:21; do - bt="${spec%%:*}"; want="${spec##*:}" - set -- $( c3row "$( mcprel "$bt" )" ) - if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = True ]; then - ok "(10) MCP budget_tokens=$bt, over its ceiling either way: $1 rows (>= the cut's own $want, none dropped for the handle), next_budget_tokens= unpaid, over_ceiling=\"1\" (est $5)" - else - no "(10) MCP budget_tokens=$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows (the cut's own), the handle, over_ceiling=\"1\"" - fi +prev_names='[]' +for bt in 2000 2600 3800; do + MB="$( mcprel "$bt" )" + v="$( python3 -c 'import json,sys +b=json.loads(sys.argv[1]); d=json.loads(sys.argv[2]); p=json.loads(sys.argv[3]) +ok = b["names"] == d["names"][:len(b["names"])] and b["names"][:len(p)] == p and not b["over"] \ + and ( b["shown"] == b["total"] or b["next_budget_tokens"] is not None ) +print(("OK" if ok else "BAD"), b["shown"], b["total"], b["over"], b["next_budget_tokens"])' "${MB:-{\}}" "${M1:-{\}}" "$prev_names" 2>/dev/null )" + case "$v" in + OK*) ok "(10) MCP budget_tokens=$bt: a rank prefix of the whole head, monotone, the cut continued, no over_ceiling ($v)" ;; + *) no "(10) MCP budget_tokens=$bt: ${v:-} — want a monotone rank prefix, next_budget_tokens= on a cut, no over_ceiling" ;; + esac + prev_names="$( cfield "${MB:-{\}}" names 2>/dev/null || echo '[]' )" done -set -- $( c3row "$( mcprel 3800 )" ) -if [ "$#" -eq 5 ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le 3800 ]; then - ok "(10) MCP budget_tokens=3800, payable: the handle is paid from rows ($1 shown) and that is what makes the answer fit (est $5 <= 3800, no over_ceiling=)" -else - no "(10) MCP budget_tokens=3800: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" -fi -# ── (10c) THE at= STAMP IS READ ONCE PER RUN, WHICHEVER PAYMENT MODE SERVES (CodeRabbit on #383, verbs_for.h:3843) ───────────── -# A --token-budget run renders in up to three payment modes (unpaid, paid, unpaid again) before a byte reaches stdout; each +# ── (10c) THE at= STAMP IS READ ONCE PER RUN (CodeRabbit on #383, verbs_for.h:3843) ───────────── +# A --token-budget run rendered in up to three payment modes (unpaid, paid, unpaid again) before a byte reached stdout; each # render re-read the git stamp (rev-parse + status --porcelain), so a run over its ceiling either way spawned three -# `status --porcelain` children. The ranking is computed once per run and so is the stamp now. Fixture: fxcut as a one-commit -# git repository (the stamp only reads a git root); budget 500 against an answer of ~1100 est tokens is over either way -# (over_ceiling="1" — the premise); a PATH git shim logs every git call and runs the real git. RED on the pre-fix binary -# (3 status children); GREEN: exactly 1, and at= still rides the answer. +# `status --porcelain` children. The ranking is computed once per run and so is the stamp. forbudget-soft: one render per run +# now (no payment modes); the premise is a budgeted run whose rows the target cuts. Fixture: fxcut as a one-commit git +# repository (the stamp only reads a git root); a PATH git shim logs every git call and runs the real git. RED on 255dc199's +# successor before the fix (3 status children); GREEN: exactly 1, and at= still rides the answer. FXG="$TMP/fxcutgit"; cp -R "$TMP/fxcut" "$FXG" git -C "$FXG" init -q && git -C "$FXG" add -A && git -C "$FXG" -c user.name=t -c user.email=t@t commit -qm init GSHIM="$TMP/gitshim"; mkdir -p "$GSHIM"; REALGIT="$( command -v git )" @@ -612,46 +625,37 @@ EOF chmod +x "$GSHIM/git"; : > "$GSHIM/calls.log" out="$( cd "$TMP" && PATH="$GSHIM:$PATH" "$BIN" fxcutgit --for="gadget assembler" --no-cache --token-budget=500 2>/dev/null )" st="$( grep -c 'status --porcelain' "$GSHIM/calls.log" | tr -d ' ' )" -if ! printf '%s' "$out" | grep -q 'over_ceiling="1"'; then - no "(10c) premise: --token-budget=500 on fxcutgit is not over its ceiling either way (no over_ceiling=\"1\"): $( printf '%s' "$out" | grep -o ']*>' | head -c 300 )" +if ! printf '%s' "$out" | grep -q ']*next_offset='; then + no "(10c) premise: --token-budget=500 on fxcutgit cuts no row (no ): $( printf '%s' "$out" | grep -o ']*>' | head -c 300 )" elif [ "$st" = 1 ] && printf '%s' "$out" | grep -q ' at="'; then - ok "(10c) a three-render --token-budget run reads the git stamp once (1 status --porcelain child), and at= rides the answer" + ok "(10c) a budgeted, cut --token-budget run reads the git stamp once (1 status --porcelain child), and at= rides the answer" else no "(10c) status --porcelain children: $st (want 1; one per render = 3 before the fix); at= present: $( printf '%s' "$out" | grep -c ' at="' )" fi -# ── (11) C3 COMPLETED: PAY FOR next= ONLY WHEN PAYING IS WHAT MAKES THE ANSWER FIT (orchestrator ruling 2026-10-08) ───────────── -# The answer with every row the cut leaves PLUS the unpaid handle is the first candidate; it is served whenever it fits, and only -# when it lands past its ceiling are rows dropped to pay (and served only if THAT fits — (10)'s @3800). 45a2eeba paid first and -# kept the paid answer whenever it fit, so it dropped rows the unpaid answer had room for: the served answer had fewer rows than -# an alternative that also fit. Same fixture and relative path as (10). Pins are 255dc199's own rows at each budget (the cut's -# rows; re-pin from a pre-continuation binary if row bytes or the header change), budgets chosen with room on both sides: -# CLI XML @2160: 255dc199 17 rows (est 2046); 45a2eeba 16 (est 1998) — RED there; head 17 + next= (est 2078 <= 2160). -# train 26c re-pin (2026-10-10, lego-charge): the lane charges / to the budget at their served size, so -# the cut alone leaves 16 rows at 2160 on this fixture (the lane's own pre-continuation binary 1cfbfdcc: 16 rows, est 1970; -# the 26c parent and the seven phase-A lanes: 17, est 1985); the merged binary serves those 16 + the unpaid next= (est 2002 -# <= 2160, no over_ceiling=). Re-pinned from the pre-continuation binary, as this comment asks; the assertions are unchanged. -# --json @2000: 255dc199 16 rows (est 1657); 45a2eeba 15 — RED there; head 16 + "sigs_next" (est 1691). -# MCP @4340: 255dc199 40 rows (est 4257); 45a2eeba 39 — RED there; head 40 + next_budget_tokens= (est 4308 <= 4340). -# Each: rows >= the cut's own, the handle rides, est_tokens <= the budget, no over_ceiling. A binary that always pays when it -# can fit (the 45a2eeba order) is red on all three. -c11row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); h=d["next"] or d["next_budget_tokens"]; print(d["shown"], d["capped"], bool(h), d["over"], d["est_tokens"])' 2>/dev/null; } -c11check(){ # LABEL BUDGET WANT ROWS CAPPED HANDLE OVER EST +# ── (11) A SOFT TARGET NEVER SERVES FEWER ROWS THAN THE HARD CEILING DID (forbudget-soft; rule 4: replaces C3-completed) ───────── +# C3 completed (2026-10-08) ruled "pay for next= only when paying is what makes the answer fit" under a HARD budget, pinning the +# rows the cut alone left: CLI --for @2160 16 rows, --json @2000 16, MCP @4340 40. A soft target charges the rows only (never the +# header), so at the same budgets it serves AT LEAST those rows, the cut (if any) carries its continuation, and nothing is +# labelled over_ceiling. The est_tokens <= budget half is gone with the byte promise (the owner's decision); the row bound +# that replaces it is forsoftbudgetcheck's ST. +c11row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); h=d["next"] or d["next_budget_tokens"]; print(d["shown"], d["total"], bool(h), d["over"])' 2>/dev/null; } +c11check(){ # LABEL BUDGET WANT ROWS TOTAL HANDLE OVER local label="$1" bt="$2" want="$3"; shift 3 - if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le "$bt" ]; then - ok "(11) $label @$bt fits with every row the cut leaves: $1 rows (>= $want, none dropped to pay), the handle unpaid, est $5 <= $bt, no over_ceiling=" + if [ "$#" -eq 4 ] && [ "$1" -ge "$want" ] && { [ "$1" = "$2" ] || [ "$3" = True ]; } && [ "$4" = False ]; then + ok "(11) $label @$bt: $1 rows (>= the hard ceiling's $want), the cut continued, no over_ceiling=" else - no "(11) $label @$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows, the handle, est <= $bt, no over_ceiling= (paid where it already fit?)" + no "(11) $label @$bt: shown/total/handle/over = ${*:-} — want >= $want rows, a continued cut, no over_ceiling=" fi } c11check "CLI --for" 2160 16 $( c11row "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=2160 --no-cache 2>/dev/null | cut_arm )" ) c11check "MCP for" 4340 40 $( c11row "$( mcprel 4340 )" ) set -- $( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=2000 --json --no-cache 2>/dev/null \ - | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["sigs"]), "sigs_next" in d, bool(d.get("over_ceiling")), d["est_tokens"])' 2>/dev/null ) -if [ "$#" -eq 4 ] && [ "$1" -ge 16 ] && [ "$2" = True ] && [ "$3" = False ] && [ "$4" -le 2000 ]; then - ok "(11) --for --json @2000 fits with every row the cut leaves: $1 rows (>= 16), \"sigs_next\" unpaid, est $4 <= 2000, no over_ceiling" + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["sigs"]), d.get("sigs_total", len(d["sigs"])), "sigs_next" in d, bool(d.get("over_ceiling")))' 2>/dev/null ) +if [ "$#" -eq 4 ] && [ "$1" -ge 16 ] && { [ "$1" = "$2" ] || [ "$3" = True ]; } && [ "$4" = False ]; then + ok "(11) --for --json @2000: $1 rows (>= 16), the cut continued (\"sigs_next\"), no over_ceiling" else - no "(11) --for --json @2000: rows/sigs_next/over/est = ${*:-} — want >= 16 rows, \"sigs_next\", est <= 2000, no over_ceiling" + no "(11) --for --json @2000: rows/total/sigs_next/over = ${*:-} — want >= 16 rows, a continued cut, no over_ceiling" fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 2c7dbd676..a28ece213 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -131,9 +131,13 @@ cat > "$TMP/legend.py" <<'PY' # normalised) of WIDE that NARROW lacks, one per line — present-only clauses (each rides only with its own attribute) # excluded. Empty output = no clause was dropped. exit 3 when either file has no header comment. import re, sys -PRESENT_ONLY = re.compile( r'dropped_positive|docs_dropped|capped=1 with shown|sigs next|next_offset|next_budget|past_target|' +# Present-only = rides only with its own attribute or section on THIS answer (fewer rows can legitimately not carry it): the +# cut/target readings, e= (rows carry it only where endLinesFitCeiling says), sc=, the lego/compose stub (only when a +# section collapsed), the shrunk-not-dropped reading. +PRESENT_ONLY = re.compile( r'dropped_positive|docs_dropped|capped=[#1] with shown|sigs next|next_offset|next_budget|past_target|' r'budget_bytes|BYTE target|over_ceiling|runaway|docs_after_code|relevance floor|this budget left out|' - r'e= on a d row|d e= its last line|sc=scope|cut, recoverable|bodies shown|hops shown=0', re.I ) + r'e= on a d row|d e= its last line|extent is not known|line of the definition|the name.s line|' + r'sc=scope|cut, recoverable|bodies shown|hops shown=#|collapse to a counted stub', re.I ) def clauses( path ): s = open( path ).read() head = s.split( '" ); sys.exit( 0 ) ab, ar = a; rr = ref[ 1 ] k = len( ar ) fits = ab <= T or k <= 4 -if k >= len( rr ): print( "OK fits=%d rows=%d/%d bytes=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) +if k >= len( rr ): print( "OK fits=%d rows=%d/%d charged=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) nxt = rr[ k ][ 1 ] maximal = ab + nxt > T - 160 -print( "%s fits=%d maximal=%d rows=%d/%d bytes=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T ) ) +print( "%s fits=%d maximal=%d rows=%d/%d charged=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T ) ) PY )" case "$st" in diff --git a/test/tokenbudgetcheck.sh b/test/tokenbudgetcheck.sh index 8a97a432b..f2641b0c0 100755 --- a/test/tokenbudgetcheck.sh +++ b/test/tokenbudgetcheck.sh @@ -319,30 +319,46 @@ rc_for_tiny=$? && ok "--for --token-budget=1: exit 0 (shapes to fit, never the map/--query exit-3 gate)" \ || no "--for --token-budget=1: expected exit 0 (shaping), got $rc_for_tiny" -# ── #17 (W3FIX H2): the ABSOLUTE ceiling, not only the RELATIVE shape ────────────────────────────────── +# ── #17 (W3FIX H2): the ABSOLUTE bound, not only the RELATIVE shape ───────────────────────────────────── # #15/#16 are relative arms — a tighter budget shapes to fewer-or-equal tokens, and shaping never gates — and a -# FLOOR satisfies both perfectly: --for on a 900-char task delivered 5.3x its own stated ceiling at EVERY -# budget, which is monotone, exits 0, and shrinks as the budget shrinks. Every relative arm in this file passed -# it, which is precisely why an absolute one has to exist. The bar is the ceiling PLUS the single-entry -# overshoot the design allows (src/serialize.h kCeilingFirstEntryTolerance = 1.15 — the ranking section emits -# its first entry whole): the delivered document either fits that, or SAYS over_ceiling. Never neither. -TB_LONG_TASK="$( python3 -c "print(('budget accounting for the ranked lens header attributes '*20)[:900])" 2>/dev/null )" +# FLOOR satisfies both perfectly: --for on a 900-char task delivered 5.3x its own stated ceiling at EVERY budget, +# which is monotone, exits 0, and shrinks as the budget shrinks. Every relative arm in this file passed it, which +# is precisely why an absolute one has to exist. +# forbudget-soft (owner decision 2026-10-14; rule 4: the by-design break of the old byte promise, replaced here): +# an explicit --token-budget on --for is a SOFT TARGET charged against the ROWS only — the header (here a +# 900-char task echoed verbatim) always ships and nothing is labelled over it. The absolute bound is therefore on +# the rows: their bytes fit T = N x 2.36 x 0.9 (the ladder's charge, with the zero metrics the lean spelling elides) +# or they are the rank 1..4 floor; the header is whole (task= verbatim, no task_echo drop) and carries no +# over_ceiling=. The soft regime's own gate is test/forsoftbudgetcheck.sh (ST, E2b, E1). +TB_LONG_TASK="$( python3 -c "print(('budget accounting for the ranked lens header attributes '*20)[:900].rstrip())" 2>/dev/null )" if [ -z "$TB_LONG_TASK" ]; then - no "#17: python3 unavailable — the absolute-ceiling arm could not build its 900-char task" + no "#17: python3 unavailable — the absolute-bound arm could not build its 900-char task" else for TB_B in 600 1200 2400; do "$BIN" src --for="$TB_LONG_TASK" --token-budget=$TB_B --no-cache >"$TMP_TB_DIR/abs.$TB_B" 2>/dev/null - TB_BYTES="$( wc -c < "$TMP_TB_DIR/abs.$TB_B" | tr -d ' ' )" - TB_ALLOW="$( python3 -c "print(int($TB_B*2.36*1.15))" )" - # the disclosure lives in the header COMMENT — a body that quotes the word must not satisfy this arm - TB_HDR="$( head -c 6000 "$TMP_TB_DIR/abs.$TB_B" | sed -e 's/-->.*//' )" - if [ "$TB_BYTES" -le "$TB_ALLOW" ]; then - ok "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B fits the $TB_ALLOW B allowance" - elif printf '%s' "$TB_HDR" | grep -q 'over_ceiling'; then - ok "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B over $TB_ALLOW B and DISCLOSES over_ceiling" - else - no "#17 --for 900-char task --token-budget=$TB_B: $TB_BYTES B past the $TB_ALLOW B allowance in SILENCE" - fi + TB_VERDICT="$( python3 - "$TMP_TB_DIR/abs.$TB_B" "$TB_B" "$TB_LONG_TASK" <<'PY_EOF' +import re, sys, html +s = open( sys.argv[ 1 ] ).read(); n = int( sys.argv[ 2 ] ); task = sys.argv[ 3 ] +T = int( n * 2.36 * 0.9 ) +root = re.search( r']*)>', s ); m = re.search( r']*>(.*?)', s, re.S ) +if not root or not m: print( "BAD no root or no block" ); sys.exit() +rows = re.findall( r']*)>.*?', m.group( 1 ), re.S ) +elided = sum( n2 for a in rows for k, n2 in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +charged = len( m.group( 1 ).encode() ) + elided +t = re.search( r'\btask="([^"]*)"', root.group( 1 ) ) +head = s.split( ' when the top hit's carrier has reads. Labels come from the index: the symbol kind, the use-site table and the value-reference index. The MCP grep twin shares the computation (src/grepcarrier.h). Reads are captured only by the rich ingest family; a lean --grep whose page holds such a carrier ingests the root once more through the rich warm cache. Gate: test/countfloorcheck.sh arm P (red on the base binary). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++ docs/LIMITS.md | 18 ++- src/compactlegend.h | 5 + src/grepcarrier.h | 285 ++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 1 + src/mcpverbs.h | 34 ++++- src/verbs_grep.h | 129 +++++++++++++++++- test/countfloorcheck.sh | 90 ++++++++++++- 8 files changed, 560 insertions(+), 14 deletions(-) create mode 100644 src/grepcarrier.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5feb62e..fb9967e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,18 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `--grep` names what a hit's variable carries and who reads it + +When a hit sits inside a variable, constant or initialiser (its enclosing definition's kind is a variable), the `` +row now says where that value goes instead of only a call count nobody can satisfy: `reads=N reads_at=` are the +uses verb's read sites for the name (first three, the uses verb's order; only where that verb indexes reads — +Python, C++ and ObjC outside headers — and absent, never zero, elsewhere), and `vals=` names up to three function +values the carrier stores, as `slot=fn@line` of the one definition each resolves to (`vals_total=` beyond three). +When the top hit's carrier has reads, the root `next=` is `--uses=`. The MCP `grep` verb's `enclosing` +rows carry the same `reads`/`reads_at`/`vals` keys (`src/grepcarrier.h`, shared). Reads are rich-family facts, so a +lean `--grep` run whose page holds such a carrier ingests the same root once more through the rich warm cache; a page +without one costs nothing extra. Gate: `test/countfloorcheck.sh` arm P. + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index b83ac804a..e33f9ad5b 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 253 | 98 | 149 | **104** | +| 254 | 99 | 149 | **105** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 253 + 7 is the 260 constants this generator parses out of `src/`. +are not counted as caps, and 254 + 7 is the 261 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 253 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 114 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 254 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 115 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 98 files that declare a cap — the 253 caps counted above, and no parameter. +One table for each of the 99 files that declare a cap — the 254 caps counted above, and no parameter. ### `src/abicheck.h` @@ -369,6 +369,14 @@ Discloses: `importers_capped` | `kMaxTerminals` | `16` | — | >16 is the CALLER's usage error; the core CLAMPS (never ASSUMEs on hostile input) | | `kMemberSpellingsShown` | `6` | OUTPUT | — | +### `src/grepcarrier.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kGrepCarrierShown` | `3` | — | reads_at= sites and vals= entries printed per row (reads=/vals_total= count all) | + ### `src/handlershape.h` Discloses: **none** diff --git a/src/compactlegend.h b/src/compactlegend.h index bfcb2316b..0156c6193 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -892,6 +892,11 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // count-floor (src/countfloor.h): the row's own floor marker, emitted only beside evidence of a miss for THAT row. { "callers_floor", "callers_floor=1: callers= may be short for this row: a declined or unbound call spelled like it, a value use, or a kind used by reading/naming it (var, class, struct, interface); not proof more callers exist; absent = no such evidence", true, "enc", MapHeaderRead::No, {}, "grep" }, { "floor_next", "floor_next=: the call that lists what that count could not see (the uses verb; the literal scan where reads are not indexed)", true, "enc", MapHeaderRead::No, {}, "grep" }, + // L4 (src/grepcarrier.h): a carrier row (every def a variable/constant/initialiser) — its read sites and stored function values. + { "reads", "reads=N: read sites the uses verb lists for this carrier's name (by name; present only when nonzero where reads are indexed; absent = none counted)", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "reads_at", "reads_at=: the first three of those sites, file:line, uses order; the root next= is then the uses verb", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "vals", "vals=: up to three function values the carrier stores, slot=fn@line of its one resolved def (path:line elsewhere; no @ if several)", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "vals_total", "vals_total=N: every stored function value, when more than vals= shows", true, "enc", MapHeaderRead::No, {}, "grep" }, // count-floor + CALLSITE-LINE on safe-delete (src/verbs_navigate.h runSafeDelete): root markers beside the counts they qualify. { "callers_floor", "callers_floor=1: callers= and impact_reaches= may be short (a declined, unbound or value use, or a kind read not called); not proof of more", false, {}, MapHeaderRead::No, {}, "safe-delete" }, { "uses_floor", "uses_floor=1: this kind's reads and type mentions are not indexed in this run; uses= does not count them", false, {}, MapHeaderRead::No, {}, "safe-delete" }, diff --git a/src/grepcarrier.h b/src/grepcarrier.h new file mode 100644 index 000000000..407ac10fa --- /dev/null +++ b/src/grepcarrier.h @@ -0,0 +1,285 @@ +#pragma once + +// grepcarrier.h — what a grep hit's CARRIER holds and who reads it (L4, train 28). +// +// WHY. A literal often sits in a variable or constant rather than a function: `DRIVER = get_environ( "TEXTUAL_DRIVER" )`, +// or a C initialiser `cmd_split_window_entry = { .name = "split-window", … .exec = cmd_split_window_exec }`. The grep +// row then names that variable with callers="0" — a call count on a thing nobody calls — and the answer stops one +// hop short of what the reader wanted: the place the value is READ (app.py's `constants.DRIVER`) or the FUNCTION the +// initialiser stores (`.exec = cmd_split_window_exec`). Both facts are already in the index; this header reads them. +// +// THE RULE (labels from the index, never from text): +// * a carrier is an enclosing definition of kind Var (a variable, constant or initialiser) — the parser's kind; +// * reads= / reads_at= are the uses verb's role="read" sites for the carrier's NAME (matched by name, the uses verb's own +// unit), counted only where that verb can see reads at all: an ingest that captured value uses and a language whose +// reads reach the use-site table (countfloor.h valueUsesArmedFor), never a C-family header, whose readers are `.c` +// files the uses verb never reads (the countFloorNext rule). Anywhere else the attributes are ABSENT: unknown, never 0; +// * vals= are the function values the carrier itself stores: value references (valuerefindex.h) made inside the +// carrier's definition whose name resolves to an indexed function, in source order, as slot=fn@line: the slot key as +// the value index normalised it, and the line of the ONE definition the name resolves to (@path:line when that +// definition is in another file) — the hop the reader wants next. A name resolving to several definitions keeps no +// @: which one the slot holds is not something the index decided. +// The CLI (verbs_grep.h) and the MCP grep twin (mcpverbs.h grepEnclosingJson) both call grepCarriers, so the two +// surfaces cannot disagree about a row. + +#include "countfloor.h" // valueUsesArmedFor — where reads reach the use-site table +#include "filter.h" // pathTierIndexOver, compareTierThenPath — the uses verb's base row order +#include "model.h" +#include "sarif.h" // rootRelativeUri — the root-relative spelling every grep path takes +#include "search.h" // GrepEncRow — the rows this decorates +#include "valuerefindex.h" // ValueRefIndex — madeIn / targetsOf, the stored function values +#include "infra/Diagnostics.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +inline constexpr std::size_t kGrepCarrierShown = 3; // reads_at= sites and vals= entries printed per row (reads=/vals_total= count all) + +struct GrepCarrierSite +{ + std::string path; // the READS ingest's own file spelling (the CLI's may be a second, rich ingest of the same root) + std::uint32_t line = 0; +}; + +struct GrepCarrierVal +{ + std::string slot; // the normalised key without its leading '.', "" for a plain assignment + std::string fn; // the function name as written + std::string path; // the definition's file when it is not the carrier's own, else "" + std::uint32_t line = 0; // the definition's line; 0 = the name resolves to several definitions (no @) +}; + +struct GrepCarrier +{ + bool isCarrier = false; // every definition on the row is a Var + bool readsKnown = false; // reads= is a count the uses verb could make (see the rule above) + std::uint32_t reads = 0; // role="read" sites spelled like the carrier's name + std::vector at; // the first kGrepCarrierShown of them, in the uses verb's base order + std::vector vals; // the first kGrepCarrierShown stored function values + std::uint32_t valsTotal = 0; // every stored function value on the row +}; + +// Does the uses verb see reads of a definition of `s`'s language at that path? (countfloor.h countFloorNext's rule.) +inline bool grepCarrierReadsVisible( const IngestResult& ing, const Symbol& s ) noexcept +{ + const std::string_view path = s.fileId < ing.files.size() ? std::string_view( ing.files[ s.fileId ] ) : std::string_view(); + return valueUsesArmedFor( s.lang ) && !path.ends_with( ".h" ); +} + +inline bool isGrepCarrierRow( const IngestResult& ing, const GrepEncRow& row ) noexcept +{ + return !row.ids.empty() && std::ranges::all_of( row.ids, [ & ]( NodeId id ) { return id < ing.symbols.size() && ing.symbols[id].kind == SymKind::Var; } ); +} + +// Do any rows want reads= — a carrier whose language/path the uses verb reads? The CLI asks this before paying for a +// rich ingest, so an answer with no such row costs nothing extra. +inline bool grepCarriersWantReads( const IngestResult& ing, std::span rows ) noexcept +{ + return std::ranges::any_of( rows, [ & ]( const GrepEncRow& row ) + { + return isGrepCarrierRow( ing, row ) && grepCarrierReadsVisible( ing, ing.symbols[ row.ids.front() ] ); + } ); +} + +namespace grepcarrier +{ + +// The value-reference sites that replace a read row in the uses verb (valuerefindex.h UsesValueFilter): a Value +// reference spelled like `names[k]` that resolves to a function. Built only when such a reference exists, so the +// common case (a variable name no function shares) never builds the reads ingest's value index. +inline std::vector valueSitesOf( const IngestResult& rd, std::span names ) +{ + std::vector candidates; + for( std::uint32_t i = 0; i < rd.references.size(); ++i ) + { + const Reference& r = rd.references[i]; + if( r.role == RefRole::Value && !r.fieldName.starts_with( '@' ) && std::ranges::binary_search( names, std::string_view( r.calleeName ) ) ) + { + candidates.push_back( i ); + } + } + std::vector sites; + if( candidates.empty() ) + { + return sites; + } + const ValueRefIndex vri( rd ); + for( const std::uint32_t i : candidates ) + { + if( !vri.targetsOf( i ).empty() ) + { + sites.push_back( ( std::uint64_t( rd.references[i].fileId ) << 32 ) | rd.references[i].startByte ); + } + } + std::ranges::sort( sites ); + return sites; +} + +} // namespace grepcarrier + +// One carrier verdict per row. `ing` is the ingest the rows came from; `vri` its value index (vals=); `reads` the +// ingest whose role="read" references count (nullptr = none captured: reads= stays unknown on every row). The reads +// pass is ONE walk over `reads->references` for every carrier name on the page, never a per-row rescan. +inline std::vector grepCarriers( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, + const IngestResult* reads ) +{ + std::vector out( rows.size() ); + std::vector carrierIds; // every carrier definition on the page, for ONE madeIn pass + std::vector readNames; // the names whose reads are counted + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !isGrepCarrierRow( ing, rows[r] ) ) + { + continue; + } + out[r].isCarrier = true; + carrierIds.insert( carrierIds.end(), rows[r].ids.begin(), rows[r].ids.end() ); + const Symbol& s = ing.symbols[ rows[r].ids.front() ]; + if( reads != nullptr && grepCarrierReadsVisible( ing, s ) ) + { + out[r].readsKnown = true; + readNames.push_back( s.name ); + } + } + if( carrierIds.empty() ) + { + return out; + } + + // vals=: the stored function values, attributed to the row whose definition made them (source order: madeIn is + // ascending reference order, which is file then byte order within one ingest). + for( const std::uint32_t v : vri.madeIn( carrierIds, RefRole::Value ) ) + { + const Reference& ref = ing.references[v]; + if( ref.fieldName.starts_with( '@' ) || vri.targetsOf( v ).empty() ) + { + continue; // a decorator row is a fact about a definition; an unresolved name is not a function value + } + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !out[r].isCarrier || std::ranges::find( rows[r].ids, ref.fromSymbol ) == rows[r].ids.end() ) + { + continue; + } + ++out[r].valsTotal; + if( out[r].vals.size() < kGrepCarrierShown ) + { + const std::string_view key = std::string_view( ref.composeRel ).starts_with( '.' ) ? std::string_view( ref.composeRel ).substr( 1 ) + : std::string_view( ref.composeRel ); + GrepCarrierVal val{ std::string( key ), ref.calleeName, {}, 0 }; + if( const std::vector targets = vri.targetsOf( v ); targets.size() == 1 && targets[0] < ing.symbols.size() ) + { + const Symbol& def = ing.symbols[ targets[0] ]; + val.line = def.line; + if( def.fileId != ref.fileId && def.fileId < ing.files.size() ) + { + val.path = ing.files[ def.fileId ]; + } + } + out[r].vals.push_back( std::move( val ) ); + } + break; // one row per name (grepEnclosingRows groups by chain): a definition sits on one row + } + } + + if( readNames.empty() ) + { + return out; + } + EXPECTS( reads != nullptr, "a name is queued for reads only when a reads ingest was handed in" ); + std::ranges::sort( readNames ); + readNames.erase( std::unique( readNames.begin(), readNames.end() ), readNames.end() ); + const std::vector valueSites = grepcarrier::valueSitesOf( *reads, readNames ); + + // ONE pass: the uses verb's read rows (collectUseSites' filters) for every queued name. + struct ReadHit { std::uint32_t name; std::uint32_t fileId; std::uint32_t line; }; + std::vector hits; + for( const Reference& r : reads->references ) + { + if( r.role != RefRole::Read || r.isCompose || r.isDocLink || r.lang == Lang::Markdown ) + { + continue; + } + const auto it = std::ranges::lower_bound( readNames, std::string_view( r.calleeName ) ); + if( it == readNames.end() || *it != r.calleeName ) + { + continue; + } + if( std::ranges::binary_search( valueSites, ( std::uint64_t( r.fileId ) << 32 ) | r.startByte ) ) + { + continue; // the uses verb serves this site as role="value", not a read + } + hits.push_back( { std::uint32_t( it - readNames.begin() ), r.fileId, r.line } ); + } + const std::vector tierOfFile = pathTierIndexOver( *reads, hits, []( const ReadHit& h ) { return h.fileId; } ); + std::ranges::sort( hits, [ & ]( const ReadHit& a, const ReadHit& b ) + { + if( a.name != b.name ) + { + return a.name < b.name; + } + if( const int c = compareTierThenPath( *reads, tierOfFile, a.fileId, b.fileId ); c != 0 ) + { + return c < 0; + } + return a.line < b.line; + } ); + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !out[r].readsKnown ) + { + continue; + } + const std::string_view name = ing.symbols[ rows[r].ids.front() ].name; + const std::uint32_t k = std::uint32_t( std::ranges::lower_bound( readNames, name ) - readNames.begin() ); + ASSUME( k < readNames.size() && readNames[k] == name, "every readsKnown row queued its name" ); + const auto [ first, last ] = std::ranges::equal_range( hits, k, {}, &ReadHit::name ); // hits are sorted by name first + out[r].reads = std::uint32_t( last - first ); + for( auto h = first; h != last && out[r].at.size() < kGrepCarrierShown; ++h ) + { + out[r].at.push_back( { reads->files[ h->fileId ], h->line } ); + } + } + return out; +} + +// The two list attributes as plain text items (the emitters escape them for their own dialect): reads_at= as +// path:line, vals= as slot=fn@line / slot=fn@path:line / slot=fn — paths root-relative exactly when the hit rows' are. +struct GrepCarrierText +{ + std::vector readsAt; + std::vector vals; +}; + +inline GrepCarrierText grepCarrierText( const GrepCarrier& c, bool singleRoot, std::string_view rootPrefix ) +{ + const auto rel = [ & ]( const std::string& p ) { return std::string( singleRoot ? sarif::rootRelativeUri( p, rootPrefix ) : std::string_view( p ) ); }; + GrepCarrierText t; + if( c.readsKnown && c.reads > 0 ) + { + for( const GrepCarrierSite& site : c.at ) + { + t.readsAt.push_back( rel( site.path ) + ":" + std::to_string( site.line ) ); + } + } + for( const GrepCarrierVal& v : c.vals ) + { + std::string item = v.slot.empty() ? v.fn : v.slot + "=" + v.fn; + if( v.line != 0 ) + { + item += "@" + ( v.path.empty() ? std::string() : rel( v.path ) + ":" ) + std::to_string( v.line ); + } + t.vals.push_back( std::move( item ) ); + } + return t; +} + +} // namespace rw diff --git a/src/main.cpp b/src/main.cpp index 42637b9ec..405de2fcf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,6 +43,7 @@ #include "tracelocus.h" // L4: the shared --from-trace / MCP from_trace bundle assembler (fromTraceBundleText) #include "callhierarchy.h" // H14/M13: the shared --callers/--callees / MCP find_* 1-hop computation (CLI == MCP rows) #include "countfloor.h" // count-floor: a row's callers=/implementors= floor marker and its follow-up (CLI and MCP share it) +#include "grepcarrier.h" // L4: a grep carrier's reads=/reads_at=/vals= (CLI and MCP share it) #include "editcheck.h" // L4: the shared --edit-check / MCP edit_check contract-comparison core (editCheckBundleText) #include "slice.h" // lane/paper-slice: --slice=SYM[:VAR] — the ARISE-motivated def-use slice core (sliceBundleText) #include "editpreview.h" // card A1: the PRE-APPLY contract preview (editpreview::run) — BEFORE mcp.h, which diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f3e720b51..b500e2043 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -24,6 +24,7 @@ #include "partition.h" // the explore verb's `partition` argument (packTaskPartitionText) #include "tracelocus.h" // L4: the shared --from-trace / MCP from_trace bundle assembler (fromTraceBundleText) #include "countfloor.h" // count-floor: a row's callers=/implementors= floor marker and its follow-up (CLI and MCP share it) +#include "grepcarrier.h" // L4: a grep enclosing row's carrier reads/vals (CLI and MCP share it) #include "editcheck.h" // L4: the shared --edit-check / MCP edit_check contract-comparison core (editCheckBundleText) #include "callhierarchy.h" // H14/M13: the shared 1-hop call-hierarchy computation — find_symbol / find_referencing_symbols #include "graphlegend.h" // §H4 §3.4: the ONE counts_floor= marker + shared graph-count legend wording (CLI ≡ MCP) @@ -887,7 +888,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // under --handles mints (search.h grepEncHandleCandidate decides WHICH row may carry one; handleFor mints it from the index's // own byte hash) — or `handle_omitted` with the CLI legend's reason. Before this a grep answer named enclosing symbols that // fetch_body could not take, so the agent re-read every file it had just searched. -inline std::string grepEnclosingJson( const McpIndex& ix, std::span hits ) +inline std::string grepEnclosingJson( const McpIndex& ix, std::span hits, bool singleRoot, std::string_view rootPrefix ) { const IngestResult& ing = ix.ing; const Graph& g = ix.g; @@ -905,6 +906,10 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span floors = callerFloors( ing, g, rowIds, &valueRefIndexOf( ix ) ); ENSURES( floors.size() == encRows.size() ); + // L4: the CLI carrier attributes (grepcarrier.h), by the same function. The MCP index is the RICH family + // (mcpindex.h ingests with value uses), so its own references carry the reads: no second ingest here. + const std::vector carriers = grepCarriers( ing, valueRefIndexOf( ix ), encRows, &ing ); + ENSURES( carriers.size() == encRows.size() ); std::string out = ",\"enclosing\":["; for( std::size_t r = 0; r < encRows.size(); ++r ) { @@ -926,6 +931,31 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span& items ) + { + std::string list = "["; + for( std::size_t k = 0; k < items.size(); ++k ) + { + list += ( k > 0 ? ",\"" : "\"" ) + mcpdetail::jsonEscape( items[k] ) + "\""; + } + return list + "]"; + }; + if( !t.readsAt.empty() ) + { + out += ",\"reads\":" + std::to_string( c.reads ) + ",\"reads_at\":" + jsonList( t.readsAt ); + } + if( !t.vals.empty() ) + { + out += ",\"vals\":" + jsonList( t.vals ); + if( c.valsTotal > c.vals.size() ) + { + out += ",\"vals_total\":" + std::to_string( c.valsTotal ); + } + } + } const char* omitted = nullptr; const NodeId id = grepEncHandleCandidate( ing, row, omitted ); if( id == kNoNode ) @@ -1278,7 +1308,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat singleRootJ, rootPrefixJ ); // R1 (the 2026-08-12 usage mine): the CLI / twins, appended AFTER "hits" so the // historic key order three other gates read (files,total,shown,capped) is byte-untouched. - out += grepEnclosingJson( ix, std::span( hits ) ); + out += grepEnclosingJson( ix, std::span( hits ), singleRootJ, rootPrefixJ ); if( collected.raw.empty() ) { out += grepSuggestJson( ing, pattern ); diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 2e07f03d1..1f72c6075 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -41,8 +41,41 @@ struct GrepEncOptions bool handles; std::string_view root; std::vector& esc; + std::span carriers; // L4: parallel to the rows (grepcarrier.h), or empty when no row is a carrier + bool singleRoot; // reads_at= paths strip rootPrefix exactly when the hit rows' p= do + std::string_view rootPrefix; }; +// L4 (grepcarrier.h): a carrier row's reads=/reads_at= (the uses verb's read sites for its name; absent = not counted) +// and vals=/vals_total= (the function values it stores). Pure serialization of grepCarriers' verdict. +std::string grepCarrierAttrs( const rw::GrepCarrier& c, bool singleRoot, std::string_view rootPrefix, std::vector& esc ) +{ + const rw::GrepCarrierText t = rw::grepCarrierText( c, singleRoot, rootPrefix ); + const auto joined = [ & ]( const std::vector& items ) + { + std::string out; + for( std::size_t k = 0; k < items.size(); ++k ) + { + out += ( k > 0 ? " " : "" ) + std::string( rw::escapeXml( items[k], esc ) ); + } + return out; + }; + std::string out; + if( !t.readsAt.empty() ) + { + out += " reads=\"" + std::to_string( c.reads ) + "\" reads_at=\"" + joined( t.readsAt ) + "\""; + } + if( !t.vals.empty() ) + { + out += " vals=\"" + joined( t.vals ) + "\""; + if( c.valsTotal > c.vals.size() ) + { + out += " vals_total=\"" + std::to_string( c.valsTotal ) + "\""; + } + } + return out; +} + class GrepHandleAttrs { public: @@ -145,11 +178,78 @@ void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span { rw::emitTo( stdout, " tested=\"1\"" ); } + if( r < opt.carriers.size() && opt.carriers[r].isCarrier ) + { + std::fputs( grepCarrierAttrs( opt.carriers[r], opt.singleRoot, opt.rootPrefix, opt.esc ).c_str(), stdout ); + } std::fputs( handleAttrs.forRow( row ).c_str(), stdout ); rw::emitTo( stdout, "/>" ); } } +// L4 (grepcarrier.h): the carrier verdict for a page's rows. vals= reads this run's own value index (both ingest +// families capture value references). reads= needs role="read" references, which only the RICH family captures, and +// --grep runs LEAN (cli.h needsValueUses): so when a carrier on THIS page is one the uses verb can read for (an armed +// language, not a header) and this run is lean, the same root is ingested once more, rich, through that family's own +// warm cache (the uses verb's blob, so a tree that answered the uses verb already pays a cache load, not a parse), and +// dropped before the answer prints. A page with no such carrier — every hit inside a function, a C initialiser, a +// JS constant — pays nothing. Multi-root lean runs leave reads= unknown (absent): the rich re-ingest is single-root. +struct GrepCarrierReadsSource +{ + const rw::IngestResult* reads = nullptr; + enum class DisclosureWhy : std::uint8_t { MemoryStop }; + void disclose( DisclosureWhy ) noexcept { reads = nullptr; } // a stopped ingest's reads would be short: leave reads= unknown +}; + +std::vector grepPageCarriers( const rw::Config& cfg, const rw::IngestResult& ing, const std::string& root, + std::span rows, bool valueUses ) +{ + if( std::ranges::none_of( rows, [ & ]( const rw::GrepEncRow& row ) { return rw::isGrepCarrierRow( ing, row ); } ) ) + { + return {}; + } + const rw::ValueRefIndex vri( ing ); + const bool singleRoot = ing.realPaths.empty() && cfg.roots.size() == 1; + if( valueUses || !singleRoot || !rw::grepCarriersWantReads( ing, rows ) ) + { + return rw::grepCarriers( ing, vri, rows, valueUses ? &ing : nullptr ); + } + const std::string cachePath = cfg.noCache ? std::string() : defaultCachePath( root, true ); + rw::IngestResult rich = rw::ingest( root.c_str(), cfg.excludes, cachePath, cfg.maxFileBytes, /*captureValueUses=*/true, + /*excludeLabel=*/{}, /*respectGitignore=*/!cfg.noIgnore ); + if( cfg.ignoreTests ) + { + rw::applyIgnoreTests( rich ); // the same population the lean ingest answers from + } + GrepCarrierReadsSource source{ &rich }; + if( rich.memoryStop.isSet() ) + { + rw::memguard::answerStops(); // answered here: reads= stays absent (unknown), the rest of the answer is the lean run's + DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::MemoryStop, "grep: the rich re-ingest for reads= stopped at the memory guard" ); + } + return rw::grepCarriers( ing, vri, rows, source.reads ); +} + +// L4: the root next= on an uncut answer whose TOP hit sits in a carrier with reads= — the uses verb on that carrier's name +// (the spelling floor_next= uses, countfloor.h countFloorNext), because where the value is read is the hop the at verb on +// the hit's own line cannot give. "" otherwise: the at verb on the top hit stays the follow-up. +std::string grepCarrierNext( const rw::IngestResult& ing, std::span hits, std::span rows, + std::span carriers ) +{ + if( hits.empty() || hits[0].enclosingId == rw::kNoNode ) + { + return {}; + } + for( std::size_t r = 0; r < rows.size() && r < carriers.size(); ++r ) + { + if( std::ranges::find( rows[r].ids, hits[0].enclosingId ) != rows[r].ids.end() ) + { + return carriers[r].readsKnown && carriers[r].reads > 0 ? rw::nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ) : std::string(); + } + } + return {}; +} + // count-floor: callers= beside evidence of a miss for THAT row is a floor (countfloor.h). Computed once per page, before // the legend, so the full dialect defines callers_floor= exactly when a row carries it; the MCP twin calls the same // function over the same rows (mcpverbs.h grepEnclosingJson), so the two surfaces cannot disagree. @@ -166,6 +266,19 @@ std::vector grepEncFloors( const rw::IngestResult& ing, const r // count-floor (countfloor.h): the full dialect's reading of a row's own floor marker — no attribute=value literal (this // legend's rule) — spliced only when a row on the page carries it, so a page without one keeps every byte. +// L4 (grepcarrier.h): the carrier attributes and the next= rule they switch, spliced only when a row on the page carries +// one (so a page without a carrier keeps every legend byte). No attribute=value literal (this legend's rule) and no +// double dash (an XML comment). +inline constexpr const char* kGrepCarrierClause = + "On a row whose definitions are all variables, constants or initialisers (a carrier), reads= counts the read sites the " + "uses verb lists for that NAME (matched by name, every same-named definition; present only when nonzero and only where " + "that verb indexes reads, so its absence means none counted, never none exist) and reads_at= names the first three as " + "file:line in that verb's order (source before tests, then path and line); vals= names up to three function values the " + "carrier itself stores (resolved by name to an indexed function), as slot=function@line: the slot key (absent for a " + "plain assignment) and the line of the one definition that name resolves to (path:line when it is in another file, no " + "line when the name resolves to several); vals_total= counts all of them when more than three. When the top hit sits in a carrier with " + "reads=, the root next= is the uses verb on that carrier instead of the at verb. "; + inline constexpr const char* kGrepEncFloorClause = "callers_floor= (value 1, present only on such a row) marks a count the index holds evidence of a miss for: a declined or " "unbound call spelled like the name, a use as a value, or a kind used by reading or naming it (a variable, a class, an " @@ -687,7 +800,7 @@ std::vector grepHitRefs( const rw::Config& cfg, const rw::IngestResu int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector* amp, const std::vector* tested, - const GrepScanPhases* prefetched ) + const GrepScanPhases* prefetched, const std::string& root, bool valueUses ) { using namespace rw; const std::string pat( cfg.grep ); @@ -764,6 +877,8 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw const std::vector encRows = grepEnclosingRows( ing, g, std::span( hits ) ); // R1b, before the legend const std::vector encFloors = grepEncFloors( ing, g, encRows ); const bool anyEncFloor = std::ranges::any_of( encFloors, []( const CallerFloor& f ) { return f.isFloor; } ); + const std::vector encCarriers = grepPageCarriers( cfg, ing, root, encRows, valueUses ); // L4, before the legend + const bool anyCarrierAttr = std::ranges::any_of( encCarriers, []( const GrepCarrier& c ) { return ( c.readsKnown && c.reads > 0 ) || !c.vals.empty(); } ); const std::vector hitRefs = grepHitRefs( cfg, ing, tierReport, encRows, hits ); // idea #5: ref= (empty off a code+string answer) std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; @@ -912,7 +1027,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // parse this verb's header counters by grep, and a quoted numeric example here would be matched first — the quality-delta legend's rule. "After the hit rows, rows list each DISTINCT enclosing symbol NAME of THIS page (first-appearance order, bounded by the page) with callers= its 1-hop DISTINCT-caller count, " "unioned across same-named defs like the callers verb (a FLOOR — dynamic dispatch contributes no edge), defs= how many defs the name grouped (only when more than one), cx= complexity; " - "{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=) + "{}{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=); L4: kGrepCarrierClause, the same "amp=/tested= join only when a metrics co-run already computed that lens. On a zero-hit answer a element may follow: SUGGESTIONS, never matches — near= the nearest indexed " "symbol name (did-you-mean), next= a ready-to-paste conceptual fallback; absent for regex/non-word-like patterns or when nothing plausible exists. " // T1: the completeness claim, defined where it appears (no attribute=value literal in these @@ -965,7 +1080,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // P3 (L7): next= on the root, defined where the reader meets it "next= is the one pasteable follow-up: the at verb on the top hit; the next page (compact legend) when cut; " "the conceptual lens on a zero-hit answer. " - "{} -->", anyEncFloor ? kGrepEncFloorClause : "", rw::kPageRaiseCapClause ); + "{} -->", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", rw::kPageRaiseCapClause ); } emitGrepHandleLegend( cfg.grepHandles ); // G3: terms=/scope=/suppressed= — only when AND/NOT was actually given, so a plain --grep answer @@ -1005,6 +1120,10 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw { grepNext = rw::nextFlag( cfg.grepRegex ? "--regex=" : "--grep=", pat ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact"; } + else if( const std::string carrierNext = grepCarrierNext( ing, hits, encRows, encCarriers ); !carrierNext.empty() ) + { + grepNext = carrierNext; // L4: the top hit sits in a carrier the uses verb lists reads for — those reads are the next hop + } else if( !hits.empty() ) { grepNext = rw::nextFlag( "--at=", std::string( pathFor( hits[ 0 ].fileId ) ) + ":" + std::to_string( hits[ 0 ].line ) ); @@ -1113,7 +1232,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // ── R1b: the block — the map's context on the answer, no second call (helper above) ────── const GrepEncOptions encOpt{ amp, tested, cfg.grepHandles, - cfg.roots.size() == 1 ? cfg.roots[0] : std::string_view(), esc }; + cfg.roots.size() == 1 ? cfg.roots[0] : std::string_view(), esc, encCarriers, singleRoot, rootPrefix }; emitGrepEncRows( ing, g, encRows, encFloors, encOpt ); // ── R1a: the zero-hit follow-up — suggestions, labeled as such, never matches (helper above) ─── @@ -1136,7 +1255,7 @@ std::optional runGrep( const MainDispatch& d ) } // body: emitGrepReport() above. amp/tested are non-null only when a co-run (--metrics) computed // them at dispatch build time — grep itself never asks for the analysis (R1's no-new-analysis rule). - return emitGrepReport( d.cfg, d.ing, d.g, d.ampPtr, d.testedPtr, d.grepPhases ); + return emitGrepReport( d.cfg, d.ing, d.g, d.ampPtr, d.testedPtr, d.grepPhases, d.root, d.valueUses ); } } // namespace — verbs_grep.h section of main.cpp diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index 14ed41c79..c506cc296 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -47,6 +47,12 @@ # GO grep on the Go kinds honesty-small-068 added (t="type" `type N string`, t="functype"): read as a struct # is (a conversion is one call, a type mention is never counted) => callers_floor="1" + floor_next=, like the # Go struct beside them (the kinds' rows in countfloor.h kUseFormOfKind, train 26b). +# P (L4) the grep CARRIER: a hit inside a variable/constant/initialiser prints reads=/reads_at= (the uses verb's read +# sites, Python and C++ where that verb indexes reads; the same sites --uses lists) and vals= (the function values it +# stores, slot=fn@definition line, path:line in another file); root next= becomes --uses= when reads > 0. +# Near misses: a carrier read nowhere (no reads=, next= stays the at verb), a string-only carrier (no vals=), a hit +# inside a function (row and next= unchanged), C/TS carriers (no reads=: not indexed), a C++ header carrier (no +# reads=: its .c readers are unseen). MCP twin: the same reads/reads_at/vals keys. # N legend: every new attribute is defined in the answer that carries it (compact and full dialects). # O determinism (x2) + xmllint. # @@ -406,14 +412,88 @@ for n in TestName Pair Handler; do fi done +# ── P: the grep CARRIER (L4, src/grepcarrier.h) ────────────────────────────────────────────────────────── +# A hit inside a variable/constant/initialiser: reads=/reads_at= (the uses verb's read sites, where it indexes reads) +# and vals= (the function values the carrier stores, @ the one definition each resolves to); next= the uses verb. +echo "=== P: grep on a carrier names its reads and the functions it stores ===" +mkdir -p "$W/pc/lib" "$W/cc" "$W/tsc" +printf 'def handle_get():\n return 1\n\ndef handle_put():\n return 2\n' > "$W/pc/lib/handlers.py" +printf 'from .handlers import handle_get\n\ndef local_put():\n return 3\n\nUNREAD = "PC_UNREAD_MARK"\nNAMED = "PC_NAMED_MARK"\nTABLE = {"get": handle_get, "put": local_put, "label": "PC_TABLE_MARK"}\n\ndef inner():\n return "PC_INNER_MARK"\n' > "$W/pc/lib/conf.py" +printf 'from . import conf\n\ndef first():\n return conf.NAMED\n\ndef second():\n x = conf.NAMED\n return x\n' > "$W/pc/lib/reader.py" +printf 'from lib import conf\n\ndef test_named():\n assert conf.NAMED\n' > "$W/pc/test_reader.py" +printf 'struct entry { const char *name; int (*exec)(int); int (*undo)(int); };\nint other_exec(int);\nstatic int local_exec(int v) { return v; }\nconst struct entry ent = {\n\t.name = "C_ENT_MARK",\n\t.exec = local_exec,\n\t.undo = other_exec\n};\nstatic const char *plain = "C_PLAIN_MARK";\nint body(void) { return 1 + (plain != 0); }\n' > "$W/cc/ent.c" +printf 'int other_exec(int v)\n{\n\treturn v + 1;\n}\n' > "$W/cc/other.c" +printf 'const char *kCppName = "CPP_NAME_MARK";\n' > "$W/cc/names.cpp" +printf 'extern const char *kCppName;\nconst char *use_name() { return kCppName; }\n' > "$W/cc/use.cpp" +printf 'extern const char *kHdrName;\nconst char *kHdrName = "CPP_HDR_MARK";\n' > "$W/cc/hdr.h" +printf '#include "hdr.h"\nconst char *use_hdr() { return kHdrName; }\n' > "$W/cc/usehdr.cpp" +printf 'function homeHandler() { return 1; }\nexport const ROUTES = { home: homeHandler, name: "TS_ROUTES_MARK" };\nexport function useRoutes() { return ROUTES.home; }\n' > "$W/tsc/routes.ts" + +OUT_P="$( run pc --grep=PC_NAMED_MARK )"; ROW_P="$( enc_row "$OUT_P" NAMED )" +[ -n "$ROW_P" ] || no "(P) premise: no row" +[ "$( attr "$ROW_P" reads )" = 3 ] && ok "(P) reads=\"3\" — the uses verb's three read sites of NAMED" || no "(P) reads on NAMED: $ROW_P" +[ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ] \ + && ok "(P) reads_at= lists them in the uses verb's order (source before tests, then path, line)" || no "(P) reads_at on NAMED: $ROW_P" +USES_P="$( run pc --uses=NAMED | grep -oE ' row" +case "$ROW_PU" in *reads=*|*reads_at=*) no "(P) near miss: a carrier read nowhere printed reads: $ROW_PU" ;; *) ok "(P) near miss: a carrier read nowhere carries no reads= (absent, never a zero)" ;; esac +[ "$( attr "$R_PU" next )" = "--at=lib/conf.py:6" ] && ok "(P) near miss: no reads => next= stays the at verb on the top hit" || no "(P) near miss next: $R_PU" + +ROW_PT="$( enc_row "$( run pc --grep=PC_TABLE_MARK )" TABLE )" +[ "$( attr "$ROW_PT" vals )" = "[get]=handle_get@lib/handlers.py:1 [put]=local_put@3" ] \ + && ok "(P) vals= on a dict carrier: [key]=fn@its definition (path:line in another file)" || no "(P) vals on TABLE: $ROW_PT" + +ROW_PI="$( enc_row "$( run pc --grep=PC_INNER_MARK )" inner )"; R_PI="$( root_tag "$( run pc --grep=PC_INNER_MARK )" grep )" +[ -n "$ROW_PI" ] || no "(P) premise: no row" +case "$ROW_PI" in *reads=*|*vals=*) no "(P) negative: a literal in a function body got carrier attributes: $ROW_PI" ;; *) ok "(P) negative: a hit inside a function keeps its row unchanged" ;; esac +[ "$( attr "$R_PI" next )" = "--at=lib/conf.py:11" ] && ok "(P) negative: next= stays the at verb inside a function" || no "(P) negative next: $R_PI" + +ROW_PC="$( enc_row "$( run cc --grep=C_ENT_MARK )" ent )" +[ "$( attr "$ROW_PC" vals )" = "exec=local_exec@3 undo=other_exec@other.c:1" ] \ + && ok "(P) C initialiser: vals=\"exec=local_exec@3 undo=other_exec@other.c:1\"" || no "(P) C initialiser row: $ROW_PC" +case "$ROW_PC" in *reads=*) no "(P) C reads are not indexed, yet reads= printed: $ROW_PC" ;; *) ok "(P) C: no reads= (the uses verb indexes no C reads; floor_next stays the literal scan)" ;; esac +ROW_PCP="$( enc_row "$( run cc --grep=C_PLAIN_MARK )" plain )" +[ -n "$ROW_PCP" ] || no "(P) premise: no row" +case "$ROW_PCP" in *vals=*|*reads=*) no "(P) near miss: a C string constant got carrier attributes: $ROW_PCP" ;; *) ok "(P) near miss: a C constant holding a string and read in C carries neither attribute" ;; esac +ROW_PX="$( enc_row "$( run cc --grep=CPP_NAME_MARK )" kCppName )" +[ "$( attr "$ROW_PX" reads )" = 1 ] && [ "$( attr "$ROW_PX" reads_at )" = "use.cpp:2" ] && ok "(P) C++ (reads indexed): reads=\"1\" reads_at=\"use.cpp:2\"" || no "(P) C++ row: $ROW_PX" +ROW_PH="$( enc_row "$( run cc --grep=CPP_HDR_MARK )" kHdrName )" +[ -n "$ROW_PH" ] || no "(P) premise: no row" +case "$ROW_PH" in *reads=*) no "(P) near miss: a header carrier counted reads its .c users hide: $ROW_PH" ;; *) ok "(P) near miss: a .h carrier carries no reads= (its readers may be .c files the uses verb never reads)" ;; esac +ROW_PS="$( enc_row "$( run tsc --grep=TS_ROUTES_MARK )" ROUTES )" +[ "$( attr "$ROW_PS" vals )" = "home=homeHandler@1" ] && ok "(P) TS object carrier: vals=\"home=homeHandler@1\"" || no "(P) TS row: $ROW_PS" +case "$ROW_PS" in *reads=*) no "(P) TS reads are not indexed, yet reads= printed: $ROW_PS" ;; *) ok "(P) TS: no reads= (not indexed for TS)" ;; esac + +MCP_P="$( mcp_call grep "{\"path\":\"$W/pc\",\"pattern\":\"PC_NAMED_MARK\"}" )" +printf '%s' "$MCP_P" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "NAMED", {} ) +sys.exit( 0 if r.get( "reads" ) == 3 and r.get( "reads_at" ) == [ "lib/reader.py:4", "lib/reader.py:7", "test_reader.py:4" ] else 1 )' \ + && ok "(P) MCP grep enclosing NAMED: reads 3 and the same reads_at as the CLI" || no "(P) MCP grep NAMED: $MCP_P" +MCP_PC="$( mcp_call grep "{\"path\":\"$W/cc\",\"pattern\":\"C_ENT_MARK\"}" )" +printf '%s' "$MCP_PC" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "ent", {} ) +sys.exit( 0 if r.get( "vals" ) == [ "exec=local_exec@3", "undo=other_exec@other.c:1" ] and "reads" not in r else 1 )' \ + && ok "(P) MCP grep enclosing ent: the same vals as the CLI, no reads key" || no "(P) MCP grep ent: $MCP_PC" + # ── N: legend ──────────────────────────────────────────────────────────────────────────────────────────── echo "=== N: every new attribute is defined where it rides ===" for dialect in compact full; do L_A="$( run c --safe-delete=cell --legend=$dialect )"; L_C="$( run c --safe-delete=width_of --legend=$dialect )" L_E="$( run py --grep=APP_BACKEND_ENV --legend=$dialect )" L_K="$( run tsl '--for=which router classes implement the Router interface' --legend=$dialect )" + L_P="$( run pc --grep=PC_TABLE_MARK --legend=$dialect )"; L_PN="$( run pc --grep=PC_NAMED_MARK --legend=$dialect )" for pair in "L_A:callers_floor=" "L_A:uses_floor=" "L_A:unmodelled" "L_C:sites_at=" "L_E:callers_floor=" "L_E:floor_next=" \ - "L_K:implementors_floor=" "L_K:implementors_shown=" "L_K:floor_next="; do + "L_K:implementors_floor=" "L_K:implementors_shown=" "L_K:floor_next=" "L_P:vals=" "L_PN:reads=" "L_PN:reads_at="; do var="${pair%%:*}"; term="${pair#*:}" comments "${!var}" | grep -qF -- "$term" \ && ok "(N/$dialect) $term defined in the legend of the $var answer" || no "(N/$dialect) $term is not defined in the $var legend" @@ -422,10 +502,16 @@ done OUT_N="$( run c --safe-delete=helper_dead --legend=compact )" [ -n "$( root_tag "$OUT_N" safe-delete )" ] || no "(N) premise: no answer for helper_dead (an empty answer must not read as present-only)" case "$OUT_N" in *callers_floor*|*uses_floor*|*sites_l*|*sites_at*) no "(N) a legend defines a floor/sites term its answer does not carry" ;; *) ok "(N) present-only: no floor/sites term on an answer without them" ;; esac +for dialect in compact full; do + OUT_NP="$( run pc --grep=PC_INNER_MARK --legend=$dialect )" + [ -n "$( root_tag "$OUT_NP" grep )" ] || no "(N) premise: no answer for PC_INNER_MARK" + case "$OUT_NP" in *reads_at*|*vals=*) no "(N/$dialect) a legend defines a carrier term its answer does not carry" ;; *) ok "(N/$dialect) present-only: no carrier term on an answer without a carrier attribute" ;; esac +done # ── O: determinism + xmllint ───────────────────────────────────────────────────────────────────────────── echo "=== O: determinism and well-formed XML ===" -for spec in "c --safe-delete=cell" "c --safe-delete=width_of" "py --grep=APP_BACKEND_ENV" "tsl --lego=Router"; do +for spec in "c --safe-delete=cell" "c --safe-delete=width_of" "py --grep=APP_BACKEND_ENV" "tsl --lego=Router" "pc --grep=PC_NAMED_MARK" "cc --grep=C_ENT_MARK" \ + "pc --grep=PC_TABLE_MARK --legend=full"; do set -- $spec; a="$( run "$@" )"; b="$( run "$@" )" if [ -n "$a" ] && [ "$a" = "$b" ]; then ok "(O) deterministic: $spec"; else no "(O) not deterministic (or empty): $spec"; fi if command -v xmllint >/dev/null 2>&1; then From a0a4f1c200883dac45b624a9c6d83cfbf3703a78 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 13:59:33 -0400 Subject: [PATCH 030/162] fix(mcp grep): next mirrors the CLI carrier rule The MCP grep twin computed its next before the enclosing rows existed, so a top hit inside a carrier with reads kept next=--at= while the CLI root said --uses=. The rows and the carrier verdict are now computed once before the root, and grepCarrierNext lives in src/grepcarrier.h for both surfaces. Gate: test/countfloorcheck.sh arm P (MCP next). Co-Authored-By: Claude Opus 5.5 --- src/grepcarrier.h | 22 ++++++++++++++++++++++ src/mcpverbs.h | 23 ++++++++++++++--------- src/verbs_grep.h | 22 +--------------------- test/countfloorcheck.sh | 4 ++++ 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/grepcarrier.h b/src/grepcarrier.h index 407ac10fa..777d4106a 100644 --- a/src/grepcarrier.h +++ b/src/grepcarrier.h @@ -25,6 +25,7 @@ #include "countfloor.h" // valueUsesArmedFor — where reads reach the use-site table #include "filter.h" // pathTierIndexOver, compareTierThenPath — the uses verb's base row order #include "model.h" +#include "nextverb.h" // nextFlag — the root next= spelling #include "sarif.h" // rootRelativeUri — the root-relative spelling every grep path takes #include "search.h" // GrepEncRow — the rows this decorates #include "valuerefindex.h" // ValueRefIndex — madeIn / targetsOf, the stored function values @@ -251,6 +252,27 @@ inline std::vector grepCarriers( const IngestResult& ing, const Val return out; } +// The root next= on an uncut answer whose TOP hit sits in a carrier with reads= — the uses verb on that carrier's name +// (the spelling floor_next= uses, countfloor.h countFloorNext), because where the value is read is the hop the at verb +// on the hit's own line cannot give. "" otherwise: the at verb on the top hit stays the follow-up. The CLI grep root +// and the MCP grep twin both call it; each caller keeps its cut-answer rule (the next page) ahead of this one. +inline std::string grepCarrierNext( const IngestResult& ing, std::span hits, std::span rows, + std::span carriers ) +{ + if( hits.empty() || hits[0].enclosingId == kNoNode ) + { + return {}; + } + for( std::size_t r = 0; r < rows.size() && r < carriers.size(); ++r ) + { + if( std::ranges::find( rows[r].ids, hits[0].enclosingId ) != rows[r].ids.end() ) + { + return carriers[r].readsKnown && carriers[r].reads > 0 ? nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ) : std::string(); + } + } + return {}; +} + // The two list attributes as plain text items (the emitters escape them for their own dialect): reads_at= as // path:line, vals= as slot=fn@line / slot=fn@path:line / slot=fn — paths root-relative exactly when the hit rows' are. struct GrepCarrierText diff --git a/src/mcpverbs.h b/src/mcpverbs.h index b500e2043..49c831a5c 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -888,11 +888,11 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // under --handles mints (search.h grepEncHandleCandidate decides WHICH row may carry one; handleFor mints it from the index's // own byte hash) — or `handle_omitted` with the CLI legend's reason. Before this a grep answer named enclosing symbols that // fetch_body could not take, so the agent re-read every file it had just searched. -inline std::string grepEnclosingJson( const McpIndex& ix, std::span hits, bool singleRoot, std::string_view rootPrefix ) +inline std::string grepEnclosingJson( const McpIndex& ix, std::span encRows, std::span carriers, + bool singleRoot, std::string_view rootPrefix ) { - const IngestResult& ing = ix.ing; - const Graph& g = ix.g; - const std::vector encRows = grepEnclosingRows( ing, g, hits ); + const IngestResult& ing = ix.ing; + const Graph& g = ix.g; if( encRows.empty() ) { return {}; @@ -906,10 +906,7 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span floors = callerFloors( ing, g, rowIds, &valueRefIndexOf( ix ) ); ENSURES( floors.size() == encRows.size() ); - // L4: the CLI carrier attributes (grepcarrier.h), by the same function. The MCP index is the RICH family - // (mcpindex.h ingests with value uses), so its own references carry the reads: no second ingest here. - const std::vector carriers = grepCarriers( ing, valueRefIndexOf( ix ), encRows, &ing ); - ENSURES( carriers.size() == encRows.size() ); + EXPECTS( carriers.size() == encRows.size(), "the caller computed one carrier verdict per row (grepCarriers)" ); std::string out = ",\"enclosing\":["; for( std::size_t r = 0; r < encRows.size(); ++r ) { @@ -1174,6 +1171,12 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat const PageWindow grepPage = pageWindow( collected.raw.size(), effectiveRowCap( page.limit, grepRowCap ), page.offset ); const std::size_t rowCount = grepPage.end - grepPage.begin; const std::vector hits = grepEnrich( ing, std::span( collected.raw ).subspan( grepPage.begin, rowCount ), 0, 0 ); + // R1 rows and L4's carrier verdict, once, before the root: next= reads them (grepCarrierNext) and so does the + // enclosing block. The MCP index is the RICH family (mcpindex.h ingests with value uses), so its own references + // carry the reads — no second ingest here, unlike the lean CLI run. + const std::vector encRowsJ = grepEnclosingRows( ing, ix.g, std::span( hits ) ); + const std::vector carriersJ = grepCarriers( ing, valueRefIndexOf( ix ), encRowsJ, &ing ); + const std::string carrierNextJ = grepCarrierNext( ing, hits, encRowsJ, carriersJ ); // §R-J: the CLI's unindexed_files_scanned=/unindexed block twin (search.h grepCollectAux) — literal-only, // matching this verb. No --max-file-size equivalent on the MCP surface, so this uses the same @@ -1249,6 +1252,8 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat // P3 (L7): the CLI grep root's next= — the same three-way rule (verbs_grep.h), mcpclidiffcheck LENS2 + nextFieldJson( ( grepPage.end < collected.raw.size() || collected.isBudgetReached ) ? nextFlag( "--grep=", pattern ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact" + : !carrierNextJ.empty() + ? carrierNextJ : !collected.raw.empty() && grepPage.begin < collected.raw.size() ? nextFlag( "--at=", std::string( pathForJ( collected.raw[ grepPage.begin ].fileId ) ) + ":" + std::to_string( collected.raw[ grepPage.begin ].line ) ) : nextFlag( "--for=", pattern ) ) @@ -1308,7 +1313,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat singleRootJ, rootPrefixJ ); // R1 (the 2026-08-12 usage mine): the CLI / twins, appended AFTER "hits" so the // historic key order three other gates read (files,total,shown,capped) is byte-untouched. - out += grepEnclosingJson( ix, std::span( hits ), singleRootJ, rootPrefixJ ); + out += grepEnclosingJson( ix, encRowsJ, carriersJ, singleRootJ, rootPrefixJ ); if( collected.raw.empty() ) { out += grepSuggestJson( ing, pattern ); diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 1f72c6075..d6916a52c 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -230,26 +230,6 @@ std::vector grepPageCarriers( const rw::Config& cfg, const rw:: return rw::grepCarriers( ing, vri, rows, source.reads ); } -// L4: the root next= on an uncut answer whose TOP hit sits in a carrier with reads= — the uses verb on that carrier's name -// (the spelling floor_next= uses, countfloor.h countFloorNext), because where the value is read is the hop the at verb on -// the hit's own line cannot give. "" otherwise: the at verb on the top hit stays the follow-up. -std::string grepCarrierNext( const rw::IngestResult& ing, std::span hits, std::span rows, - std::span carriers ) -{ - if( hits.empty() || hits[0].enclosingId == rw::kNoNode ) - { - return {}; - } - for( std::size_t r = 0; r < rows.size() && r < carriers.size(); ++r ) - { - if( std::ranges::find( rows[r].ids, hits[0].enclosingId ) != rows[r].ids.end() ) - { - return carriers[r].readsKnown && carriers[r].reads > 0 ? rw::nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ) : std::string(); - } - } - return {}; -} - // count-floor: callers= beside evidence of a miss for THAT row is a floor (countfloor.h). Computed once per page, before // the legend, so the full dialect defines callers_floor= exactly when a row carries it; the MCP twin calls the same // function over the same rows (mcpverbs.h grepEnclosingJson), so the two surfaces cannot disagree. @@ -1120,7 +1100,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw { grepNext = rw::nextFlag( cfg.grepRegex ? "--regex=" : "--grep=", pat ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact"; } - else if( const std::string carrierNext = grepCarrierNext( ing, hits, encRows, encCarriers ); !carrierNext.empty() ) + else if( const std::string carrierNext = rw::grepCarrierNext( ing, hits, encRows, encCarriers ); !carrierNext.empty() ) { grepNext = carrierNext; // L4: the top hit sits in a carrier the uses verb lists reads for — those reads are the next hop } diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index c506cc296..fecf10027 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -477,6 +477,10 @@ d = json.loads( sys.stdin.read() ) r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "NAMED", {} ) sys.exit( 0 if r.get( "reads" ) == 3 and r.get( "reads_at" ) == [ "lib/reader.py:4", "lib/reader.py:7", "test_reader.py:4" ] else 1 )' \ && ok "(P) MCP grep enclosing NAMED: reads 3 and the same reads_at as the CLI" || no "(P) MCP grep NAMED: $MCP_P" +printf '%s' "$MCP_P" | python3 -c ' +import json, sys +sys.exit( 0 if json.loads( sys.stdin.read() ).get( "next" ) == "--uses=NAMED" else 1 )' \ + && ok "(P) MCP grep next is --uses=NAMED, as the CLI root next=" || no "(P) MCP grep next: $MCP_P" MCP_PC="$( mcp_call grep "{\"path\":\"$W/cc\",\"pattern\":\"C_ENT_MARK\"}" )" printf '%s' "$MCP_PC" | python3 -c ' import json, sys From 755b2d3c39c902cd2608e97513d5588fc35fe41b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 14:21:12 -0400 Subject: [PATCH 031/162] fix(valuerefs): capture a function stored through a value-preserving operator Value-reference capture climbed parentheses and ternary branches but stopped at a logical operator, so `options.getPath ?? getPath` stored no getPath row. The shared walk (transparent) now reads a per-family table of value-preserving operators: JS/TS ?? || && (left operand of && only decides) and Python or/and (likewise for and), plus the JS logical assignments ??= ||= &&= read as an assignment. C/C++ and Go || && yield a truth value and stay no row; Ruby, Java, C#, Swift, Rust and Bash have no value-reference capture. kParserVer 173 (the cached Value rows change; the train renumbers if a concurrent lane took 172); test/qschemetrip.hash re-pinned. Gate: test/recallshapecheck.sh arms OP1-OP17 (positives red on the base). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++ src/ingest_cache.h | 6 ++- src/ingest_valuerefs.h | 52 ++++++++++++++++++++++- src/quality.h | 3 +- test/qschemetrip.hash | 2 +- test/recallshapecheck.sh | 91 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 160 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9967e39..4b6d0d903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — a function stored through `??`, `||`, `&&`, `or` or `and` is a value use + +`this.getPath = (strict ?? true) ? (options.getPath ?? getPath) : getPathNoStrict` recorded only `getPathNoStrict`: +value-reference capture walked parentheses and ternary branches but stopped at a logical operator, so `--callers`, +`--impact`, `--safe-delete` and `--dead-code` never saw `getPath` stored there. The walk now climbs the +value-preserving operators — JS/TS `??`, `||`, `&&` and the logical assignments `??=`, `||=`, `&&=`; Python `or`, +`and` — in every position it already read (assignment, return, argument, parameter default). The left operand of +`&&`/`and` only decides and stays no row; C/C++ and Go `||`/`&&` yield a truth value and stay no row. Parser version +173 (caches re-parse once). Gate: `test/recallshapecheck.sh` arms OP1–OP17. + ### Added — `--grep` names what a hit's variable carries and who reads it When a hit sits inside a variable, constant or initialiser (its enclosing definition's kind is a variable), the `` diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 1c50420a9..351ce0398 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -320,7 +320,11 @@ constexpr std::uint32_t kCacheVersion = 31; // 31: train 27 — lane l // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 171; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 173; // bump on any grammar/.scm/extraction change + // 173 = 2026-10-14 (train 28 lane grep-carrier): value-reference capture walks the + // value-preserving operators (JS ?? || &&, Python or/and, JS ??= ||= &&=), so the + // Value rows a cache holds change. 173, not 172: a lean 172 would read 171's rich + // blobs, and a concurrent lane may take 172 — the train renumbers. Rich file tag 174. // 171 = 2026-10-13 (PR #349 lands, @tag-und-nacht): Clojure grammar, tags.scm and extraction // (ingest_clojure.h) and the qualified-call namespace in Reference::qualifier. The branch built // 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168). 171, not 168: a lean 168 diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index 8670585a1..eefbc71ee 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -66,6 +66,36 @@ inline constexpr std::size_t kVrTextCap = 96; // a written slot / callee // truncateUtf8WithEllipsis's (that header is not reachable from the ingest side): "…" only when text was really cut — // a text of exactly kVrTextCap bytes is whole — and never inside a UTF-8 sequence (a lead byte without its // continuation bytes would reach into=/through= as an invalid character). +// VALUE-PRESERVING OPERATORS (L4, train 28): a binary operator whose RESULT is one of its operands, so a function named +// there lands in the slot the whole expression fills — `this.getPath = options.getPath ?? getPath` stores getPath. +// One shared walk (transparent() below) reads this table, per family, as data: +// * leftLands: the LEFT operand can be the result. `f || g`, `f ?? g`, `f or g` yield f (a function is truthy and +// never nullish); `f && g`, `f and g` yield g, never f — a function on the left of && only decides, so it is no row. +// * the right operand always can. +// Families with no row here, and why: C/C++ and Go `||` / `&&` yield a truth value (int or bool), never an operand, so +// a function there is a test, not a stored value. The conditional `c ? f : g` (C, C++, JS/TS; Python `f if c else g`) +// and parentheses are handled by transparent() for every family already. Value references are captured for the C, JS, +// Python and Go families only (model.h kValueRefFamilyOfLang); Ruby `a || m`, Java, C#, Swift, Rust, Bash have no +// value-reference capture at all, so they have no operator to walk either. +struct VrPassOp +{ + VrFam fam; + std::string_view nodeKind; + std::string_view op; + bool leftLands; +}; +inline constexpr VrPassOp kVrPassOps[] = { + { VrFam::Js, "binary_expression", "??", true }, + { VrFam::Js, "binary_expression", "||", true }, + { VrFam::Js, "binary_expression", "&&", false }, + { VrFam::Py, "boolean_operator", "or", true }, + { VrFam::Py, "boolean_operator", "and", false }, +}; +// The JS logical ASSIGNMENTS (`x ??= f`, `x ||= f`, `x &&= f`) store the right operand into the left exactly as `=` +// may: classify() reads them as an assignment. No other family has a value-preserving compound assignment +// (C `x |= f` and Python `x |= f` are bitwise/set operators whose result is not f). +inline constexpr std::string_view kVrJsLogicalAssignOps[] = { "\?\?=", "||=", "&&=" }; // escaped: unescaped, those three characters spell a trigraph + inline std::string vrClean( std::string_view s ) { std::string out; @@ -975,9 +1005,28 @@ class ValueRefWalk } return c.field != 0 && ( c.field == m_fConsequence || c.field == m_fAlternative ); } + return c.field != 0 && passesOperator( p, c ); + } + + // A value-preserving binary operator (kVrPassOps) whose operand `c` can be the expression's result. + bool passesOperator( const VrAnc& p, const VrAnc& c ) const noexcept + { + for( const VrPassOp& po : kVrPassOps ) + { + if( po.fam == m_fam && std::string_view( p.kind ) == po.nodeKind && opText( p.node ) == po.op ) + { + return c.field == m_fRight || ( po.leftLands && c.field == m_fLeft ); + } + } return false; } + bool isLogicalAssignment( TSNode n ) const noexcept + { + const std::string_view op = opText( n ); + return m_fam == VrFam::Js && std::ranges::find( kVrJsLogicalAssignOps, op ) != std::ranges::end( kVrJsLogicalAssignOps ); + } + // The callee of the call whose argument list is anc[j] (the list itself), as written, plus its bare name. std::pair calleeOf( std::size_t j ) const { @@ -1391,7 +1440,8 @@ class ValueRefWalk s.scope = 'f'; return !nm.empty(); } - if( ( kindIs( pt, "assignment_expression" ) || kindIs( pt, "assignment" ) ) && c.field == m_fRight ) + if( ( kindIs( pt, "assignment_expression" ) || kindIs( pt, "assignment" ) + || ( kindIs( pt, "augmented_assignment_expression" ) && isLogicalAssignment( p.node ) ) ) && c.field == m_fRight ) { const TSNode lhs = child( p.node, m_fLeft ); const char* lt = ts_node_type( lhs ); diff --git a/src/quality.h b/src/quality.h index 0a0b746df..350d84712 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2268,7 +2268,8 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 31; // MUST equal ingest.cpp's kCacheVersion (gated); 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 171; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 173; // MUST equal ingest.cpp's kParserVer (gated) + // 173 = 2026-10-14 (train 28 grep-carrier: value refs through ?? || && / or and) // 171 = 2026-10-13 (PR #349 Clojure lands above main's 167 / rich tag 168 and the // branch's own 169 / rich tag 170; see the kParserVer note) // 167 = 2026-10-11 (train 27 release: lane fix27-cpp-subscript's 166 renumbered above the lane build; diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index f1ce3205d..189e81021 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -23cb6ecf5949981b75965273ff9af22fc68cdfe109bbd7f99b7167d59bdbc0cb +82085ad1d2fbf866750fb7e3f928f611464f8ae1227e4453c9b6e12d6af122de diff --git a/test/recallshapecheck.sh b/test/recallshapecheck.sh index f55584c5b..35394f9a6 100755 --- a/test/recallshapecheck.sh +++ b/test/recallshapecheck.sh @@ -68,6 +68,10 @@ # a row (REG#arg0). # F6 a class used as a value (`{"x": MyClass}`) is out of scope: functions and methods only. # +# OP (L4, train 28): the walk climbs value-preserving operators (ingest_valuerefs.h kVrPassOps) — JS/TS ?? || &&, +# Python or/and, the JS logical assignments — with near misses (the left operand of && / and, C ||, a non-function +# name, a same-named parameter) that a looser walk would turn into rows. +# # MUTATION PROOF (checklist 1; sim/refval_mutate.sh, measured at the implementation head — the pre-registered list was # corrected to what each mutant measured): disabling the local/parameter shadow guard turns C5 C14 C2f C2g J2 J2g T2 P2 # P3 P2i G3 X3 red; disabling the keyed-element / keyword-LABEL guard turns P10 P12 P13 G7 G9 red (a C designated field @@ -1308,6 +1312,93 @@ arm "Z3 uses hot: 300 role=value sites, paged by the verb's own window (shown=10 arm_roots "Z4 multi-root (big c) callers hot: the same window on a two-root run" "big c" --callers=hot \ attr:value_refs=300 el:vrs:total=300 el:vrs:shown=64 el:vrs:capped=1 nvr:64 +# ── OP (L4, train 28): value-preserving operators — `a ?? f`, `a || f`, `c && f` (JS/TS), `a or f`, `c and f` (Python), +# and the JS logical assignments `x ??= f` — land f in the slot the whole expression fills, in assignment, return, +# argument and parameter-default positions. A function on the LEFT of && / and only decides (the result is the right +# operand): no row. C/C++ and Go || / && yield a truth value: no row. Ternaries and parentheses were already walked. ── +echo "-- value-preserving operators" +mkdir -p "$FX/opjs" "$FX/opts" "$FX/oppy" "$FX/opc" +cat >"$FX/opjs/a.js" <<'EOF' +function getPath(r) { return r; } +function noStrict(r) { return r; } +function defH() { return 1; } +function andH() { return 2; } +function andLeft() { return 3; } +function argH() { return 4; } +function laH() { return 5; } +function orH() { return 6; } +const plainVal = 3; +class App { + constructor(options, strict) { + this.getPath = (strict ?? true) ? (options.getPath ?? getPath) : noStrict; // @OJ_HONO + } +} +function withDefault(cb = opts.cb || defH) { return cb(); } // @OJ_DEF +function pick(flag) { return flag && andH; } // @OJ_RET +function leftAnd(flag) { return andLeft && flag; } // @OJ_ANDL +function reg(cfg) { return register(cfg.h ?? argH); } // @OJ_ARG +function logical(slot) { let s = slot; s ??= laH; return s; } // @OJ_LA +const t = orH || other; // @OJ_ORL +function negPlain(cfg) { const v = cfg.x ?? plainVal; return v; } // @OJ_NEG +function negShadow(getPath) { return other ?? getPath; } // @OJ_SHADOW +EOF +cat >"$FX/opts/a.ts" <<'EOF' +function tsH(): number { return 1; } +export function pickTs(o: { h?: () => number }) { return o.h ?? tsH; } // @OT_RET +export class Holder { run: () => number = (globalThis as any).x || tsH; } // @OT_FIELD +EOF +cat >"$FX/oppy/m.py" <<'EOF' +def py_h(): + return 1 +def py_and(): + return 2 +def py_left(): + return 3 +def py_def(): + return 4 +def py_arg(): + return 5 +PLAIN = 3 +def choose(o): + return o.h or py_h # @OP_OR +def gate(flag): + return flag and py_and # @OP_AND +def left_and(flag): + return py_left and flag # @OP_ANDL +def with_default(cb=None or py_def): # @OP_DEF + return cb() +register(None or py_arg) # @OP_ARG +def neg(o): + return o.x or PLAIN # @OP_NEG +EOF +cat >"$FX/opc/c.c" <<'EOF' +static int c_h(int v) { return v; } +static int c_t(int v) { return v + 1; } +int use_or(int f) { return f || c_h; } /* @OC_OR */ +int (*use_ternary(int f))(int) { return f ? c_t : 0; } /* @OC_TERN */ +EOF +arm "OP1 callers getPath: the hono shape — getPath inside ?? inside a ternary branch lands in this.getPath" \ + opjs --callers=getPath attr:value_refs=1 'vr:bind=@OJ_HONO;into=this.getPath;in_id=constructor' 'novr:bind=@OJ_SHADOW' +arm "OP1b callers noStrict (control): the ternary alternative was already a row" \ + opjs --callers=noStrict attr:value_refs=1 'vr:bind=@OJ_HONO;into=this.getPath' +arm "OP2 callers defH: a parameter default through || (JS)" opjs --callers=defH attr:value_refs=1 'vr:bind=@OJ_DEF;into=withDefault#cb' +arm "OP3 callers andH: the right operand of && is returned" opjs --callers=andH attr:value_refs=1 'vr:bind=@OJ_RET;into=(return)' +arm "OP4 callers andLeft (near miss): the LEFT operand of && only decides — no row" opjs --callers=andLeft attr:count=0 noattr:value_refs nvr:0 +arm "OP5 callers argH: an argument through ??" opjs --callers=argH attr:value_refs=1 'vr:bind=@OJ_ARG;into=register#arg0' +arm "OP6 callers laH: a logical assignment ??= stores the right operand" opjs --callers=laH attr:value_refs=1 'vr:bind=@OJ_LA;into=s' +arm "OP7 callers orH: the LEFT operand of || can be the result" opjs --callers=orH attr:value_refs=1 'vr:bind=@OJ_ORL;into=t' +arm "OP8 callees negPlain (negative): a non-function identifier in ?? is no value reference" opjs --callees=negPlain attr:count=0 noattr:value_refs nvr:0 +arm "OP9 callers tsH (TypeScript): a return through ??; FLOOR: a class FIELD initialiser is no value position at all (operator or not)" \ + opts --callers=tsH attr:value_refs=1 'vr:bind=@OT_RET;into=(return)' 'novr:bind=@OT_FIELD' +arm "OP10 callers py_h (Python): the right operand of or is returned" oppy --callers=py_h attr:value_refs=1 'vr:bind=@OP_OR;into=(return)' +arm "OP11 callers py_and (Python): the right operand of and is returned" oppy --callers=py_and attr:value_refs=1 'vr:bind=@OP_AND;into=(return)' +arm "OP12 callers py_left (near miss): the LEFT operand of and only decides — no row" oppy --callers=py_left attr:count=0 noattr:value_refs nvr:0 +arm "OP13 callers py_def (Python): a parameter default through or" oppy --callers=py_def attr:value_refs=1 'vr:bind=@OP_DEF;into=with_default#cb' +arm "OP14 callers py_arg (Python): an argument through or" oppy --callers=py_arg attr:value_refs=1 'vr:bind=@OP_ARG;into=register#arg0' +arm "OP15 callees neg (negative): a non-function name in or is no value reference" oppy --callees=neg attr:count=0 noattr:value_refs nvr:0 +arm "OP16 callers c_h (C, negative): || yields a truth value — no row" opc --callers=c_h attr:count=0 noattr:value_refs nvr:0 +arm "OP17 callers c_t (C control): the ternary branch is a returned value" opc --callers=c_t attr:value_refs=1 'vr:bind=@OC_TERN;into=(return)' + # ── Controls: no value reference → no new attribute, element, row or legend clause ─────────────────────── echo "-- controls" ctl_arm() { # ctl_arm ARGV PRESENCE-TOKEN… From 77e22558854ff9a426166b0a6dd6b83a9ebe41a4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 14:53:59 -0400 Subject: [PATCH 032/162] fix(for): guard at its pre-registered 30,000 B; lego scoped at the target; rule-4 gate twins - kForRunawayGuardBytes = 30,000 B by the frozen rule: max(4 x 7,500, 2 x the largest complete default payload on the round-2 --for argv (12,519 B)); zero hits on the round-4 and capsweep argv (max 13,005 B). - The default answer narrows / to the rows at its target cut (sigsRowsAtTarget), so the rows kept past the target do not turn a served lego section into a stub (the section invariant). - An exhausted soft target's hops marker clears the hop-only readings (via=, owner qword), so the owner plan adds nothing there (ownerhopcheck B2). - MCP from_trace names its budget_tokens on the root and reads it as a soft target, like the CLI twin; the compact from-trace reading says "soft". - Rule-4 replacements (no gate erosion; each replaced arm green on head and red on base where it asserts the new behaviour, twins green on both): estchargecheck, ceilingverdictcheck (FORGE kept), formaxtokenscheck (E), w3fixbudgetcheck, forsigspancheck (T), tracecheck (T3), mcpforparitycheck (5), fordisclosurecheck, bundleidcheck, droppedpositivecheck #6, jsonparitycheck R1, forrootlegendcheck arm2, forbudgetmonotoncheck #1/#5, legendcoveragecheck (E), ownerhopcheck (R). Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 2 +- src/compactlegend.h | 2 +- src/mcpverbs.h | 5 +- src/serialize.h | 32 +- src/verbs_for.h | 12 +- test/bundleidcheck.sh | 70 ++- test/ceilingverdictcheck.sh | 383 ++++++++++------ test/droppedpositivecheck.sh | 17 +- test/estchargecheck.sh | 796 ++++++++++++++++++---------------- test/forbudgetmonotoncheck.sh | 10 +- test/fordisclosurecheck.sh | 86 ++-- test/formaxtokenscheck.sh | 104 ++++- test/forrootlegendcheck.sh | 22 +- test/forsigspancheck.sh | 57 ++- test/jsonparitycheck.sh | 21 +- test/legendcoveragecheck.sh | 12 +- test/mcpforparitycheck.sh | 47 +- test/ownerhopcheck.sh | 6 +- test/tracecheck.sh | 33 +- test/w3fixbudgetcheck.sh | 271 +++++++----- 20 files changed, 1295 insertions(+), 693 deletions(-) diff --git a/src/cli.h b/src/cli.h index 4f8ff80d9..b75100f71 100644 --- a/src/cli.h +++ b/src/cli.h @@ -924,7 +924,7 @@ inline constexpr char kHelpHead[] = " the answer and every row of it ships. A ~7.5KB payload TARGET shortens row text toward\n" " it (tail signatures and doc excerpts first; with S=T\n" " marks a shrink); rows served past it carry past_target=\"K\" (the last K rows). Only a\n" - " runaway guard (32KB, a bug signal) cuts rows: over_ceiling=\"1\" and serving\n" + " runaway guard (30KB, a bug signal) cuts rows: over_ceiling=\"1\" and serving\n" " them all (S printed of the T rows handed to the trim). The r=1 (top-ranked)\n" // §B7.5 (CA4): this said — the STRING ENUM the §P8 vocabulary migration // replaced with the boolean capped="1" every other truncating element spells (serialize.h's own diff --git a/src/compactlegend.h b/src/compactlegend.h index b6f55246c..9596b471f 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1296,7 +1296,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "core_overlap", "core_overlap=: share of the core surface a partition reaches anyway", false, "ctx-partitions", MapHeaderRead::No, {}, "pack-task" }, // from-trace: src/tracelocus.h (renderTraceBlock, buildTraceHeader via ctxRootOpen, the budget_tokens attr) { "task", "task=: the trace source this bundle maps, verbatim (a file path, stdin, or an MCP label)", false, "ctx", MapHeaderRead::No, {}, "from-trace" }, - { "budget_tokens", "budget_tokens=N: the token budget passed (absent when none); over_ceiling=1 if est_tokens= exceeds it", false, "ctx", MapHeaderRead::No, {}, "from-trace" }, + { "budget_tokens", "budget_tokens=N: the soft token target passed (absent when none); est_tokens= may exceed it, no over_ceiling for it", false, "ctx", MapHeaderRead::No, {}, "from-trace" }, // forbudget-soft: the caller's budget is a soft target (tracelocus.h) { "src", "src=/format=: the trace read and its dominant frame format, python|asan|node|compiler|generic", true, "trace", MapHeaderRead::No, {}, "from-trace" }, // also defines format= { "frame_lines", "frame_lines=/parsed=: frame-shaped input lines / those yielding a path:line; the rest matched no format", true, "trace", MapHeaderRead::No, {}, "from-trace" }, // also defines parsed= { "in_corpus", "in_corpus=: parsed frames in indexed files; always suspects= + merged= + unresolved=", true, "trace", MapHeaderRead::No, {}, "from-trace" }, diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 188c1520c..00b9c675a 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2081,7 +2081,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin // whole block away (serialize.h narrowLegoToRenderedSigs' own R-E note): MCP `for` served no at all where the // CLI served six rows. const std::string mcpLegoRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr, mcpLegoRootPrefix ) ) + if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsRowsAtTarget( sigsStr, mcpSigsCut ), mcpLegoRootPrefix ) ) // forbudget-soft { legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed } @@ -4568,6 +4568,9 @@ inline FromTraceResult fromTraceText( const std::string& root, const std::string FromTraceInputs in; in.bundleBudgetBytes = budgetTokens > 0 ? rw::budgetBytesForTokens( budgetTokens ) : rw::kForPayloadBudgetBytes; + // forbudget-soft (sibling of the CLI --from-trace): the caller's budget_tokens is named on the root and read as a SOFT + // target (no ceiling ladder, no over_ceiling for it) — this twin used to shape by it without naming it at all + in.budgetTokens = budgetTokens; in.fanIn = &fanIn; in.impure = &impure; in.redact = redact; diff --git a/src/serialize.h b/src/serialize.h index c3840a358..3442896d0 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -931,10 +931,11 @@ inline constexpr std::size_t kForCapTailSigBytes = 96; // sections + ), far above a typical answer: hitting it is a BUG SIGNAL (a recursion, a dump, a head far wider than any // question needs), never a budget. A default answer that hits it discloses over_ceiling="1" on the root, (the continuation serves every row: an explicit target is never cut by this guard, see max(guard, T)) and -// kForRunawayGuardNote. The value is set by the pre-registered rule (forbudget-soft PLAN §G): at least 4 x the target and -// 2 x the largest complete default payload measured on the round-2 --for argv, verified with zero hits on the round-4 and -// capsweep argv. Gate: test/forsoftbudgetcheck.sh (D4, D4b, NX2). -inline constexpr std::size_t kForRunawayGuardBytes = 32000; +// kForRunawayGuardNote. The value is set by the pre-registered rule (forbudget-soft PLAN §G): roundup_1000( max( 4 x the +// target, 2 x the largest complete default payload on the round-2 --for argv ) ) = max( 30,000, 2 x 12,519 ) = 30,000 B (44 +// default-regime round-2 answers, measured on the provisional 32,000 build); verified with zero hits on the round-4-only (max +// 13,005 B) and capsweep (max 12,094 B) argv. Gate: test/forsoftbudgetcheck.sh (D4, D4b, NX2). +inline constexpr std::size_t kForRunawayGuardBytes = 30000; static_assert( kForRunawayGuardBytes >= 4 * kForPayloadBudgetBytes, "the runaway guard sits far above the default target" ); // The reading spliced on an answer that hit the guard (and only there). It names the value so a reader (and the gate) reads the // guard from the answer itself. "A bug signal" is true of a default-size head; an explicitly wide --pack-top-n head is named @@ -8833,6 +8834,29 @@ inline std::vector> legoImplementorsOnSurface( const IngestR // every --for run whose rendered sigs happened to hit this path at all. Caught by legobundlecheck.sh going // red for the wrong reason (an empty lego, not a mis-narrowed one) rather than by a dedicated assertion — // worth a gate of its own if this class recurs. +// forbudget-soft: the text a DEFAULT answer's sections are scoped by — the rows up to the target cut, without the +// past_target= rows (the last K rows in emission order), so / narrow as they did when the target was a +// ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). +// The whole block on a soft target or an answer with nothing past its target. Emission order is rank order except where the +// docs reorder moved a doc row below the code rows (docs_after_code=): then the cut falls one row early or late — a scope +// over- or under-approximation by that row's file, never a wrong row. +inline std::string_view sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) noexcept +{ + if( !cut.pastTargetDisclosed || cut.pastTarget == 0 || cut.pastTarget >= cut.shown ) + { + return sigsRendered; + } + const std::size_t keep = cut.shown - cut.pastTarget; + std::size_t seen = 0; + for( std::size_t at = sigsRendered.find( ">& legoScoped, std::string_view sigsRendered, std::string_view rootPrefix = {} ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index f2f4e5327..94c12b36e 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -3573,7 +3573,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // block to the RENDERED sigs' files and re-render (a byte-subset of what the budget already charged // for, so the bundle only shrinks; before est_tokens so the header reports the delivered size). if( sigsPreRendered && legoPreRendered && !legoStr.empty() - && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr, flRootArg.empty() ? std::string_view() : rw::sarif::rootPrefixOf( flRootArg ) ) ) + && narrowLegoToRenderedSigs( ing, legoScoped, rw::sigsRowsAtTarget( sigsStr, forSigsCut ), // forbudget-soft: scoped at the target + flRootArg.empty() ? std::string_view() : rw::sarif::rootPrefixOf( flRootArg ) ) ) { legoStr = captureXml( [ & ]( std::FILE* f ) { packLego( f, ing, legoScoped, lensRank, 12, redactPtr, impurePtr, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &g.implementors ); } ); } @@ -3748,6 +3749,15 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { // fix round 4: the hops budget-cut marker's present-only clause says what the empty element is sectionsStubNote += headerParts.compactLegend ? kForHopsBudgetCutLegendCompact : kForHopsBudgetCutLegend; + // forbudget-soft: the marker carries no row, so the readings only hop rows could need (the via="name" and the + // owner qword clauses) have nothing to define — cleared as the old attribute-only shape cleared them, so a + // spent target answers byte-identically with and without the owner plan (ownerhopcheck (B2)); the bundle legend stays + if( headerParts.viaPresent || headerParts.ownerPresent ) + { + headerParts.viaPresent = false; + headerParts.ownerPresent = false; + headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + } } // ── DEEP-TAIL d2: render the file-grain tail, funded LAST (see serialize.h kForFileTailShownCap) ── diff --git a/test/bundleidcheck.sh b/test/bundleidcheck.sh index b99fb460e..f9ad2a17c 100755 --- a/test/bundleidcheck.sh +++ b/test/bundleidcheck.sh @@ -15,8 +15,10 @@ # for the same symbol (one canonical id form across lenses — the --plan-lanes prerequisite). # * --for and --pack-task agree on in= for every symbol both bundles name (never 0-vs-real). # * no bundle asserts in="0" without a fan-in source: --from-trace OMITS the attribute instead. -# * the added attributes are ACCOUNTED for, not overflowed — a --token-budget=N run still fits its own -# reported byte budget, and --for's est_tokens still matches the bytes actually delivered. +# * the added attributes are ACCOUNTED for, not overflowed — a --token-budget=N run's SECTIONS still fit its own +# reported ceiling (forbudget-soft: an explicit --pack-task --token-budget is a soft target — the header is never +# charged and never labelled over_ceiling; the default --pack-task keeps the hard fit-or-label rule), and --for's +# est_tokens still matches the bytes actually delivered. # * determinism (run twice → byte-identical) + well-formed XML on all three verbs. # Mutation-tested: the n=-presence assertion is checked to actually FAIL on a row stripped of n=. # Exits non-zero on any failure. @@ -198,16 +200,20 @@ for B in 600 1200 6000; do # label is the ladder's colon note (or the root attribute), never the bare word. OVERLBL="$( sed -n 's/.*.*/\1/p' "$TMP/b.$B" | grep -cE 'over_ceiling(="1"|:)' )" [ "${OVERLBL:-0}" -gt 0 ] && OVERLBL=1 || OVERLBL=0 - [ "$BYTES" -gt "$CEILTOL" ] && OVERREAL=1 || OVERREAL=0 - if [ -z "$BUDGET" ] || [ -z "$CEIL" ]; then - no "--token-budget=$B: header reported no budget/ceiling — the ledger is unreadable" - elif [ "$OVERREAL" = "$OVERLBL" ]; then - if [ "$OVERREAL" = 0 ]; then ok "--token-budget=$B: delivered $BYTES B <= ceiling $CEIL B * 1.15 first-entry tolerance (budget $BUDGET B), no over_ceiling claimed" - else ok "--token-budget=$B: delivered $BYTES B past ceiling $CEIL B * 1.15 and SAYS over_ceiling (the ladder's honest rung)"; fi - elif [ "$OVERREAL" = 1 ]; then - no "--token-budget=$B: delivered $BYTES B OVERFLOWS ceiling $CEIL B even at the 1.15 single-entry tolerance, with NO over_ceiling label" + grep -qE '^]* over_ceiling=' "$TMP/b.$B" && OVERLBL=1 + # forbudget-soft (rule 4): an EXPLICIT --pack-task --token-budget is a SOFT target. The biconditional above (whole document + # <= ceiling*1.15 iff no over_ceiling) priced the header against a hard ceiling; it is replaced by NOLBL (never over_ceiling= + # / an "over_ceiling:" note on an explicit budget) + ST (the SECTIONS — everything past the root and its header comments — + # fit the ledger's ceiling*1.15: the quota cascade is unchanged, the header is never charged) + the legend names the soft + # target. RED on base (d4325a8c): 600/1200 carry over_ceiling="1" and a legend without "soft token target". The hard twin + # (the DEFAULT --pack-task keeps fit-or-label) follows the loop. + PAY="$( python3 -c 'import re,sys; s=open(sys.argv[1],"rb").read(); m=re.match(rb"]*>(?:)*",s,re.S); print(len(s)-m.end() if m else "")' "$TMP/b.$B" 2>/dev/null )" + if [ -z "$BUDGET" ] || [ -z "$CEIL" ] || [ -z "$PAY" ]; then + no "--token-budget=$B: header reported no budget/ceiling (or no root to split the sections off) — the ledger is unreadable" + elif [ "$OVERLBL" = 0 ] && [ "$PAY" -le "$CEILTOL" ] && grep -q 'soft token target' "$TMP/b.$B"; then + ok "--token-budget=$B: soft target — sections $PAY B <= ceiling $CEIL B * 1.15 (document $BYTES B, header uncharged), no over_ceiling, legend names the soft target" else - no "--token-budget=$B: delivered $BYTES B FITS ceiling $CEIL B * 1.15 yet the header claims over_ceiling" + no "--token-budget=$B: soft target broken — over_ceiling=$OVERLBL (must be 0), sections $PAY B vs ceiling*1.15 $CEILTOL B, soft-target legend $( grep -c 'soft token target' "$TMP/b.$B" )" fi # the truncation ledger must still reconcile: every "kept X of Y" has X <= Y python3 - "$TMP/b.$B" <<'PY' || no "--token-budget=$B: truncation ledger does not reconcile" @@ -218,6 +224,19 @@ sys.exit( 1 if bad else 0 ) PY done [ $fail -eq 0 ] && ok "truncation ledgers reconcile (kept <= total on every section)" +# (twin) the DEFAULT --pack-task (no --token-budget) keeps its HARD ceiling: the biconditional this arm used to state for every +# budget — the document fits ceiling*1.15 iff it carries no over_ceiling label (a fitting answer that claims over_ceiling fails too). +"$BIN" "$FIX" --no-cache --pack-task="$TASK" --legend=full >"$TMP/b.def" 2>/dev/null +D_BYTES="$( wc -c < "$TMP/b.def" | tr -d ' ' )"; D_CEIL="$( grep -o 'ceiling [0-9]*' "$TMP/b.def" | head -1 | tr -dc '0-9' )" +D_LBL="$( sed -n 's/.*.*/\1/p' "$TMP/b.def" | grep -cE 'over_ceiling(="1"|:)' )" +grep -qE '^]* over_ceiling="1"' "$TMP/b.def" && D_LBL=1 +if [ -z "$D_CEIL" ] || [ -z "$D_BYTES" ]; then + no "(twin) default --pack-task: header reported no ceiling — the hard ledger is unreadable" +elif { [ "$D_BYTES" -le $(( D_CEIL * 115 / 100 )) ] && [ "${D_LBL:-0}" -eq 0 ]; } || { [ "$D_BYTES" -gt $(( D_CEIL * 115 / 100 )) ] && [ "${D_LBL:-0}" -gt 0 ]; }; then + ok "(twin) default --pack-task: hard ceiling honest — $D_BYTES B vs ceiling $D_CEIL B * 1.15, over_ceiling label=${D_LBL:-0}" +else + no "(twin) default --pack-task: $D_BYTES B vs ceiling $D_CEIL B * 1.15 with over_ceiling label=${D_LBL:-0} — fit-or-label broken on the hard default" +fi # ── 5b) W3FIX M1 — the FIXTURE BLIND SPOT the arm above had: a THREE-WORD task ────────────────────────── # $TASK is "compute perimeter distance" — 26 bytes. The header echoes the task TWICE (verbatim in task=, and @@ -234,17 +253,24 @@ else "$BIN" "$FIX" --no-cache --pack-task="$LONGTASK" --token-budget=$B --legend=full >"$TMP/lt.$B" 2>/dev/null LT_BYTES="$( wc -c < "$TMP/lt.$B" | tr -d ' ' )" LT_CEIL="$( grep -o 'ceiling [0-9]*' "$TMP/lt.$B" | head -1 | tr -dc '0-9' )" - # the disclosure lives in the header COMMENT — read only up to the first "-->" so a body cannot supply it - LT_HDR="$( head -c 6000 "$TMP/lt.$B" | sed -e 's/-->.*//' )" - if [ -z "$LT_CEIL" ]; then - no "5b --token-budget=$B (320-byte task): header reported no ceiling — the ledger is unreadable" - elif [ "$LT_BYTES" -le $(( LT_CEIL * 115 / 100 )) ]; then - ok "5b --token-budget=$B (320-byte task): delivered $LT_BYTES B <= ceiling $LT_CEIL B * 1.15" - elif printf '%s' "$LT_HDR" | grep -q 'over_ceiling'; then - ok "5b --token-budget=$B (320-byte task): $LT_BYTES B over ceiling $LT_CEIL B and DISCLOSES over_ceiling" - else - no "5b --token-budget=$B (320-byte task): $LT_BYTES B past ceiling $LT_CEIL B * 1.15 in SILENCE" - fi + # forbudget-soft (rule 4): "fit ceiling*1.15 or SAY over_ceiling" is gone on an explicit budget (a soft target labels + # nothing; the old grep -q 'over_ceiling' then passed on the legend's own "no over_ceiling" words). Replaced by HL + NOLBL + # + the floor: the 320-byte task rides VERBATIM in task= and in the header echo, no clause is dropped "(ceiling)", no + # task_echo: dropped, no over_ceiling= / "over_ceiling:" note, a ranking section with >= 1 row ships, and the sections + # fit ceiling*1.15. RED on base (d4325a8c: over_ceiling="1" + the "over_ceiling: the header floor" note). + LT_HDR="$( python3 -c 'import re,sys; s=open(sys.argv[1],encoding="utf-8").read(); m=re.match(r"]*>(?:)*",s,re.S); print(m.group(0) if m else "")' "$TMP/lt.$B" 2>/dev/null )" + LT_PAY="$( python3 -c 'import re,sys; s=open(sys.argv[1],"rb").read(); m=re.match(rb"]*>(?:)*",s,re.S); print(len(s)-m.end() if m else "")' "$TMP/lt.$B" 2>/dev/null )" + lt_bad="" + [ -n "$LT_CEIL" ] && [ -n "$LT_PAY" ] || lt_bad="$lt_bad; no ceiling ledger / no root (premise)" + printf '%s' "$LT_HDR" | grep -qF "]* over_ceiling=' && lt_bad="$lt_bad; over_ceiling on a soft target" + grep -qE ']*> budget_tokens, and the BYTE comparison -# the ladder's last rung is — the document past ceilingAllowanceBytes( budget ) = budget x 2.36 x 1.15 -# (serialize.h). The second is deliberately WIDER than the first, so "over_ceiling implies est > budget" would -# be a false invariant. The honest one, which is what arm (4) sweeps, is the disjunction: the label may ride -# only on a document that is over one of the two ceilings its own root states, in numbers a reader can check. +# forbudget-soft (rule 4, owner decisions 2026-10-14) MOVED WHERE THE VERDICTS LIVE, and this gate follows them +# rather than the old premise "a tight --token-budget emits over_ceiling". An explicit --token-budget on --for, +# --pack-task and --from-trace is now a SOFT target: there is no over_ceiling for budget_tokens at all, and the +# --for header ladder (and with it the last-rung notes this gate used to impersonate) is gone. The verdicts that +# still exist, and so the surfaces the forgery class can still reach: +# · the --for RUNAWAY GUARD (serialize.h kForRunawayGuardBytes / forRunawayGuardNote): over_ceiling="1" only +# from the structural row-drop cut (verbs_for.h forGuardCut = forSigsCut.rowDropCut) — e.g. --pack-top-n=400; +# · --max-tokens on --for --detail (the body ceiling, est_tokens > max_tokens); +# · the DEFAULT --pack-task (no flag, 6000 tokens): its hard ceiling ladder + over_ceiling are unchanged; +# · --from-trace --max-tokens (est_tokens > max_tokens). +# On each: the FORGED task (the verdict's own clause, verbatim) and a PLAIN twin (the same words, the marker +# punctuation removed, so the ranking is the same) must get the SAME verdict and the SAME shape — and where the +# verdict is real (the guard cut, a tight --max-tokens, a header floor past 6000 tokens) BOTH carry it, so a +# green "no forged label" cannot come from a deleted attribute. On the soft lenses the arms are now +# FORGE + NOLBL: forged vs plain give identical shown/total/next=/rows, and neither is labelled. +# +# WHAT IS ASSERTED, per arm (codes from the lane brief: FORGE, NOLBL): +# (0)/(0b)/(0c)/(0d) the probes are LIVE: each forged clause is what the code emits / splices today; +# (1)/(6) the forged text reaches the emitted document (the injection vector is exercised); +# (2) wide soft budget, (2b) default regime, (3) tight soft budget — --for: FORGE + NOLBL; +# (3g) the guard cut: forged and plain BOTH labelled, identical shape (the verdict still fires when real); +# (3m) --for --detail --max-tokens: forged vs plain identical verdict at a wide and a tight ceiling; +# (4) the soft sweep over both lenses: FORGE + NOLBL at every point (counted, never vacuous); +# (7)/(8) --pack-task wide/tight soft: FORGE + NOLBL; (8d) the DEFAULT --pack-task: FORGE, and a real label; +# (8t) --from-trace --max-tokens: FORGE at a wide and a tight ceiling; +# (5) the source-shape recurrence guard; (9) the map's unmeasured --max-tokens fit. # # bash test/ceilingverdictcheck.sh # build/ripwire # bash test/ceilingverdictcheck.sh build_base/ripwire # or RIPWIRE_BIN=… — both seams honored @@ -54,42 +73,43 @@ ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write th no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "ceilingverdictcheck: python3 is required (shape comparison)"; exit 2; } CORPUS="$ROOT/src" [ -d "$CORPUS" ] || { echo "missing corpus $CORPUS"; exit 2; } echo "ceilingverdictcheck: BIN=$BIN" -# The forged marker is the ladder's LAST-RUNG note, verbatim. Pinned here as a literal rather than parsed out -# of the C++ (it is a two-line string concatenation), with a PRESENCE GUARD below so a re-worded note reds -# this gate instead of silently turning the probe into a task that forges nothing (trap: a vanishing probe -# target). Both halves are checked, because the source splits the constant between them. -MARK_A='over_ceiling= is 1 on the root: the header floor (verbatim task echo + fixed legend) exceeds this budget' -MARK_B='- no payload left to trim]' -MARK="[$MARK_A $MARK_B" -FORGED="serialize the header $MARK" -PLAIN='serialize the header ranking' +# THE FORGED MARKERS — each verdict's own clause, verbatim, pinned as literals with a LIVENESS guard (arm 0*) +# so a re-worded clause reds this gate instead of silently turning a probe into a task that forges nothing. +# --for: the runaway guard's reading (serialize.h forRunawayGuardNote), the one over_ceiling a default --for root +# can still carry. Its liveness is checked on the EMITTED guard document, not the source (the value is spliced in). +# The guard's byte value is spliced into the clause, so it is read from src/serialize.h (a stale binary then reds (0)). +GUARD_BYTES="$( sed -n 's/^inline constexpr [a-z:_ ]*kForRunawayGuardBytes *= *\([0-9]*\);.*/\1/p' "$ROOT/src/serialize.h" | head -1 )" +GUARD_NOTE="[over_ceiling=1: the ranked head passed the runaway guard of ${GUARD_BYTES:-MISSING} B (header plus ranked payload), so the rows past it are cut and sigs next= serves them all; on a default-size head this is a bug signal, not a budget (a wide pack-top-n head can reach it too)]" +FORGED="serialize the header $GUARD_NOTE" +# the PLAIN twin: every marker character replaced by a space, byte for byte (the ranker reads words, so both rank alike — +# arms (2b)/(3g) prove it by comparing the served rows), with no substring a marker sniff could match. Same LENGTH on +# purpose: the default text tier and the continuation's budget are charged with the header, which echoes the task, so a +# shorter twin would move docs_dropped= / next= for a reason that is not the marker. +plainOf(){ printf '%s' "$1" | sed -E 's/[^A-Za-z0-9 ]/ /g'; } +PLAIN="$( plainOf "$FORGED" )" +# --for --detail --max-tokens: the legend clause that defines the body-ceiling label (serialize.h kOverCeilingMaxTokensLegend) +MT_NOTE='over_ceiling=1 says est_tokens exceeds max_tokens' +MT_FORGED="serialize the header [$MT_NOTE]" +MT_PLAIN="$( plainOf "$MT_FORGED" )" # --pack-task's own last-rung note, same role, its own punctuation (packtask.h kNotes). The forgeable part is # the marker WORD, which is all the old sniff looked for — so the probe task carries only that. PT_MARK='over_ceiling:' PT_FORGED="why does $PT_MARK fire on this root" -PT_PLAIN='why does over ceiling fire on this root' - -# (0) the probe is live: the note this task impersonates is still the note the code splices. +PT_PLAIN="$( plainOf "$PT_FORGED" )" +# --from-trace: the named-ceiling last-rung note (tracelocus.h kNotesNamedCeiling), up to its first apostrophe (an +# attribute escapes it, and grep -F reads the raw bytes). The trace SOURCE is what this lens echoes (task=, src=), so +# the forged text rides the trace file's NAME. +TR_NOTE='[over_ceiling: the header floor (verbatim src echo + fixed legend) plus the innermost frame' SRC="$ROOT/src/verbs_for.h" PSRC="$ROOT/src/packtask.h" -if grep -qF -e "$MARK_A" "$SRC" && grep -qF -e "$MARK_B" "$SRC"; then # -e: MARK_B starts with '-' - ok "(0) the forged marker is still the ladder's last-rung note in src/verbs_for.h (the probe can forge)" -else - no "(0) the last-rung note in src/verbs_for.h no longer matches this gate's MARK_A/MARK_B — re-pin them, or the forgery probe is inert and every arm below passes for the wrong reason" -fi -if grep -qF -e "| $PT_MARK the header floor" "$PSRC"; then - ok "(0b) '$PT_MARK' is still the marker in --pack-task's last-rung note in src/packtask.h (the pack-task probe can forge)" -else - no "(0b) src/packtask.h's last-rung note no longer spells '$PT_MARK' — re-pin PT_MARK, or arms (6)-(8) prove nothing" -fi +TSRC="$ROOT/src/tracelocus.h" -run(){ "$BIN" "$CORPUS" --no-cache --token-budget="$1" --for="$2" 2>/dev/null; } -runPack(){ "$BIN" "$CORPUS" --no-cache --token-budget="$1" --pack-task="$2" 2>/dev/null; } attr(){ grep -o " $2=\"[0-9]*\"" <<< "$1" | head -1 | tr -cd '0-9'; } # leading space: est_tokens must not match inside a longer name # THE LABEL IS A ROOT ATTRIBUTE, so every arm reads it off the ROOT ELEMENT and not off the document. Both # lenses echo the caller's task into an XML COMMENT with no quote escaping, so `over_ceiling="1"` typed into a @@ -99,146 +119,257 @@ attr(){ grep -o " $2=\"[0-9]*\"" <<< "$1" | head -1 | tr -cd '0-9'; } # leadin rootOf(){ grep -o ']*>' "$1" | head -1; } hasLabel(){ rootOf "$1" | grep -c 'over_ceiling="1"' || true; } -WIDE=100000 -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --for="$FORGED" > "$TMP/forged.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --for="$PLAIN" > "$TMP/plain.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget=300 --for="$FORGED" > "$TMP/forged.tight" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --pack-task="$PT_FORGED" > "$TMP/pt.forged.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --pack-task="$PT_PLAIN" > "$TMP/pt.plain.wide" 2>/dev/null -"$BIN" "$CORPUS" --no-cache --token-budget=300 --pack-task="$PT_FORGED" > "$TMP/pt.forged.tight" 2>/dev/null +# THE SHAPE a verdict could be forged into: the root's posture attributes, every windowed section's +# shown/total/capped/next_offset/past_target/docs_dropped and its next= (the task text replaced by TASK, since the +# continuation re-asks the caller's own task), the counts, and the served rows in rank order. Two +# documents whose tasks differ only in marker punctuation must print the SAME shape. Exit 3 = no root. +SHAPE="$TMP/shape.py" +cat > "$SHAPE" <<'PY_EOF' +import html, re, sys +s = open( sys.argv[1], encoding = 'utf-8', errors = 'replace' ).read() +task = sys.argv[2] +s = re.sub( r'', '', s, flags = re.S ) # a body's text is not the shape +s = re.sub( r'', '', s, flags = re.S ) # nor is the legend (it echoes the task) +root = re.search( r']*)>', s ) +if root is None: sys.exit( 3 ) +def attrs( text ): return dict( re.findall( r'(\w+)="([^"]*)"', text ) ) +def norm( v ): + v = html.unescape( v ) + return v.replace( task, 'TASK' ) if task else v +ra = attrs( root.group( 1 ) ) +print( 'root ' + ' '.join( '%s=%s' % ( k, ra[k] ) for k in ( 'route', 'bundle', 'bodies', 'reason', 'budget_tokens', 'max_tokens', 'over_ceiling' ) if k in ra ) ) +for m in re.finditer( r'<(sigs|bodies|callers|far|hops|tail|tests|notes|trace)\b([^>]*)>', s ): + a = attrs( m.group( 2 ) ) + keep = [ '%s=%s' % ( k, a[k] ) for k in ( 'shown', 'total', 'capped', 'next_offset', 'past_target', 'docs_dropped', + 'format', 'frame_lines', 'parsed', 'in_corpus', 'suspects', 'merged', 'unresolved', 'skipped' ) if k in a ] + if 'next' in a: keep.append( 'next=' + norm( a['next'] ) ) + print( m.group( 1 ) + ' ' + ' '.join( keep ) ) +sg = re.search( r']*>(.*?)', s, re.S ) +rows = [] +if sg: + for d in re.finditer( r']*)>', sg.group( 1 ) ): + a = attrs( d.group( 1 ) ) + r = a.get( 'r', '' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, a.get( 'p', '?' ) + ':' + a.get( 'l', '?' ) + ':' + a.get( 'n', '?' ) ) ) +rows.sort() +print( 'rows %d %s' % ( len( rows ), ' '.join( x[1] for x in rows ) ) ) +PY_EOF +# sameShape FILE_A TASK_A FILE_B TASK_B — 0 when both documents parse to the same shape; else prints the first diff +sameShape(){ + python3 "$SHAPE" "$1" "$2" > "$TMP/sa" || { echo " no root in $( basename "$1" )"; return 1; } + python3 "$SHAPE" "$3" "$4" > "$TMP/sb" || { echo " no root in $( basename "$3" )"; return 1; } + grep -q '^rows [1-9]' "$TMP/sa" || { echo " $( basename "$1" ) serves no row — nothing to compare"; return 1; } + cmp -s "$TMP/sa" "$TMP/sb" && return 0 + diff "$TMP/sa" "$TMP/sb" | head -4 | cut -c1-220 | sed 's/^/ /' + return 1 +} +# forgeArm LABEL FORGED_DOC FORGED_TASK PLAIN_DOC PLAIN_TASK WANT — FORGE: identical shape; both labels == WANT (0|1) +forgeArm(){ + local fo po + [ -s "$2" ] && [ -s "$4" ] || { no "$1: a probe produced no document (forged $( wc -c < "$2" | tr -d ' ' ) B, plain $( wc -c < "$4" | tr -d ' ' ) B)"; return; } + fo="$( hasLabel "$2" )"; po="$( hasLabel "$4" )" + if [ "$fo" != "$po" ]; then + no "$1: FORGED — the marker text changed the root's verdict (forged over_ceiling=$fo vs plain $po)" + elif [ "$fo" != "$6" ]; then + no "$1: both roots carry over_ceiling count=$fo, expected $6 ($( [ "$6" = 1 ] && echo 'the verdict is real here and must still fire' || echo 'no ceiling this root names is exceeded' ))" + elif ! sameShape "$2" "$3" "$4" "$5"; then + no "$1: the forged and plain tasks served different shapes (diff above) — the marker text moved the answer" + else + ok "$1: forged and plain tasks get the same verdict (over_ceiling $( [ "$6" = 1 ] && echo 'on both' || echo 'on neither' )) and the same shown/total/next=/rows" + fi +} + +cliFor(){ "$BIN" "$CORPUS" --no-cache "$@" 2>/dev/null; } + +# ── the runs. The soft sweep first: its 300 and 100000 points are also arms (2), (3), (7), (8). ──────────────── +SWEEP="300 500 1000 2000 4000 20000 100000" +for b in $SWEEP; do + cliFor --token-budget="$b" --for="$FORGED" > "$TMP/for.$b.f" + cliFor --token-budget="$b" --for="$PLAIN" > "$TMP/for.$b.p" + cliFor --token-budget="$b" --pack-task="$PT_FORGED" > "$TMP/pt.$b.f" + cliFor --token-budget="$b" --pack-task="$PT_PLAIN" > "$TMP/pt.$b.p" +done +cliFor --for="$FORGED" > "$TMP/for.def.f" +cliFor --for="$PLAIN" > "$TMP/for.def.p" +cliFor --pack-top-n=400 --for="$FORGED" > "$TMP/for.guard.f" +cliFor --pack-top-n=400 --for="$PLAIN" > "$TMP/for.guard.p" + +# (0) the --for probe is live: the clause the forged task carries is, byte for byte, the clause the runaway guard +# splices — read off the PLAIN guard document, whose task carries no marker, so only the code can have put it there. +if [ "$( hasLabel "$TMP/for.guard.p" )" -ge 1 ] && grep -qF -e "$GUARD_NOTE" "$TMP/for.guard.p"; then + ok "(0) the forged clause is the runaway guard's own reading, as emitted on a guard cut (the --for probe can forge)" +else + no "(0) the plain --pack-top-n=400 run carries no over_ceiling=\"1\" with this gate's GUARD_NOTE verbatim — re-pin GUARD_NOTE (serialize.h forRunawayGuardNote), or the forgery probe is inert and every --for arm passes for the wrong reason" +fi +if grep -qF -e "| $PT_MARK the header floor" "$PSRC"; then + ok "(0b) '$PT_MARK' is still the marker in --pack-task's last-rung note in src/packtask.h (the pack-task probe can forge)" +else + no "(0b) src/packtask.h's last-rung note no longer spells '$PT_MARK' — re-pin PT_MARK, or the pack-task arms prove nothing" +fi +if grep -qF -e "\" $MT_NOTE\"" "$ROOT/src/serialize.h"; then + ok "(0c) the --max-tokens label's definition is still '$MT_NOTE' in src/serialize.h (the --detail probe can forge)" +else + no "(0c) src/serialize.h no longer defines the max_tokens label as '$MT_NOTE' — re-pin MT_NOTE" +fi +if grep -qF -e "\" $TR_NOTE" "$TSRC"; then + ok "(0d) the --from-trace named-ceiling note still opens '$TR_NOTE' in src/tracelocus.h (the trace probe can forge)" +else + no "(0d) src/tracelocus.h's named-ceiling note no longer opens '$TR_NOTE' — re-pin TR_NOTE" +fi -# (1) the vector really reaches the emitted document — the task echo carries the marker into the header the -# old code searched. Without this the forgery arm could pass because nothing was injected at all. -if grep -qF "$MARK_A" "$TMP/forged.wide"; then - ok "(1) the forged marker reaches the emitted header (the injection vector is exercised)" +# (1) the vector really reaches the emitted document — the task echo carries the marker into the header a +# sniff would search. Without this the forgery arms could pass because nothing was injected at all. +if grep -qF -e "$GUARD_NOTE" "$TMP/for.def.f" && ! grep -qF -e "$GUARD_NOTE" "$TMP/for.def.p"; then + ok "(1) the forged clause reaches the emitted default --for document, and only through the task (the injection vector is exercised)" else - no "(1) the forged task text does not appear in the emitted document — the injection never happened, so arm (2) proves nothing" + no "(1) the forged task text does not appear in the default --for document (or the plain twin carries it too) — the injection never happened, so arms (2)-(4) prove nothing" fi -# (2) THE HEADLINE: a document 27x inside its own byte allowance must not be labelled over its ceiling, and -# must not state a self-contradicting pair of numbers. -fw_est="$( attr "$( rootOf "$TMP/forged.wide" )" est_tokens )" -fw_bud="$( attr "$( rootOf "$TMP/forged.wide" )" budget_tokens )" -fw_over="$( hasLabel "$TMP/forged.wide" )" +# (2) wide SOFT budget: a document far inside the target it states must not be labelled, and must not state a +# self-contradicting pair of numbers; forbudget-soft (rule 4): its plain twin must also be shape-identical (FORGE). +fw_est="$( attr "$( rootOf "$TMP/for.100000.f" )" est_tokens )" +fw_bud="$( attr "$( rootOf "$TMP/for.100000.f" )" budget_tokens )" if [ -z "$fw_est" ] || [ -z "$fw_bud" ]; then no "(2) the wide-budget forged run emitted no est_tokens=/budget_tokens= pair to judge (est='$fw_est' budget='$fw_bud')" -elif [ "$fw_over" -ne 0 ]; then - no "(2) FORGED: task text put over_ceiling=\"1\" on a root that states est_tokens=$fw_est under budget_tokens=$fw_bud ($( wc -c < "$TMP/forged.wide" | tr -d ' ' ) B) — the verdict was read off the task echo, not off the document" elif [ "$fw_est" -ge "$fw_bud" ]; then - no "(2) the wide-budget probe is not wide enough to be unambiguous: est_tokens=$fw_est is not under budget_tokens=$fw_bud — raise WIDE" + no "(2) the wide-budget probe is not wide enough to be unambiguous: est_tokens=$fw_est is not under budget_tokens=$fw_bud — raise the sweep's top" else - ok "(2) task text cannot forge over_ceiling=\"1\" (est_tokens=$fw_est under budget_tokens=$fw_bud, attribute absent)" + forgeArm "(2) --for --token-budget=100000 (est_tokens=$fw_est)" "$TMP/for.100000.f" "$FORGED" "$TMP/for.100000.p" "$PLAIN" 0 fi -# (2b) fix round 5: the last rung has a SECOND reason now (src/verbs_for.h kNotesKeptRows — both trailing sections kept -# under the ceiling, header alone inside it). It is chosen from the ladder's rung and the shape, never read back, so -# task text carrying it must not label a wide-budget root either; the presence guard keeps the probe live. -KEPT_A='over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections, and with the' -if grep -qF -e "$KEPT_A" "$SRC"; then - "$BIN" "$CORPUS" --no-cache --token-budget="$WIDE" --detail=3 --with-graph --for="serialize the header [$KEPT_A" > "$TMP/kept.wide" 2>/dev/null - kw_est="$( attr "$( rootOf "$TMP/kept.wide" )" est_tokens )" - if grep -qF "$KEPT_A" "$TMP/kept.wide" && [ -n "$kw_est" ] && [ "$kw_est" -lt "$WIDE" ] && [ "$( hasLabel "$TMP/kept.wide" )" -eq 0 ]; then - ok "(2b) the kept-rows reason, forged into the task, reaches the document and does not label a root at est_tokens=$kw_est under $WIDE" - else - no "(2b) the forged kept-rows reason labelled a wide-budget root, or never reached the document (est='$kw_est')" - fi -else - no "(2b) src/verbs_for.h no longer carries the kept-rows last-rung reason — re-pin KEPT_A" -fi +# (2b) forbudget-soft (rule 4) — REPLACES the kept-rows last-rung reason (src/verbs_for.h kNotesKeptRows, deleted with +# the --for header ladder). The DEFAULT regime is where the --for guard verdict lives: the guard's own clause in +# the task must not label a default answer, nor move one row of it. +forgeArm "(2b) default --for (no budget)" "$TMP/for.def.f" "$FORGED" "$TMP/for.def.p" "$PLAIN" 0 -# (3) CONTRAST — the SAME task at a budget it genuinely blows must still be labelled. Arms (2) and (3) differ -# in NOTHING but the number after --token-budget, so (2) cannot be green because the attribute was deleted. -ft_est="$( attr "$( rootOf "$TMP/forged.tight" )" est_tokens )" -ft_over="$( hasLabel "$TMP/forged.tight" )" -if [ "$ft_over" -ge 1 ]; then - ok "(3) the same task at --token-budget=300 still carries over_ceiling=\"1\" (est_tokens=$ft_est) — the verdict still fires when it is real" +# (3) forbudget-soft (rule 4) — REPLACES "the same task at --token-budget=300 still carries over_ceiling" (a soft target +# has no ceiling verdict now). FORGE + NOLBL at a target the answer blows: same shown/total/next=/rows, no label. +ft_est="$( attr "$( rootOf "$TMP/for.300.f" )" est_tokens )" +if [ -n "$ft_est" ] && [ "$ft_est" -gt 300 ]; then + forgeArm "(3) --for --token-budget=300 (est_tokens=$ft_est, past the soft target)" "$TMP/for.300.f" "$FORGED" "$TMP/for.300.p" "$PLAIN" 0 else - no "(3) --token-budget=300 emitted no over_ceiling=\"1\" (est_tokens=$ft_est) — the label is gone, not fixed, and arm (2) is green for the wrong reason" + no "(3) --for --token-budget=300 is not past its target (est_tokens='$ft_est') — the tight point no longer probes a blown target" fi -# (4) THE INVARIANT SWEEP — over_ceiling="1" may ride only on a document that is over one of the two ceilings -# its own root names: est_tokens > budget_tokens (tokens) or bytes > budget x 2.36 x 1.15 (the ladder's -# allowance, serialize.h ceilingAllowanceBytes). Swept over both tasks so a pass needs the property to hold -# on the forged AND the honest text, and counted so the arm cannot be vacuous. -# Swept over BOTH lenses now, because both climb this ladder and only one of them was ever gated. -labelled=0; violations=0 -for b in 300 500 1000 2000 4000 20000 100000; do - # "lens:task" pairs — the split is on the FIRST colon, and every lens name is colon-free, so a task that - # contains one (the --for marker does) survives intact. - for probe in "for:$FORGED" "for:$PLAIN" "pack-task:$PT_FORGED" "pack-task:$PT_PLAIN"; do - lens="${probe%%:*}"; task="${probe#*:}" - if [ "$lens" = for ]; then run "$b" "$task" > "$TMP/sweep.xml"; else runPack "$b" "$task" > "$TMP/sweep.xml"; fi - [ -s "$TMP/sweep.xml" ] || { no "(4) $lens --token-budget=$b produced no document"; continue; } - [ "$( hasLabel "$TMP/sweep.xml" )" -ne 0 ] || continue - labelled=$(( labelled + 1 )) - est="$( attr "$( rootOf "$TMP/sweep.xml" )" est_tokens )"; bytes="$( wc -c < "$TMP/sweep.xml" | tr -d ' ' )" - allow="$( awk -v b="$b" 'BEGIN{ printf "%d", b * 2.36 * 1.15 }' )" - if [ -n "$est" ] && [ "$est" -le "$b" ] && [ "$bytes" -le "$allow" ]; then - violations=$(( violations + 1 )) - no "(4) $lens budget=$b: over_ceiling=\"1\" on a document inside BOTH ceilings it names (est_tokens=$est <= $b, bytes=$bytes <= allowance $allow) — task: $( printf '%.40s' "$task" )…" +# (3g) THE CONTRAST the old (3) was for — the verdict still fires when it is real. --pack-top-n=400 passes the runaway +# guard: BOTH tasks are labelled (from the structural cut), and the forged text moves nothing. +forgeArm "(3g) --for --pack-top-n=400 (the runaway guard cut)" "$TMP/for.guard.f" "$FORGED" "$TMP/for.guard.p" "$PLAIN" 1 + +# (3m) the --max-tokens body ceiling on --for --detail (a hard verdict that lives): forged definition vs plain, at a +# ceiling the answer fits and at one it blows — the same verdict both times, real where real. +cliFor --detail=3 --max-tokens=100000 --for="$MT_FORGED" > "$TMP/mt.wide.f" +cliFor --detail=3 --max-tokens=100000 --for="$MT_PLAIN" > "$TMP/mt.wide.p" +cliFor --detail=3 --max-tokens=300 --for="$MT_FORGED" > "$TMP/mt.tight.f" +cliFor --detail=3 --max-tokens=300 --for="$MT_PLAIN" > "$TMP/mt.tight.p" +grep -qF -e "$MT_NOTE" "$TMP/mt.tight.p" \ + && ok "(3m) the tight plain run defines its label with '$MT_NOTE' (the clause the forged task carries)" \ + || no "(3m) the tight plain --max-tokens run does not define over_ceiling with '$MT_NOTE' — the probe impersonates nothing" +forgeArm "(3m) --for --detail=3 --max-tokens=100000" "$TMP/mt.wide.f" "$MT_FORGED" "$TMP/mt.wide.p" "$MT_PLAIN" 0 +forgeArm "(3m) --for --detail=3 --max-tokens=300" "$TMP/mt.tight.f" "$MT_FORGED" "$TMP/mt.tight.p" "$MT_PLAIN" 1 + +# (4) forbudget-soft (rule 4) — REPLACES "every labelled document in the sweep is genuinely over a ceiling" (labelled == 0 +# was a FAIL). On the soft lenses a label is now never genuine, so the sweep asserts FORGE + NOLBL at every +# (lens, budget) point, counted so the arm cannot pass on zero comparisons. The real labels are (3g)/(3m)/(8d)/(8t). +compared=0; bad=0 +for b in $SWEEP; do + for lens in for pt; do + if [ "$lens" = for ]; then ft="$FORGED"; pt="$PLAIN"; else ft="$PT_FORGED"; pt="$PT_PLAIN"; fi + fd="$TMP/$lens.$b.f"; pd="$TMP/$lens.$b.p" + if [ ! -s "$fd" ] || [ ! -s "$pd" ]; then bad=$(( bad + 1 )); echo " $lens budget=$b: a probe produced no document"; continue; fi + compared=$(( compared + 1 )) + fo="$( hasLabel "$fd" )"; po="$( hasLabel "$pd" )" + if [ "$fo" != 0 ] || [ "$po" != 0 ]; then + bad=$(( bad + 1 )); echo " $lens budget=$b: over_ceiling on a soft target (forged $fo, plain $po)" + elif ! sameShape "$fd" "$ft" "$pd" "$pt"; then + bad=$(( bad + 1 )); echo " $lens budget=$b: ^ forged and plain shapes differ" fi done done -if [ "$labelled" -eq 0 ]; then - no "(4) no swept run carried over_ceiling=\"1\" at all — the invariant held vacuously; the sweep's tight budgets must produce at least one labelled document" -elif [ "$violations" -eq 0 ]; then - ok "(4) every one of the $labelled labelled documents in the sweep is genuinely over a ceiling its own root states" +if [ "$compared" -eq 0 ]; then + no "(4) the sweep compared nothing — the invariant held vacuously" +elif [ "$bad" -eq 0 ]; then + ok "(4) at all $compared (lens x soft budget) points: no over_ceiling, and forged/plain tasks serve the same shape" +else + no "(4) $bad of $( echo "$SWEEP" | wc -w | tr -d ' ' )x2 soft sweep points labelled a soft target or let the marker text move the answer" fi # (6) THE PACK-TASK INJECTION VECTOR is live — the same presence guard arm (1) is for the --for lens. -if grep -qF "$PT_MARK" "$TMP/pt.forged.wide"; then +if grep -qF "$PT_MARK" "$TMP/pt.100000.f"; then ok "(6) the forged marker reaches the emitted --pack-task document (the injection vector is exercised)" else no "(6) the forged pack-task text does not appear in the emitted document — the injection never happened, so arm (7) proves nothing" fi # (7) THE PACK-TASK HEADLINE, arm (2)'s twin: a 13-character substring in the task must not label the root. -pw_est="$( attr "$( rootOf "$TMP/pt.forged.wide" )" est_tokens )" -pw_bud="$( attr "$( rootOf "$TMP/pt.forged.wide" )" budget_tokens )" -pw_over="$( hasLabel "$TMP/pt.forged.wide" )" -pp_over="$( hasLabel "$TMP/pt.plain.wide" )" +pw_est="$( attr "$( rootOf "$TMP/pt.100000.f" )" est_tokens )" +pw_bud="$( attr "$( rootOf "$TMP/pt.100000.f" )" budget_tokens )" if [ -z "$pw_est" ] || [ -z "$pw_bud" ]; then no "(7) the wide-budget forged --pack-task run emitted no est_tokens=/budget_tokens= pair to judge (est='$pw_est' budget='$pw_bud')" -elif [ "$pw_over" -ne "$pp_over" ]; then - no "(7) FORGED: the marker word in a --pack-task task changed the root's verdict (forged over_ceiling=$pw_over vs plain $pp_over) on est_tokens=$pw_est under budget_tokens=$pw_bud — the verdict was read off the task echo" -elif [ "$pw_over" -ne 0 ]; then - no "(7) both --pack-task probes are labelled at budget_tokens=$pw_bud (est_tokens=$pw_est) — WIDE is not wide enough for the contrast to mean anything; raise it" elif [ "$pw_est" -ge "$pw_bud" ]; then - no "(7) the wide-budget --pack-task probe is not unambiguous: est_tokens=$pw_est is not under budget_tokens=$pw_bud — raise WIDE" + no "(7) the wide-budget --pack-task probe is not unambiguous: est_tokens=$pw_est is not under budget_tokens=$pw_bud — raise the sweep's top" else - ok "(7) task text cannot forge over_ceiling=\"1\" on --pack-task (est_tokens=$pw_est under budget_tokens=$pw_bud, attribute absent on the root, and absent on the plain twin too)" + forgeArm "(7) --pack-task --token-budget=100000 (est_tokens=$pw_est)" "$TMP/pt.100000.f" "$PT_FORGED" "$TMP/pt.100000.p" "$PT_PLAIN" 0 fi -# (8) PACK-TASK CONTRAST, arm (3)'s twin: same task, a budget it genuinely blows, label still there. -pt_est="$( attr "$( rootOf "$TMP/pt.forged.tight" )" est_tokens )" -if [ "$( hasLabel "$TMP/pt.forged.tight" )" -ge 1 ]; then - ok "(8) the same --pack-task at --token-budget=300 still carries over_ceiling=\"1\" (est_tokens=$pt_est) — the verdict still fires when it is real" +# (8) forbudget-soft (rule 4) — REPLACES "--pack-task --token-budget=300 still carries over_ceiling" (an explicit budget is +# a soft target): FORGE + NOLBL at a target the bundle blows. +pt_est="$( attr "$( rootOf "$TMP/pt.300.f" )" est_tokens )" +if [ -n "$pt_est" ] && [ "$pt_est" -gt 300 ]; then + forgeArm "(8) --pack-task --token-budget=300 (est_tokens=$pt_est, past the soft target)" "$TMP/pt.300.f" "$PT_FORGED" "$TMP/pt.300.p" "$PT_PLAIN" 0 else - no "(8) --pack-task --token-budget=300 emitted no over_ceiling=\"1\" (est_tokens=$pt_est) — the label is gone, not fixed, and arm (7) is green for the wrong reason" + no "(8) --pack-task --token-budget=300 is not past its target (est_tokens='$pt_est') — the tight point no longer probes a blown target" fi +# (8d) THE CONTRAST (8) was for, where the hard ladder still lives: the DEFAULT --pack-task (no flag, 6000 tokens). A +# short forged task is not labelled; a task whose header floor alone passes 6000 tokens IS — forged or plain alike. +cliFor --pack-task="$PT_FORGED" > "$TMP/ptd.f" +cliFor --pack-task="$PT_PLAIN" > "$TMP/ptd.p" +forgeArm "(8d) default --pack-task" "$TMP/ptd.f" "$PT_FORGED" "$TMP/ptd.p" "$PT_PLAIN" 0 +PT_HUGE_F="$( python3 -c 'import sys; print( ( ( sys.argv[1] + " " ) * 420 ).strip() )' "$PT_FORGED" )" +PT_HUGE_P="$( python3 -c 'import sys; print( ( ( sys.argv[1] + " " ) * 420 ).strip() )' "$PT_PLAIN" )" +cliFor --pack-task="$PT_HUGE_F" > "$TMP/pth.f" +cliFor --pack-task="$PT_HUGE_P" > "$TMP/pth.p" +forgeArm "(8d) default --pack-task, a ${#PT_HUGE_F}-char task (header floor past 6000 tokens)" "$TMP/pth.f" "$PT_HUGE_F" "$TMP/pth.p" "$PT_HUGE_P" 1 + +# (8t) --from-trace --max-tokens (the hard ceiling this lens still labels): the trace file's NAME carries the note. +mkdir -p "$TMP/tr" || no "(8t) cannot create $TMP/tr" +TR_F="$TMP/tr/trace $TR_NOTE.txt" +TR_P="$TMP/tr/$( plainOf "trace $TR_NOTE" ).txt" +printf 'src/graph.h:120:5: error: no member named zz\nsrc/serialize.h:200: in escapeXml\n' > "$TR_F" +cp "$TR_F" "$TR_P" +for mt in 100000 200; do + cliFor --from-trace="$TR_F" --max-tokens="$mt" > "$TMP/tr.$mt.f" + cliFor --from-trace="$TR_P" --max-tokens="$mt" > "$TMP/tr.$mt.p" +done +grep -qF -e "$TR_NOTE" "$TMP/tr.100000.f" \ + && ok "(8t) the forged trace name reaches the emitted --from-trace document (the injection vector is exercised)" \ + || no "(8t) the forged trace name does not appear in the --from-trace document — the injection never happened" +forgeArm "(8t) --from-trace --max-tokens=100000" "$TMP/tr.100000.f" "$TR_F" "$TMP/tr.100000.p" "$TR_P" 0 +forgeArm "(8t) --from-trace --max-tokens=200" "$TMP/tr.200.f" "$TR_F" "$TMP/tr.200.p" "$TR_P" 1 + # (5) RECURRENCE GUARD on the SOURCE SHAPE — and read its limits before you lean on it. It is a grep over one # spelling, in two directions, with comments stripped so a sentence ABOUT the defect cannot red it: # · NEGATIVE — no marker sniff: no `header.find(` in the --for finishing path, no `lastRungNote` field # for one to search for, and no `chosen.find(` in --pack-task. spliceBefore's `doc.find( boundary )` # is a different operation on a different string (a boundary the emitter itself wrote), not matched. -# · POSITIVE — each lens's verdict is ASSIGNED from the value the ladder returned. This half is what a -# RENAME cannot walk past: reintroducing the defect through a differently-named field (a `rungMarker` -# carrying the note text, say) deletes these assignments and reds here, where the negative half alone -# stayed green through exactly that mutation while arms (2)/(4) went red. -# WHAT IT IS NOT: a proof. Any guard shaped like a grep can be evaded by a spelling it does not know, and -# the arms that DISCRIMINATE are (2)/(4) for --for and (7)/(4) for --pack-task — behavioural, over real -# documents. Treat a green (5) as "the known shapes are absent", never as "the verdict cannot be forged". +# · POSITIVE — each lens's verdict is ASSIGNED from a structural value. This half is what a RENAME cannot +# walk past: reintroducing the defect through a differently-named field deletes these assignments and reds here. +# forbudget-soft (rule 4): the --for verdict no longer comes from a ladder rung (`lastRungFired = ( chosen.rung`, +# deleted with the header ladder); it is the runaway guard's row-drop cut, `forGuardCut = forSigsCut.rowDropCut`, +# handed to the root as `.guardOver = forGuardCut`. --pack-task's default ladder keeps its rung assignment. +# WHAT IT IS NOT: a proof. Any guard shaped like a grep can be evaded by a spelling it does not know; the arms +# that DISCRIMINATE are the behavioural ones above. Treat a green (5) as "the known shapes are absent". stripped(){ sed 's|//.*||' "$1"; } badfind="$( stripped "$SRC" | grep -c 'header\.find(' || true )" badnote="$( stripped "$SRC" | grep -c 'lastRungNote' || true )" badpack="$( stripped "$PSRC" | grep -c 'chosen\.find(' || true )" -forasn="$( stripped "$SRC" | grep -cE 'lastRungFired *= *\( *chosen\.rung' || true )" +forcut="$( stripped "$SRC" | grep -cE 'forGuardCut *= *forSigsCut\.rowDropCut' || true )" +forasn="$( stripped "$SRC" | grep -cE '\.guardOver *= *forGuardCut' || true )" packasn="$( stripped "$PSRC" | grep -cE 'chosen\.rung == (rw::)?CeilingRung::OverCeiling' || true )" -if [ "$badfind" -eq 0 ] && [ "$badnote" -eq 0 ] && [ "$badpack" -eq 0 ] && [ "$forasn" -ge 1 ] && [ "$packasn" -ge 1 ]; then - ok "(5) both lenses assign the verdict from the rung the ladder returned, and no marker sniff remains (spelling-level guard only — (2)/(4)/(7) are the discriminating arms)" +if [ "$badfind" -eq 0 ] && [ "$badnote" -eq 0 ] && [ "$badpack" -eq 0 ] && [ "$forcut" -ge 1 ] && [ "$forasn" -ge 1 ] && [ "$packasn" -ge 1 ]; then + ok "(5) --for assigns its verdict from the guard's row-drop cut and --pack-task from the rung its ladder returned; no marker sniff remains (spelling-level guard only)" else - no "(5) the rung verdict is re-derived from emitted text, or no longer assigned from the ladder's return: verbs_for.h header.find(=$badfind lastRungNote=$badnote rung-assignment=$forasn; packtask.h chosen.find(=$badpack rung-assignment=$packasn — each of the last two must be >= 1)" + no "(5) a verdict is re-derived from emitted text, or no longer assigned from a structural value: verbs_for.h header.find(=$badfind lastRungNote=$badnote guard-cut=$forcut guardOver-assignment=$forasn; packtask.h chosen.find(=$badpack rung-assignment=$packasn — the first three must be 0, the last three >= 1)" fi # ── (9) AN UNMEASURED FIT IS NOT A HELD CAP — the map's --max-tokens verdict when its probe cannot measure ────────── diff --git a/test/droppedpositivecheck.sh b/test/droppedpositivecheck.sh index 857474263..04b8ac6d4 100755 --- a/test/droppedpositivecheck.sh +++ b/test/droppedpositivecheck.sh @@ -194,12 +194,23 @@ INIT='{"jsonrpc":"2.0","id":1,"method":"initialize"}' # dialects differ in the NUMBER dropped because the CLI folds churn=/amp=/tested= onto each row and reserves # for auto , which is exactly why this arm asserts presence and test/mcpforparitycheck.sh asserts the # candidate POOL both ladders start from). +# forbudget-soft (owner decisions 2026-10-14; rule 4): the MCP `for` DEFAULT now serves its whole ranked head (stop when +# complete; only the runaway guard drops rows), so the default no longer drops a positive and stays silent — asserted as +# the twin below. The arm's intent ("present when a budget genuinely cuts positives on the MCP dialect") moves to an +# explicit soft target: budget_tokens=1500 cuts the same 40-row head to its rank prefix. MCPCONC="resolve call edges by name" -mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\"}}}" \ +mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\",\"budget_tokens\":1500}}}" \ | result_text >"$TMP/mcp_for.xml" grep -q 'dropped_positive="[0-9]*"' "$TMP/mcp_for.xml" \ - && ok "#6 MCP for verb: dropped_positive= present on a query wide enough to drop" \ - || no "#6 MCP for verb: no dropped_positive= at all (the default MCP budget should trim this broad a query)" + && ok "#6 MCP for verb: dropped_positive= present where a budget_tokens soft target cuts positives" \ + || no "#6 MCP for verb: no dropped_positive= at budget_tokens=1500 on a broad query (the soft target cuts this head)" +mcp_call "$INIT" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"for\",\"arguments\":{\"path\":\"$CORPUS\",\"task\":\"$MCPCONC\"}}}" \ + | result_text >"$TMP/mcp_for_def.xml" +if [ -s "$TMP/mcp_for_def.xml" ] && grep -q ') and goes over the budget, labelled -# over_ceiling="1" with a TRUE reason and a next= on every cut: answers first, a budget never silently removes an -# answer. The old assertion is kept as A7 twin wherever it must still hold (each flag alone at 2000, and both flags at -# a budget the rows fit); A7B asserts the new contract for both-flags-over-budget. +# BY-DESIGN BREAK (forbudget-soft (rule 4); owner decisions 2026-10-14). An explicit --token-budget=N on --for (and on +# --pack-task, --from-trace, MCP for/explore) is a SOFT TARGET, not a byte ceiling: T = N x 2.36 x 0.90 bytes is charged +# against the ROWS only, rows are added in rank order while they fit T (the rank 1..4 floor always ships), the cut +# carries next=, est_tokens may exceed N, and nothing is labelled over_ceiling= or dropped from the header for it (no rung +# zero, no "(ceiling)" drop, no trailing-section reserve, no paid/unpaid continuation). The arms that asserted the old +# byte promise (delivered <= N x 2.36 x 1.15, or over_ceiling="1"; the kept-rows / unpaid / header-floor reasons; the +# reserve; rung zero) are REPLACED below by the invariants that guard the new behaviour on the same fixtures: ST (rows fit +# T or are the floor, and a cut is maximal), NOLBL (no over_ceiling= on a soft lens), SEC (--detail / --with-graph never +# remove a row vs the bare run at the same N), RP (rows a rank prefix, monotone in N), NX (every cut carries a +# next= that runs), HL (no legend clause, task echo or route= dropped). What still holds is kept unchanged: est_tokens +# prices the delivered document exactly (#1, A7/A8 bands, A9/A10, §C1, #19), and every HARD ceiling keeps its twins +# (--max-tokens #5/§C4, the map/payload --token-budget exit-3 gate #3/#12, --handoff #15(c)). # # Usage: bash test/estchargecheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/estchargecheck.sh # Exits non-zero on any failure; prints PASS/FAIL per check, ALL PASS on success. @@ -602,150 +607,180 @@ for label in for_graph for_detail for_both; do || no "#11 A7/A8 $label: est_tokens=$E did not move above the bare bundle's $E_FOR — that section is uncharged" done -# A7 — and --token-budget must SHAPE the bodies, not just the signatures. D10: --for trims to fit, exit 0. -# The pre-fix binary streamed 68 035 B here against a 4 248 B ceiling; the bar is the stated ceiling plus the -# documented single-entry overshoot tolerance (serialize.h kCeilingFirstEntryTolerance = 1.15). -# BY-DESIGN BREAK (owner ruling 2026-10-09, option B — see this file's header): the old arm ran ONLY the both-flags shape -# at 2000, which now keeps its rows and goes over on purpose. A7 TWIN keeps the old assertion, unchanged in substance -# (exit 0, delivered <= the allowance), on every shape where it must still hold: each trailing flag alone at 2000, and -# both flags at 5000 where the ranked rows fit (measured at fix round 5: 4727 / 4769 / 12337 B vs 5428 / 5428 / 13570 B). +# ── forbudget-soft (rule 4): THE SOFT TARGET's invariants, shared by the A7/A7B/sweep/floor/C3/A7R/#15/#19 arms below. An +# explicit --token-budget=N on --for charges T = N x 2.36 x 0.90 bytes (serialize.h kMinBytesPerToken x kBudgetHeadroom) +# against the rows inside ONLY — never the header, a disclosure or a trailing section — so the old "delivered <= N x +# 2.36 x 1.15 or over_ceiling=1" bar is gone by design (est_tokens may exceed N, unlabelled) and these replace it: +# soft_rows FILE the served rows as p:l:n in RANK order (r=), one per line; exit 3 when there is no +# soft_st FILE N [REF] ST: the bytes inside (the rows as printed) fit T, or the rows are only the rank 1..4 floor; +# with REF (the same shape's complete, no-budget answer) a cut must also be MAXIMAL — in the ladder's +# own currency (serialize.h entryCost: a row's printed bytes plus the zero cx=/ccx=/in= the lean +# spelling elides), the served rows plus REF's next row would cross T (160 B slack, as +# forsoftbudgetcheck's (ST)). Prints "OK|BAD ...", exit 0|1. +# rows_prefix A B RP: rows file A is a non-empty rank prefix of rows file B +# ctx_root FILE the root open tag (an over_ceiling= is read off the root only: a can carry its own) +cat >"$TMP/soft_rows.py" <<'PY' +import re, sys +s = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( "\n".join( x[ 1 ] for x in rows ) ) +PY +cat >"$TMP/soft_st.py" <<'PY' +import re, sys +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rb( p ): + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return None + inner = m.group( 1 ); rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + r = re.search( r'\br="(\d+)"', a ) + printed = len( d.group( 0 ).encode() ); c = charge( a, printed ); elided += c - printed + rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return len( inner.encode() ), len( inner.encode() ) + elided, rows +a = rb( sys.argv[ 1 ] ); T = int( int( sys.argv[ 2 ] ) * 2.36 * 0.90 ) +if a is None: print( "BAD no " ); sys.exit( 1 ) +pb, ab, ar = a; k = len( ar ) +fits = pb <= T or k <= 4 +if len( sys.argv ) < 4: + print( "%s fits=%d rows=%d bytes=%d T=%d" % ( "OK" if fits else "BAD", fits, k, pb, T ) ); sys.exit( 0 if fits else 1 ) +ref = rb( sys.argv[ 3 ] ) +if ref is None: print( "BAD no in the reference answer" ); sys.exit( 1 ) +rr = ref[ 2 ] +if k >= len( rr ): + print( "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) ); sys.exit( 0 if fits else 1 ) +maximal = ab + rr[ k ][ 1 ] > T - 160 +print( "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T ) ) +sys.exit( 0 if fits and maximal else 1 ) +PY +soft_rows(){ python3 "$TMP/soft_rows.py" "$1"; } +soft_st(){ python3 "$TMP/soft_st.py" "$@"; } +rows_prefix(){ [ -s "$1" ] && [ "$( head -n "$( wc -l < "$1" | tr -d ' ' )" "$2" )" = "$( cat "$1" )" ]; } +ctx_root(){ grep -aoE '^]*>' "$1" | head -1; } +# the soft-target legend clauses (verbs_for.h / graphlegend.h): the compact dialect's reading of budget_tokens=, and the full one's +SOFT_CLAUSE_COMPACT='budget_tokens= the soft row target asked (rows stop once it is reached)' +SOFT_CLAUSE_FULL='[budget_tokens=: the soft token target; rows stop once it is reached, the rank 1..4 rows and every disclosure always ship, so est_tokens may exceed it]' + +# A7 TWIN — REPLACED (forbudget-soft (rule 4)). Was: exit 0 and delivered <= N x 2.36 x 1.15 for --detail=20 / --with-graph at +# 2000 and both flags at 5000 (the trailing sections bounded by the budget). Under a soft target the trailing sections are +# funded from what the rows left of T and the document may land past N, unlabelled — so the byte bar asserts the deleted +# contract. The invariant that must hold on the same three shapes: exit 0; NOLBL (no over_ceiling= on the root); SEC (the +# trailing flag never removes a row — the bare run's rows at the same N are a rank prefix of the shape's rows); and ST +# (the shape's rows fit T or are the floor, and a cut is maximal against the same shape's no-budget answer from A7/A8 above). +# MEASURED on d099ec21: 20/20/40-row prefixes, rows identical with and without the flags; the base binary (d4325a8c) served +# 10 / 11 rows with --detail=20 / --with-graph against the bare run's 14 — the reserve the soft target deleted. TBF=2000 -ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" -for tw in "2000:--detail=20" "2000:--with-graph" "5000:--detail=20 --with-graph"; do - tw_n="${tw%%:*}"; tw_args="${tw#*:}" - tw_allow="$( awk "BEGIN{printf \"%d\", $tw_n*2.36*1.15}" )" +ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" # still read by the no-budget --detail arm below +"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --no-cache >"$TMP/f_tbp.out" 2>/dev/null +"$BIN" src --for="$FOR_TASK" --token-budget=5000 --no-cache >"$TMP/f_tbp5.out" 2>/dev/null +for tw in "2000:--detail=20:f_tbp:f_for_detail" "2000:--with-graph:f_tbp:f_for_graph" "5000:--detail=20 --with-graph:f_tbp5:f_for_both"; do + tw_n="${tw%%:*}"; tw_rest="${tw#*:}"; tw_args="${tw_rest%%:*}"; tw_rest="${tw_rest#*:}"; tw_bare="${tw_rest%%:*}"; tw_ref="${tw_rest#*:}" # shellcheck disable=SC2086 "$BIN" src --for="$FOR_TASK" --token-budget=$tw_n $tw_args --no-cache >"$TMP/f_tw.out" 2>/dev/null rc_tw=$? - tw_b="$( bytes_of "$TMP/f_tw.out" )" - { [ "$rc_tw" -eq 0 ] && [ -n "$tw_b" ] && [ "$tw_b" -le "$tw_allow" ]; } 2>/dev/null \ - && ok "#11 A7 twin --for --token-budget=$tw_n $tw_args: $tw_b B within the $tw_allow B allowance (exit 0, SHAPED)" \ - || no "#11 A7 twin --for --token-budget=$tw_n $tw_args: ${tw_b:-?} B vs the $tw_allow B allowance, exit $rc_tw — the budget does not bound the appended sections" + tw_root="$( ctx_root "$TMP/f_tw.out" )" + soft_rows "$TMP/f_tw.out" >"$TMP/tw.rows" 2>/dev/null; soft_rows "$TMP/$tw_bare.out" >"$TMP/tw_bare.rows" 2>/dev/null + tw_st="$( soft_st "$TMP/f_tw.out" "$tw_n" "$TMP/$tw_ref.out" )"; tw_st_rc=$? + tw_why="" + { [ "$rc_tw" -eq 0 ] && [ -n "$tw_root" ]; } || tw_why="$tw_why exit-$rc_tw-or-no-root" + [ -n "$tw_root" ] && [ "${tw_root#* over_ceiling=}" = "$tw_root" ] || tw_why="$tw_why over_ceiling-on-a-soft-target" + rows_prefix "$TMP/tw_bare.rows" "$TMP/tw.rows" || tw_why="$tw_why rows:$( wc -l < "$TMP/tw.rows" | tr -d ' ' )-lost-a-bare-row(bare $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' ))" + [ "$tw_st_rc" -eq 0 ] || tw_why="$tw_why ST:[$tw_st]" + [ -z "$tw_why" ] \ + && ok "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args: exit 0, no over_ceiling=, keeps all $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' ) bare rows ($tw_st)" \ + || no "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args —$tw_why" done -# A7B — THE NEW CONTRACT for both flags OVER the budget (owner ruling, option B). Same query, same 2000 tokens. It must: -# (premise) deliver past the allowance at exit 0 — else the shape now fits and this arm must be re-anchored, not passed; -# (rows) keep every row the same budget shows WITHOUT the trailing flags, and those rows are the uncapped -# answer's top rows in its order (no higher-ranked row of the uncapped answer is missing) — the trailing -# sections never remove an answer row; -# (label) carry over_ceiling="1" on the root; -# (reason) name the reason TRUTHFULLY: the last-rung note is the kept-rows one, not "the header floor ... exceeds", -# and what it names is there — the header alone fits the allowance, and rows, the graph block and a -# first --detail body are all present; -# (next) a cut first body carries a next= that recovers exactly the lines it cut (checked on A7R's corpus below, -# whose first body is longer than the floor; here a first body served whole carries no truncated=). -KEPT_NOTE='[over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections' -FLOOR_NOTE='[over_ceiling= is 1 on the root: the header floor' -rows_of(){ grep -aoE ']*>' "$1" | sed -nE 's/.* n="([^"]*)".*/\1/p'; } +# A7B — REPLACED (forbudget-soft (rule 4)). Was the owner-option-B contract for both flags OVER the budget: a premise past the +# allowance, over_ceiling="1", the kept-rows reason. All three die with the ceiling; its ROWS clause is the soft target's own +# invariant and is kept: --detail=20 --with-graph at 2000 keeps every row of the flagless 2000 answer, and its rows are +# the uncapped answer's top rows in rank order (no higher-ranked row missing). Plus NOLBL, both trailing sections present, and +# a cut first body still carries the next= that serves the rest. "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --no-cache >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? -"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --no-cache >"$TMP/f_tbp.out" 2>/dev/null -FTB="$( bytes_of "$TMP/f_tb.out" )" -# THE UNPAID TWIN (knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5). A capped under a -# hard ceiling carries next= — the call that recovers its cut — and pays for it from its rows ONLY where that makes the -# answer fit (ruling C3). Where it cannot (the rank 1..4 floor has nothing left to shed, or the answer lands past its ceiling -# paid or not) no row is dropped for it: the recovery handle ALWAYS ships, unpaid, and the document may then land past the -# allowance, and only if it says so. This is not a wider allowance: every condition of the old bound still holds for -# everything except the disclosed handle. Train 26c: the single A7 arm it guarded is superseded by the lane's A7 twin loop + -# A7B below; the twin itself still serves the sweep and the floor arm. sigs_unpaid_twin FILE ALLOW → exit 0 iff ALL of: the root carries -# over_ceiling="1"; the legend carries the unpaid clause defining it; is capped and carries next="--for=…"; -# est_tokens is positive and prices the delivered bytes inside the 2.00-4.20 B/tok band (honest: it moved with the handle); -# and the document WITHOUT the handle and its label (the next= attribute, over_ceiling="1", the clause) is within ALLOW — -# the old bound, intact — or carries the ladder's last-rung disclosure (the header floor alone exceeds the budget: the one -# overshoot the old bound already admitted). The rows are the ones the cut alone leaves (the C3 arm below pins them). -sigs_unpaid_twin(){ python3 - "$1" "$2" <<'PYF' -import sys, re -d = open( sys.argv[ 1 ], 'rb' ).read().decode( 'utf-8', 'replace' ); allow = int( sys.argv[ 2 ] ) -clause = ' [over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]' -root = re.search( r']*>', d ); sigs = re.search( r']*>', d ) -why = [] -if not root or ' over_ceiling="1"' not in root.group( 0 ): why.append( 'no root over_ceiling="1"' ) -if clause not in d: why.append( 'no unpaid clause' ) -nxt = re.search( r' next="--for=[^"]*"', sigs.group( 0 ) ) if sigs else None -if not sigs or ' capped="1"' not in sigs.group( 0 ): why.append( ' not capped' ) -if not nxt: why.append( ' carries no next=' ) -est = re.search( r' est_tokens="(\d+)"', root.group( 0 ) ) if root else None -b = len( d.encode( 'utf-8' ) ) -if not est or int( est.group( 1 ) ) <= 0 or not ( 200 <= b * 100 // int( est.group( 1 ) ) <= 420 ): why.append( 'est_tokens absent or out of band' ) -rest = b - ( len( nxt.group( 0 ) ) if nxt else 0 ) - len( ' over_ceiling="1"' ) - len( clause ) -lastRung = '[over_ceiling= is 1 on the root: ' in d # either last-rung reason: the header floor, or (lego-charge) the kept rows -if rest > allow and not lastRung: why.append( f'{rest} B without the handle > {allow} B (and no last-rung disclosure)' ) -print( '; '.join( why ) if why else f'{b} B, {rest} B without the handle <= {allow} B, est_tokens={est.group( 1 )}' ) -sys.exit( 1 if why else 0 ) -PYF -} -a7b_root="$( grep -aoE '^]*>' "$TMP/f_tb.out" | head -1 )" -a7b_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b""$TMP/a7b.rows"; rows_of "$TMP/f_tbp.out" >"$TMP/a7b.plain"; rows_of "$TMP/f_for.out" >"$TMP/a7b.unc" +a7b_root="$( ctx_root "$TMP/f_tb.out" )" +soft_rows "$TMP/f_tb.out" >"$TMP/a7b.rows" 2>/dev/null; soft_rows "$TMP/f_tbp.out" >"$TMP/a7b.plain" 2>/dev/null; soft_rows "$TMP/f_for.out" >"$TMP/a7b.unc" 2>/dev/null a7b_n="$( wc -l < "$TMP/a7b.rows" | tr -d ' ' )"; a7b_pn="$( wc -l < "$TMP/a7b.plain" | tr -d ' ' )" a7b_why="" -{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ] && [ -n "$FTB" ] && [ "$FTB" -gt "$ALLOW" ]; } 2>/dev/null \ - || a7b_why="$a7b_why premise:${FTB:-?}B<=${ALLOW}B-or-exit-$rc_ftb(re-anchor)" -{ [ "$a7b_pn" -ge 1 ] && [ "$a7b_n" -ge "$a7b_pn" ] && [ -z "$( grep -vxF -f "$TMP/a7b.rows" "$TMP/a7b.plain" )" ] \ - && [ "$( head -n "$a7b_n" "$TMP/a7b.unc" )" = "$( cat "$TMP/a7b.rows" )" ]; } 2>/dev/null \ - || a7b_why="$a7b_why rows:$a7b_n-vs-plain-$a7b_pn-or-not-the-uncapped-prefix" -[ "${a7b_root#* over_ceiling=\"1\"}" != "$a7b_root" ] || a7b_why="$a7b_why no-over_ceiling" -{ grep -aqF "$KEPT_NOTE" "$TMP/f_tb.out" && ! grep -aqF "$FLOOR_NOTE" "$TMP/f_tb.out" && [ "${a7b_hdr:--1}" -gt 0 ] && [ "$a7b_hdr" -le "$ALLOW" ] \ - && grep -aq ']*>/dev/null \ - || a7b_why="$a7b_why reason:not-the-kept-rows-note-or-untrue(header ${a7b_hdr:-?}B)" +{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ]; } || a7b_why="$a7b_why exit-$rc_ftb-or-no-root" +rows_prefix "$TMP/a7b.plain" "$TMP/a7b.rows" || a7b_why="$a7b_why rows:$a7b_n-lost-a-flagless-row($a7b_pn)" +rows_prefix "$TMP/a7b.rows" "$TMP/a7b.unc" || a7b_why="$a7b_why rows:not-the-uncapped-answer's-rank-prefix" +[ -n "$a7b_root" ] && [ "${a7b_root#* over_ceiling=}" = "$a7b_root" ] || a7b_why="$a7b_why over_ceiling-on-a-soft-target" +{ grep -aq ']*>/dev/null 2>&1; then "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full --no-cache >"$TMP/f_tbl.out" 2>/dev/null a7x_bad="" + grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/f_tb.out" || a7x_bad="$a7x_bad f_tb:no-soft-target-clause" + grep -aqF "$SOFT_CLAUSE_FULL" "$TMP/f_tbl.out" || a7x_bad="$a7x_bad f_tbl:no-soft-target-clause" for f in f_tb f_tbl; do - grep -aqF "$KEPT_NOTE" "$TMP/$f.out" || a7x_bad="$a7x_bad $f:no-kept-rows-note(premise)" + [ -s "$TMP/$f.out" ] || a7x_bad="$a7x_bad $f:empty" xmllint --noout "$TMP/$f.out" 2>/dev/null || a7x_bad="$a7x_bad $f:xmllint-rejected" done [ -z "$a7x_bad" ] \ - && ok "#11 A7B-X the kept-rows answers (compact + --legend=full) carry the note and are well-formed XML" \ - || no "#11 A7B-X the kept-rows note breaks the answer's XML or no longer rides —$a7x_bad" + && ok "#11 A7B-X (forbudget-soft) the soft-target answers (compact + --legend=full) define budget_tokens= as a soft target and are well-formed XML" \ + || no "#11 A7B-X (forbudget-soft) the soft-target disclosure is missing or breaks the answer's XML —$a7x_bad" else printf ' SKIP #11 A7B-X xmllint not installed\n' fi -# A7B-N (fix round 6, checklist 1 — the negative of A7B's reason): the kept-rows note is chosen only when the header as -# built fits the allowance by itself. Same both-flags shape, but a 700-char task at 300 tokens: the verbatim task echo -# alone puts the header past the 814 B allowance, so the TRUE reason is the header floor and the kept-rows note would be -# false. Red on the mutant that drops the header-fits term from keptTrailingOverrun (review mutant MX). +# A7B-N — REPLACED (forbudget-soft (rule 4)). Was the negative of A7B's reason: a 700-char task at 300 tokens puts the header +# alone past the allowance, so the last rung must name the header floor. There is no ladder on a soft target — so the same +# shape now pins HL: the root keeps task= VERBATIM (all 700 chars) and route=, nothing is dropped "(ceiling)", no task_echo / +# legend-clause drop note, no last-rung note, no over_ceiling=; the rank 1..4 floor ships and the cut carries next=. A7N_TASK="$( python3 -c 'w="serialize the map rows into the output buffer while keeping the row order stable and flushing the writer "; print((w*10)[:700].strip())' )" "$BIN" src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph --no-cache >"$TMP/f_tbn.out" 2>/dev/null rc_a7n=$? -a7n_allow="$( awk "BEGIN{printf \"%d\", 300*2.36*1.15}" )" -a7n_root="$( grep -aoE '^]*>' "$TMP/f_tbn.out" | head -1 )" -a7n_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || a7n_why="$a7n_why premise:header-${a7n_hdr:-?}B-not-past-${a7n_allow}B-or-exit-$rc_a7n(re-anchor)" -[ "${a7n_root#* over_ceiling=\"1\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-over_ceiling" -grep -aqF "$FLOOR_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why no-header-floor-note" -! grep -aqF "$KEPT_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why kept-rows-note-false-here" +{ [ "$rc_a7n" -eq 0 ] && [ -n "$a7n_root" ]; } || a7n_why="$a7n_why exit-$rc_a7n-or-no-root" +python3 -c 'import re,sys; m=re.search(r" task=\"([^\"]*)\"", sys.argv[1]); sys.exit(0 if m and m.group(1)==sys.argv[2] else 1)' "$a7n_root" "$A7N_TASK" \ + || a7n_why="$a7n_why task=-not-verbatim" +[ "${a7n_root#* route=\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-route=" +[ "${a7n_root#* over_ceiling=}" = "$a7n_root" ] || a7n_why="$a7n_why over_ceiling-on-a-soft-target" +! grep -aqE '\(ceiling\)|task_echo: dropped|\[legend clauses:|\[over_ceiling= is 1 on the root' "$TMP/f_tbn.out" || a7n_why="$a7n_why header-dropped-for-the-budget" +{ [ -n "$a7n_sh" ] && [ -n "$a7n_tot" ] && { [ "$a7n_sh" -ge 4 ] || [ "$a7n_sh" -eq "$a7n_tot" ]; } \ + && { [ "$a7n_sh" -eq "$a7n_tot" ] || [ "${a7n_sigs#* next=\"--for=}" != "$a7n_sigs" ]; }; } 2>/dev/null \ + || a7n_why="$a7n_why floor-or-next:shown=${a7n_sh:-?}/${a7n_tot:-?}" [ -z "$a7n_why" ] \ - && ok "#11 A7B-N 700-char task at --token-budget=300 --detail=3 --with-graph: header ${a7n_hdr} B alone past the ${a7n_allow} B allowance, the last rung names the header floor, not the kept rows" \ - || no "#11 A7B-N header-alone-over shape names the wrong reason —$a7n_why" + && ok "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 --detail=3 --with-graph: task= verbatim, route= kept, no drop, no over_ceiling=, floor $a7n_sh of $a7n_tot rows with next=" \ + || no "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 —$a7n_why" # and without an explicit --token-budget the bodies keep their own budget: the bundle must NOT have shrunk { [ "$( bytes_of "$TMP/f_for_detail.out" )" -gt "$ALLOW" ]; } 2>/dev/null \ && ok "#11 A7 no --token-budget: --detail keeps its --pack-budget-bytes budget (unbudgeted bundle unshrunk)" \ || no "#11 A7 no --token-budget: --detail was trimmed anyway — the budget bound leaked into the default path" -# A7 SWEEP (PR #135 CI, 2026-09-11) — the budget must bound the document at EVERY budget, not at the one operating -# point above. That arm went red at 5 429 B against its 5 428 B allowance when a merge moved the live src/ corpus, -# and the main binary emits the same 5 429 bytes on the same tree, so the corpus only exposed it. The defect: the -# ceiling ladder priced the document WITHOUT the root over_ceiling="1" (17 B) and the legend clause defining it -# (53 B), which runForLens splices on AFTER the ladder whenever est_tokens exceeds budget_tokens. est_tokens prices -# markup at 2.50 B/tok and the allowance is sized at 2.36 x 1.15 = 2.714 B/tok, so every bundle in that band -# carries 70 unpriced bytes, and one the ladder fitted within 70 B of the allowance is pushed past it with no rung -# fired. One budget on the live tree only sees that when the corpus lands a bundle in the 70 B window, so this arm -# builds its OWN git-less corpus in $TMP and runs it from a relative path (no at=, no churn, a fixed root=, nothing -# read from the live repo) and SWEEPS the budget in 10-token steps: the window recurs with every trimmed row -# (~50 tokens), so a sweep this dense crosses it whatever the legend lengths are. Two shapes — the default bundle -# and A7's own --detail=20 --with-graph. THE PROPERTY: delivered bytes <= N x 2.36 x 1.15 at exit 0, or the -# ladder's LAST rung fired and says so (the header floor alone exceeds the budget — the one overshoot it documents). +# A7 SWEEP — the harness is kept, its property REPLACED (forbudget-soft (rule 4)). Was (PR #135 CI, 2026-09-11): delivered +# bytes <= N x 2.36 x 1.15 at exit 0 at EVERY budget, or the ladder's disclosed last rung / the unpaid twin — the guard +# against a byte spliced in after the ladder priced the document. A soft target makes no byte promise, so the sweep now pins +# the soft target's own laws at every budget of a dense sweep over the same git-less corpus (run from a relative path: no +# at=, no churn, a fixed root=): exit 0; NOLBL (no root over_ceiling=, no last-rung note); ST (rows fit T or are the floor, +# and a cut is maximal against the spec's own no-budget answer); RP (rows(N) a rank prefix of rows(N + step) — monotone); +# NX (a with shown < total carries next=). The byte-exactness the old arm protected lives on in est_tokens' +# identities (A9/A10, §C1, #19): est_tokens prices the delivered document, whatever its size. +# RE-ANCHORED (forbudget-soft): the --detail=20 --with-graph spec ran 2880..3080, the old late-label band; on the soft target +# that range serves all 32 rows (complete), so it now sweeps 300..2500 step 50, where the rows run from the 4-row floor to +# complete (d099ec21: 4 rows at 300, 28 at 2000, 32 at 2500). The default spec keeps 760..1500 step 10 (8..20 rows); the +# one-row named task keeps 400..600 (step 20) — its document prices at est_tokens=719, past every N there: the soft overshoot. A7S="$TMP/a7sweep" mkdir -p "$A7S/corpus" python3 - "$A7S/corpus" <<'PYG' @@ -764,96 +799,85 @@ for i in range( 4 ): with open( os.path.join( out, f"mod{i}.cpp" ), "w" ) as fh: fh.write( "\n".join( lines ) ) PYG -a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0; a7s_floor=0 -# RE-ANCHORED 2026-09-13 (PR #215): the default sweep starts at 760, not 1200. --for's rung zero now triggers on the -# EXACT ceiling (verbs_for.h), so a document 1..15% over its budget drops its three explanatory clauses before the -# allowance is consulted; on this corpus the late-label band (over_ceiling="1" INSIDE the allowance — the residual -# after that drop) therefore sits at 780..810 instead of inside 1200..1500, and the control below would otherwise be -# inert. Swept 700..3300 step 10 on the new binary: default hits at 780 790 800 810, none on the --detail=20 arm. -# RE-ANCHORED 2026-10-07 (knob-honesty-068, orchestrator ruling C3): a capped, CHARGED whose answer is past its ceiling -# paid or not is now served with the rows the cut alone leaves and its next= unpaid — bigger than the paid answer, so the -# capped documents that sat in the late-label band (dd6e4c8e: the --detail=20 arm's 2880..3080, est_tokens > N inside the -# allowance) now cross the allowance on the ladder's disclosed last rung, and no capped document on this corpus is left in -# the band (swept 700..1500 and 2880..3080 step 10). The band itself is untouched by the ruling: an UNCAPPED block owes no -# continuation, so the third spec — a name-exact task serving ONE row — keeps a document whose header floor lands in it -# (480..488 at the time of writing; step 4 so a band 9 budgets wide is always crossed). Fields: label:from:to:step:task:args. -for spec in "default:760:1500:10:serialize the map:" "detail_graph:2880:3080:10:serialize the map:--detail=20 --with-graph" \ - "named_one_row:400:600:4:serializeRow0_3:"; do +a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_over=0; a7s_cut=0 +for spec in "default:760:1500:10:serialize the map:" "detail_graph:300:2500:50:serialize the map:--detail=20 --with-graph" \ + "named_one_row:400:600:20:serializeRow0_3:"; do s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_rest="${s_rest#*:}" s_step="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" + # shellcheck disable=SC2086 + ( cd "$A7S" && "$BIN" corpus --for="$s_task" $s_args --no-cache ) >"$A7S/ref.xml" 2>/dev/null # the spec's complete answer + : >"$A7S/prev.rows" for (( N = s_from; N <= s_to; N += s_step )); do # shellcheck disable=SC2086 ( cd "$A7S" && "$BIN" corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o.xml" 2>/dev/null s_rc=$? a7s_runs=$(( a7s_runs + 1 )) - s_b="$( bytes_of "$A7S/o.xml" )" - s_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - s_root="$( grep -aoE '^]*>' "$A7S/o.xml" | head -1 )" - # fix round 5 (lego-charge): the last rung now names one of two reasons, each only where it is true — the header floor, - # or (both trailing sections under the ceiling) the kept rows. Either is the disclosure; the kept-rows one must also be - # TRUE here: only on the both-flags shape, and with the header alone inside the allowance. - s_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b" unpaid twin above - # holds in full - elif [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml" \ - && sigs_unpaid_twin "$A7S/o.xml" "$s_a" >/dev/null; then - a7s_floor=$(( a7s_floor + 1 )) - elif [ "$s_rc" -ne 0 ] || { [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml"; }; then + s_root="$( ctx_root "$A7S/o.xml" )" + s_sigs="$( grep -aoE ']*>' "$A7S/o.xml" | head -1 )" + s_sh="$( printf '%s' "$s_sigs" | sed -nE 's/.* shown="([0-9]+)".*/\1/p' )"; s_tot="$( printf '%s' "$s_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" + s_est="$( printf '%s' "$s_root" | sed -nE 's/.* est_tokens="([0-9]+)".*/\1/p' )" + soft_rows "$A7S/o.xml" >"$A7S/cur.rows" 2>/dev/null + s_why="" + { [ "$s_rc" -eq 0 ] && [ -n "$s_root" ] && [ -n "$s_est" ]; } || s_why="$s_why exit-$s_rc/no-root-or-est" + { [ "${s_root#* over_ceiling=}" = "$s_root" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml"; } || s_why="$s_why labelled" + s_st="$( soft_st "$A7S/o.xml" "$N" "$A7S/ref.xml" )" || s_why="$s_why ST:[$s_st]" + [ ! -s "$A7S/prev.rows" ] || rows_prefix "$A7S/prev.rows" "$A7S/cur.rows" || s_why="$s_why not-monotone" + if [ -n "$s_sh" ] && [ -n "$s_tot" ] && [ "$s_sh" -lt "$s_tot" ] 2>/dev/null; then + a7s_cut=$(( a7s_cut + 1 )) + [ "${s_sigs#* next=\"--for=}" != "$s_sigs" ] || s_why="$s_why cut-without-next" + fi + [ -n "$s_est" ] && [ "$s_est" -gt "$N" ] 2>/dev/null && a7s_over=$(( a7s_over + 1 )) + if [ -n "$s_why" ]; then a7s_badn=$(( a7s_badn + 1 )) - [ "$a7s_badn" -le 6 ] && a7s_bad="$a7s_bad $s_label@$N=${s_b}/${s_a}B(exit $s_rc)" - elif [ "$s_b" -le "$s_a" ] && [ "${s_root#* over_ceiling=\"1\"}" != "$s_root" ]; then - a7s_inside_labelled=$(( a7s_inside_labelled + 1 )) + [ "$a7s_badn" -le 6 ] && a7s_bad="$a7s_bad $s_label@$N:[${s_why# }]" fi + cp "$A7S/cur.rows" "$A7S/prev.rows" done done [ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10; one-row named task 400..600, step 4) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung, or the disclosed unpaid twin ($a7s_floor)" \ - || no "#11 A7 sweep: $a7s_badn of $a7s_runs budgets deliver past the allowance with no ladder rung fired (first:$a7s_bad) — a byte spliced in after the ladder priced the document" -# control: the sweep must cross the band the defect lives in — a root that says over_ceiling="1" while the document -# still fits the allowance (est_tokens > N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. -[ "$a7s_inside_labelled" -gt 0 ] \ - && ok "#11 A7 sweep control: $a7s_inside_labelled budget(s) carry a root over_ceiling=\"1\" INSIDE the allowance — the sweep crosses the late-label band" \ - || no "#11 A7 sweep control: no budget carried over_ceiling=\"1\" inside the allowance — the sweep no longer reaches the est_tokens > N band, re-anchor its ranges" -# TRAIN 26c RE-PIN (2026-10-10) of the three regime arms below — the floor twin, C3 and its near miss. lego-charge charges -# / to the budget at their served size and #362's rides the bundle, so on this corpus the -# merged binary's 4-row floor answer costs ~270 B more than 26b's: at "serialize the map" the cut never leaves 5 rows while -# both the unpaid and the paid answer land over (the paid 4-row answer fits from 920, the cut leaves 5 rows from 920 — no C3 -# window at any budget 700..1400, swept step 10 on the merged binary and on lego-charge's pre-continuation binary 1cfbfdcc). -# The task "map row serializer" on the same corpus has all three regimes: the floor at 890..910 (4 rows, the handle unpaid, -# no last-rung note; the pre-continuation binary fits with 4 rows), C3 at 920..939 (the cut leaves 5 rows — 1cfbfdcc serves -# 5, over, last rung fired — and the paid 4-row answer is over too), the payable near miss from 940 (the paid 4-row answer -# fits, no label). Pinned at 900 / 925 / 970. The sweep above keeps "serialize the map". Assertions unchanged. -# A7 UNPAID TWIN, EXERCISED EVERY RUN (knob-honesty-068; rulings 2026-10-07). The A7 arm and the sweep accept the twin only -# as an alternative, so a fixture that stopped reaching it would leave it untested. On this git-less corpus at -# --token-budget=780 the default bundle's sits at its rank 1..4 floor: the answer the cut alone leaves FITS (the -# pre-continuation binary, 255dc199, serves it at est_tokens=774 with no label), its next= cannot be paid in rows, and the -# document lands past the allowance only by the handle — the twin must hold, with no last-rung disclosure to lean on. + && ok "#11 A7 sweep (forbudget-soft): $a7s_runs budgets over a git-less corpus (default 760..1500 step 10, --detail=20 --with-graph 300..2500 step 50, one-row named task 400..600 step 20) — every answer exit 0, unlabelled, its rows fit T or the floor and a cut is maximal, rows monotone in N, every cut carries next=" \ + || no "#11 A7 sweep (forbudget-soft): $a7s_badn of $a7s_runs budgets break the soft target (first:$a7s_bad)" +# control (REPLACED, forbudget-soft (rule 4); was ">= 1 budget labelled over_ceiling INSIDE the allowance", a band the soft +# target deleted): the sweep must actually exercise both halves of the contract — rows CUT at the target (else ST and NX ran +# on complete answers only) and an answer whose est_tokens exceeds its N (the soft overshoot, which NOLBL then pins unlabelled). +{ [ "$a7s_cut" -gt 0 ] && [ "$a7s_over" -gt 0 ]; } \ + && ok "#11 A7 sweep control: $a7s_cut budget(s) cut the rows at the target and $a7s_over priced past their N — both halves exercised" \ + || no "#11 A7 sweep control: cut=$a7s_cut over-N=$a7s_over — the sweep no longer reaches a cut or a soft overshoot, re-anchor its ranges" +# A7 FLOOR / NEXT @900 — REPLACED (forbudget-soft (rule 4)). Was the UNPAID TWIN (knob-honesty-068): at the rank 1..4 +# floor the next= rode unpaid, the root said over_ceiling="1" and the legend carried the unpaid clause. The paid/unpaid +# continuation is gone — one render, nothing paid from rows. The same call ("map row serializer" at 900 on this corpus) now +# pins: exit 0; NOLBL and no unpaid clause; ST against the task's no-budget answer (d099ec21: 10 rows, base d4325a8c: the +# 4-row floor, not maximal); and NX — the cut carries next=, and that next= RUNS (from the same root) and serves the +# whole head: every one of total= rows, the cut's rows its rank prefix. ( cd "$A7S" && "$BIN" corpus --for="map row serializer" --token-budget=900 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? -fl_a="$( awk 'BEGIN{printf "%d", 900*2.36*1.15}' )" -if [ "$fl_rc" -eq 0 ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/floor.xml" \ - && fl_why="$( sigs_unpaid_twin "$A7S/floor.xml" "$fl_a" )"; then - ok "#11 A7 unpaid twin @900: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" +( cd "$A7S" && "$BIN" corpus --for="map row serializer" --no-cache ) >"$A7S/floor_ref.xml" 2>/dev/null +fl_root="$( ctx_root "$A7S/floor.xml" )"; fl_sigs="$( grep -aoE ']*>' "$A7S/floor.xml" | head -1 )" +fl_tot="$( printf '%s' "$fl_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +fl_next="$( printf '%s' "$fl_sigs" | sed -nE 's/.* next="(--for=[^"]*)".*/\1/p' | sed "s/'/'/g; s/"/\"/g; s/&/\\&/g" )" +fl_why="" +{ [ "$fl_rc" -eq 0 ] && [ -n "$fl_root" ]; } || fl_why="$fl_why exit-$fl_rc-or-no-root" +[ "${fl_root#* over_ceiling=}" = "$fl_root" ] || fl_why="$fl_why over_ceiling-on-a-soft-target" +! grep -aqF 'next= rides unpaid' "$A7S/floor.xml" || fl_why="$fl_why unpaid-clause" +fl_st="$( soft_st "$A7S/floor.xml" 900 "$A7S/floor_ref.xml" )" || fl_why="$fl_why ST:[$fl_st]" +if [ -z "$fl_next" ] || [ -z "$fl_tot" ]; then + fl_why="$fl_why -cut-without-next=-or-total=" else - no "#11 A7 unpaid twin @900 (exit $fl_rc): ${fl_why:-the ladder last rung fired, so the twin is not the only disclosure} — the floor did not ship its next= with a labelled, honestly priced overshoot" + eval "set -- $fl_next" + ( cd "$A7S" && "$BIN" corpus "$@" --no-cache ) >"$A7S/floor_next.xml" 2>/dev/null + soft_rows "$A7S/floor.xml" >"$A7S/floor.rows" 2>/dev/null; soft_rows "$A7S/floor_next.xml" >"$A7S/floor_next.rows" 2>/dev/null + { [ "$( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )" = "$fl_tot" ] && rows_prefix "$A7S/floor.rows" "$A7S/floor_next.rows"; } \ + || fl_why="$fl_why next=-does-not-serve-the-head($( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )-of-$fl_tot)" fi -# C3 (orchestrator ruling 2026-10-07): NEVER DROP ROWS TO PAY FOR next= WHEN THE ANSWER IS OVER ITS CEILING ANYWAY. At 790 the -# answer the cut alone leaves is past its ceiling already (255dc199 serves 5 rows there at est_tokens=952, last rung fired); -# paying from rows (dd6e4c8e: 4 rows, est_tokens=885) still lands over, so it bought nothing. The answer must keep the cut's -# own rows — at least 255dc199's 5 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it) — -# and MORE than the paid near miss at 850 serves (4 rows: there paying is what makes the answer fit), with next= riding -# unpaid and over_ceiling="1" + the unpaid clause on the root. RED on dd6e4c8e (4 rows). -# TRAIN 27 RE-PIN (2026-10-11) of C3 and its near miss (the floor twin above is unchanged). Under a spent ceiling the compact -# header no longer carries clauses for sections served as stubs nor a false task_echo / "(total= shown= capped=)" note, and the -# compact dialect pays a measured overrun from the rows (verbs_for.h runForLens), so on this corpus the paid 4-row answer FITS -# from 910 and no budget 700..1500 serves the C3 shape on any task tried (C3 then correctly serves the paid answer: 925 = 4 rows, -# est_tokens=907, no label). The regime lives on a twin of the same corpus WITHOUT the doc comments (smaller rows, so the cut -# alone leaves 5 rows while the 4-row floor is still over): C3 at 740..750 (494f97b6: 740..770, 5 rows, last rung + unpaid -# clause), the payable near miss from 760 (494f97b6: from 780). Pinned at 745 / 790, inside both binaries' windows. RED on a -# mutant whose C3 fallback pays from the rows (4 rows, over). Assertions unchanged. +[ -z "$fl_why" ] \ + && ok "#11 A7 floor/next @900 (forbudget-soft): unlabelled, no unpaid clause, $fl_st; its next= serves all $fl_tot rows, the cut's its prefix" \ + || no "#11 A7 floor/next @900 (forbudget-soft) —$fl_why" +# C3 @745 / NEAR MISS @790 — REPLACED (forbudget-soft (rule 4)). Was orchestrator ruling C3 (never drop rows to pay for next= +# on an answer over its ceiling either way: 745 keeps >= 5 rows, more than the paid 790 answer, labelled) and its payable near +# miss (790: the handle paid from rows, inside the allowance, unlabelled). Under a soft target next= is never paid from rows +# at all, which is what made the 745 answer serve MORE rows than the 790 one; that inversion is now impossible by contract. +# The same doc-less corpus and the same two budgets pin RP: rows(745) is a rank prefix of rows(790), both a rank prefix of +# the task's no-budget answer; both unlabelled with no unpaid clause; both ST-maximal (the handle bought with no row). +# MEASURED: d099ec21 serves 11 / 12 rows; base d4325a8c 5 / 4 (745 labelled) — not monotone. mkdir -p "$A7S/c3/corpus" python3 - "$A7S/c3/corpus" <<'PYG' import os, sys @@ -872,38 +896,34 @@ for i in range( 4 ): PYG ( cd "$A7S/c3" && "$BIN" corpus --for="map row serializer" --token-budget=745 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? ( cd "$A7S/c3" && "$BIN" corpus --for="map row serializer" --token-budget=790 --no-cache ) >"$A7S/paid.xml" 2>/dev/null; pd_rc=$? -sigs_shown(){ grep -aoE ']*>' "$1" | tail -1 | grep -oE ' shown="[0-9]+"' | tr -dc '0-9'; } -c3_sh="$( sigs_shown "$A7S/c3.xml" )"; pd_sh="$( sigs_shown "$A7S/paid.xml" )" -c3_root="$( grep -aoE '^]*>' "$A7S/c3.xml" | head -1 )" -if [ "$c3_rc" -eq 0 ] && [ -n "$c3_sh" ] && [ -n "$pd_sh" ] && [ "$c3_sh" -ge 5 ] && [ "$c3_sh" -gt "$pd_sh" ] \ - && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/c3.xml" && [ "${c3_root#* over_ceiling=\"1\"}" != "$c3_root" ] \ - && grep -aqF 'next= rides unpaid: no row is dropped to pay for it' "$A7S/c3.xml"; then - ok "#11 A7 C3 @745 (doc-less twin): over its ceiling either way, the answer keeps the cut's own $c3_sh rows (>= 5, > the paid 790 answer's $pd_sh) — next= unpaid, over_ceiling=\"1\" + the unpaid clause" -else - no "#11 A7 C3 @745 (doc-less twin, exit $c3_rc): shown=${c3_sh:-?} (want >= 5 and > the paid 790 answer's ${pd_sh:-?}), next= + over_ceiling=\"1\" + the unpaid clause — rows were dropped to pay for a handle that cannot make the answer fit" -fi -# Its near miss (790 since the train-27 re-pin above) — the PAYABLE case, unchanged: paying is what makes the answer fit (255dc199: 5 rows, est_tokens=911, -# over), so the handle is paid (the block sheds its bytes above the floor) — next= rides inside the allowance, and neither -# over_ceiling="1" nor the unpaid clause appears (a label that rode on every capped answer would pass the arms above alone). -pd_b="$( bytes_of "$A7S/paid.xml" )"; pd_a="$( awk 'BEGIN{printf "%d", 790*2.36*1.15}' )" -pd_root="$( grep -aoE '^]*>' "$A7S/paid.xml" | head -1 )" -if [ "$pd_rc" -eq 0 ] && [ "$pd_b" -le "$pd_a" ] && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/paid.xml" \ - && [ "${pd_root#* over_ceiling=\"1\"}" = "$pd_root" ] && ! grep -aqF 'next= rides unpaid' "$A7S/paid.xml"; then - ok "#11 A7 unpaid twin near miss @790 (doc-less twin): the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no unpaid clause" -else - no "#11 A7 unpaid twin near miss @790 (doc-less twin, exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" -fi +( cd "$A7S/c3" && "$BIN" corpus --for="map row serializer" --no-cache ) >"$A7S/c3_ref.xml" 2>/dev/null +soft_rows "$A7S/c3.xml" >"$A7S/c3.rows" 2>/dev/null; soft_rows "$A7S/paid.xml" >"$A7S/paid.rows" 2>/dev/null; soft_rows "$A7S/c3_ref.xml" >"$A7S/c3_ref.rows" 2>/dev/null +c3_n="$( wc -l < "$A7S/c3.rows" | tr -d ' ' )"; pd_n="$( wc -l < "$A7S/paid.rows" | tr -d ' ' )" +c3_why="" +for c3f in c3:745:$c3_rc paid:790:$pd_rc; do + c3_lbl="${c3f%%:*}"; c3_rest="${c3f#*:}"; c3_bud="${c3_rest%%:*}"; c3_frc="${c3_rest#*:}" + c3_root="$( ctx_root "$A7S/$c3_lbl.xml" )" + { [ "$c3_frc" -eq 0 ] && [ -n "$c3_root" ]; } || c3_why="$c3_why @$c3_bud:exit-$c3_frc-or-no-root" + [ "${c3_root#* over_ceiling=}" = "$c3_root" ] || c3_why="$c3_why @$c3_bud:over_ceiling" + ! grep -aqF 'next= rides unpaid' "$A7S/$c3_lbl.xml" || c3_why="$c3_why @$c3_bud:unpaid-clause" + c3_st="$( soft_st "$A7S/$c3_lbl.xml" "$c3_bud" "$A7S/c3_ref.xml" )" || c3_why="$c3_why @$c3_bud:ST:[$c3_st]" +done +rows_prefix "$A7S/c3.rows" "$A7S/paid.rows" || c3_why="$c3_why rows(745)=$c3_n-not-a-prefix-of-rows(790)=$pd_n" +rows_prefix "$A7S/paid.rows" "$A7S/c3_ref.rows" || c3_why="$c3_why rows(790)-not-a-prefix-of-the-no-budget-answer" +[ -z "$c3_why" ] \ + && ok "#11 A7 C3/near miss (forbudget-soft, doc-less twin): rows(745)=$c3_n is a rank prefix of rows(790)=$pd_n, itself a prefix of the no-budget answer; both unlabelled, no unpaid clause, both ST-maximal" \ + || no "#11 A7 C3/near miss (forbudget-soft, doc-less twin) —$c3_why" -# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE. Under --token-budget, --with-graph's block and the -# first --detail body were charged only AFTER had claimed the whole sig-side ceiling, so both rode past the -# budget, and the ladder's last rung then blamed the HEADER floor ("no payload left to trim") while was the -# trimmable payload. MEASURED on the parent binary over this arm's corpus: --detail=20 --with-graph at 2000 tokens = -# 6054 B vs a 5428 B allowance; --detail=3 served its first body as a one-line over_ceiling stub at every budget. -# runForLens now reserves the graph bytes and a MEASURED first-body floor out of the sig claim -# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor). The corpus is git-less, run from a relative path, every -# body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path runs), and its -# 40 signature rows overflow the default sig share (so is capped="1" in every regime below — the negatives -# compare a CUT section, not a whole one). +# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE — and its forbudget-soft (rule 4) REPLACEMENT. Under the +# old hard ceiling --with-graph's block and the first --detail body were charged only AFTER had claimed the whole +# sig-side ceiling, so lego-charge reserved the graph bytes and a measured first-body floor OUT of the sig claim +# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor) and these arms pinned "inside N x 2.36 x 1.15, unlabelled". The +# soft target deletes both the byte promise and the reserve: T is charged against the rows only, and a trailing +# section takes what the rows left of T. So on this corpus the arms now pin SEC — a trailing section NEVER removes a +# row: with --with-graph / --detail the rows are the bare run's rows at the same N — plus ST, NOLBL, and the first body's +# floor (never the one-line stub; a head-cut carries the next= that recovers it). The corpus is git-less, run from a +# relative path, every body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path +# runs), and its 72 rows overflow the default byte target (so is capped="1", rows cut or shrunk, in every regime below). A7R="$TMP/a7reserve" mkdir -p "$A7R/corpus" python3 - "$A7R/corpus" <<'PYG' @@ -923,26 +943,32 @@ PYG A7R_Q="serialize the map row" a7r_out(){ ( cd "$A7R" && "$BIN" corpus --for="$A7R_Q" "$@" --no-cache ) 2>/dev/null; } a7r_sigs(){ python3 -c 'import re,sys; m=re.search(r"",open(sys.argv[1]).read(),re.S); print(m.group(0) if m else "")' "$1"; } -# A7R-1 (positive): every budget, three shapes — exit 0, inside the allowance, NO over_ceiling on the root (est_tokens fits -# budget_tokens), no ladder last rung, present, and with --detail a first that is not the one-line over_ceiling -# stub; a first body the floor cut carries the next= that serves the rest (rule 5: every cut recoverable). +a7r_out >"$A7R/n0.xml" # the no-budget answer: the ST reference below, and the A7R no-ceiling / A7D default arms +# A7R-1 — REPLACED (forbudget-soft (rule 4)). Was: --with-graph / --detail=3 at 1500..3000 — exit 0, inside N x 2.36 x 1.15, no +# over_ceiling, no last rung, first body served (the reserve made room for it). Now: exit 0; NOLBL and no last-rung note; SEC +# (rows identical to the bare run at the same N — d099ec21: 12/16/21/26 rows each; base d4325a8c: --with-graph 6/11/16/20 and +# --detail=3 4/6/10/15 against the bare 8/13/18/22, the reserve at work); ST against the no-budget answer; and with --detail a +# first that is not the one-line over_ceiling stub, a head-cut one carrying next= (rule 5: every cut recoverable). a7r_bad=""; a7r_runs=0 -for spec in "graph:--with-graph" "detail:--detail=3"; do - r_label="${spec%%:*}"; r_args="${spec#*:}" - for N in 1500 2000 2500 3000; do +for N in 1500 2000 2500 3000; do + a7r_out --token-budget=$N >"$A7R/o0.xml" + soft_rows "$A7R/o0.xml" >"$A7R/o0.rows" 2>/dev/null + for spec in "graph:--with-graph" "detail:--detail=3"; do + r_label="${spec%%:*}"; r_args="${spec#*:}" # shellcheck disable=SC2086 a7r_out --token-budget=$N $r_args >"$A7R/o.xml" r_rc=$? a7r_runs=$(( a7r_runs + 1 )) - r_b="$( bytes_of "$A7R/o.xml" )" - r_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - r_root="$( grep -aoE '^]*>' "$A7R/o.xml" | head -1 )" + r_root="$( ctx_root "$A7R/o.xml" )" + soft_rows "$A7R/o.xml" >"$A7R/o.rows" 2>/dev/null r_why="" [ "$r_rc" -eq 0 ] || r_why="exit $r_rc" [ -n "$r_root" ] && grep -aq '/dev/null || r_why="$r_why ${r_b:-?}B>${r_a}B" - [ "${r_root#* over_ceiling=\"1\"}" = "$r_root" ] || r_why="$r_why over_ceiling" - ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" # either reason (fix round 5) + [ -s "$A7R/o0.rows" ] && cmp -s "$A7R/o0.rows" "$A7R/o.rows" \ + || r_why="$r_why rows:$( wc -l < "$A7R/o.rows" | tr -d ' ' )-vs-bare-$( wc -l < "$A7R/o0.rows" | tr -d ' ' )" + r_st="$( soft_st "$A7R/o.xml" "$N" "$A7R/n0.xml" )" || r_why="$r_why ST:[$r_st]" + [ "${r_root#* over_ceiling=}" = "$r_root" ] || r_why="$r_why over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" if [ "${r_args#*--detail}" != "$r_args" ]; then r_b1="$( grep -aoE ']*>' "$A7R/o.xml" | head -1 )" [ -n "$r_b1" ] || r_why="$r_why no-first-body" @@ -953,48 +979,43 @@ for spec in "graph:--with-graph" "detail:--detail=3"; do done done [ "$a7r_runs" -eq 8 ] && [ -z "$a7r_bad" ] \ - && ok "#11 A7R reserve: 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) — inside N x 2.36 x 1.15, no over_ceiling, no last rung, first body served (cut ones carry next=)" \ - || no "#11 A7R reserve: spent the bytes of the graph block / first --detail body —$a7r_bad" -# A7R-K (the PRE-REGISTERED KILL, reports/lego-charge.md fix round 4): with BOTH --detail and --with-graph the reserve is -# off — reserving both cut a gold-relevant owner row out of on the measured question set. So that shape's ranked -# rows keep their claim: never FEWER rows than the same budget's --with-graph run (which reserves the graph alone), -# a delivery past the allowance is disclosed (root over_ceiling="1"), and the first body still gets its floor — not the -# one-line over_ceiling stub, and a head-cut carries next=. + && ok "#11 A7R no reserve (forbudget-soft): 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) serve exactly the bare run's rows, fit T, unlabelled, first body served (cut ones carry next=)" \ + || no "#11 A7R no reserve (forbudget-soft): a trailing section changed the rows or the soft target broke —$a7r_bad" +# A7R-K — REPLACED in part (forbudget-soft (rule 4)). Was the pre-registered kill (reports/lego-charge.md fix round 4): with BOTH +# --detail and --with-graph the reserve was off, so the rows were never FEWER than the --with-graph run's; a delivery past +# the allowance was labelled over_ceiling="1"; the first body got its floor. The rows clause tightens to SEC (both flags serve +# exactly the bare rows); the allowance-or-label clause becomes ST + NOLBL; the first-body floor clause is unchanged. a7k_bad="" for N in 2000 2500 3000; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k.xml"; k_rc=$? - a7r_out --token-budget=$N --with-graph >"$A7R/kg.xml" - k_rows="$( grep -aoE ']*>' "$A7R/k.xml" | wc -l | tr -d ' ' )"; g_rows="$( grep -aoE ']*>' "$A7R/kg.xml" | wc -l | tr -d ' ' )" - k_b="$( bytes_of "$A7R/k.xml" )"; k_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k_root="$( grep -aoE '^]*>' "$A7R/k.xml" | head -1 )" + a7r_out --token-budget=$N >"$A7R/kb.xml" + soft_rows "$A7R/k.xml" >"$A7R/k.rows" 2>/dev/null; soft_rows "$A7R/kb.xml" >"$A7R/kb.rows" 2>/dev/null + k_root="$( ctx_root "$A7R/k.xml" )" k_b1="$( grep -aoE ']*>' "$A7R/k.xml" | head -1 )" k_why="" [ "$k_rc" -eq 0 ] && [ -n "$k_root" ] || k_why="exit $k_rc / no root" - [ "${k_rows:-0}" -ge 1 ] && [ "${k_rows:-0}" -ge "${g_rows:-0}" ] 2>/dev/null || k_why="$k_why rows ${k_rows:-?}/dev/null || [ "${k_root#* over_ceiling=\"1\"}" != "$k_root" ] || k_why="$k_why ${k_b:-?}B>${k_a}B-unlabelled" + [ -s "$A7R/kb.rows" ] && cmp -s "$A7R/kb.rows" "$A7R/k.rows" || k_why="$k_why rows $( wc -l < "$A7R/k.rows" | tr -d ' ' )-vs-bare-$( wc -l < "$A7R/kb.rows" | tr -d ' ' )" + k_st="$( soft_st "$A7R/k.xml" "$N" "$A7R/n0.xml" )" || k_why="$k_why ST:[$k_st]" + [ "${k_root#* over_ceiling=}" = "$k_root" ] || k_why="$k_why over_ceiling" [ -n "$k_b1" ] && [ "${k_b1#* over_ceiling=}" = "$k_b1" ] || k_why="$k_why first-body-stub" { [ "${k_b1#* truncated=\"1\"}" = "$k_b1" ] || [ "${k_b1#* next=\"--expand=}" != "$k_b1" ]; } || k_why="$k_why cut-without-next" [ -z "$k_why" ] || a7k_bad="$a7k_bad @$N:[${k_why# }]" done [ -z "$a7k_bad" ] \ - && ok "#11 A7R kill: --detail=20 --with-graph at 2000..3000 keeps at least the --with-graph run's rows, labels any overshoot over_ceiling, serves the first body at its floor" \ - || no "#11 A7R kill: the both-sections shape —$a7k_bad" -# A7R-K2 (fix round 5, owner ruling option B — the A7B contract on a corpus whose first body is LONGER than the floor): -# at 2000 and 2500 tokens --detail=20 --with-graph delivers past the allowance with over_ceiling="1", the last rung names -# the kept rows (never the header floor: the header alone fits), and the head-cut first body's next= RECOVERS EXACTLY the -# lines it cut — the cut CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). + && ok "#11 A7R both sections (forbudget-soft): --detail=20 --with-graph at 2000..3000 serves exactly the bare rows, fits T, unlabelled, first body at its floor" \ + || no "#11 A7R both sections (forbudget-soft): —$a7k_bad" +# A7R-K2 — REPLACED in part (forbudget-soft (rule 4)). Was the A7B contract on this corpus: past the allowance with +# over_ceiling="1" and the kept-rows reason (both gone with the ceiling), well-formed despite the note, and the head-cut first +# body's next= RECOVERING EXACTLY the lines it cut. The recovery half is the soft target's NX invariant and is kept unchanged: +# at 2000 and 2500 tokens --detail=20 --with-graph serves its first body head-cut (T minus the rows funds it), and the cut +# CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). Plus NOLBL and xmllint. a7k2_bad="" for N in 2000 2500; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k2.xml"; k2_rc=$? - k2_b="$( bytes_of "$A7R/k2.xml" )"; k2_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k2_root="$( grep -aoE '^]*>' "$A7R/k2.xml" | head -1 )" - k2_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || k2_why="$k2_why premise/label:${k2_b:-?}B-vs-${k2_a}B" - { grep -aqF "$KEPT_NOTE" "$A7R/k2.xml" && ! grep -aqF "$FLOOR_NOTE" "$A7R/k2.xml" && [ "${k2_hdr:--1}" -gt 0 ] && [ "$k2_hdr" -le "$k2_a" ]; } 2>/dev/null \ - || k2_why="$k2_why reason" - # fix round 6 (checklist 25): the kept-rows note rides in the header XML comment here too — the answer must parse + { [ "$k2_rc" -eq 0 ] && [ -n "$k2_root" ] && [ "${k2_root#* over_ceiling=}" = "$k2_root" ]; } || k2_why="$k2_why exit-$k2_rc/no-root/over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/k2.xml" || k2_why="$k2_why last-rung-note" if command -v xmllint >/dev/null 2>&1; then xmllint --noout "$A7R/k2.xml" 2>/dev/null || k2_why="$k2_why xmllint-rejected" fi @@ -1022,8 +1043,8 @@ PYR [ -z "$k2_why" ] || a7k2_bad="$a7k2_bad @$N:[${k2_why# }]" done [ -z "$a7k2_bad" ] \ - && ok "#11 A7R kill+next (by-design, owner option B): --detail=20 --with-graph at 2000/2500 is over_ceiling with the kept-rows reason, and the cut first body's next= returns exactly the lines it cut" \ - || no "#11 A7R kill+next (by-design, owner option B): —$a7k2_bad" + && ok "#11 A7R cut body next= (forbudget-soft): --detail=20 --with-graph at 2000/2500 is unlabelled and well-formed, and the cut first body's next= returns exactly the lines it cut" \ + || no "#11 A7R cut body next= (forbudget-soft): —$a7k2_bad" # A7R-2 (the floor's cut is the one the floor priced): at 1500 tokens the first body is longer than the floor, so it ships # head-cut — truncated="1" with lines= and next= — never dropped and never the bare one-line stub. a7r_out --token-budget=1500 --detail=3 >"$A7R/f.xml" @@ -1035,7 +1056,7 @@ r_hi="$( printf '%s' "$r_b1" | sed -nE 's/.* lines="1-([0-9]+)\/[0-9]+".*/\1/p' || no "#11 A7R floor: the first --detail body at 1500 tokens is not the floor's head-cut of >= 2 lines (got '${r_b1:-none}')" # A7R-N1 (near-miss negative, NO explicit ceiling): the reserve must not exist — is byte-identical with and # without --with-graph (the default regime's graph rides on top, as before). -a7r_out >"$A7R/n0.xml"; a7r_out --with-graph >"$A7R/n1.xml" +a7r_out --with-graph >"$A7R/n1.xml" n0="$( a7r_sigs "$A7R/n0.xml" )"; n1="$( a7r_sigs "$A7R/n1.xml" )" { [ -n "$n0" ] && [ "$n0" = "$n1" ] && [ "${n0#*capped=\"1\"}" != "$n0" ]; } \ && ok "#11 A7R no ceiling: (capped=1) byte-identical with and without --with-graph — nothing reserved without --token-budget" \ @@ -1050,43 +1071,57 @@ w0="$( a7r_sigs "$A7R/w0.xml" )"; w1="$( a7r_sigs "$A7R/w1.xml" )" || no "#11 A7R wide ceiling: differs with --with-graph at --token-budget=8000 (or is not cut) — the reserve shrank a sig share that had room" # ── #11 A7D (fix round 4, review rv-wrap-recipe-k29 F1; rule 5 — never a silent cut): under --token-budget the compact -# section is no longer DROPPED with no element when the ceiling is spent. The arm corpus is on the conceptual route. -# A7D-1: at 2000 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with -# no budget — its legend clause rides the header, the answer stays inside its budget (the disclosure's bytes are -# reserved), and the next= RECOVERS the rows: that call's serves at least one row of the same total. -a7r_out --token-budget=2000 >"$A7R/h.xml" +# section is never DROPPED with no element when the target is spent. The arm corpus is on the conceptual route. +# forbudget-soft (rule 4): a trailing section now takes T minus the rows already served, and the DEFAULT compact serves +# all its rows (decision D-3: no 1,000 B cap). So the spent-target marker moved from 2000 to 1500 tokens on this corpus (the +# rows now fill more of T: d099ec21 serves 3 of 6 hop rows at 2000, 0 at 1500), the partial cut moved from 5000 to 2000 (5000 +# is wide enough for the whole set), and the no-budget negative inverts: the default is WHOLE (base d4325a8c: 5 of 6, +# capped). Re-anchored with the invariant unchanged where it still holds: every cut is counted and carries next=. +# A7D-1: at 1500 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with no +# budget — its legend clause rides the header, no over_ceiling=, and the next= RECOVERS the rows: that call (the default +# answer) serves every one of the same total= hop rows. +a7r_out --token-budget=1500 >"$A7R/h.xml" h_tag="$( grep -aoE ']*>' "$A7R/h.xml" | head -1 )" -h_root="$( grep -aoE '^]*>' "$A7R/h.xml" | head -1 )" +h_root="$( ctx_root "$A7R/h.xml" )" h_tot="$( printf '%s' "$h_tag" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" -a7r_out >"$A7R/hd.xml" -hd_tag="$( grep -aoE ']*>' "$A7R/hd.xml" | head -1 )" +hd_tag="$( grep -aoE ']*>' "$A7R/n0.xml" | head -1 )" hd_shown="$( printf '%s' "$hd_tag" | sed -nE 's/" ] && [ "${h_tot:-0}" -ge 1 ] 2>/dev/null \ && grep -aqF '; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)' "$A7R/h.xml" && [ "${h_root#* over_ceiling=}" = "$h_root" ] \ - && [ "${hd_shown:-0}" -ge 1 ] 2>/dev/null && [ "$hd_tot" = "$h_tot" ]; } \ - && ok "#11 A7D hops: the spent-ceiling is the counted marker ($h_tag), defined, inside budget; its next= serves $hd_shown of $hd_tot rows" \ - || no "#11 A7D hops: spent-ceiling not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" + && [ -n "$h_root" ] && [ -n "$hd_shown" ] && [ "$hd_shown" = "$h_tot" ] && [ "$hd_tot" = "$h_tot" ]; } \ + && ok "#11 A7D hops: the spent-target at 1500 is the counted marker ($h_tag), defined, unlabelled; its next= serves all $hd_shown of $hd_tot rows" \ + || no "#11 A7D hops: spent-target at 1500 not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" # A7D-1 (train 27): the compact dialect above defines the marker in its own short clause — it already reads total= shown= # capped=1 and next= in its un-droppable clauses (verbs_for.h kForHopsBudgetCutLegendCompact). The FULL dialect keeps the -# long clause that defines both: the same marker, the same next=, inside the same budget, with shown=0 capped=1 and next= read. -a7r_out --token-budget=2000 --legend=full >"$A7R/hf.xml" -hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( grep -aoE '^]*>' "$A7R/hf.xml" | head -1 )" -{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] \ +# long clause that defines both: the same marker, the same next=, with shown=0 capped=1 and next= read. +a7r_out --token-budget=1500 --legend=full >"$A7R/hf.xml" +hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( ctx_root "$A7R/hf.xml" )" +{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] && [ -n "$hf_root" ] \ && grep -aqF '; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them' "$A7R/hf.xml" \ && [ "${hf_root#* over_ceiling=}" = "$hf_root" ] && ! grep -aqF '; hops: this budget' "$A7R/hf.xml"; } \ - && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), inside budget" \ - || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause or the budget fit is missing" -# A7D-2: a PARTIAL hops cut under the budget carries the same next= (5000 tokens: some rows fit, not all). -a7r_out --token-budget=5000 >"$A7R/hp.xml" + && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), unlabelled" \ + || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause is missing or the root is labelled" +# A7D-2: a PARTIAL hops cut under the target carries the same next= (2000 tokens: some rows fit, not all). +a7r_out --token-budget=2000 >"$A7R/hp.xml" hp_tag="$( grep -aoE ']*>' "$A7R/hp.xml" | head -1 )" -{ [ "${hp_tag#*shown=\"0\"}" = "$hp_tag" ] && [ "${hp_tag#* capped=\"1\"}" != "$hp_tag" ] && [ "${hp_tag#* next=\"--for='$A7R_Q'\"}" != "$hp_tag" ]; } \ - && ok "#11 A7D partial hops cut at 5000 tokens carries next= ($hp_tag)" \ - || no "#11 A7D partial hops cut at 5000 tokens: no next= on a cut with rows (got '${hp_tag:-none}')" -# A7D-N (near-miss negative): WITHOUT --token-budget the default regime is unchanged — its cut (capped="1") gets no -# next= from this change (a named deferral: adding it there moves every default --for answer). -{ [ "${hd_tag#* capped=\"1\"}" != "$hd_tag" ] && [ "${hd_tag#* next=}" = "$hd_tag" ]; } \ - && ok "#11 A7D no budget: the default regime's cut is byte-unchanged (capped=1, no next= added): $hd_tag" \ - || no "#11 A7D no budget: the default changed or is not cut (got '${hd_tag:-none}') — the disclosure leaked into the default regime" +hp_sh="$( printf '%s' "$hp_tag" | sed -nE 's//dev/null \ + && ok "#11 A7D partial hops cut at 2000 tokens carries next= ($hp_tag)" \ + || no "#11 A7D partial hops cut at 2000 tokens: not a partial cut with next= (got '${hp_tag:-none}')" +# A7D-W (forbudget-soft, replaces the old 5000-token partial cut): a target wide enough for the rows AND the hop rows serves the +# whole hop set, like the default answer — stop when complete, nothing left to continue (no capped=, so no next= owed). +a7r_out --token-budget=5000 >"$A7R/hw.xml" +hw_tag="$( grep -aoE ']*>' "$A7R/hw.xml" | head -1 )" +{ [ -n "$hw_tag" ] && [ -n "$hd_tot" ] && [ "$hw_tag" = "" ]; } \ + && ok "#11 A7D wide target (forbudget-soft): at 5000 tokens serves the whole set ($hw_tag)" \ + || no "#11 A7D wide target (forbudget-soft): at 5000 tokens is not the whole set of $hd_tot (got '${hw_tag:-none}')" +# A7D-N — REPLACED (forbudget-soft (rule 4), decision D-3). Was: WITHOUT --token-budget the default regime's cut +# (capped="1") gets no next= (a named deferral). The default compact route now serves ALL its hop rows (no 1,000 B cap), so +# the no-budget is whole: shown == total, capped="0" — nothing cut, nothing to continue. +{ [ -n "$hd_tag" ] && [ -n "$hd_tot" ] && [ "$hd_tag" = "" ] && [ "$hd_tot" -ge 1 ]; } 2>/dev/null \ + && ok "#11 A7D no budget (forbudget-soft): the default serves its whole set ($hd_tag)" \ + || no "#11 A7D no budget (forbudget-soft): the default is not whole (got '${hd_tag:-none}') — the compact route still caps its hop rows" # A9/A10 — the header's own spliced attributes are inside the number. IDENTITY, not a band: for a bundle with # no --detail bodies, est_tokens is markup-only, so it must equal round(delivered bytes / 2.50) EXACTLY @@ -1709,11 +1744,16 @@ jsonEnvelopeCase "--token-budget=600" --token-budget=600 jsonEnvelopeCase "--token-budget=3000" --token-budget=3000 jsonEnvelopeCase "--token-budget=16000" --token-budget=16000 -# the over_ceiling key must actually FIRE somewhere in that sweep, or the §C2 arm above is vacuous. +# §C2 control — REPLACED (forbudget-soft (rule 4)). Was: the over_ceiling key must FIRE at --token-budget=200, or the §C2 arm +# above is vacuous. A --for --json budget is a soft target: est_tokens may exceed it and the key is never emitted for it (it +# survives only at the default regime's runaway guard). The control now pins both halves of that at the same call: the +# overshoot is REAL (est_tokens > 200 — the §C1 identity above already proved that number prices the whole document) and the +# document carries NO over_ceiling key (NOLBL). Base d4325a8c: "over_ceiling":true here. "$BIN" src --for="$JSON_TASK" --json --no-cache --token-budget=200 > "$TMP/fj200.out" 2>/dev/null || true -grep -q '"over_ceiling":true' "$TMP/fj200.out" \ - && ok "§C2 control: over_ceiling DOES fire at a small budget (the charge arm above is not vacuous)" \ - || no "§C2 control: no budget in the sweep produced over_ceiling - the §C2 arms proved nothing" +c2_est="$( python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("est_tokens","")); sys.exit(0 if "over_ceiling" not in d else 1)' "$TMP/fj200.out" 2>/dev/null )"; c2_rc=$? +{ [ "$c2_rc" -eq 0 ] && [ -n "$c2_est" ] && [ "$c2_est" -gt 200 ]; } 2>/dev/null \ + && ok "§C2 control (forbudget-soft): --for --json --token-budget=200 prices past its target (est_tokens=$c2_est) with no over_ceiling key — a soft target is never labelled" \ + || no "§C2 control (forbudget-soft): --for --json --token-budget=200: est_tokens='${c2_est:-unreadable}', over_ceiling key $( [ "$c2_rc" -eq 1 ] && echo present || echo 'absent/unparseable' ) — want est_tokens > 200 and no over_ceiling key" # ── §C4 (capture-audit-4, wave 3): the --max-tokens ceiling VERDICT measures the EMITTED dialect ──────── # @@ -1795,14 +1835,26 @@ band15 "--pack-task --token-budget=1500" "$TMP/p15_pt.xml" ctx 420 [ "$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --pack-task: root budget_tokens=\"1500\" — the ceiling in the SAME unit as est_tokens=" \ || no "#15 --pack-task: root carries no budget_tokens=\"1500\" beside est_tokens= (got '$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --pack-task budget is a SOFT target (decision 3) — the section cascade still shapes +# toward it, but nothing is labelled over it and the header is never dropped for it. Was: root over_ceiling="1" at 50 (the +# header floor exceeds the ceiling). Now NOLBL + HL at the same call: no over_ceiling= on the root, task= verbatim and route= +# kept, no "(ceiling)" / task-echo drop; the full dialect reads budget_tokens= as the soft target (packtask.h +# kPackTaskSoftBudgetSentence). The overshoot stays real and priced: est_tokens > 50. "$BIN" src --pack-task="$FOR_TASK" --token-budget=50 --no-cache >"$TMP/p15_pt50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --pack-task --token-budget=50: root over_ceiling=\"1\" (the header floor exceeds the ceiling)" \ - || no "#15 --pack-task --token-budget=50: no over_ceiling=\"1\" on the root while the ledger prose says the floor exceeds the budget" +"$BIN" src --pack-task="$FOR_TASK" --token-budget=50 --legend=full --no-cache >"$TMP/p15_pt50f.xml" 2>/dev/null +pt50_why="" +[ -n "$( perl -0pe 's###gs' "$TMP/p15_pt50.xml" | grep -oE ']*)?>' | head -1 )" ] || pt50_why="$pt50_why no-root" +[ -z "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" ] || pt50_why="$pt50_why over_ceiling-on-a-soft-target" +[ "$( root_attr "$TMP/p15_pt50.xml" ctx task )" = "$FOR_TASK" ] && [ -n "$( root_attr "$TMP/p15_pt50.xml" ctx route )" ] || pt50_why="$pt50_why task=/route=-dropped" +! grep -aqE '\(ceiling\)|task_echo: dropped' "$TMP/p15_pt50.xml" || pt50_why="$pt50_why header-dropped-for-the-budget" +grep -aqF 'budget_tokens= is the soft token target asked' "$TMP/p15_pt50f.xml" || pt50_why="$pt50_why full-legend-not-soft" +[ -z "$pt50_why" ] \ + && ok "#15 --pack-task --token-budget=50 (forbudget-soft): no over_ceiling=, task=/route= kept, nothing dropped, the full legend reads a soft target" \ + || no "#15 --pack-task --token-budget=50 (forbudget-soft) —$pt50_why" E50="$( root_est "$TMP/p15_pt50.xml" ctx )" [ -n "$E50" ] && [ "$E50" -gt 50 ] 2>/dev/null \ - && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the label and the number agree" \ - || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the budget it is labelled over" + && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the soft overshoot is priced on the root" \ + || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the 50-token target (the floor cannot fit 50 tokens)" # (b) --from-trace: the same two shapes on a name-resolved frame printf 'Traceback (most recent call last):\n File "src/graph.h", line 1, in rankGraphTeleport\n' >"$TMP/p15_trace.txt" "$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=1500 --no-cache >"$TMP/p15_ft.xml" 2>/dev/null @@ -1810,65 +1862,70 @@ band15 "--from-trace --token-budget=1500" "$TMP/p15_ft.xml" ctx 420 [ "$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --from-trace: root budget_tokens=\"1500\"" \ || no "#15 --from-trace: root carries no budget_tokens=\"1500\" (got '$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --from-trace budget is a SOFT target (decision 4): no ladder, the trace echo always ships, +# over_ceiling= only against max_tokens=. Was: root over_ceiling="1" at 50. Now at the same call: est_tokens > 50 (the overshoot +# is priced), no over_ceiling= (no max_tokens= was passed), the echo of the trace file ships, and the full dialect +# reads budget_tokens= as the soft target (tracelocus.h). "$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --no-cache >"$TMP/p15_ft50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --from-trace --token-budget=50: root over_ceiling=\"1\"" \ - || no "#15 --from-trace --token-budget=50: no over_ceiling=\"1\" on the root (the prose label alone is what a parser discards)" +"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --legend=full --no-cache >"$TMP/p15_ft50f.xml" 2>/dev/null +FT50="$( root_est "$TMP/p15_ft50.xml" ctx )" +ft50_why="" +{ [ -n "$FT50" ] && [ "$FT50" -gt 50 ]; } 2>/dev/null || ft50_why="$ft50_why est_tokens='${FT50:-absent}'-not-past-50" +[ -z "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" ] || ft50_why="$ft50_why over_ceiling-on-a-soft-target" +grep -aqF ". SWEPT across the usable range like arm #5 (the small end is where the -# fixed floor dominates): the root carries est_tokens=; delivered bytes sit inside the ladder's own -# allowance (N x kMinBytesPerToken x kCeilingFirstEntryTolerance = N x 2.36 x 1.15, serialize.h) OR the -# root says over_ceiling="1"; and a labelled root's est_tokens exceeds N (the label and the number agree). -# verify-wave2 F2: the byte half above cannot see the defect it was written for. At --token-budget=1600 -# the bundle is 3,485 B against a 4,342 B allowance — so the allowance arm PASSES — while the very same -# root says budget_tokens="1600" est_tokens="1665" and withholds the one attribute that reconciles them. -# over_ceiling= fired only on the ladder's LAST RUNG, so a bundle that overshoots by a little (the common -# case) was silently unlabelled while one that overshoots by a lot was labelled. The property is about the -# two NUMBERS the root itself prints, in the unit it prints them in: est_tokens <= N OR over_ceiling="1", -# on EVERY rung. 1600/1700 are in the sweep because they are the rungs that were red. +# fixed floor dominates): the root carries est_tokens= — KEPT, unchanged. +# REPLACED (forbudget-soft (rule 4)): the two halves that read the old hard ceiling — "delivered <= N x 2.36 x 1.15 OR the +# root says over_ceiling=1" and verify-wave2 F2's "est_tokens > N <=> over_ceiling=1" — assert the deleted contract: an +# explicit --for budget is a soft target, est_tokens may exceed N and nothing labels it (decision 2). The two numbers the +# root prints no longer contradict each other because the legend reads budget_tokens= as a target the rows stop at, not a +# ceiling the document fits. Each rung now pins, on the same src corpus and budgets: NOLBL (no over_ceiling= on the root); +# ST (the rows fit T = N x 2.36 x 0.90 or are the rank 1..4 floor, and a cut is maximal against the no-budget answer +# A7/A8 captured); RP (rows(N) a rank prefix of the next rung's rows); NX (a cut carries next=). MEASURED on +# d099ec21: 4 rows at 100..600, 5 at 800, 10 at 1200, 20 at 2000, every rung unlabelled with est_tokens > N. +f15_prev="" for N in 100 300 400 600 800 1200 1500 1600 1700 2000 2500 3000; do "$BIN" src --for="$FOR_TASK" --token-budget=$N --no-cache >"$TMP/p15_for$N.xml" 2>/dev/null FB="$( bytes_of "$TMP/p15_for$N.xml" )" FE="$( root_est "$TMP/p15_for$N.xml" ctx )" FO="$( root_attr "$TMP/p15_for$N.xml" ctx over_ceiling )" - FLIM="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" if [ -z "$FE" ]; then no "#15 --for --token-budget=$N: root carries no est_tokens= ($FB B delivered; the price is not where a parser reads it)" else ok "#15 --for --token-budget=$N: root est_tokens=\"$FE\" ($FB B)" fi - if [ "$FB" -le "$FLIM" ] 2>/dev/null; then - ok "#15 --for --token-budget=$N: $FB B within the $FLIM B ladder allowance" - elif [ "$FO" = "1" ]; then - ok "#15 --for --token-budget=$N: $FB B over the $FLIM B allowance and the ROOT says so (over_ceiling=\"1\")" - { [ -n "$FE" ] && [ "$FE" -gt "$N" ]; } 2>/dev/null \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N — the label and the number agree" \ - || no "#15 --for --token-budget=$N: root over_ceiling=\"1\" but est_tokens='$FE' does not exceed the budget it is labelled over" - else - no "#15 --for --token-budget=$N: $FB B EXCEEDS the $FLIM B allowance with NO over_ceiling=\"1\" on the root (over_ceiling on root='$FO')" - fi - # F2 — the TOKEN property, independent of the byte allowance above: the root prints budget_tokens= and - # est_tokens= in the same unit, so a reader can subtract them. Whenever it does and the answer is - # positive, the root must say over_ceiling="1"; silence there means "inside the budget". - if [ -n "$FE" ] && [ "$FE" -gt "$N" ] 2>/dev/null; then - [ "$FO" = "1" ] \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N and the root says over_ceiling=\"1\"" \ - || no "#15 --for --token-budget=$N: root prints budget_tokens=$N est_tokens=$FE (over by $(( FE - N ))) with NO over_ceiling= — the two numbers contradict each other on one root" - elif [ -n "$FE" ]; then - [ "$FO" = "1" ] \ - && no "#15 --for --token-budget=$N: root says over_ceiling=\"1\" but est_tokens=$FE is inside the $N budget — the label is a lie in the other direction" \ - || ok "#15 --for --token-budget=$N: est_tokens=$FE <= $N and the root is silent (absent = inside the budget)" + soft_rows "$TMP/p15_for$N.xml" >"$TMP/p15_for$N.rows" 2>/dev/null + f15_sigs="$( grep -aoE ']*>' "$TMP/p15_for$N.xml" | head -1 )" + f15_sh="$( printf '%s' "$f15_sigs" | sed -nE 's/.* shown="([0-9]+)".*/\1/p' )"; f15_tot="$( printf '%s' "$f15_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" + f15_why="" + [ -n "$( ctx_root "$TMP/p15_for$N.xml" )" ] || f15_why="$f15_why no-root" + [ -z "$FO" ] || f15_why="$f15_why over_ceiling=\"$FO\"-on-a-soft-target" + f15_st="$( soft_st "$TMP/p15_for$N.xml" "$N" "$TMP/f_for.out" )" || f15_why="$f15_why ST:[$f15_st]" + [ -z "$f15_prev" ] || rows_prefix "$TMP/p15_for$f15_prev.rows" "$TMP/p15_for$N.rows" || f15_why="$f15_why rows-not-a-superset-of-N=$f15_prev's" + if [ -n "$f15_sh" ] && [ -n "$f15_tot" ] && [ "$f15_sh" -lt "$f15_tot" ] 2>/dev/null; then + [ "${f15_sigs#* next=\"--for=}" != "$f15_sigs" ] || f15_why="$f15_why cut-without-next" fi + [ -z "$f15_why" ] \ + && ok "#15 --for --token-budget=$N (forbudget-soft): unlabelled (est_tokens=${FE:-?}), $f15_st, rows grow monotonically" \ + || no "#15 --for --token-budget=$N (forbudget-soft) —$f15_why" + f15_prev="$N" done # the two root attributes must be DEFINED by the legend of the document that carries them (§B7 class) perl -0pe 's##\1#gs' "$TMP/p15_for3000.xml" | grep -qE '(^|[^[:alnum:]_:.-])est_tokens\s*=' \ && ok "#15 --for: the legend defines est_tokens=" \ || no "#15 --for: est_tokens= rides the root but no legend clause defines it" -grep -o '' "$TMP/p15_for100.xml" | grep -qE '(^|[^[:alnum:]_:.-])over_ceiling\s*=' \ - && ok "#15 --for --token-budget=100: the legend defines over_ceiling=" \ - || no "#15 --for --token-budget=100: over_ceiling= rides the root but no legend clause defines it" +# REPLACED (forbudget-soft (rule 4)): was "the legend at --token-budget=100 defines over_ceiling=" — the soft target never emits +# that label, so the attribute to define is budget_tokens= itself, read as the soft row target the rows stop at. +[ -n "$( root_attr "$TMP/p15_for100.xml" ctx budget_tokens )" ] && grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/p15_for100.xml" \ + && ok "#15 --for --token-budget=100 (forbudget-soft): the legend defines budget_tokens= as the soft row target the rows stop at" \ + || no "#15 --for --token-budget=100 (forbudget-soft): budget_tokens= rides the root but the legend does not read it as a soft target" # (c) --handoff: priced, labelled, and withheld= is a boolean with the count beside it "$BIN" . --handoff --token-budget=100 --no-cache >"$TMP/p15_ho100.xml" 2>/dev/null "$BIN" . --handoff --token-budget=100000 --no-cache >"$TMP/p15_hobig.xml" 2>/dev/null @@ -2073,7 +2130,8 @@ fi # >= E, by the root's own arithmetic — so this arm READS E off a wide run where nothing is dropped and probes # just above it. No pinned byte count: if the corpus or the legend moves, E moves with it and the probe follows. # Both guards against an empty pass are asserted rather than assumed — the wide run must carry the clauses (else -# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). +# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). forbudget-soft (rule 4): +# the soft target deleted rung zero on purpose, so that control now asserts the opposite law (no clause is ever dropped). # GIT-LESS and relative, the #11 A7 sweep's discipline: no at=, no churn, a fixed root=, nothing from the live # repo. --detail=1 is what puts bytes at the BODY rate, which is the half of the defect a bodiless bundle cannot # see; the fixture's one long body exceeds the tight budget's residual, so the first-entry-whole floor emits a @@ -2105,7 +2163,10 @@ rz_note(){ grep -acF '[legend clauses:' "$RZ/o.xml"; } # file defines with no import, so their rows are via="name" and --for's header now defines it (~190 B, never # dropped). The full document prices at est_tokens=1269 (it was 1162, 38 under the old 1200), so 1200 no longer carried # its legend; 1400 is the wide control again with ~130 tokens of headroom. The probe and the control still read E off it. -RZ_WIDE=1400 +# forbudget-soft (rule 4): 1400 -> 4000. Under the soft target the rows' text tier grows with N and nothing in the header is +# charged, so the document at 1400 prices at est_tokens=1770 and the widest form (e= extents on the rows) at 1875 from ~4000 +# (d099ec21); the probe E+5 must sit below the wide control, so the control moved up. Measured: no budget prices past 1875. +RZ_WIDE=4000 rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" # THE CLAUSES THIS ARM COUNTS — all THREE of the droppable trio (CodeRabbit, PR #215). It counted two: the # confidence reading and the tail reading, but not the route= reading the arm's own paragraph above names. @@ -2141,21 +2202,26 @@ else no "#18 rung zero at --token-budget=$RZ_PROBE dropped a legend it could afford: dropped-note=$RZ_PN clauses=$RZ_PC/3 est_tokens=${RZ_PE:-unreadable} over_ceiling=$RZ_PO ($RZ_PB B) — the same document prices at $RZ_E at --token-budget=$RZ_WIDE, so it fits every budget >= $RZ_E" fi fi - # …and the rung must still FIRE where the kept document genuinely does not fit. Without this, deleting - # rung zero outright would turn the arm above green. - RZ_CTRL=$(( RZ_E - 200 )) - rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" - # …and the tight control asserts the ABSENCE of the same three, route= included: a present-in-wide / - # unchecked-in-tight assertion is the one-sided shape that let the missing clause through. - RZ_CC=0 - grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - if [ "$RZ_CN" != "0" ] && [ "$RZ_CC" = "0" ]; then - ok "#18 rung zero control at --token-budget=$RZ_CTRL (200 under the $RZ_E the full document prices at): all three clauses dropped and the note says so (est_tokens=$RZ_CE) — the rung still fires when the drop is real" - else - no "#18 rung zero control at --token-budget=$RZ_CTRL: dropped-note=$RZ_CN clauses still riding=$RZ_CC/3 (est_tokens=${RZ_CE:-unreadable}) — rung zero no longer fires at all, so the arm above is green for the wrong reason" - fi + # …the CONTROL — REPLACED (forbudget-soft (rule 4)). Was: 200 under E the rung must still FIRE (all three clauses dropped + # and the '[legend clauses:' note), or deleting rung zero would turn the arm above green. The soft target deletes rung zero + # by design (decision 2: no header ladder — no legend clause is ever dropped for a budget), so the control now asserts the + # law that replaced it, HL, at the same E-200 and at a budget far under the floor (400: base d4325a8c dropped all three + # there, est_tokens=1050, over_ceiling="1"): all three clauses ride, no drop note, no over_ceiling= on the root. The arm + # above then cannot be green for the wrong reason either: it pins the three clauses at E+5, this one below every price. + rz_hl_bad="" + for RZ_CTRL in $(( RZ_E - 200 )) 400; do + rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" + RZ_CC=0 + grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + RZ_CO=0; grep -aoE '^]*>' "$RZ/o.xml" | grep -qF 'over_ceiling=' && RZ_CO=1 + { [ -n "$RZ_CE" ] && [ "$RZ_CN" = "0" ] && [ "$RZ_CC" = "3" ] && [ "$RZ_CO" = "0" ]; } \ + || rz_hl_bad="$rz_hl_bad @$RZ_CTRL:[note=$RZ_CN clauses=$RZ_CC/3 over_ceiling=$RZ_CO est_tokens=${RZ_CE:-unreadable}]" + done + [ -z "$rz_hl_bad" ] \ + && ok "#18 no rung zero (forbudget-soft): at --token-budget=$(( RZ_E - 200 )) and 400 (under the $RZ_E the document prices at) all three clauses ride, no drop note, no over_ceiling=" \ + || no "#18 no rung zero (forbudget-soft): a soft target dropped a legend clause or labelled the root —$rz_hl_bad" fi # ── #19 (CodeRabbit 4054594308, train 8): THE ROWS ARE PRICED ───────────────────────────────────── @@ -2168,7 +2234,7 @@ fi # this arm's fixture: --signatures-only printed est_tokens=2616 over 8183 B (round(bytes/2.50) = 3273), and at # --token-budget=1500 the root said est_tokens=1360 with no over_ceiling= over a 5043 B document (2017 tokens). # WHAT IS ASSERTED: the #11 identities (markup at 2.50, bodies at 3.80), exact, on three shapes of a document -# that really carries six rows — and under the explicit ceiling, est_tokens > budget iff over_ceiling="1". +# that really carries six rows — and under the explicit budget (a soft target since forbudget-soft) no over_ceiling=. # Git-less and relative (#18's discipline): nothing from the live repo, a fixed root=. HX="$TMP/hdrprice" mkdir -p "$HX" @@ -2206,13 +2272,17 @@ for hx_mode in "--signatures-only" "" "--token-budget=1500"; do hx_out="$( hx_identity "$HX/o.xml" 2>/dev/null || true )" no "#19 rows unpriced ($hx_label): bytes/span/est/expected = ${hx_out:-unreadable} (six rows required; a gap of ~bytes-of-rows/2.50 is the defect)" fi + # REPLACED (forbudget-soft (rule 4)): was "over_ceiling= agrees with est_tokens vs 1500, both ways" — the ceiling verdict + # had to see the rows. A soft target emits no ceiling verdict (the rows are header, never charged against T), + # so the arm pins NOLBL on the same document: the identity above prices the rows, and the root carries no over_ceiling= + # although est_tokens exceeds 1500 (d099ec21: 3726; base d4325a8c labelled it). Read off the root only. if [ "$hx_mode" = "--token-budget=1500" ]; then - HX_E="$( grep -aoE 'est_tokens="[0-9]+"' "$HX/o.xml" | head -1 | tr -dc '0-9' )" - HX_O=0; grep -aqF 'over_ceiling="1"' "$HX/o.xml" && HX_O=1 - if [ -n "$HX_E" ] && { { [ "$HX_E" -gt 1500 ] && [ "$HX_O" = 1 ]; } || { [ "$HX_E" -le 1500 ] && [ "$HX_O" = 0 ]; }; }; then - ok "#19 --token-budget=1500: over_ceiling=$HX_O agrees with est_tokens=$HX_E (the ceiling verdict sees the rows)" + HX_ROOT="$( ctx_root "$HX/o.xml" )" + HX_E="$( printf '%s' "$HX_ROOT" | sed -nE 's/.* est_tokens="([0-9]+)".*/\1/p' )" + if [ -n "$HX_ROOT" ] && [ -n "$HX_E" ] && [ "${HX_ROOT#* over_ceiling=}" = "$HX_ROOT" ]; then + ok "#19 --token-budget=1500 (forbudget-soft): est_tokens=$HX_E with the rows priced, no over_ceiling= on the soft target's root" else - no "#19 --token-budget=1500: over_ceiling=$HX_O disagrees with est_tokens=${HX_E:-unreadable} — the ceiling verdict was made without the rows" + no "#19 --token-budget=1500 (forbudget-soft): root '${HX_ROOT:0:120}' — want est_tokens= present and no over_ceiling= on a soft target" fi fi done diff --git a/test/forbudgetmonotoncheck.sh b/test/forbudgetmonotoncheck.sh index 2ca870521..d5934f27a 100755 --- a/test/forbudgetmonotoncheck.sh +++ b/test/forbudgetmonotoncheck.sh @@ -140,7 +140,10 @@ MISSING=$( comm -23 "$TMP/set_def" "$TMP/set_8k" | tr '\n' ' ' ) sigsblock "$TMP/def" >"$TMP/sigs_def" sigsblock "$TMP/tb8k" >"$TMP/sigs_8k" [ -s "$TMP/sigs_def" ] || no "#1 presence: could not extract a block from the default run" -diff -q "$TMP/sigs_def" "$TMP/sigs_8k" >/dev/null \ +# forbudget-soft (rule 4): the default answer discloses the rows it serves past its 7.5 KB target (); +# a soft target names none (its rows are inside its own target). Everything else — every row, its text tier, the +# continuation — must still be the default's byte for byte: the frozen-share invariant, minus that one disclosure. +diff -q <( sed 's/ past_target="[0-9]*"//' "$TMP/sigs_def" ) "$TMP/sigs_8k" >/dev/null \ && ok "#1 the sig section at the explicit ceiling is byte-identical to the default's (frozen share)" \ || no "#1 the explicit ceiling re-inflated the sig section (default $( wc -c <"$TMP/sigs_def" | tr -d ' ' ) B vs 8000 $( wc -c <"$TMP/sigs_8k" | tr -d ' ' ) B)" @@ -183,7 +186,10 @@ grep -q 'bundle="compact"' "$TMP/cdef" \ sigsblock "$TMP/cdef" >"$TMP/sigs_cdef" sigsblock "$TMP/c8k" >"$TMP/sigs_c8k" [ -s "$TMP/sigs_cdef" ] || no "#5 presence: could not extract a block from the compact default run" -diff -q "$TMP/sigs_cdef" "$TMP/sigs_c8k" >/dev/null \ +# forbudget-soft (rule 4): the default answer discloses the rows it serves past its 7.5 KB target (); +# a soft target names none (its rows are inside its own target). Everything else — every row, its text tier, the +# continuation — must still be the default's byte for byte: the frozen-share invariant, minus that one disclosure. +diff -q <( sed 's/ past_target="[0-9]*"//' "$TMP/sigs_cdef" ) "$TMP/sigs_c8k" >/dev/null \ && ok "#5 the compact route's sig section is byte-identical at the explicit ceiling (frozen share)" \ || no "#5 the explicit ceiling re-inflated the COMPACT route's sig section (default $( wc -c <"$TMP/sigs_cdef" | tr -d ' ' ) B vs 8000 $( wc -c <"$TMP/sigs_c8k" | tr -d ' ' ) B)" diff --git a/test/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index 5893c5821..f0f9d8048 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -24,14 +24,13 @@ # against a trunk where every conceptual query still took the body walk. The rule is unchanged; # only the set of shapes it has to hold across grew, and the compact shape had inherited its own # fresh copy of the silent branch, fixed at that merge.) -# 2) the ceiling-exhausted case (allowance fully consumed by the signature bundle) discloses as -# the ATTRIBUTE ALONE — bodies="0" reason="budget", no legend, no section element, because -# only the attribute has reserved bytes at a spent ceiling (D10's "trims to fit" is a contract -# too: fornotesbudgetcheck/forrootlegendcheck hold est_tokens to the stated ceiling at these -# budgets, and the ~half-KB legend has no reserve there). The block stays intact and -# byte-identical to the --signatures-only run, and est_tokens moves by at most the attribute's -# own worth. The no-fit-but-allowance-exists case (forautobodycheck #3b) keeps its richer -# shell + legend disclosure — those budgets can pay for them. +# 2) the target-exhausted case (the explicit --token-budget spent by the signature rows) discloses on +# the root — bodies="0" reason="budget" — and, since forbudget-soft (2026-10-14: an explicit +# --token-budget is a SOFT target over the rows; no header ladder, no over_ceiling=), it +# KEEPS the bundle legend and serves the empty shell (no body +# bytes): the old attribute-only shape existed to fit a hard byte ceiling that is gone. The +# block stays intact and byte-identical to the --signatures-only run, and est_tokens prices the +# extra disclosure bytes (rises, at no more than one token per kMinBytesPerToken bytes). # 3) --for --json serves no bodies BY DESIGN and says so: "bundle":"sigs" is always present # (the B1.4 rule — a reader must be able to tell "genuinely none" from "not in this dialect"). # 4) the MCP `for` verb (and batch's for sub-query — same builder, forTaskText) serves the @@ -112,8 +111,10 @@ else no "#1b compact route, ceiling exhausted: a other than the empty counted marker leaked past a spent ceiling ($ct_hops)" fi # #1c (fix round 4): the cut file tail under the same spent ceiling carries the call that lists the files it left out — -# the --for file page — and that call answers (rc 0, a page holding at least the tail's total). Negative: the -# default regime's tail (no --token-budget) is unchanged — no next= (a named deferral). +# the --for file page — and that call answers (rc 0, a page holding at least the tail's total). +# forbudget-soft (rule 4, decision D-4): a capped is continued in EVERY regime, so the old negative "the default +# regime's tail carries no next=" is replaced by its positive: the default (no --token-budget) capped tail carries the same +# file-page next=, and that page holds at least the default tail's total. RED on base (d4325a8c: the default tail has no next=). ct_tail="$( grep -oE ']*>' "$CT" | head -1 )" ct_ttot="$( printf '%s' "$ct_tail" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" "$BIN" src --for="$CONCTASK" --limit=40 --no-cache >"$TMP/cpage" 2>/dev/null; cp_rc=$? @@ -124,12 +125,26 @@ cp_tot="$( grep -oE '^]*>' "$TMP/cpage" | sed -nE 's/.* total="([0-9]+ || no "#1c compact route, ceiling exhausted: the cut has no working next= (tail '$ct_tail', page rc=$cp_rc total='${cp_tot:-none}')" "$BIN" src --for="$CONCTASK" --no-cache >"$TMP/cdef" 2>/dev/null cd_tail="$( grep -oE ']*>' "$TMP/cdef" | head -1 )" -{ [ -n "$cd_tail" ] && [ "${cd_tail#* next=}" = "$cd_tail" ]; } \ - && ok "#1c default regime: unchanged, no next= added ($cd_tail)" \ - || no "#1c default regime: changed or missing ('$cd_tail') — the explicit-budget next= leaked into the default" -grep -q 'bundle=compact:' "$CT" \ - && no "#1b compact route, ceiling exhausted: the legend leaked past a spent ceiling (no reserve there)" \ - || ok "#1b compact route, ceiling exhausted: legend dropped (only the attribute's reserved bytes are spent)" +cd_ttot="$( printf '%s' "$cd_tail" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +if ! printf '%s' "$cd_tail" | grep -q ' capped="1"'; then + no "#1c default regime premise: the default is not capped ('$cd_tail') — this arm measures nothing; re-anchor it on a capped tail" +elif printf '%s' "$cd_tail" | grep -qE "capped=\"1\" next=\"--for='$CONCTASK' --limit=40\">\$" && [ "$cp_rc" -eq 0 ] \ + && [ -n "$cp_tot" ] && [ -n "$cd_ttot" ] && [ "$cp_tot" -ge "$cd_ttot" ] 2>/dev/null; then + ok "#1c default regime: the capped carries the file-page next= (D-4), which pages $cp_tot files >= the tail's $cd_ttot" +else + no "#1c default regime: the capped has no working file-page next= ('$cd_tail', page rc=$cp_rc total='${cp_tot:-none}')" +fi +# forbudget-soft (rule 4): a spent SOFT target has no header ladder, so the old "legend dropped (no reserve at a spent ceiling)" +# arm (which only ever read the FULL legend's spelling, so it passed vacuously on the compact default) is replaced by HL: the +# --legend=full twin keeps the bundle=compact: clause, and neither answer drops a clause "(ceiling)" or carries over_ceiling=. +# RED on base (d4325a8c: the full twin drops route=/r=/tail "(ceiling)" and labels over_ceiling="1"). +"$BIN" src --for="$CONCTASK" --token-budget=400 --legend=full --no-cache >"$TMP/c400full" 2>/dev/null; cf_rc=$? +if [ "$cf_rc" -eq 0 ] && grep -q 'bundle=compact:' "$TMP/c400full" && ! grep -q '(ceiling)' "$TMP/c400full" "$CT" \ + && ! grep -q 'over_ceiling="1"' "$TMP/c400full" "$CT"; then + ok "#1b compact route, target exhausted: the bundle legend is kept (--legend=full carries bundle=compact:), no (ceiling) drop, no over_ceiling" +else + no "#1b compact route, target exhausted: legend/label (full rc=$cf_rc; bundle=compact: $( grep -c 'bundle=compact:' "$TMP/c400full" ), (ceiling) drops $( cat "$TMP/c400full" "$CT" | grep -c '(ceiling)' ), over_ceiling $( cat "$TMP/c400full" "$CT" | grep -c 'over_ceiling="1"' ))" +fi grep -q ' block is intact" \ || no "#1b compact route, ceiling exhausted: the block vanished" @@ -140,12 +155,21 @@ T="$TMP/n400" grep -q 'bundle="auto" bodies="0" reason="budget"' "$T" \ && ok "#2 ceiling-exhausted: bodies=\"0\" reason=\"budget\" disclosed" \ || no "#2 ceiling-exhausted: missing bodies=\"0\" reason=\"budget\" on the root" -grep -q ' element leaked past a spent ceiling (attribute-only there)" \ - || ok "#2 ceiling-exhausted: no element (the attribute IS the disclosure at a spent ceiling)" -grep -q 'bundle=auto:' "$T" \ - && no "#2 ceiling-exhausted: the ~half-KB legend leaked past a spent ceiling (it has no reserve there)" \ - || ok "#2 ceiling-exhausted: legend dropped (only the attribute's reserved bytes are spent)" +# forbudget-soft (rule 4): the attribute-only shape existed to fit a hard ceiling; on a soft target the empty shell +# rides (shown="0", capped="1", no row — the no-body-bytes arm below still holds) and the bundle legend is kept (HL, read +# on the --legend=full twin; the compact default never spelled bundle=auto:), with no (ceiling) drop and no over_ceiling. +# RED on base (d4325a8c: no shell, the full legend drops clauses "(ceiling)", over_ceiling="1"). +t_bodies="$( grep -oE ']*>' "$T" | head -1 )" +{ printf '%s' "$t_bodies" | grep -qE '^$' && ! grep -q ' shell rides ($t_bodies), no body row" \ + || no "#2 target exhausted: expected the empty shell, got '${t_bodies:-none}'" +"$BIN" src --for="$NAMETASK" --token-budget=400 --legend=full --no-cache >"$TMP/n400full" 2>/dev/null; nf_rc=$? +if [ "$nf_rc" -eq 0 ] && grep -q 'bundle=auto:' "$TMP/n400full" && ! grep -q '(ceiling)' "$TMP/n400full" "$T" \ + && ! grep -q 'over_ceiling="1"' "$TMP/n400full" "$T"; then + ok "#2 target exhausted: the bundle legend is kept (--legend=full carries bundle=auto:), no (ceiling) drop, no over_ceiling" +else + no "#2 target exhausted: legend/label (full rc=$nf_rc; bundle=auto: $( grep -c 'bundle=auto:' "$TMP/n400full" ), (ceiling) drops $( cat "$TMP/n400full" "$T" | grep -c '(ceiling)' ), over_ceiling $( cat "$TMP/n400full" "$T" | grep -c 'over_ceiling="1"' ))" +fi grep -q ']*> block is intact" \ # first (a floor document: root over_ceiling="1", the LAST ]* over_ceiling="1"' "$T" && grep -oE ']*>' "$T" | tail -1 | grep -q ' capped="1".* next="'; then +# forbudget-soft (rule 4): the premise is re-anchored on what still holds. A soft target labels nothing (no over_ceiling=), +# but the 400 document still has a capped carrying next= with the header legend's sigs/next= clauses BEFORE the real +# tag, so the two anti-forgery greps keep their teeth. Premise: no over_ceiling on the root, the LAST ]* over_ceiling=' "$T" && grep -oE ']*>' "$T" | tail -1 | grep -q ' capped="1".* next="'; then python3 -c 'import sys; s=open(sys.argv[1],"rb").read(); a=s.rfind(b"",a); sys.stdout.buffer.write(s[:a]+s[b+7:] if a>=0 and b>=0 else s)' "$T" >"$TMP/n400_nosigs" grep -q ' block removed — a legend clause spells the tag" \ @@ -167,7 +194,7 @@ if grep -qE '^]* over_ceiling="1"' "$T" && grep -oE ']*>' "$T" && no "#2c sigsblock() starts inside the header legend comment, not at the real tag" \ || ok "#2c sigsblock() starts at the real tag" else - no "#2c premise: the --token-budget=400 document is no longer a floor document (root over_ceiling=\"1\", a capped with next=) — re-anchor this arm on one that is" + no "#2c premise: the --token-budget=400 document has no capped with next= (or labels over_ceiling= on a soft target) — re-anchor this arm on one that is" fi "$BIN" src --for="$NAMETASK" --token-budget=400 --signatures-only --no-cache >"$TMP/n400so" 2>/dev/null sigsblock "$T" >"$TMP/s_a"; sigsblock "$TMP/n400so" >"$TMP/s_s" @@ -177,9 +204,14 @@ diff -q "$TMP/s_a" "$TMP/s_s" >/dev/null \ || no "#2 ceiling-exhausted: the disclosure changed the bytes" ET_T=$( grep -o 'est_tokens="[0-9]*"' "$T" | head -1 | grep -o '[0-9]*' ) ET_S=$( grep -o 'est_tokens="[0-9]*"' "$TMP/n400so" | head -1 | grep -o '[0-9]*' ) -{ [ -n "${ET_T:-}" ] && [ -n "${ET_S:-}" ] && [ $(( ET_T - ET_S )) -ge 0 ] && [ $(( ET_T - ET_S )) -le 30 ]; } \ - && ok "#2 ceiling-exhausted: est_tokens moved by only the attribute's worth ($ET_T vs $ET_S, band 0..30)" \ - || no "#2 ceiling-exhausted: est_tokens delta out of band (tight=$ET_T vs sig-only=$ET_S — the disclosure leaked more than its reserve)" +# forbudget-soft (rule 4): the old band (0..30 tokens: the attribute's reserved worth) priced a disclosure squeezed under a hard +# ceiling; the legend clause and the shell now ride, so the band is replaced by honest pricing of the extra bytes: +# est_tokens rises (>= 1) and by no more than one token per kMinBytesPerToken (2.36) bytes of the byte delta (+1 rounding). +BY_T=$( wc -c <"$T" | tr -d ' ' ); BY_S=$( wc -c <"$TMP/n400so" | tr -d ' ' ) +ET_MAX=$( python3 -c 'import math,sys; print(math.ceil(max(0,int(sys.argv[1])-int(sys.argv[2]))/2.36)+1)' "${BY_T:-0}" "${BY_S:-0}" 2>/dev/null ) +{ [ -n "${ET_T:-}" ] && [ -n "${ET_S:-}" ] && [ -n "${ET_MAX:-}" ] && [ "$BY_T" -gt "$BY_S" ] && [ $(( ET_T - ET_S )) -ge 1 ] && [ $(( ET_T - ET_S )) -le "$ET_MAX" ]; } \ + && ok "#2 target exhausted: est_tokens prices the disclosure bytes ($ET_T vs $ET_S for +$(( BY_T - BY_S )) B, band 1..$ET_MAX)" \ + || no "#2 target exhausted: est_tokens delta out of band (tight=${ET_T:-?} vs sig-only=${ET_S:-?}, bytes ${BY_T:-?} vs ${BY_S:-?}, max ${ET_MAX:-?})" # ── #3: the --json dialect names its own posture ──────────────────────────────────────────────────────── "$BIN" src --for="$NAMETASK" --json --no-cache >"$TMP/j1" 2>/dev/null; rc=$? diff --git a/test/formaxtokenscheck.sh b/test/formaxtokenscheck.sh index 505510902..6ab9af22b 100755 --- a/test/formaxtokenscheck.sh +++ b/test/formaxtokenscheck.sh @@ -47,8 +47,11 @@ # (D) LEGEND. The attribute is defined by the legend of the document that carries it, against the # ceiling ACTUALLY on that root — max_tokens, budget_tokens, or both when both were asked for — and # a document inside its ceiling pays none of those bytes. -# (E) --token-budget INTEROP. The pre-existing budget contract is unchanged, and with both flags set the -# label answers to EITHER ceiling. +# (E) --token-budget INTEROP. forbudget-soft (rule 4, owner decisions 2026-10-14): an explicit --token-budget on +# --for is a SOFT row target, never a ceiling this root labels against — so alone it carries no over_ceiling= +# (est_tokens may exceed budget_tokens), and beside --max-tokens the label answers to max_tokens ONLY and its +# legend says so. The served rows are a rank prefix that grows with the target, untouched by --max-tokens. +# (Was: "the budget contract is unchanged, and with both flags set the label answers to EITHER ceiling".) # (F) DETERMINISM across the band, and (G) MUTATION CONTROLS: every judge above is re-run over a # deliberately corrupted copy of a real document and must go RED. An arm that cannot fail is not a # gate, and three of these four judges are cheap to write in an always-true form. @@ -99,8 +102,9 @@ QUERY='processEntry input trim' # the same code path the arm did. Reads the ROOT ELEMENT through a real XML parser (the :719 trap), and # re-derives est_tokens from the delivered bytes. Prints one diagnostic line per violation; exit 0 clean. # -# judge.py DOC MAX_TOKENS TOKEN_BUDGET +# judge.py DOC MAX_TOKENS TOKEN_BUDGET [soft] # MAX_TOKENS / TOKEN_BUDGET: the value passed on the command line, or 0 for "flag not given". +# soft: TOKEN_BUDGET names a soft target (forbudget-soft) — stated on the root, never a ground for the label. JUDGE="$TMP/judge.py" cat > "$JUDGE" <<'PY_EOF' import re, sys, xml.etree.ElementTree as ET @@ -114,6 +118,7 @@ def tokens( byteCount, rate ): return int( byteCount / rate + 0.5 ) # tokensForEmittedBytes path, wantMax, wantBudget = sys.argv[1], int( sys.argv[2] ), int( sys.argv[3] ) +softBudget = len( sys.argv ) > 4 and sys.argv[4] == 'soft' raw = open( path, 'rb' ).read() bad = [] @@ -138,7 +143,8 @@ est = int( attrs['est_tokens'] ) # ── (A/B/E) THE BICONDITIONAL, in the unit the root prints both numbers in ─────────────────────────────── overMax = wantMax > 0 and est > wantMax -overBudget = wantBudget > 0 and est > wantBudget +# forbudget-soft (rule 4): a soft target is never "over" — only max_tokens= is a ceiling on such a root +overBudget = wantBudget > 0 and est > wantBudget and not softBudget labelled = attrs.get( 'over_ceiling' ) == '1' if ( overMax or overBudget ) and not labelled: bad.append( 'est_tokens=%d exceeds the ceiling on its own root (max_tokens=%d budget_tokens=%d) ' @@ -178,6 +184,8 @@ if labelled: else: named = set( re.findall( r'(?:budget|max)_tokens', clause.group( 1 ) ) ) onRoot = set( k for k in ( 'budget_tokens', 'max_tokens' ) if k in attrs ) + if softBudget: + onRoot.discard( 'budget_tokens' ) # forbudget-soft: the label must not be defined against a soft target if named != onRoot: bad.append( 'the legend defines over_ceiling against %s, but this root carries %s' % ( sorted( named ) or [ 'nothing' ], sorted( onRoot ) or [ 'nothing' ] ) ) @@ -241,23 +249,73 @@ python3 "$JUDGE" "$TMP/nomax" 0 0 \ || no "(B) --detail=30 with no --max-tokens still emits a ceiling attribute or a verdict" # ══ (E) --token-budget INTEROP ═══════════════════════════════════════════════════════════════════════════ -# The pre-existing budget contract is unchanged (this fix must not move it), and with BOTH ceilings on one -# root the single label answers to either — which is also the case whose legend has two names to get right. -INTEROP=0; INTEROP_BAD=0 +# forbudget-soft (rule 4) — REPLACES "the pre-existing budget contract is unchanged, and with both ceilings on one root the +# single label answers to either" (the JUDGE's biconditional against budget_tokens=). An explicit --token-budget is now a +# SOFT row target (owner decisions 2026-10-14): the JUDGE runs with `soft`, so at every point +# · alone: NOLBL — no over_ceiling=, though est_tokens may exceed budget_tokens (asserted reachable below); +# · beside --max-tokens: the label answers to max_tokens ONLY, and the legend defines it against max_tokens alone; +# · (C) byte accounting and budget_tokens= on the root are judged exactly as before. +# Plus RP: rows(N) is a rank prefix of rows(N') for N < N' and of the default answer's rows, and --max-tokens (a body +# ceiling) never moves a row. The --max-tokens band (A) above is this arm's hard-ceiling twin. +INTEROP=0; INTEROP_BAD=0; SOFT_PAST=0; BOTH_LABELLED=0 +rowsOf(){ python3 -c " +import re, sys +s = open( sys.argv[1] ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = dict( re.findall( r'(\w+)=\"([^\"]*)\"', d.group( 1 ) ) ) + r = a.get( 'r', '' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, a.get( 'p', '?' ) + ':' + a.get( 'l', '?' ) + ':' + a.get( 'n', '?' ) ) ) +rows.sort() +print( '\n'.join( x[1] for x in rows ) )" "$1"; } +isPrefix(){ [ -s "$1" ] && [ "$( wc -l < "$1" )" -le "$( wc -l < "$2" )" ] && [ "$( head -n "$( wc -l < "$1" )" "$2" )" = "$( cat "$1" )" ]; } +rowsOf "$TMP/nomax" > "$TMP/rows.nomax" || no "(E) the default --detail=30 answer has no block — RP has no reference" +PREV="" for TB in 800 2000 20000; do "$BIN" "$CORPUS" --no-cache "--for=$QUERY" --detail=30 --token-budget="$TB" >"$TMP/tb.$TB" 2>/dev/null INTEROP=$(( INTEROP + 1 )) - python3 "$JUDGE" "$TMP/tb.$TB" 0 "$TB" || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB"; } + python3 "$JUDGE" "$TMP/tb.$TB" 0 "$TB" soft || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB"; } + EST="$( python3 -c " +import sys, xml.etree.ElementTree as ET +print( ET.parse( sys.argv[1] ).getroot().attrib.get( 'est_tokens', '' ) )" "$TMP/tb.$TB" 2>/dev/null )" + case "$EST" in ''|*[!0-9]*) ;; *) [ "$EST" -gt "$TB" ] && SOFT_PAST=$(( SOFT_PAST + 1 )) ;; esac + if rowsOf "$TMP/tb.$TB" > "$TMP/rows.$TB"; then + isPrefix "$TMP/rows.$TB" "$TMP/rows.nomax" \ + && ok "(E) RP: rows(--token-budget=$TB) — $( wc -l < "$TMP/rows.$TB" | tr -d ' ' ) row(s) — are a rank prefix of the default answer's $( wc -l < "$TMP/rows.nomax" | tr -d ' ' )" \ + || no "(E) RP: rows(--token-budget=$TB) are not a rank prefix of the default answer's rows" + if [ -n "$PREV" ]; then + isPrefix "$TMP/rows.$PREV" "$TMP/rows.$TB" \ + && ok "(E) RP: rows(--token-budget=$PREV) are a prefix of rows(--token-budget=$TB) (monotone in the target)" \ + || no "(E) RP: rows(--token-budget=$PREV) are not a prefix of rows(--token-budget=$TB) — not monotone" + fi + PREV="$TB" + else + no "(E) --token-budget=$TB: no block — no rows to judge" + fi for MT in 300 20000; do "$BIN" "$CORPUS" --no-cache "--for=$QUERY" --detail=30 --token-budget="$TB" --max-tokens="$MT" >"$TMP/both.$TB.$MT" 2>/dev/null INTEROP=$(( INTEROP + 1 )) - python3 "$JUDGE" "$TMP/both.$TB.$MT" "$MT" "$TB" \ + python3 "$JUDGE" "$TMP/both.$TB.$MT" "$MT" "$TB" soft \ || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " ^ at --token-budget=$TB --max-tokens=$MT"; } + grep -o ']*>' "$TMP/both.$TB.$MT" | head -1 | grep -q ' over_ceiling="1"' && BOTH_LABELLED=$(( BOTH_LABELLED + 1 )) + rowsOf "$TMP/both.$TB.$MT" > "$TMP/rows.$TB.$MT" 2>/dev/null + cmp -s "$TMP/rows.$TB.$MT" "$TMP/rows.$TB" && [ -s "$TMP/rows.$TB" ] \ + || { INTEROP_BAD=$(( INTEROP_BAD + 1 )); echo " --token-budget=$TB --max-tokens=$MT: the rows differ from --token-budget=$TB alone — the body ceiling moved a row"; } done done [ "$INTEROP_BAD" = "0" ] \ - && ok "(E) --token-budget alone and beside --max-tokens: $INTEROP point(s), one label answering to either ceiling" \ + && ok "(E) --token-budget alone and beside --max-tokens: $INTEROP point(s), no label for the soft target, the label answering to max_tokens only, same rows either way" \ || no "(E) VIOLATED at $INTEROP_BAD of $INTEROP --token-budget point(s)" +# NON-VACUITY for both halves: a soft target the answer really exceeds (so NOLBL was tested, not assumed), and a +# combined point really over max_tokens (so the max_tokens-only label was read, not absent). +[ "$SOFT_PAST" -gt 0 ] \ + && ok "(E) non-vacuity: $SOFT_PAST --token-budget point(s) deliver est_tokens past the soft target (the no-label half is exercised)" \ + || no "(E) VACUOUS: no --token-budget point exceeds its target — the no-label half never ran" +[ "$BOTH_LABELLED" -gt 0 ] \ + && ok "(E) non-vacuity: $BOTH_LABELLED combined point(s) carry over_ceiling=\"1\" for max_tokens" \ + || no "(E) VACUOUS: no --token-budget + --max-tokens point is labelled — the max_tokens-only legend half never ran" # ══ (F) DETERMINISM across the band ══════════════════════════════════════════════════════════════════════ # The verdict is decided inside an est_tokens fixpoint that measures the document containing its own label. @@ -308,6 +366,32 @@ d = re.sub( rb' over_ceiling=1 says est_tokens exceeds [a-z_ ]+', b'', d, count # G5 — the legend arm, other direction: the clause naming a ceiling this root does not carry. mutate "a legend naming the wrong ceiling" "$TMP/band.300" 300 0 \ "d = d.replace( b'over_ceiling=1 says est_tokens exceeds max_tokens', b'over_ceiling=1 says est_tokens exceeds budget_tokens', 1 )" +# G6/G7 — forbudget-soft (rule 4): the soft judge of (E), on the old behaviour manufactured — a label on a soft target the +# answer exceeds, and a legend defining the label against the soft target. A pasted attribute also moves the byte count, +# which (C) rejects on its own, so each control requires the judge's rejection to name the SOFT reason ($6), not just exit 1. +# On a binary whose real document already has the broken shape (the pre-soft base), the edit is a no-op and the real +# document is judged as is — the control then reads the shape the binary emitted, which must be rejected all the same. +softMutate(){ # $1 label, $2 source doc, $3 max, $4 budget, $5 python edit expr, $6 expected reason + python3 - "$2" "$TMP/mut.xml" < "$TMP/mut.why" 2>&1; then + no "(G) $1: the soft judge ACCEPTS a deliberately broken document — arm (E) is inert" + elif grep -qF -e "$6" "$TMP/mut.why"; then + ok "(G) $1: the soft judge rejects it, for that reason" + else + no "(G) $1: the soft judge rejects it only for another reason ($( head -1 "$TMP/mut.why" | cut -c1-120 )) — the soft half is inert" + fi +} +softMutate "a label on a soft target (over_ceiling pasted onto --token-budget=800 alone)" "$TMP/tb.800" 0 800 \ + "d = d.replace( b' over_ceiling=\"1\"', b'', 1 ).replace( b' est_tokens=', b' over_ceiling=\"1\" est_tokens=', 1 )" \ + 'root claims over_ceiling="1" but est_tokens=' +softMutate "a legend defining the label against the soft target too" "$TMP/both.800.300" 300 800 \ + "d = d.replace( b'over_ceiling=1 says est_tokens exceeds max_tokens', b'over_ceiling=1 says est_tokens exceeds budget_tokens or max_tokens', 1 )" \ + 'the legend defines over_ceiling against' if [ "$fail" = "0" ]; then echo "formaxtokenscheck: ALL PASS"; else echo "formaxtokenscheck: FAILURES"; fi exit "$fail" diff --git a/test/forrootlegendcheck.sh b/test/forrootlegendcheck.sh index b4b6fe245..79fdaa147 100755 --- a/test/forrootlegendcheck.sh +++ b/test/forrootlegendcheck.sh @@ -83,18 +83,22 @@ done # 942, 1000 six rows at 940 / 989 — 60 tokens of headroom in the full legend, the dialect this arm measures (the 2026-09-12 # re-anchor's 36-token posture or better); 1025+ seven rows at 987. The assertions are unchanged. OUT2="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache --legend=full 2>/dev/null )" -EST2="$( printf '%s' "$OUT2" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$EST2" ] && [ "$EST2" -le 1000 ]; then - ok "arm2: --token-budget=1000 fits the ceiling in the full legend (est_tokens=$EST2)" +# forbudget-soft (owner decisions 2026-10-14; rule 4): --token-budget on --for is a SOFT target charged against the +# ROWS only — the header (this root= clause included) is never charged and never labelled "over". The arm's subject, "the +# clause's cost", is therefore asserted where it now lives: the clause costs NO row (the full legend, which carries it at +# length, serves exactly the rows the compact default does at the same target), and neither answer carries over_ceiling=. +OUT2D="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache 2>/dev/null )" +rows2(){ printf '%s' "$1" | python3 -c 'import re,sys; m=re.search(r"]*>(.*?)",sys.stdin.read(),re.S); print("|".join(re.findall(r"]*\bn=\"([^\"]*)\"",m.group(1))) if m else "")'; } +R2F="$( rows2 "$OUT2" )"; R2D="$( rows2 "$OUT2D" )" +if [ -n "$R2F" ] && [ "$R2F" = "$R2D" ]; then + ok "arm2: --token-budget=1000: the full legend's clause costs no row (full rows == compact rows: $( printf '%s' "$R2F" | tr '|' '\n' | grep -c . ))" else - no "arm2: --token-budget=1000 est_tokens=${EST2:-unreadable} exceeds the ceiling — the legend clause is too expensive" + no "arm2: --token-budget=1000: full rows '${R2F:0:80}' != compact rows '${R2D:0:80}' — a header clause is being charged to the rows" fi -OUT2D="$( "$BIN" "$TMP/tiny" --for="widget routine dispatcher" --token-budget=1000 --no-cache 2>/dev/null )" -EST2D="$( printf '%s' "$OUT2D" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$EST2D" ] && { [ "$EST2D" -le 1000 ] || printf '%s' "$OUT2D" | head -c 600 | grep -q ' over_ceiling="1"'; }; then - ok "arm2: the default answer at --token-budget=1000 fits or says it does not (est_tokens=$EST2D)" +if [ -n "$OUT2D" ] && ! printf '%s%s' "$OUT2" "$OUT2D" | grep -q ' over_ceiling="1"'; then + ok "arm2: --token-budget=1000: neither dialect labels a soft target over_ceiling" else - no "arm2: the default answer at --token-budget=1000 is over (est_tokens=${EST2D:-unreadable}) with no over_ceiling label" + no "arm2: --token-budget=1000: over_ceiling=\"1\" on a soft target (or the call failed)" fi if printf '%s' "$OUT2" | grep -qF "$CLAUSE_SNIPPET"; then ok "arm2: the clause SURVIVES at --token-budget=1000 (not dropped by the ceiling ladder)" diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 7fe51c441..68aaeafad 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -55,8 +55,12 @@ # docs describe the context lifecycle?" keeps a docs row in its top five. # (T) a TIGHT explicit ceiling (--token-budget / MCP budget_tokens below the default signature share) gets neither the # reorder nor its uncharged note: the answer is byte-equal to the RIPWIRE_NO_DOCS_AFTER_CODE=1 twin (which is the -# pre-reorder answer), XML, JSON and MCP, at 700 and 1200 on the TS fixture; at 1200 the XML/JSON est_tokens stay -# within the budget with no over_ceiling the twin lacks (RED at 9d0ad7aa: est 1277 > 1200, over_ceiling="1"). +# pre-reorder answer), XML, JSON and MCP, at 700 and 1200 on the TS fixture. At 1200 (forbudget-soft, rule 4: an +# explicit --token-budget is a SOFT target over the rows, est_tokens may exceed N) the old "est_tokens <= 1200, +# no over_ceiling the twin lacks" half is replaced by NOLBL (no over_ceiling= on either answer) + ST (the rows' charge +# fits T = N x 2.36 x 0.9 bytes or is the rank 1..4 floor; a cut is maximal against the uncut plain-order answer and +# carries capped + next=), XML and JSON (RED on base d4325a8c: the hard ceiling cut 10 of 16 / 13 of 16 rows with +# room left under T). # MCP est_tokens is the twin's (already above 1200 on base; not this rule's to fix). Near misses: no ceiling and # --token-budget=8000 still reorder and carry docs_after_code= with its reading, XML, JSON and MCP; a question that # names docs keeps score order at 1200 too. @@ -418,11 +422,52 @@ if mode == "tight": bad.append( "differs from the NO_DOCS_AFTER_CODE=1 twin (the pre-reorder answer)" ) if moved or note: bad.append( "carries docs_after_code= or its reading" ) + # forbudget-soft (rule 4): an explicit --token-budget (a REF is passed) is a SOFT target over the rows (T = N x kMinBytesPerToken x + # kBudgetHeadroom bytes), so est_tokens may exceed N and nothing is labelled over_ceiling. The old "est_tokens <= N, no + # over_ceiling the twin lacks" half is replaced by NOLBL (no over_ceiling= on either answer) + ST (the rows fit T, or are + # the rank 1..4 floor; a cut is maximal against the uncut plain-order REF and carries next=). RED on base (d4325a8c): the + # hard ceiling cut the XML rows at 10 of 16 / the JSON rows at 13 with room left under T. m = re.search( r']*\best_tokens="([0-9]+)"', A ) or re.search( r'"est_tokens":([0-9]+)', A ) - if budget and not on.endswith( ".mcp.json" ): + if budget and not on.endswith( ".mcp.json" ) and len( sys.argv ) <= 7: + # a HARD body ceiling (--max-tokens; no REF passed): est_tokens stays <= N, no over_ceiling the twin lacks — unchanged if not m: bad.append( "no est_tokens to compare with the budget (premise)" ) elif int( m.group( 1 ) ) > budget: bad.append( "est_tokens %s > budget %d" % ( m.group( 1 ), budget ) ) if "over_ceiling" in A and "over_ceiling" not in B: bad.append( "over_ceiling the twin lacks" ) + elif budget and not on.endswith( ".mcp.json" ): + tb = float( sys.argv[ 8 ] ) if len( sys.argv ) > 8 and re.fullmatch( r"[0-9.]+", sys.argv[ 8 ] ) else 0.0 + if tb <= 0: bad.append( "no target bytes-per-token constant (premise: src/serialize.h kMinBytesPerToken x kBudgetHeadroom)" ) + if re.search( r'over_ceiling', A ) or re.search( r'over_ceiling', B ): + bad.append( "NOLBL: over_ceiling on a soft --token-budget answer" ) + def charged( doc ): + """(row charges in rank order, total charged bytes, cut?, next?) — XML: the bytes inside plus the zero + cx=/ccx=/in= the lean spelling elides (the ladder's entryCost); JSON: the compact bytes of each "sigs" entry.""" + if doc.lstrip().startswith( "{" ): + try: j = json.loads( doc ) + except ValueError: return None + rows = [ ( int( e.get( "r", 10**9 ) ), len( json.dumps( e, separators=( ",", ":" ), ensure_ascii=False ).encode() ) + 1 ) + for e in j.get( "sigs", [] ) ] + rows.sort() + cut = bool( j.get( "capped" ) ) or j.get( "sigs_shown", len( rows ) ) != j.get( "sigs_total", len( rows ) ) + return [ c for _, c in rows ], sum( c for _, c in rows ), cut, bool( j.get( "sigs_next" ) ) + m2 = re.search( r']*)>(.*?)', doc, re.S ) + if not m2: return None + rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', m2.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ); r = re.search( r'\br="(\d+)"', a ); pr = len( d.group( 0 ).encode() ) + c = pr + sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + elided += c - pr; rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return [ c for _, c in rows ], len( m2.group( 2 ).encode() ) + elided, 'capped="1"' in m2.group( 1 ), " next=" in m2.group( 1 ) + a, ref = charged( A ), charged( body( sys.argv[ 7 ] ) ) if len( sys.argv ) > 7 else None + T = int( budget * tb ) + if a is None or ref is None or not ref[ 0 ]: bad.append( "ST: no rows to charge (premise: answer and uncut reference)" ) + elif tb > 0: + k = len( a[ 0 ] ); fits = a[ 1 ] <= T or k <= 4 + if not fits: bad.append( "ST: rows charged %d B > target %d B with %d rows (floor 4)" % ( a[ 1 ], T, k ) ) + if k < len( ref[ 0 ] ): + if a[ 1 ] + ref[ 0 ][ k ] <= T - 160: bad.append( "ST: cut at %d of %d rows with %d B charged, the next row (%d B) fits T=%d" % ( k, len( ref[ 0 ] ), a[ 1 ], ref[ 0 ][ k ], T ) ) + if not ( a[ 2 ] and a[ 3 ] ): bad.append( "NX: rows cut (%d of %d) without capped + next=" % ( k, len( ref[ 0 ] ) ) ) + print( " .... %s: ST rows=%d/%d charged=%d T=%d" % ( label, k, len( ref[ 0 ] ), a[ 1 ], T ) ) else: if not moved: bad.append( "no docs_after_code= (the reorder did not apply)" ) if not note and not isjson: bad.append( "no reading for docs_after_code=" ) # the JSON twin carries the count only @@ -438,13 +483,17 @@ mcpfor(){ local out="$1" twin="$2" task="$3" extra="${4:-}"; mkdir -p "$TMP/mcph printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":"'"$CORPUS/ts"'","task":"'"$task"'"'"$extra"'}}}' \ | ( cd "$CORPUS/ts" && if [ "$twin" = 1 ]; then TMPDIR="$TMP/mcphome" RIPWIRE_NO_DOCS_AFTER_CODE=1 "$BIN" --mcp; else TMPDIR="$TMP/mcphome" env -u RIPWIRE_NO_DOCS_AFTER_CODE "$BIN" --mcp; fi >"$out" 2>/dev/null ); printf '%s' "$?" >"$out.rc"; } +# forbudget-soft (rule 4): the soft target's bytes per token, read from source (a missing constant FAILS the ST half, never skips) +TBPT="$( python3 -c "import re,sys; s=open(sys.argv[1]).read(); g=lambda k:float(re.search(r'inline constexpr double '+k+r' *= *([0-9.]+);',s).group(1)); print(g('kMinBytesPerToken')*g('kBudgetHeadroom'))" "$ROOT/src/serialize.h" 2>/dev/null )" for b in 700 1200; do for spec in "xml|" "json|--json"; do tag="${spec%%|*}"; extra="${spec#*|}" if [ -n "$extra" ]; then ON="$( onr ts "t$b$tag" "$Q1" "--token-budget=$b" "$extra" )"; OFF="$( offr ts "t$b$tag" "$Q1" "--token-budget=$b" "$extra" )" else ON="$( onr ts "t$b$tag" "$Q1" "--token-budget=$b" )"; OFF="$( offr ts "t$b$tag" "$Q1" "--token-budget=$b" )"; fi if ran_ok "$ON" "T/$b/$tag" && ran_ok "$OFF" "T/$b/$tag (twin)"; then - tcheck "T/$tag --token-budget=$b: byte-equal to the plain-order twin, no docs_after_code=$([ "$b" = 1200 ] && echo ", est_tokens <= 1200")" tight "$ON" "$OFF" "$([ "$b" = 1200 ] && echo 1200 || echo 0)" + REF="$( offr ts "r$tag" "$Q1" $extra )" # the uncut plain-order answer (no budget): the rank order a soft cut is a prefix of + ran_ok "$REF" "T/$b/$tag (uncut reference)" || continue + tcheck "T/$tag --token-budget=$b: byte-equal to the plain-order twin, no docs_after_code=$([ "$b" = 1200 ] && echo ", NOLBL + ST (soft target)")" tight "$ON" "$OFF" "$([ "$b" = 1200 ] && echo 1200 || echo 0)" "$REF" "$TBPT" fi done mcpfor "$TMP/t$b.on.mcp.json" 0 "$Q1" ",\"budget_tokens\":$b"; mcpfor "$TMP/t$b.off.mcp.json" 1 "$Q1" ",\"budget_tokens\":$b" diff --git a/test/jsonparitycheck.sh b/test/jsonparitycheck.sh index 3b6abcde3..aa1a23ff4 100755 --- a/test/jsonparitycheck.sh +++ b/test/jsonparitycheck.sh @@ -362,7 +362,8 @@ XML_ROUTES="$( "$BIN" "$ROOT/test/routeedgefix" --for="$RQ" --no-cache 2>/dev/nu # that sweep is what still holds the line. Three clauses, per rung: # • budget_tokens is on the JSON root whenever it is on the XML root, and names the SAME N; # • est_tokens is served in both (the price a budget shapes against is never withheld); -# • est_tokens > N implies over_ceiling in that dialect's own spelling — ONE unit, both dialects. +# • est_tokens > N implies over_ceiling in that dialect's own spelling — ONE unit, both dialects. (forbudget-soft: N is a +# soft target now; the clause is "neither dialect labels it, both continue the cut" — still one rule, both dialects.) # The XML side is asserted here too rather than assumed: this is a PARITY arm, and a parity that only ever # reads one side cannot tell a fixed dialect from a regressed reference. echo @@ -389,12 +390,18 @@ for BTB in 880 900 1500; do else no "R1: budget=$BTB — est_tokens missing (xml='$XE' json='$JE'); a budget that shapes a bundle must price it" fi - { [ -n "$XE" ] && { [ "$XE" -le "$BTB" ] || [ "$XL" -eq 1 ]; }; } 2>/dev/null \ - && ok "R1: budget=$BTB xml — est_tokens=$XE is inside the ceiling or labelled (over_ceiling=$XL)" \ - || no "R1: budget=$BTB xml — est_tokens=$XE exceeds the stated ceiling with NO over_ceiling" - { [ -n "$JE" ] && { [ "$JE" -le "$BTB" ] || [ "$JL" -eq 1 ]; }; } 2>/dev/null \ - && ok "R1: budget=$BTB json — est_tokens=$JE is inside the ceiling or labelled (over_ceiling=$JL)" \ - || no "R1: budget=$BTB json — est_tokens=$JE exceeds the stated ceiling with NO over_ceiling — the XML twin's unit did not reach this dialect" + # forbudget-soft (owner decisions 2026-10-14; rule 4): budget_tokens= on --for is a SOFT target — est_tokens may exceed it + # by contract and NEITHER dialect labels it over_ceiling (one rule, both dialects: the parity this arm exists for). The + # cut the target makes is continued in both: / "sigs_next". + XN=0; JN=0 + case "$BX" in *'/dev/null \ + && ok "R1: budget=$BTB xml — soft target: no over_ceiling (est_tokens=$XE), the cut continued ()" \ + || no "R1: budget=$BTB xml — over_ceiling=$XL on a soft target, or the cut carries no next= (est_tokens=$XE next=$XN)" + { [ -n "$JE" ] && [ "$JL" -eq 0 ] && [ "$JN" -eq 1 ]; } 2>/dev/null \ + && ok "R1: budget=$BTB json — soft target: no over_ceiling (est_tokens=$JE), the cut continued (\"sigs_next\")" \ + || no "R1: budget=$BTB json — over_ceiling=$JL on a soft target, or no \"sigs_next\" (est_tokens=$JE) — the twins disagree" done echo diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index 53666b4c2..f2c6c49e5 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -735,7 +735,17 @@ PY if [ -s "$TMP/noteerr" ]; then no "(E) the dropped-note sweep itself failed: $( head -c 300 "$TMP/noteerr" )"; fi cat "$TMP/noteout" -if [ ! -s "$TMP/notegaps" ] && ! grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then +# forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget on --for is a SOFT target, so no ceiling +# ladder runs and rung zero's note can no longer ride: at the tight probe budget the legend must be WHOLE in every dialect +# (HL) — the note ABSENT, which is exactly what the sweep reports as "ABSENT". Every first-screen attribute of that answer is +# still defined by its own legend (arm (A) runs the same roster). A note that rides again is the regression this now catches. +ABSENT_ONLY=0 +if [ -s "$TMP/notegaps" ] && ! grep -qv 'the dropped-legend note is ABSENT at --token-budget=' "$TMP/notegaps"; then ABSENT_ONLY=1; fi +if [ "$ABSENT_ONLY" = 1 ] && [ "$( wc -l < "$TMP/notegaps" | tr -d ' ' )" -ge 3 ]; then + ok "(E) no dropped-legend note in any dialect at the tight probe budget — a soft target never sheds a legend clause (HL; full, compact, default)" +elif [ ! -s "$TMP/notegaps" ] && grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then + no "(E) a dropped-legend note rides a soft --for target again — the header was shed (HL)" +elif [ ! -s "$TMP/notegaps" ] && ! grep -q 'names_checked=[1-9]' "$TMP/noteout" 2>/dev/null; then no "(E) the dropped-note sweep checked ZERO attribute names — a sweep with an empty population passes for the wrong reason" elif [ -s "$TMP/notegaps" ]; then no "(E) $( wc -l < "$TMP/notegaps" | tr -d ' ' ) dropped-legend note problem(s) — the note names a definition its own dialect does not have, or the probe has drifted off the rung:" diff --git a/test/mcpforparitycheck.sh b/test/mcpforparitycheck.sh index f5e1d6847..3818d05c3 100755 --- a/test/mcpforparitycheck.sh +++ b/test/mcpforparitycheck.sh @@ -173,6 +173,9 @@ cmp -s "$TMP/ck5.xml" "$TMP/ck400.xml" \ || no "(4) CLI --for moved with --top-k — the reference this parity is measured against is not what it claims" # ── (5) budget_tokens on this twin is never a SILENT overshoot — the CLI rule, stated for the dialect ───── +# forbudget-soft (2026-10-14, rule 4): budget_tokens is now a SOFT target over the rows on both the CLI and this twin — +# est_tokens may exceed it and over_ceiling= never fires on it. The "est_tokens <= budget OR over_ceiling" clause described +# below is replaced by NOLBL + ST + NX (see the arm); the echo and served-or-declared price clauses are unchanged. # THE FAMILY RULE (METHODOLOGY §9 #2 + #4, and the CLI half of it: fornotesbudgetcheck arm 6). A caller who # states a ceiling gets one of exactly two honest answers, never a third: # • the price is SERVED — est_tokens= on the root — and then est_tokens <= budget_tokens, or the root @@ -191,6 +194,36 @@ cmp -s "$TMP/ck5.xml" "$TMP/ck400.xml" \ # removing the budget_tokens= splice (mcpverbs.h, the H9 insert) fails the echo clause on every rung. mcp_for_budget(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s","budget_tokens":%s}}}\n' \ "$CORPUS" "$1" "$2" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +# forbudget-soft (rule 4): the soft target's bytes per token (read from source: a missing constant FAILS every rung) and the +# uncut reference (a wide target serves the MCP rows uncut) the ST maximality reads the next row's bytes from. +TBPT="$( python3 -c "import re,sys; s=open(sys.argv[1]).read(); g=lambda k:float(re.search(r'inline constexpr double '+k+r' *= *([0-9.]+);',s).group(1)); print(g('kMinBytesPerToken')*g('kBudgetHeadroom'))" src/serialize.h 2>/dev/null )" +mcp_for_budget "$INERT_Q" 100000 >"$TMP/mcp_ref.xml" +cat > "$TMP/mcpst.py" <<'PY' +# mcpst.py REF N BYTES_PER_TOKEN < answer: "OK|BAD ST ..." — the rows' charge (bytes inside plus the zero cx=/ccx=/in= +# the lean spelling elides: the ladder's entryCost) fits T or is the 1..4 floor; a cut is maximal and carries next_budget_tokens > N +import re, sys +def rows( s ): + m = re.search( r']*)>(.*?)', s, re.S ) + if not m: return None + out = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', m.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ); r = re.search( r'\br="(\d+)"', a ); pr = len( d.group( 0 ).encode() ) + c = pr + sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + elided += c - pr; out.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + out.sort() + return m.group( 1 ), [ c for _, c in out ], len( m.group( 2 ).encode() ) + elided +try: n, bpt = int( sys.argv[ 2 ] ), float( sys.argv[ 3 ] ) +except ValueError: print( "BAD premise: no numeric budget / bytes-per-token constant" ); sys.exit( 0 ) +a, ref = rows( sys.stdin.read() ), rows( open( sys.argv[ 1 ] ).read() ) +if a is None or ref is None or not ref[ 1 ]: print( "BAD premise: no rows (answer or uncut reference)" ); sys.exit( 0 ) +T = int( n * bpt ); attrs, ar, charged = a; k = len( ar ); bad = [] +if not ( charged <= T or k <= 4 ): bad.append( "rows charged %d B > T=%d with %d rows (floor 4)" % ( charged, T, k ) ) +if k < len( ref[ 1 ] ): + if charged + ref[ 1 ][ k ] <= T - 160: bad.append( "cut at %d with the next row (%d B) fitting T" % ( k, ref[ 1 ][ k ] ) ) + nb = re.search( r'\bnext_budget_tokens="([0-9]+)"', attrs ) + if not ( 'capped="1"' in attrs and nb and int( nb.group( 1 ) ) > n ): bad.append( "NX: a cut without capped + next_budget_tokens > %d" % n ) +print( "%s ST rows=%d/%d charged=%d T=%d%s" % ( "BAD" if bad else "OK", k, len( ref[ 1 ] ), charged, T, ( ": " + "; ".join( bad ) ) if bad else "" ) ) +PY for tb in 900 1200 1600 2000 6000; do b="$( mcp_for_budget "$INERT_Q" "$tb" )" root="$( printf '%s' "$b" | sed -n 's/^\(<[^>]*>\).*/\1/p' | head -1 )" @@ -204,11 +237,17 @@ for tb in 900 1200 1600 2000 6000; do esac est="$( printf '%s' "$root" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" if [ -n "$est" ]; then - case "$root" in *'over_ceiling="1"'*) lab=1 ;; *) lab=0 ;; esac - if [ "$est" -le "$tb" ] || [ "$lab" -eq 1 ]; then - ok "(5) budget_tokens=$tb: price SERVED and honest (est_tokens=$est, over_ceiling=$lab)" + # forbudget-soft (rule 4): MCP `for` budget_tokens is a SOFT target over the rows, so "est_tokens <= budget OR + # over_ceiling" is replaced by NOLBL (never over_ceiling= on budget_tokens; est_tokens may exceed it) + ST (the rows' + # charged bytes fit T = N x kMinBytesPerToken x kBudgetHeadroom, or are the rank 1..4 floor; a cut is maximal against + # the uncut wide-target answer) + NX (a cut carries next_budget_tokens= above N). RED on base (d4325a8c): over_ceiling="1" + # at 900/2000 and a hard cut far short of T (900: 4 rows). + st="$( printf '%s' "$b" | python3 "$TMP/mcpst.py" "$TMP/mcp_ref.xml" "$tb" "$TBPT" )" + case "$root" in *'over_ceiling'*) lab=1 ;; *) lab=0 ;; esac + if [ "$lab" -eq 0 ] && [ "${st%% *}" = OK ]; then + ok "(5) budget_tokens=$tb: price SERVED (est_tokens=$est), no over_ceiling on the soft target, $st" else - no "(5) budget_tokens=$tb: est_tokens=$est exceeds the stated ceiling by $(( est - tb )) with NO over_ceiling — a silent overshoot on the MCP twin" + no "(5) budget_tokens=$tb: est_tokens=$est over_ceiling=$lab (NOLBL: must be 0) — $st" fi else case "$root" in diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 6ca1ae7db..a2fe84a4b 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -379,7 +379,11 @@ missing = [ident(r) for r in base if ident(r) not in hd or not same(calls(r), ca gained = [r['n'] for r in base if ident(r) in hd and len(calls(hd[ident(r)])) > len(calls(r))] moved = [r['n'] for r in off['rows'] if ident(r) in owners] order_ok = [ident(r) for r in head if ident(r) in {ident(b) for b in base}] == [ident(r) for r in base if ident(r) in hd] -premise = bool(gained) and bool(moved) if mode == 'prefix' else True +# forbudget-soft (decision D-3; rule 4): the DEFAULT is no longer cut at its 1,000 B allowance, so the A/B handle's rows +# are already complete and a moved owner can free nothing — "gained" is then impossible by construction. The premise keeps +# "an owner moved" and accepts either a gain or an A/B listing with no cut left to widen. +base_complete = all((r.get('ca') or {}).get('capped') != '1' for r in base) +premise = bool(moved) and (bool(gained) or base_complete) if mode == 'prefix' else True print('OK' if not missing and order_ok and off['rows'] and premise else 'BAD missing=%s order_ok=%s base_rows=%d gained=%s moved=%s' % (missing[:3], order_ok, len(off['rows']), gained, moved)) PY diff --git a/test/tracecheck.sh b/test/tracecheck.sh index d168a785e..161fbfc0d 100755 --- a/test/tracecheck.sh +++ b/test/tracecheck.sh @@ -18,6 +18,8 @@ # CA4 §B3 adds the budget half: --from-trace is the third member of the family cli.h enumerates by name # ("--for / --pack-task / --from-trace"), and it was the one that stated a budget and never labelled an # overrun. Arms (T1)-(T5) below pin the ledger, all three ceiling rungs, and the root attrs. +# forbudget-soft (rule 4): an explicit --token-budget on --from-trace is now a SOFT target — no ladder (the src echo always +# ships), no over_ceiling against budget_tokens; (T3) asserts that, and its twin keeps --max-tokens' hard over_ceiling. # # TWO GATE DEFECTS FIXED HERE (CA4 §B15's lens — "what would make this gate pass without the property # holding?"), both of which this file had: @@ -522,18 +524,31 @@ for tb in 50 500 2000; do || no "(T2) [default] --token-budget=$tb allowance='$got', expected $want in a kept comment — the default dropped the ceiling it applied" done -# (T3) an overrun is LABELLED. A budget this small cannot fit the first whole signature, so the honest -# answer is the complete bundle + over_ceiling — never a silent 15x overshoot, never a mutilated bundle. +# (T3) forbudget-soft (rule 4): an explicit --from-trace --token-budget is a SOFT target. It used to be a hard ceiling whose +# overrun was LABELLED (over_ceiling, the header ladder ready to drop the echo); now the answer is the complete bundle +# with NO over_ceiling (est_tokens may exceed N), the trace source echo is never dropped, and nothing is mutilated: +# every suspect row ships (shown == total) and the innermost in-corpus body is served. RED on base (d4325a8c): base +# labels these overruns over_ceiling="1". The premise (the bundle really is past the old allowance) stays numeric. for tb in 50 100 500; do out="$( tb_run "$tb" )"; n=$( printf '%s' "$out" | wc -c | tr -d ' ' ); want="$( allowance_of "$tb" )" - if [ "$n" -gt "$want" ]; then - printf '%s' "$out" | grep -qE 'over_ceiling(="1"|:)' \ - && ok "(T3) --token-budget=$tb: ${n}B over a ${want}B allowance AND says over_ceiling" \ - || no "(T3) --token-budget=$tb: ${n}B over a ${want}B allowance with NO over_ceiling label" - else - no "(T3) --token-budget=$tb did not overshoot ($n <= $want) — this arm no longer measures anything" - fi + case "$n$want" in ''|*[!0-9]*) no "(T3) --token-budget=$tb: no byte count / allowance to compare (n='$n' allowance='$want')"; continue ;; esac + if [ "$n" -le "$want" ]; then no "(T3) --token-budget=$tb did not overshoot ($n <= $want) — this arm no longer measures anything"; continue; fi + t3bad="" + printf '%s' "$out" | grep -qE 'over_ceiling(="1"|:)' && t3bad="$t3bad; over_ceiling on a soft target (NOLBL)" + printf '%s' "$out" | grep -q '^]*task="traces/py.txt"' || t3bad="$t3bad; root task= echo missing (HL)" + printf '%s' "$out" | grep -q '/dev/null )" +printf '%s' "$T3M" | grep -qE '^]*max_tokens="50"[^>]* over_ceiling="1"' \ + && ok "(T3 twin) --max-tokens=50 overrun carries over_ceiling=\"1\" (the hard ceiling twin)" \ + || no "(T3 twin) --max-tokens=50 overrun lacks max_tokens=50 + over_ceiling=\"1\" on the root" # (T4) the label is not unconditional: a budget the bundle FITS under must stay silent (a lens that always # cries over_ceiling is as useless as one that never does). 20000 tokens is ~54 KB of allowance. diff --git a/test/w3fixbudgetcheck.sh b/test/w3fixbudgetcheck.sh index 05d11b504..9ccac8cbe 100755 --- a/test/w3fixbudgetcheck.sh +++ b/test/w3fixbudgetcheck.sh @@ -18,7 +18,8 @@ # # H2 — --for's header floor (fixed legend + the task echoed twice) can exceed the ceiling --help promises it # "trims to fit", and it shipped the overrun silently: 5.3x at a 900-char task. An absolute-ceiling arm, -# which is what monotonicity could never catch — a floor is monotone. +# which is what monotonicity could never catch — a floor is monotone. (forbudget-soft: the explicit budget is +# a soft target now, so the H2 arms assert the soft contract instead — see below.) # M1 — --pack-task charged the verbatim task= attribute and not the comment's echo of the same text, so a # fixed 1024-byte reserve was bounding user-length bytes; and its "route_attr: dropped (ceiling)" note # claimed a remedy that had not been measured. Same class at --recall --max-tokens, where the byte @@ -29,16 +30,19 @@ # M3 — the comment echo was dash-scrubbed and nothing else, so a C0 byte or an invalid UTF-8 sequence in the # task made xmllint reject the whole document. # -# THE ONE INVARIANT the ceiling arms assert (and the reason they can be absolute at all): a bundle is -# conformant at or under ceiling x 1.15 — the single-entry overshoot the design allows, because the ranking -# section emits its first entry WHOLE (src/serialize.h kCeilingFirstEntryTolerance; the same 1.15 -# bundleidcheck.sh and partitioncheck.sh already commit to). Past that bar the lens has provably failed to trim -# to fit, and must SAY SO. So the assertion is a biconditional, not a bound: -# -# delivered > ceiling * 1.15 <=> the document discloses over_ceiling -# -# A one-sided bound would be satisfiable by a lens that never trims and never admits it (the pre-fix state) or -# by one that labels everything over_ceiling (honest but useless). Both directions are checked. +# forbudget-soft (rule 4, owner decisions 2026-10-14) RETIRED the absolute-ceiling invariant these arms were built on. +# An explicit --token-budget on --for / --pack-task is now a SOFT target: T = N x kMinBytesPerToken x kBudgetHeadroom bytes +# charged against the ranked ROWS only (--for), or each section stopping once its share is reached (--pack-task). There is +# no byte promise, so nothing is labelled over_ceiling and nothing in the header — the verbatim task echo, route=, any +# legend clause — is ever dropped "(ceiling)" to fit. So the H2 arms no longer assert "fits ceiling x 1.15 <=> discloses +# over_ceiling"; at every (task length x budget x lens) point they assert what must hold now: +# ST (--for) the rows' charged bytes inside fit T, or are only the rank 1..4 floor; +# RP the served rows are a rank prefix of the default answer's rows, growing with the budget; +# NX (--for) every cut element (capped="1") carries next=; +# NOLBL no over_ceiling= on the root and no over_ceiling ladder note; +# HL no "(ceiling)" drop and no "task_echo: dropped" in the header; task= verbatim; route= kept when the default has it. +# The user-text findings (M2/M3 hostile bytes, the M1 charge, the --recall disclosure, determinism) are unchanged: +# --recall --max-tokens keeps its hard ceiling and its biconditional (arm 5). set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" @@ -132,17 +136,13 @@ PY_EOF HOSTILE="nl tab cr c0 badu8 mixed" # The disclosure lives in the header COMMENT. Grepping the whole document for it would also match a body that -# quotes the word — this gate's own corpus does, deliberately, so that mistake cannot pass unnoticed. Every -# ceiling arm below reads the header region ONLY: everything up to the first "-->". +# quotes the word — this gate's own corpus does, deliberately, so that mistake cannot pass unnoticed. The charge +# arm (4) reads the header region ONLY: everything up to the first "-->" (arms 3 and 7 read every header comment). hdr(){ python3 -c " import sys d = open( sys.argv[1], 'rb' ).read() i = d.find( b'-->' ) sys.stdout.buffer.write( d if i < 0 else d[ :i + 3 ] )" "$1"; } -# M11 (2026-09-04): the legends now DEFINE over_ceiling= ("over_ceiling=1 when …"), so a bare word-grep reads -# the definition as the label. The label is the root attribute over_ceiling="1" or the ladder's own colon -# note (" over_ceiling: the header floor …"); the map spells over_ceiling=1 in its stats comment. -saysOverCeiling(){ hdr "$1" | grep -qE 'over_ceiling(="1"|=1|:)'; } # ═══ 1) M2 + M3 — G4 AND xmllint under hostile task bytes, every verb that echoes user text ═══════════════════ # RED (build_base): xmllint FAILS on {--for,--pack-task} x {c0,badu8}; the newline arms emit a literal \n @@ -223,73 +223,145 @@ if key != 'anchor--the\n\x01budget\tceiling�': sys.exit( 0 ) PY_EOF -# ═══ 3) H2 + M1 — THE ABSOLUTE CEILING BICONDITIONAL, across task length x budget ════════════════════════════ -# tokenbudgetcheck asserts MONOTONICITY, which a floor satisfies trivially (5.3x over at every budget is -# perfectly monotone). This is the absolute arm: for every (task length, budget) point, the delivered document -# either fits ceiling*1.15 or says over_ceiling — never neither, never both. -# -# RE-PIN 2026-09-05 (capture-audit verify-wave2 F2, lane V2) — to the NEW contract stated precisely, not to a -# loosened assertion. over_ceiling= used to be a statement about BYTES vs the 1.15-tolerance allowance, which -# is why the reverse half could be written in bytes. It is now a statement about the two numbers the ROOT -# ITSELF PRINTS, in the unit it prints them in: over_ceiling="1" exactly when est_tokens > budget_tokens, on -# every rung (F2: `--for --token-budget=1600` shipped `budget_tokens="1600" est_tokens="1665"` and no label, -# because the flag fired only on the ladder's last rung). The two halves are therefore asymmetric on purpose: -# FORWARD (unchanged, still in bytes): delivered > allowance => over_ceiling. Sound, because -# allowance = N x 2.36 x 1.15 and the emitter prices at 2.50 B/tok, so bytes past the allowance -# are >= 1.085 x N tokens — over the budget by construction. -# REVERSE (re-pinned, now in tokens): over_ceiling => est_tokens > budget_tokens. The old byte form -# would now fire on every HONEST label inside the 15% tolerance band, which is precisely the -# window F2 lived in. -# A label with no overshoot behind it still fails, so the arm cannot be satisfied by labelling everything. -CEILING_ARM_POINTS=0 -CEILING_ARM_BAD=0 +# ═══ 3) H2 — THE SOFT TARGET, across task length x budget ════════════════════════════════════════════════════ +# forbudget-soft (rule 4) — REPLACES the absolute ceiling biconditional (delivered > B x 2.36 x 1.15 <=> over_ceiling, and +# over_ceiling <=> est_tokens > budget_tokens; re-pinned 2026-09-05 for F2). An explicit --token-budget is a soft target +# now, so a bundle past B x 2.36 x 1.15 is legitimate (the header always ships; the floor rows always ship) and a label is +# never legitimate. Kept from the old arm: the 24-point grid (tasks mid/long x 300..3000 x both lenses) and the premise +# that the root prints a numeric est_tokens=. Per point: ST + RP + NX (--for), RP (--pack-task), NOLBL + HL (both). +srcnum(){ sed -n "s/^inline constexpr [a-z:_ ]*$1 *= *\([0-9.]*\);.*/\1/p" "$ROOT/src/serialize.h" | head -1; } +MINBPT="$( srcnum kMinBytesPerToken )"; HEADROOM="$( srcnum kBudgetHeadroom )" +case "$MINBPT$HEADROOM" in ''|*[!0-9.]*) no "H2: cannot read kMinBytesPerToken/kBudgetHeadroom from src/serialize.h ('$MINBPT' '$HEADROOM') — the ST arm has no target to judge" ;; esac +SOFT="$TMP/soft.py" +cat > "$SOFT" <<'PY_EOF' +# soft.py DOC DEFAULT_DOC TASKFILE LENS T — prints "k=", one "row p:l:n" per served row in rank order, then +# one line per violation; exit 1 on any +import re, sys, xml.etree.ElementTree as ET +doc, ref, taskFile, lens, T = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], int( float( sys.argv[5] ) ) +raw = open( doc, 'rb' ).read() +bad = [] +if not raw: + print( 'k=0' ); print( ' no document' ); sys.exit( 1 ) +s = raw.decode( 'utf-8', 'replace' ) +comments = ' '.join( re.findall( r'', s, re.S ) ) +def strip( t ): + t = re.sub( r'', '', t, flags = re.S ) + return re.sub( r'', '', t, flags = re.S ) # the legend quotes and defines attributes; it is not the document's tags +body = strip( s ) +root = re.search( r']*>', body ) +if root is None: + print( 'k=0' ); print( ' no root' ); sys.exit( 1 ) +root = root.group( 0 ) +# a row's CHARGE is the ladder's (serialize.h entryCost): its printed bytes plus the zero cx=/ccx=/in= the lean spelling elides +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rowsOf( t ): + m = re.search( r']*)>(.*?)', t, re.S ) + if not m: return None, None, 0 + rows, total = [], 0 + for d in re.finditer( r']*)>.*?|]*)/>', m.group( 2 ), re.S ): + a = d.group( 1 ) or d.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, '?' ] )[ 1 ] + r = g( 'r' ) + c = charge( a, len( d.group( 0 ).encode() ) ) + total += c - len( d.group( 0 ).encode() ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( 'p' ) + ':' + g( 'l' ) + ':' + g( 'n' ) ) ) + rows.sort() + return m.group( 1 ), rows, len( m.group( 2 ).encode() ) + total +sigAttrs, rows, charged = rowsOf( body ) +refAttrs, refRows, _ = rowsOf( strip( open( ref, 'rb' ).read().decode( 'utf-8', 'replace' ) ) ) +k = len( rows ) if rows is not None else 0 +print( 'k=%d' % k ) +for _, x in ( rows or [] ): print( 'row ' + x ) +# est_tokens present and numeric (CHECKLIST 14: a missing premise is a FAIL) +if not re.search( r' est_tokens="\d+"', root ): bad.append( 'the root carries no numeric est_tokens=' ) +# NOLBL +if ' over_ceiling="1"' in root: bad.append( 'over_ceiling="1" on a soft target (est_tokens may exceed budget_tokens; no label)' ) +if re.search( r'over_ceiling:', comments ): bad.append( 'an over_ceiling ladder note rides the header' ) +# HL +if '(ceiling)' in comments: + dropped = sorted( set( re.findall( r'([^\[\]]{0,40}) dropped \(ceiling\)', comments ) ) ) + bad.append( 'a header piece was dropped "(ceiling)": %s' % ( ', '.join( dropped ) or 'see header' ) ) +if 'task_echo: dropped' in comments: bad.append( 'the task echo was dropped' ) +want = open( taskFile, 'rb' ).read().decode( 'utf-8' ) +try: + got = ET.fromstring( raw ).attrib.get( 'task' ) +except Exception as exc: + got = None; bad.append( 'the document does not parse as XML: %s' % ( exc, ) ) +if got is not None and got != want: bad.append( 'task= is not the verbatim task (%d chars vs %d)' % ( len( got ), len( want ) ) ) +refRoot = re.search( r']*>', strip( open( ref, 'rb' ).read().decode( 'utf-8', 'replace' ) ) ) +if refRoot and ' route=' in refRoot.group( 0 ) and ' route=' not in root: bad.append( 'route= dropped (the default answer carries it)' ) +# RP +if rows is None or refRows is None: bad.append( 'no block (served %s, default %s) — no rows to judge' % ( rows is not None, refRows is not None ) ) +else: + if k == 0: bad.append( 'no row served — the rank 1..4 floor always ships' ) + if [ r for r, _ in rows ] != list( range( 1, k + 1 ) ): bad.append( 'rows are not the ranks 1..%d: r=%s' % ( k, [ r for r, _ in rows ] ) ) + # the default answer may move its doc rows below its code rows (docs_after_code=, renumbering r=; an explicit budget's + # answer does not, on base as on head), so against a reordered default the rows are judged as a SET there and the rank + # PREFIX is judged across budgets by the caller (rows(N) a prefix of rows(N')) + if refAttrs is not None and 'docs_after_code=' in refAttrs: + extra = set( x for _, x in rows ) - set( x for _, x in refRows ) + if extra: bad.append( 'rows the default answer does not serve: %s' % sorted( extra )[ :3 ] ) + elif [ x for _, x in rows ] != [ x for _, x in refRows[ :k ] ]: bad.append( 'rows are not the default answer\'s first %d rows' % k ) + if k < min( 4, len( refRows ) ): bad.append( 'only %d row(s): the rank 1..4 floor always ships' % k ) +if lens == 'for' and rows is not None: + # ST: the rows' charged bytes fit T, or are only the floor + if not ( charged <= T or k <= 4 ): bad.append( 'ST: %d rows charge %d B past the %d B soft target (beyond the 4-row floor)' % ( k, charged, T ) ) + # NX: every cut section carries next= (the ranked rows, the hop rows, the file tail — the continuations --for promises) + for m in re.finditer( r'<(sigs|hops|tail)\b([^>]*)\bcapped="1"([^>]*)>', body ): + if ' next="' not in m.group( 2 ) + m.group( 3 ): bad.append( 'NX: <%s capped="1"> carries no next=' % m.group( 1 ) ) +for line in bad: print( ' ' + line ) +sys.exit( 1 if bad else 0 ) +PY_EOF +SOFT_POINTS=0; SOFT_BAD=0; SOFT_BINDS=0 for TASKFILE in mid long; do T="$( cat "$TMP/task.$TASKFILE" )" - for B in 300 600 900 1200 1600 3000; do - for V in for pack-task; do + for V in for pack-task; do + "$BIN" "$CORPUS" --no-cache "--$V=$T" >"$TMP/c.def" 2>/dev/null # the default answer: RP's reference rows + PREVK=0; : > "$TMP/rows.prev" + for B in 300 600 900 1200 1600 3000; do "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=$B >"$TMP/c.out" 2>/dev/null - BYTES="$( wc -c < "$TMP/c.out" | tr -d ' ' )" - # the bar, in shell, from the same two constants the code uses (2.36 B/tok x 1.15 tolerance) - ALLOW="$( python3 -c "print(int($B*2.36*1.15))" )" - OVER=0; saysOverCeiling "$TMP/c.out" && OVER=1 - # the root's own price, in the unit budget_tokens= is stated in (comments stripped: the legend - # DEFINES est_tokens= and a definition is not a measurement) - EST="$( perl -0pe 's###gs' "$TMP/c.out" | grep -oE ']*)?>' | head -1 \ - | grep -oE ' est_tokens="[0-9]+"' | head -1 | tr -dc '0-9' )" - CEILING_ARM_POINTS=$(( CEILING_ARM_POINTS + 1 )) - if [ "$BYTES" -gt "$ALLOW" ] && [ "$OVER" = "0" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: $BYTES B > allowance $ALLOW B and SILENT about it" - elif [ -z "$EST" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: the root carries no est_tokens= — the reverse half has no number to check" - elif [ "$OVER" = "1" ] && [ "$EST" -le "$B" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: claims over_ceiling but est_tokens=$EST is inside the $B-token budget" - elif [ "$OVER" = "0" ] && [ "$EST" -gt "$B" ]; then - CEILING_ARM_BAD=$(( CEILING_ARM_BAD + 1 )) - echo " --$V task=$TASKFILE budget=$B: root prints budget_tokens=$B est_tokens=$EST and SILENT about the overshoot" + TB="$( python3 -c "import sys; print(int(float(sys.argv[1])*float(sys.argv[2])*float(sys.argv[3])))" "$B" "${MINBPT:-0}" "${HEADROOM:-0}" )" + SOFT_POINTS=$(( SOFT_POINTS + 1 )) + OUT="$( python3 "$SOFT" "$TMP/c.out" "$TMP/c.def" "$TMP/task.$TASKFILE" "$V" "$TB" )"; RC=$? + K="$( printf '%s\n' "$OUT" | sed -n 's/^k=//p' | head -1 )" + if [ "$RC" != 0 ]; then + SOFT_BAD=$(( SOFT_BAD + 1 )) + echo " --$V task=$TASKFILE budget=$B (T=$TB B):"; printf '%s\n' "$OUT" | grep -v '^k=\|^row ' + elif [ "${K:-0}" -lt "$PREVK" ] || [ "$( printf '%s\n' "$OUT" | sed -n 's/^row //p' | awk -v n="$PREVK" 'NR <= n' )" != "$( cat "$TMP/rows.prev" )" ]; then + SOFT_BAD=$(( SOFT_BAD + 1 )) + echo " --$V task=$TASKFILE budget=$B: the $K row(s) served do not extend the $PREVK served at the budget below — RP is not a growing rank prefix" fi + printf '%s\n' "$OUT" | sed -n 's/^row //p' > "$TMP/rows.prev" + # non-vacuity: a point whose est_tokens is past its budget (so NOLBL was tested on a blown target, not assumed) + EST="$( perl -0pe 's###gs' "$TMP/c.out" | grep -oE ']*)?>' | head -1 | grep -oE ' est_tokens="[0-9]+"' | tr -dc '0-9' )" + [ -n "$EST" ] && [ "$EST" -gt "$B" ] && SOFT_BINDS=$(( SOFT_BINDS + 1 )) + PREVK="${K:-0}" done done done -[ "$CEILING_ARM_BAD" = "0" ] \ - && ok "ceiling biconditional holds at all $CEILING_ARM_POINTS (task length x budget x lens) points" \ - || no "ceiling biconditional VIOLATED at $CEILING_ARM_BAD of $CEILING_ARM_POINTS points" +[ "$SOFT_BAD" = "0" ] \ + && ok "H2 soft target: ST/RP/NX/NOLBL/HL hold at all $SOFT_POINTS (task length x budget x lens) points" \ + || no "H2 soft target: VIOLATED at $SOFT_BAD of $SOFT_POINTS points" +[ "$SOFT_BINDS" -gt 0 ] \ + && ok "H2 soft target: non-vacuity — $SOFT_BINDS point(s) deliver est_tokens past their budget (NOLBL/HL were judged on blown targets)" \ + || no "H2 soft target: VACUOUS — no point delivers past its budget, so no-label/header-kept was never tested on a blown target" -# the RED case named in the finding, asserted on its own so a regression names itself: a 900-char task at a -# tight budget must not be silently 2x+ over. +# the case named in the finding, asserted on its own so a regression names itself. forbudget-soft (rule 4) — REPLACES +# "a 900-char task at --token-budget=600 fits B x 2.36 x 1.15 or discloses over_ceiling": the soft target makes no byte +# promise, so what must hold is that the floor rows are served and the header ships whole (task= verbatim, echo kept, +# route= kept, nothing dropped "(ceiling)"), with no label. T="$( cat "$TMP/task.long" )" for V in for pack-task; do + "$BIN" "$CORPUS" --no-cache "--$V=$T" >"$TMP/c.ldef" 2>/dev/null "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=600 >"$TMP/c.long" 2>/dev/null - BYTES="$( wc -c < "$TMP/c.long" | tr -d ' ' )" - ALLOW="$( python3 -c "print(int(600*2.36*1.15))" )" - if [ "$BYTES" -le "$ALLOW" ]; then - ok "--$V: 900-char task at --token-budget=600 fits the allowance ($BYTES <= $ALLOW B)" - elif saysOverCeiling "$TMP/c.long"; then - ok "--$V: 900-char task at --token-budget=600 is $BYTES B over $ALLOW B and DISCLOSES over_ceiling" + TB="$( python3 -c "import sys; print(int(600*float(sys.argv[1])*float(sys.argv[2])))" "${MINBPT:-0}" "${HEADROOM:-0}" )" + OUT="$( python3 "$SOFT" "$TMP/c.long" "$TMP/c.ldef" "$TMP/task.long" "$V" "$TB" )"; RC=$? + K="$( printf '%s\n' "$OUT" | sed -n 's/^k=//p' | head -1 )" + if [ "$RC" = 0 ]; then + ok "--$V: 900-char task at --token-budget=600 serves its $K floor row(s) with the header whole and no over_ceiling ($( wc -c < "$TMP/c.long" | tr -d ' ' ) B; no byte promise)" else - no "--$V: 900-char task at --token-budget=600 delivers $BYTES B past the $ALLOW B allowance in SILENCE" + no "--$V: 900-char task at --token-budget=600:"; printf '%s\n' "$OUT" | grep -v '^k=\|^row ' fi done @@ -376,39 +448,44 @@ done && ok "det-gate: byte-identical run-to-run across every hostile byte x ceiling rung" \ || no "det-gate: $DET_BAD argument set(s) are NOT byte-identical run-to-run" -# ═══ 7) the ladder's rungs are REACHABLE and correctly ordered ════════════════════════════════════════════════ -# A remedy nothing ever selects is dead code that reads as a feature. Sweep a band of budgets and require that -# the cheap rung (drop the DUPLICATE comment echo, keeping the verbatim task= attribute) is actually taken -# somewhere — and that whenever it is, the verbatim attribute is still present. That ordering is the whole -# point: reclaim the duplicate before dropping unique information. +# ═══ 7) the header is NEVER dropped for a soft target ═══════════════════════════════════════════════════════════ +# forbudget-soft (rule 4) — REPLACES "the ceiling ladder's task_echo rung is REACHABLE" (and folds in its second half, "every +# echo-drop kept the verbatim task="). The header ladder existed to land a HARD ceiling; an explicit --token-budget is a soft +# target now, so the remedy that arm required to be reachable must now be unreachable. HL, across the same band and in the +# full dialect (the only one that echoes the task into the header comment): at every point `task_echo: dropped` is ABSENT, +# no header piece is dropped "(ceiling)", the comment echo is PRESENT, and the verbatim task= attribute is intact. T="$( cat "$TMP/task.long" )" -ECHO_RUNG_SEEN=0 -ECHO_RUNG_BAD=0 +ECHO_POINTS=0 +ECHO_BAD=0 for B in 900 1100 1200 1400 1600 1800 2000; do for V in for pack-task; do - # L1: the task_echo rung drops the header COMMENT's echo of the task, which only the full legend prints (the compact - # dialect never echoes it), so the rung is observable — and this arm meaningful — in the full dialect. "$BIN" "$CORPUS" --no-cache "--$V=$T" --token-budget=$B --legend=full >"$TMP/l.out" 2>/dev/null - hdr "$TMP/l.out" | grep -q 'task_echo: dropped' || continue - ECHO_RUNG_SEEN=$(( ECHO_RUNG_SEEN + 1 )) - # the verbatim copy must survive the drop of its scrubbed twin - python3 - "$TMP/l.out" "$TMP/task.long" <<'PY_EOF' || { -import sys, xml.etree.ElementTree as ET + ECHO_POINTS=$(( ECHO_POINTS + 1 )) + python3 - "$TMP/l.out" "$TMP/task.long" <<'PY_EOF' || { ECHO_BAD=$(( ECHO_BAD + 1 )); echo " ^ --$V budget=$B"; } +import re, sys, xml.etree.ElementTree as ET +raw = open( sys.argv[1], 'rb' ).read() want = open( sys.argv[2], 'rb' ).read().decode( 'utf-8' ) -got = ET.parse( sys.argv[1] ).getroot().attrib.get( 'task' ) -sys.exit( 0 if got == want else 1 ) +if not raw: + print( ' no document' ); sys.exit( 1 ) +s = raw.decode( 'utf-8', 'replace' ) +com = re.findall( r'', s, re.S ) +bad = [] +if any( 'task_echo: dropped' in c for c in com ): bad.append( 'task_echo: dropped — the comment echo was dropped for a soft target' ) +if any( '(ceiling)' in c for c in com ): bad.append( 'a header piece was dropped "(ceiling)"' ) +if not any( want[ :60 ] in c for c in com ): bad.append( 'the header comment does not echo the task' ) +try: + got = ET.fromstring( raw ).attrib.get( 'task' ) +except Exception as exc: + got = None; bad.append( 'the document does not parse as XML: %s' % ( exc, ) ) +if got is not None and got != want: bad.append( 'the verbatim task= is gone or altered' ) +for line in bad: print( ' ' + line ) +sys.exit( 1 if bad else 0 ) PY_EOF - ECHO_RUNG_BAD=$(( ECHO_RUNG_BAD + 1 )) - echo " --$V budget=$B: dropped the comment echo but the verbatim task= is gone or altered" - } done done -[ "$ECHO_RUNG_SEEN" -gt 0 ] \ - && ok "ceiling ladder: the task_echo rung is REACHABLE ($ECHO_RUNG_SEEN point(s) in the 900..2000 band)" \ - || no "ceiling ladder: the task_echo rung was never selected at any budget — an unreachable remedy" -[ "$ECHO_RUNG_BAD" = "0" ] \ - && ok "ceiling ladder: every echo-drop kept the VERBATIM task= attribute intact" \ - || no "ceiling ladder: $ECHO_RUNG_BAD echo-drop(s) also lost the verbatim task= — wrong bytes dropped" +[ "$ECHO_POINTS" -gt 0 ] && [ "$ECHO_BAD" = "0" ] \ + && ok "header kept: at all $ECHO_POINTS points of the 900..2000 band (full dialect) the task echo, every header piece and the verbatim task= ship" \ + || no "header kept: $ECHO_BAD of $ECHO_POINTS point(s) dropped part of the header for a soft target" # ═══ 8) the MCP SIBLING — the same hostile bytes through the live server ══════════════════════════════════════ # M3's scrub had to be applied at SIX echo sites, two of which are MCP-only (mcpverbs.h `for` and `exemplar`). From 64b8de2075a5a62d81ca989e73342e74becb2c74 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 14:56:36 -0400 Subject: [PATCH 033/162] refactor(grep): carrier emitters keep the grep entry points' shape; docs regenerated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitGrepReport takes the dispatch context whole instead of eight loose parameters, and the MCP enclosing block takes the page (rows, carrier verdicts, root spelling) as one struct with the carrier keys in their own helper — so neither entry point grows the parameter count or complexity the carrier round added. The gate arms report verdicts through if/else (gateexitcheck G2). docs/TUNING.md and the README cap count (254) regenerated for the new cap. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/TUNING.md | 16 ++++---- src/mcpverbs.h | 81 ++++++++++++++++++++++++++--------------- src/verbs_grep.h | 18 ++++++--- test/countfloorcheck.sh | 39 ++++++++------------ 5 files changed, 88 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 2432bb90f..78674e63d 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 253 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 254 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/TUNING.md b/docs/TUNING.md index 8836074e1..e328dec7f 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 161 | 160 | 112 | 12 | **37** | 75 | +| 162 | 161 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **161 cap -declarations** under **160 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **162 cap +declarations** under **161 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 160 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **37 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 161 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **38 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, `kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+31 more). 111 + 12 + 37 = 160, and `emit` refuses to render a partition that does not add up. The ++32 more). 111 + 12 + 38 = 161, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 161" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 162" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 161-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 162-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 49c831a5c..c3f510f52 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -888,11 +888,56 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // under --handles mints (search.h grepEncHandleCandidate decides WHICH row may carry one; handleFor mints it from the index's // own byte hash) — or `handle_omitted` with the CLI legend's reason. Before this a grep answer named enclosing symbols that // fetch_body could not take, so the agent re-read every file it had just searched. -inline std::string grepEnclosingJson( const McpIndex& ix, std::span encRows, std::span carriers, - bool singleRoot, std::string_view rootPrefix ) +// L4: the page's rows and their carrier verdict (grepcarrier.h), computed once by the caller (its next= reads them +// too), plus the root spelling the carrier's paths take. +struct GrepEncPageJson { - const IngestResult& ing = ix.ing; - const Graph& g = ix.g; + std::span rows; + std::span carriers; // parallel to rows + bool singleRoot = false; + std::string_view rootPrefix; +}; + +// The L4 carrier keys of one enclosing row — the CLI reads=/reads_at=/vals=/vals_total= twins, from the same +// grepCarrierText. "" on a row that is no carrier or carries neither. +inline std::string grepCarrierJson( const GrepCarrier& c, bool singleRoot, std::string_view rootPrefix ) +{ + if( !c.isCarrier ) + { + return {}; + } + const GrepCarrierText t = grepCarrierText( c, singleRoot, rootPrefix ); + const auto jsonList = []( const std::vector& items ) + { + std::string list = "["; + for( std::size_t k = 0; k < items.size(); ++k ) + { + list += ( k > 0 ? ",\"" : "\"" ) + mcpdetail::jsonEscape( items[k] ) + "\""; + } + return list + "]"; + }; + std::string out; + if( !t.readsAt.empty() ) + { + out += ",\"reads\":" + std::to_string( c.reads ) + ",\"reads_at\":" + jsonList( t.readsAt ); + } + if( !t.vals.empty() ) + { + out += ",\"vals\":" + jsonList( t.vals ); + if( c.valsTotal > c.vals.size() ) + { + out += ",\"vals_total\":" + std::to_string( c.valsTotal ); + } + } + return out; +} + +inline std::string grepEnclosingJson( const McpIndex& ix, const GrepEncPageJson& page ) +{ + const IngestResult& ing = ix.ing; + const Graph& g = ix.g; + const std::span encRows = page.rows; + const std::span carriers = page.carriers; if( encRows.empty() ) { return {}; @@ -928,31 +973,7 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span& items ) - { - std::string list = "["; - for( std::size_t k = 0; k < items.size(); ++k ) - { - list += ( k > 0 ? ",\"" : "\"" ) + mcpdetail::jsonEscape( items[k] ) + "\""; - } - return list + "]"; - }; - if( !t.readsAt.empty() ) - { - out += ",\"reads\":" + std::to_string( c.reads ) + ",\"reads_at\":" + jsonList( t.readsAt ); - } - if( !t.vals.empty() ) - { - out += ",\"vals\":" + jsonList( t.vals ); - if( c.valsTotal > c.vals.size() ) - { - out += ",\"vals_total\":" + std::to_string( c.valsTotal ); - } - } - } + out += grepCarrierJson( carriers[r], page.singleRoot, page.rootPrefix ); const char* omitted = nullptr; const NodeId id = grepEncHandleCandidate( ing, row, omitted ); if( id == kNoNode ) @@ -1313,7 +1334,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat singleRootJ, rootPrefixJ ); // R1 (the 2026-08-12 usage mine): the CLI / twins, appended AFTER "hits" so the // historic key order three other gates read (files,total,shown,capped) is byte-untouched. - out += grepEnclosingJson( ix, encRowsJ, carriersJ, singleRootJ, rootPrefixJ ); + out += grepEnclosingJson( ix, GrepEncPageJson{ encRowsJ, carriersJ, singleRootJ, rootPrefixJ } ); if( collected.raw.empty() ) { out += grepSuggestJson( ing, pattern ); diff --git a/src/verbs_grep.h b/src/verbs_grep.h index d6916a52c..37ad94120 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -778,11 +778,19 @@ std::vector grepHitRefs( const rw::Config& cfg, const rw::IngestResu return refs; } -int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, - const std::vector* amp, const std::vector* tested, - const GrepScanPhases* prefetched, const std::string& root, bool valueUses ) +// The dispatch context whole: amp/tested are non-null only when a co-run (--metrics) computed them at dispatch build +// time — grep itself never asks for the analysis (R1's no-new-analysis rule); root and valueUses feed L4's reads pass. +int emitGrepReport( const MainDispatch& d ) { using namespace rw; + const Config& cfg = d.cfg; + const IngestResult& ing = d.ing; + const Graph& g = d.g; + const std::vector* amp = d.ampPtr; + const std::vector* tested = d.testedPtr; + const GrepScanPhases* prefetched = d.grepPhases; + const std::string& root = d.root; + const bool valueUses = d.valueUses; const std::string pat( cfg.grep ); // §P0.4: an invalid --regex used to scan nothing and print hits="0" at exit 0 with an EMPTY stderr — @@ -1233,9 +1241,7 @@ std::optional runGrep( const MainDispatch& d ) { return std::nullopt; // not this verb — fall through the dispatch chain } - // body: emitGrepReport() above. amp/tested are non-null only when a co-run (--metrics) computed - // them at dispatch build time — grep itself never asks for the analysis (R1's no-new-analysis rule). - return emitGrepReport( d.cfg, d.ing, d.g, d.ampPtr, d.testedPtr, d.grepPhases, d.root, d.valueUses ); + return emitGrepReport( d ); // body: emitGrepReport() above } } // namespace — verbs_grep.h section of main.cpp diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index fecf10027..fdda894a9 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -431,63 +431,56 @@ printf 'function homeHandler() { return 1; }\nexport const ROUTES = { home: home OUT_P="$( run pc --grep=PC_NAMED_MARK )"; ROW_P="$( enc_row "$OUT_P" NAMED )" [ -n "$ROW_P" ] || no "(P) premise: no row" -[ "$( attr "$ROW_P" reads )" = 3 ] && ok "(P) reads=\"3\" — the uses verb's three read sites of NAMED" || no "(P) reads on NAMED: $ROW_P" -[ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ] \ - && ok "(P) reads_at= lists them in the uses verb's order (source before tests, then path, line)" || no "(P) reads_at on NAMED: $ROW_P" +if [ "$( attr "$ROW_P" reads )" = 3 ]; then ok "(P) reads=\"3\" — the uses verb's three read sites of NAMED"; else no "(P) reads on NAMED: $ROW_P"; fi +if [ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ]; then ok "(P) reads_at= lists them in the uses verb's order (source before tests, then path, line)"; else no "(P) reads_at on NAMED: $ROW_P"; fi USES_P="$( run pc --uses=NAMED | grep -oE ' row" case "$ROW_PU" in *reads=*|*reads_at=*) no "(P) near miss: a carrier read nowhere printed reads: $ROW_PU" ;; *) ok "(P) near miss: a carrier read nowhere carries no reads= (absent, never a zero)" ;; esac -[ "$( attr "$R_PU" next )" = "--at=lib/conf.py:6" ] && ok "(P) near miss: no reads => next= stays the at verb on the top hit" || no "(P) near miss next: $R_PU" +if [ "$( attr "$R_PU" next )" = "--at=lib/conf.py:6" ]; then ok "(P) near miss: no reads => next= stays the at verb on the top hit"; else no "(P) near miss next: $R_PU"; fi ROW_PT="$( enc_row "$( run pc --grep=PC_TABLE_MARK )" TABLE )" -[ "$( attr "$ROW_PT" vals )" = "[get]=handle_get@lib/handlers.py:1 [put]=local_put@3" ] \ - && ok "(P) vals= on a dict carrier: [key]=fn@its definition (path:line in another file)" || no "(P) vals on TABLE: $ROW_PT" +if [ "$( attr "$ROW_PT" vals )" = "[get]=handle_get@lib/handlers.py:1 [put]=local_put@3" ]; then ok "(P) vals= on a dict carrier: [key]=fn@its definition (path:line in another file)"; else no "(P) vals on TABLE: $ROW_PT"; fi ROW_PI="$( enc_row "$( run pc --grep=PC_INNER_MARK )" inner )"; R_PI="$( root_tag "$( run pc --grep=PC_INNER_MARK )" grep )" [ -n "$ROW_PI" ] || no "(P) premise: no row" case "$ROW_PI" in *reads=*|*vals=*) no "(P) negative: a literal in a function body got carrier attributes: $ROW_PI" ;; *) ok "(P) negative: a hit inside a function keeps its row unchanged" ;; esac -[ "$( attr "$R_PI" next )" = "--at=lib/conf.py:11" ] && ok "(P) negative: next= stays the at verb inside a function" || no "(P) negative next: $R_PI" +if [ "$( attr "$R_PI" next )" = "--at=lib/conf.py:11" ]; then ok "(P) negative: next= stays the at verb inside a function"; else no "(P) negative next: $R_PI"; fi ROW_PC="$( enc_row "$( run cc --grep=C_ENT_MARK )" ent )" -[ "$( attr "$ROW_PC" vals )" = "exec=local_exec@3 undo=other_exec@other.c:1" ] \ - && ok "(P) C initialiser: vals=\"exec=local_exec@3 undo=other_exec@other.c:1\"" || no "(P) C initialiser row: $ROW_PC" +if [ "$( attr "$ROW_PC" vals )" = "exec=local_exec@3 undo=other_exec@other.c:1" ]; then ok "(P) C initialiser: vals=\"exec=local_exec@3 undo=other_exec@other.c:1\""; else no "(P) C initialiser row: $ROW_PC"; fi case "$ROW_PC" in *reads=*) no "(P) C reads are not indexed, yet reads= printed: $ROW_PC" ;; *) ok "(P) C: no reads= (the uses verb indexes no C reads; floor_next stays the literal scan)" ;; esac ROW_PCP="$( enc_row "$( run cc --grep=C_PLAIN_MARK )" plain )" [ -n "$ROW_PCP" ] || no "(P) premise: no row" case "$ROW_PCP" in *vals=*|*reads=*) no "(P) near miss: a C string constant got carrier attributes: $ROW_PCP" ;; *) ok "(P) near miss: a C constant holding a string and read in C carries neither attribute" ;; esac ROW_PX="$( enc_row "$( run cc --grep=CPP_NAME_MARK )" kCppName )" -[ "$( attr "$ROW_PX" reads )" = 1 ] && [ "$( attr "$ROW_PX" reads_at )" = "use.cpp:2" ] && ok "(P) C++ (reads indexed): reads=\"1\" reads_at=\"use.cpp:2\"" || no "(P) C++ row: $ROW_PX" +if [ "$( attr "$ROW_PX" reads )" = 1 ] && [ "$( attr "$ROW_PX" reads_at )" = "use.cpp:2" ]; then ok "(P) C++ (reads indexed): reads=\"1\" reads_at=\"use.cpp:2\""; else no "(P) C++ row: $ROW_PX"; fi ROW_PH="$( enc_row "$( run cc --grep=CPP_HDR_MARK )" kHdrName )" [ -n "$ROW_PH" ] || no "(P) premise: no row" case "$ROW_PH" in *reads=*) no "(P) near miss: a header carrier counted reads its .c users hide: $ROW_PH" ;; *) ok "(P) near miss: a .h carrier carries no reads= (its readers may be .c files the uses verb never reads)" ;; esac ROW_PS="$( enc_row "$( run tsc --grep=TS_ROUTES_MARK )" ROUTES )" -[ "$( attr "$ROW_PS" vals )" = "home=homeHandler@1" ] && ok "(P) TS object carrier: vals=\"home=homeHandler@1\"" || no "(P) TS row: $ROW_PS" +if [ "$( attr "$ROW_PS" vals )" = "home=homeHandler@1" ]; then ok "(P) TS object carrier: vals=\"home=homeHandler@1\""; else no "(P) TS row: $ROW_PS"; fi case "$ROW_PS" in *reads=*) no "(P) TS reads are not indexed, yet reads= printed: $ROW_PS" ;; *) ok "(P) TS: no reads= (not indexed for TS)" ;; esac MCP_P="$( mcp_call grep "{\"path\":\"$W/pc\",\"pattern\":\"PC_NAMED_MARK\"}" )" -printf '%s' "$MCP_P" | python3 -c ' +if printf '%s' "$MCP_P" | python3 -c ' import json, sys d = json.loads( sys.stdin.read() ) r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "NAMED", {} ) -sys.exit( 0 if r.get( "reads" ) == 3 and r.get( "reads_at" ) == [ "lib/reader.py:4", "lib/reader.py:7", "test_reader.py:4" ] else 1 )' \ - && ok "(P) MCP grep enclosing NAMED: reads 3 and the same reads_at as the CLI" || no "(P) MCP grep NAMED: $MCP_P" -printf '%s' "$MCP_P" | python3 -c ' +sys.exit( 0 if r.get( "reads" ) == 3 and r.get( "reads_at" ) == [ "lib/reader.py:4", "lib/reader.py:7", "test_reader.py:4" ] else 1 )'; then ok "(P) MCP grep enclosing NAMED: reads 3 and the same reads_at as the CLI"; else no "(P) MCP grep NAMED: $MCP_P"; fi +if printf '%s' "$MCP_P" | python3 -c ' import json, sys -sys.exit( 0 if json.loads( sys.stdin.read() ).get( "next" ) == "--uses=NAMED" else 1 )' \ - && ok "(P) MCP grep next is --uses=NAMED, as the CLI root next=" || no "(P) MCP grep next: $MCP_P" +sys.exit( 0 if json.loads( sys.stdin.read() ).get( "next" ) == "--uses=NAMED" else 1 )'; then ok "(P) MCP grep next is --uses=NAMED, as the CLI root next="; else no "(P) MCP grep next: $MCP_P"; fi MCP_PC="$( mcp_call grep "{\"path\":\"$W/cc\",\"pattern\":\"C_ENT_MARK\"}" )" -printf '%s' "$MCP_PC" | python3 -c ' +if printf '%s' "$MCP_PC" | python3 -c ' import json, sys d = json.loads( sys.stdin.read() ) r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "ent", {} ) -sys.exit( 0 if r.get( "vals" ) == [ "exec=local_exec@3", "undo=other_exec@other.c:1" ] and "reads" not in r else 1 )' \ - && ok "(P) MCP grep enclosing ent: the same vals as the CLI, no reads key" || no "(P) MCP grep ent: $MCP_PC" +sys.exit( 0 if r.get( "vals" ) == [ "exec=local_exec@3", "undo=other_exec@other.c:1" ] and "reads" not in r else 1 )'; then ok "(P) MCP grep enclosing ent: the same vals as the CLI, no reads key"; else no "(P) MCP grep ent: $MCP_PC"; fi # ── N: legend ──────────────────────────────────────────────────────────────────────────────────────────── echo "=== N: every new attribute is defined where it rides ===" From f30c816cb5d8f5c73e468763ba41696a9bb259bc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 15:04:14 -0400 Subject: [PATCH 034/162] refactor(grep): split the carrier verdict into its vals and reads passes grepCarriers was one 56-branch function; the stored-value pass, the read-site collection and the per-row fill are now their own helpers, each one walk over its table for the whole page. targetsOf is read once per value reference. Co-Authored-By: Claude Opus 5.5 --- src/grepcarrier.h | 183 ++++++++++++++++++++++++++-------------------- 1 file changed, 105 insertions(+), 78 deletions(-) diff --git a/src/grepcarrier.h b/src/grepcarrier.h index 777d4106a..72470f029 100644 --- a/src/grepcarrier.h +++ b/src/grepcarrier.h @@ -126,113 +126,106 @@ inline std::vector valueSitesOf( const IngestResult& rd, std::spa } // namespace grepcarrier -// One carrier verdict per row. `ing` is the ingest the rows came from; `vri` its value index (vals=); `reads` the -// ingest whose role="read" references count (nullptr = none captured: reads= stays unknown on every row). The reads -// pass is ONE walk over `reads->references` for every carrier name on the page, never a per-row rescan. -inline std::vector grepCarriers( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, - const IngestResult* reads ) +namespace grepcarrier { - std::vector out( rows.size() ); - std::vector carrierIds; // every carrier definition on the page, for ONE madeIn pass - std::vector readNames; // the names whose reads are counted - for( std::size_t r = 0; r < rows.size(); ++r ) + +// One stored function value as vals= prints it: the slot key without its leading '.', the name as written, and the one +// definition it resolves to (none when it resolves to several). `targets` is vri.targetsOf of that reference. +inline GrepCarrierVal storedVal( const IngestResult& ing, const Reference& ref, std::span targets ) +{ + const std::string_view rel = ref.composeRel; + GrepCarrierVal val{ std::string( rel.starts_with( '.' ) ? rel.substr( 1 ) : rel ), ref.calleeName, {}, 0 }; + if( targets.size() == 1 && targets[0] < ing.symbols.size() ) { - if( !isGrepCarrierRow( ing, rows[r] ) ) + const Symbol& def = ing.symbols[ targets[0] ]; + val.line = def.line; + if( def.fileId != ref.fileId && def.fileId < ing.files.size() ) { - continue; + val.path = ing.files[ def.fileId ]; } - out[r].isCarrier = true; - carrierIds.insert( carrierIds.end(), rows[r].ids.begin(), rows[r].ids.end() ); - const Symbol& s = ing.symbols[ rows[r].ids.front() ]; - if( reads != nullptr && grepCarrierReadsVisible( ing, s ) ) - { - out[r].readsKnown = true; - readNames.push_back( s.name ); - } - } - if( carrierIds.empty() ) - { - return out; } + return val; +} - // vals=: the stored function values, attributed to the row whose definition made them (source order: madeIn is - // ascending reference order, which is file then byte order within one ingest). +// vals=: the stored function values of every carrier on the page, attributed to the row whose definition made them — +// ONE madeIn pass for the page (source order: madeIn is ascending reference order, file then byte within one ingest). +inline void fillVals( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, std::span carrierIds, + std::span out ) +{ for( const std::uint32_t v : vri.madeIn( carrierIds, RefRole::Value ) ) { - const Reference& ref = ing.references[v]; - if( ref.fieldName.starts_with( '@' ) || vri.targetsOf( v ).empty() ) + const Reference& ref = ing.references[v]; + const std::vector targets = vri.targetsOf( v ); + if( ref.fieldName.starts_with( '@' ) || targets.empty() ) { continue; // a decorator row is a fact about a definition; an unresolved name is not a function value } - for( std::size_t r = 0; r < rows.size(); ++r ) + const auto row = std::ranges::find_if( rows, [ & ]( const GrepEncRow& rw ) { return std::ranges::find( rw.ids, ref.fromSymbol ) != rw.ids.end(); } ); + if( row == rows.end() || !out[ std::size_t( row - rows.begin() ) ].isCarrier ) + { + continue; // madeIn was asked for carrier ids only; a row that is no carrier holds none of them + } + GrepCarrier& c = out[ std::size_t( row - rows.begin() ) ]; // a definition sits on one row (grepEnclosingRows groups by chain) + if( ++c.valsTotal <= kGrepCarrierShown ) { - if( !out[r].isCarrier || std::ranges::find( rows[r].ids, ref.fromSymbol ) == rows[r].ids.end() ) - { - continue; - } - ++out[r].valsTotal; - if( out[r].vals.size() < kGrepCarrierShown ) - { - const std::string_view key = std::string_view( ref.composeRel ).starts_with( '.' ) ? std::string_view( ref.composeRel ).substr( 1 ) - : std::string_view( ref.composeRel ); - GrepCarrierVal val{ std::string( key ), ref.calleeName, {}, 0 }; - if( const std::vector targets = vri.targetsOf( v ); targets.size() == 1 && targets[0] < ing.symbols.size() ) - { - const Symbol& def = ing.symbols[ targets[0] ]; - val.line = def.line; - if( def.fileId != ref.fileId && def.fileId < ing.files.size() ) - { - val.path = ing.files[ def.fileId ]; - } - } - out[r].vals.push_back( std::move( val ) ); - } - break; // one row per name (grepEnclosingRows groups by chain): a definition sits on one row + c.vals.push_back( storedVal( ing, ref, targets ) ); } } +} - if( readNames.empty() ) - { - return out; - } - EXPECTS( reads != nullptr, "a name is queued for reads only when a reads ingest was handed in" ); - std::ranges::sort( readNames ); - readNames.erase( std::unique( readNames.begin(), readNames.end() ), readNames.end() ); - const std::vector valueSites = grepcarrier::valueSitesOf( *reads, readNames ); +struct ReadHit +{ + std::uint32_t name; // index into the sorted, unique name list + std::uint32_t fileId; // into the reads ingest + std::uint32_t line; +}; - // ONE pass: the uses verb's read rows (collectUseSites' filters) for every queued name. - struct ReadHit { std::uint32_t name; std::uint32_t fileId; std::uint32_t line; }; - std::vector hits; - for( const Reference& r : reads->references ) +// The uses verb's role="read" rows (collectUseSites' filters) for every name in `names` (sorted, unique), in ONE pass +// over the reads ingest, sorted by name, then the uses verb's base order (tier, path, line). +inline std::vector readSitesOf( const IngestResult& rd, std::span names ) +{ + const std::vector valueSites = valueSitesOf( rd, names ); + std::vector hits; + for( const Reference& r : rd.references ) { if( r.role != RefRole::Read || r.isCompose || r.isDocLink || r.lang == Lang::Markdown ) { continue; } - const auto it = std::ranges::lower_bound( readNames, std::string_view( r.calleeName ) ); - if( it == readNames.end() || *it != r.calleeName ) + const auto it = std::ranges::lower_bound( names, std::string_view( r.calleeName ) ); + if( it == names.end() || *it != r.calleeName || std::ranges::binary_search( valueSites, ( std::uint64_t( r.fileId ) << 32 ) | r.startByte ) ) { - continue; + continue; // another name, or a site the uses verb serves as role="value", not a read } - if( std::ranges::binary_search( valueSites, ( std::uint64_t( r.fileId ) << 32 ) | r.startByte ) ) - { - continue; // the uses verb serves this site as role="value", not a read - } - hits.push_back( { std::uint32_t( it - readNames.begin() ), r.fileId, r.line } ); + hits.push_back( { std::uint32_t( it - names.begin() ), r.fileId, r.line } ); } - const std::vector tierOfFile = pathTierIndexOver( *reads, hits, []( const ReadHit& h ) { return h.fileId; } ); + const std::vector tierOfFile = pathTierIndexOver( rd, hits, []( const ReadHit& h ) { return h.fileId; } ); std::ranges::sort( hits, [ & ]( const ReadHit& a, const ReadHit& b ) { if( a.name != b.name ) { return a.name < b.name; } - if( const int c = compareTierThenPath( *reads, tierOfFile, a.fileId, b.fileId ); c != 0 ) + const int c = compareTierThenPath( rd, tierOfFile, a.fileId, b.fileId ); + return c != 0 ? c < 0 : a.line < b.line; + } ); + return hits; +} + +// reads=/reads_at= for every readsKnown row, from one readSitesOf pass over the reads ingest. +inline void fillReads( const IngestResult& ing, const IngestResult& rd, std::span rows, std::span out ) +{ + std::vector names; + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( out[r].readsKnown ) { - return c < 0; + names.push_back( ing.symbols[ rows[r].ids.front() ].name ); } - return a.line < b.line; - } ); + } + std::ranges::sort( names ); + names.erase( std::unique( names.begin(), names.end() ), names.end() ); + const std::vector hits = readSitesOf( rd, names ); for( std::size_t r = 0; r < rows.size(); ++r ) { if( !out[r].readsKnown ) @@ -240,15 +233,49 @@ inline std::vector grepCarriers( const IngestResult& ing, const Val continue; } const std::string_view name = ing.symbols[ rows[r].ids.front() ].name; - const std::uint32_t k = std::uint32_t( std::ranges::lower_bound( readNames, name ) - readNames.begin() ); - ASSUME( k < readNames.size() && readNames[k] == name, "every readsKnown row queued its name" ); + const std::uint32_t k = std::uint32_t( std::ranges::lower_bound( names, name ) - names.begin() ); + ASSUME( k < names.size() && names[k] == name, "every readsKnown row queued its name" ); const auto [ first, last ] = std::ranges::equal_range( hits, k, {}, &ReadHit::name ); // hits are sorted by name first out[r].reads = std::uint32_t( last - first ); for( auto h = first; h != last && out[r].at.size() < kGrepCarrierShown; ++h ) { - out[r].at.push_back( { reads->files[ h->fileId ], h->line } ); + out[r].at.push_back( { rd.files[ h->fileId ], h->line } ); + } + } +} + +} // namespace grepcarrier + +// One carrier verdict per row. `ing` is the ingest the rows came from; `vri` its value index (vals=); `reads` the +// ingest whose role="read" references count (nullptr = none captured: reads= stays unknown on every row). Each pass +// walks its table ONCE for the whole page, never per row. +inline std::vector grepCarriers( const IngestResult& ing, const ValueRefIndex& vri, std::span rows, + const IngestResult* reads ) +{ + std::vector out( rows.size() ); + std::vector carrierIds; // every carrier definition on the page, for ONE madeIn pass + bool anyReads = false; + for( std::size_t r = 0; r < rows.size(); ++r ) + { + if( !isGrepCarrierRow( ing, rows[r] ) ) + { + continue; } + out[r].isCarrier = true; + out[r].readsKnown = reads != nullptr && grepCarrierReadsVisible( ing, ing.symbols[ rows[r].ids.front() ] ); + anyReads = anyReads || out[r].readsKnown; + carrierIds.insert( carrierIds.end(), rows[r].ids.begin(), rows[r].ids.end() ); + } + if( !carrierIds.empty() ) + { + grepcarrier::fillVals( ing, vri, rows, carrierIds, out ); + } + if( anyReads ) + { + EXPECTS( reads != nullptr, "a row is readsKnown only when a reads ingest was handed in" ); + grepcarrier::fillReads( ing, *reads, rows, out ); } + ENSURES( out.size() == rows.size() ); return out; } From 3413622a15bf3b421e53a9c69631acd128957786 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 15:16:04 -0400 Subject: [PATCH 035/162] =?UTF-8?q?test:=20qddegradecheck=20=E2=80=94=20--?= =?UTF-8?q?quality-delta=3DA..B=20answers=20under=20memory=20pressure=20in?= =?UTF-8?q?stead=20of=20refusing=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate is written before the code it measures (#387 follow-up). Today a tripped memory guard in the range form exits 5 with nothing on stdout, so on a large repository a 4-file change cannot get even the findings those 4 files decide. Contract the arms pin: - The kinds split by input scope. LOCAL kinds (api-surface, complexity, error-masking, nesting, params, placeholder, verbosity, and defect-shape except its C++ format-arity scan, which reads the tree's macro names) need only the changed files. dead-code, duplication and new-clone-of-reused-helper need the whole tree. - When the target phase is projected past the limit (P1), or the guard stopped a commit's full ingest (P0), both commits' changed files answer the kinds that can run there. Every kind not run is named (kinds_skipped=, skipped_why=, memory_limit=, memory_need=, also_skipped=) with kinds_next=, a replay that runs exactly those kinds; degraded rows plus the replay's rows = the unpressured rows. - Exit 2 when a row of a kind that ran gates, else 5 (never 0). Exit 5 with nothing on stdout only when the changed files do not fit either. - --quality-kinds=K[,K] selects kinds (the replay's selector); all 12 named is byte-identical to no flag. - When memory suffices nothing moves (project:2 past the one reading). Red at b274e0c3: 30 FAIL, 5 PASS (the premise and four guards that hold on both). Registered: regression.sh, shard weights, gate count 685 (generator). Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/qddegradecheck.sh | 317 ++++++++++++++++++++++++++++ test/regression.sh | 2 +- 6 files changed, 327 insertions(+), 9 deletions(-) create mode 100755 test/qddegradecheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 9e06f3e73..8174b852f 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -427,6 +427,7 @@ "qbaselineproducercheck.sh": 5.0, "qchurncheck.sh": 3.7, "qchurnmemocheck.sh": 4.3, +"qddegradecheck.sh": 20.0, "qdrefpaircheck.sh": 92.2, "qextractionkeycheck.sh": 2.8, "qoriginoraclecheck.sh": 8.7, diff --git a/README.md b/README.md index 2432bb90f..bf380fff8 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-684 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **684 gate scripts** and is the authoritative list; +`test/regression.sh` names **685 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 234344482..11acf723b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 684 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **684 gate scripts**, all of which exist on disk. +naming **685 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 684. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e545797c6..db11d6c46 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["684 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "684", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["684 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh new file mode 100755 index 000000000..c623f6649 --- /dev/null +++ b/test/qddegradecheck.sh @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +# qddegradecheck.sh — #387 follow-up: --quality-delta=A..B ANSWERS under memory pressure instead of refusing. +# +# THE DEFECT. A tripped memory guard in the range form gives exit 5 and nothing on stdout. On a large repository a +# 4-file change cannot get even the findings that need only those 4 files. +# +# THE CONTRACT these arms pin. Why a kind is LOCAL: every per-symbol baseline key is path-qualified (quality.h qualityKey) +# and so is the canonical id, so an UNCHANGED file yields the same keys and metrics in both trees and no row of such a kind: +# * The 12 kinds split by input scope. LOCAL kinds (api-surface, complexity, error-masking, nesting, params, +# placeholder, verbosity) need only the changed files' trees. defect-shape is LOCAL except its format-arity scan of a +# C++ file, which reads the tree's C-family macro NAMES (the neighbourhood input). WHOLE-TREE kinds (dead-code, +# duplication, new-clone-of-reused-helper) need every file. short-horizon-churn is unavailable in the range form. +# * When the base commit fit but the target phase is projected past the limit (P1), or a commit's full ingest was +# stopped by the guard (P0), the range form ingests only the changed files of both commits and runs the kinds that +# can run there. Every kind it does not run is NAMED (kinds_skipped=, skipped_why="memory"), with the limit +# (memory_limit=), the measured or lower-bound need (memory_need=), and a replay (kinds_next=) that runs exactly the +# skipped kinds with enough memory. +# * Exit: 2 when a row of a kind that ran gates; otherwise 5 (never 0 — a partial answer is not a pass). +# * Exit 5 with nothing on stdout only when even the changed files do not fit. +# * When memory suffices the degraded path is unreachable: output byte-identical to the build before it (L). +# * --quality-kinds=K[,K] runs exactly those kinds (the replay's selector), header kinds_skipped= skipped_why="selector". +# +# Arms (RED on the build before this lane, except (A) the premise and (L) the guard): +# (A) premise: the fixture's unpressured answer has a row of each of the 11 kinds the range form measures. +# (B) P1 forced (RIPWIRE_TEST_MEMGUARD=project:1): the degraded answer's header, exit code, stderr line, ingests. +# (C) P1: each LOCAL kind's rows equal the same kind's rows of (A); no row of a skipped kind. +# (D) the kinds_next= replay yields exactly the skipped kinds' rows; union with (B) = (A); gating= adds up. +# (E) P0 (crawl seam cuts the base's FULL ingest): answers from the changed files; defect-shape is skipped because a +# changed file is C++; (E2) a Python-only change keeps defect-shape and its rows equal (A)'s. +# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5, nothing +# on stdout, ONE line that names the phase AND that the changed files alone did not fit. +# (G) near misses under project:1: an empty diff, a rename-only diff, a non-code-only diff, and A..A. +# (H) --quality-kinds: unknown kind refused naming the 12; refused beside --quality-ack; each single kind = (A)'s rows +# of that kind; all 12 named = byte-identical to no flag. +# (I) --quality-ack under pressure refuses (exit 5) and leaves the ledger untouched. +# (J) --scope naming an unchanged file under pressure is not refused as "names nothing indexed". +# (K) legend + JSON: --legend=full defines every new attribute and parses as XML; --json carries the twins; an +# unpressured --json carries none of them. +# (L) guard: a projection seam index past the run's one projection reading changes nothing (byte-identical). +# +# Usage: bash test/qddegradecheck.sh [BIN] +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v git >/dev/null 2>&1 || { echo " SKIP qddegradecheck (git not available)"; exit 0; } +command -v python3 >/dev/null 2>&1 || { echo " SKIP qddegradecheck (python3 not available)"; exit 0; } +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/tmp" +echo "qddegradecheck: BIN=$BIN" + +LOCAL_KINDS="api-surface complexity defect-shape error-masking nesting params placeholder verbosity" +WHOLE_KINDS="dead-code,duplication,new-clone-of-reused-helper" +ALL12="api-surface,complexity,dead-code,defect-shape,duplication,error-masking,nesting,new-clone-of-reused-helper,params,placeholder,short-horizon-churn,verbosity" + +g(){ git -C "$1" -c user.name=t -c user.email=t@example.com "${@:2}"; } + +# ── the fixture: a base commit and a target commit with one change per kind, beside UNCHANGED files ───────────── +# The unchanged files make the full tree larger than the changed set, so a crawl seam can cut the full ingest while the +# changed-files ingest stays under it (E). +mkfx(){ + local R="$1"; rm -rf "$R"; mkdir -p "$R/src" "$R/py" + git -C "$R" init -q + for k in 1 2 3 4 5 6; do + printf 'int keep_%s( int v ) { return v + %s; }\n' "$k" "$k" >"$R/src/u$k.cpp" + printf 'def keep_%s(v):\n return v * %s\n' "$k" "$k" >"$R/py/u$k.py" + done + printf '#pragma once\ninline int api_scale( int a ) { return a * 2; }\n' >"$R/src/api.h" + { + printf '#include \n#include "api.h"\n' + printf 'int helper( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' + printf 'int use1( int x ) { return helper( x, 1 ); }\nint use2( int x ) { return helper( x, 2 ); }\nint use3( int x ) { return helper( x, 3 ); }\n' + printf 'int legacy_used( int x ) { return x + 7; }\n' + printf 'int wide( int a, int b, int c, int d, int e ) { return a + b + c + d + e; }\n' + printf 'void risky() {}\nvoid handle() { try { risky(); } catch( ... ) { std::puts( "logged" ); } }\n' + printf 'int grow( int x )\n{\n if( x > 1 ) { return 1; }\n return 0;\n}\n' + printf 'int chatty( int x )\n{\n int r = x;\n r += 1;\n r += 2;\n return r;\n}\n' + printf 'int drive( int x ) { return use1( x ) + use2( x ) + use3( x ) + legacy_used( x ) + wide( 1, 2, 3, 4, 5 ) + grow( x ) + api_scale( x ) + chatty( x ); }\n' + } >"$R/src/core.cpp" + printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {}".format(name)\n' >"$R/py/mod.py" + printf 'notes\n' >"$R/NOTES.txt" + g "$R" add -A; g "$R" commit -qm base +} +# target: complexity+nesting (grow), verbosity (chatty), params (wide 5->6), api-surface (api_scale 1->2, a header), +# error-masking (an emptied catch), dead-code (legacy_used loses its caller), duplication + new-clone-of-reused-helper +# (helper, fan-in 3, copied), placeholder (a TODO, a NotImplementedError stub), defect-shape (a .format arity miss) +mktarget(){ + local R="$1" + printf '#pragma once\ninline int api_scale( int a, int b ) { return a * b; }\n' >"$R/src/api.h" + { + printf '#include \n#include "api.h"\n' + printf 'int helper( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' + printf 'int use1( int x ) { return helper( x, 1 ); }\nint use2( int x ) { return helper( x, 2 ); }\nint use3( int x ) { return helper( x, 3 ); }\n' + printf 'int legacy_used( int x ) { return x + 7; }\n' + printf 'int wide( int a, int b, int c, int d, int e, int f ) { return a + b + c + d + e + f; }\n' + printf 'void risky() {}\nvoid handle() { try { risky(); } catch( ... ) {} }\n' + printf 'int grow( int x )\n{\n int r = 0;\n for( int i = 0; i < x; ++i ) {\n if( i %% 2 ) {\n while( r < i ) {\n' + printf ' if( r %% 3 ) {\n for( int j = 0; j < 3; ++j ) {\n' + printf ' if( j && r ) { r += j; } else if( r > 9 ) { r -= 1; } else { r += 2; }\n }\n' + printf ' } else if( r %% 5 ) { r += 3; } else { r += 4; }\n }\n' + printf ' } else if( i %% 7 ) { r -= 1; } else if( i %% 11 ) { r += 5; } else { r -= 2; }\n' + printf ' if( r > 1000 && i > 3 ) { break; }\n if( r < -1000 || i > 900 ) { continue; }\n }\n return r;\n}\n' + printf 'int chatty( int x )\n{\n int r = x;\n' + for i in $( seq 1 69 ); do printf ' r += %s;\n' "$i"; done + printf ' return r;\n}\n' + printf 'int drive( int x ) { return use1( x ) + use2( x ) + use3( x ) + wide( 1, 2, 3, 4, 5, 6 ) + grow( x ) + api_scale( x, 2 ) + chatty( x ); }\n' + } >"$R/src/core.cpp" + printf 'int helper_copy( int a, int b ) { int s = 0; for( int i = 0; i < a; ++i ) { s += i * b; if( s > 100 ) { s -= b; } } return s + a - b; }\n' >"$R/src/copy.cpp" + printf 'def other():\n # TODO: wire this up\n return 1\n\ndef greet(name):\n return "hello {} {}".format(name)\n\ndef later():\n raise NotImplementedError\n' >"$R/py/mod.py" + g "$R" add -A; g "$R" commit -qm target +} + +FX="$WORK/fx"; mkfx "$FX"; mktarget "$FX" +BASE="$( git -C "$FX" rev-parse HEAD~1 )"; TGT="$( git -C "$FX" rev-parse HEAD )" +# run [ENV=VAL ...] -- ARGS: stdout/stderr to $WORK/o, $WORK/e; rc in $RC +qd(){ ( cd "$FX" && env TMPDIR="$WORK/tmp" RIPWIRE_CACHE_STATS=1 "$@" >"$WORK/o" 2>"$WORK/e" ); RC=$?; } +# rows: every and element on its own line, sorted (the union rule: rows compare as SORTED sets) +rows(){ python3 - "$1" "${2:-}" <<'PY' +import re, sys +doc = open(sys.argv[1], encoding="utf-8", errors="replace").read() +want = set(k for k in sys.argv[2].split(",") if k) +out = [] +for m in re.finditer(r'<(r|sa) [^>]*/>', doc): + k = re.search(r' kind="([^"]*)"', m.group(0)) + if k and (not want or k.group(1) in want): + out.append(m.group(0)) +print("\n".join(sorted(out))) +PY +} +attr(){ sed -n 's/.*]*'"$2"'="\([^"]*\)".*/\1/p' "$1" | head -n 1; } +hasattr(){ grep -q "]* $2=\"" "$1"; } +ingests(){ grep -c '^ripwire: cache-stats ' "$WORK/e"; } +memlines(){ grep -c '^ripwire: memory limit ' "$WORK/e"; } +isnum(){ case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; } + +# ── (A) the premise ─────────────────────────────────────────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/A.xml"; cp "$WORK/e" "$WORK/A.err"; ARC=$RC +missing="" +for k in api-surface complexity dead-code defect-shape duplication error-masking nesting new-clone-of-reused-helper params placeholder verbosity; do + [ -n "$( rows "$WORK/A.xml" "$k" )" ] || missing="$missing $k" +done +if [ "$ARC" = 2 ] && [ -z "$missing" ]; then ok "(A) premise: the unpressured answer has a row of each of the 11 measured kinds (rc=2)" +else no "(A) premise: rc=$ARC, kinds with no row:$missing — every arm below compares against this answer, so it proves nothing"; fi +AGATE="$( attr "$WORK/A.xml" gating )" + +# ── (B) P1 forced: the base fit, the target phase is projected past the limit ─────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/B.xml"; cp "$WORK/e" "$WORK/B.err"; BRC=$RC; BING="$( ingests )" +bgate="$( attr "$WORK/B.xml" gating )"; need="$( attr "$WORK/B.xml" memory_need )"; needn="${need%M}" +wantrc=5; isnum "$bgate" && [ "$bgate" -gt 0 ] && wantrc=2 +bnext="$( attr "$WORK/B.xml" kinds_next )" +if [ -s "$WORK/B.xml" ] && python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$WORK/B.xml" 2>/dev/null \ + && [ "$( attr "$WORK/B.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/B.xml" skipped_why )" = memory ] \ + && [ "$( attr "$WORK/B.xml" ingest_scope )" = changed-files ] && hasattr "$WORK/B.xml" memory_limit \ + && isnum "$needn" && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ + && ! hasattr "$WORK/B.xml" register-macro-excluded && isnum "$bgate" && [ "$BRC" = "$wantrc" ]; then + ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need}, also_skipped, no stale=/dead-code counts" +else + no "(B) P1: rc=$BRC (want $wantrc) gating=$bgate kinds_skipped=$( attr "$WORK/B.xml" kinds_skipped ) why=$( attr "$WORK/B.xml" skipped_why ) scope=$( attr "$WORK/B.xml" ingest_scope ) need=$need stdout=$( wc -c <"$WORK/B.xml" | tr -d ' ' )B" +fi +if [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=$need" ]; then ok "(B) kinds_next= replays exactly the skipped kinds at the measured need" +else no "(B) kinds_next='$bnext'"; fi +if [ "$( memlines )" = 1 ] && grep -q "^ripwire: memory limit .*dead-code, duplication, new-clone-of-reused-helper" "$WORK/B.err" && [ "$BING" = 2 ]; then + ok "(B) one stderr line names the skipped kinds; two ingests (the base in full, then the target's changed files)" +else no "(B) memory lines=$( memlines ) ingests=$BING stderr: $( grep '^ripwire: [^c]' "$WORK/B.err" | head -c 300 )"; fi + +# ── (C) P1: LOCAL kinds equal the unpressured answer, kind by kind ───────────────────────────────────────────── +if [ -n "$( attr "$WORK/B.xml" kinds_skipped )" ]; then + for k in $LOCAL_KINDS; do + if [ "$( rows "$WORK/B.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(C) P1 $k: rows identical to the unpressured answer" + else no "(C) P1 $k: rows differ: $( diff <( rows "$WORK/A.xml" "$k" ) <( rows "$WORK/B.xml" "$k" ) | head -c 300 )"; fi + done + [ -z "$( rows "$WORK/B.xml" "$WHOLE_KINDS" )" ] && ok "(C) P1: no row of a skipped kind" || no "(C) P1: a skipped kind has rows" + [ "$( attr "$WORK/B.xml" api-new-surface )" = "$( attr "$WORK/A.xml" api-new-surface )" ] && ok "(C) P1: api-new-surface= equal" || no "(C) api-new-surface differs" +else + no "(C) P1: premise — (B) is not a degraded answer (no kinds_skipped=), so per-kind identity proves nothing" +fi + +# ── (D) the replay ───────────────────────────────────────────────────────────────────────────────────────────── +if [ -n "$bnext" ]; then + # shellcheck disable=SC2086 + qd "$BIN" . $bnext --no-cache; cp "$WORK/o" "$WORK/D.xml"; DRC=$RC + dgate="$( attr "$WORK/D.xml" gating )" + if [ "$( rows "$WORK/D.xml" )" = "$( rows "$WORK/A.xml" "$WHOLE_KINDS" )" ] && [ -n "$( rows "$WORK/D.xml" )" ]; then ok "(D) the replay's rows are exactly (A)'s skipped-kind rows" + else no "(D) replay rc=$DRC rows differ: $( diff <( rows "$WORK/A.xml" "$WHOLE_KINDS" ) <( rows "$WORK/D.xml" ) | head -c 300 )"; fi + if [ "$( { rows "$WORK/B.xml"; rows "$WORK/D.xml"; } | sort )" = "$( rows "$WORK/A.xml" )" ]; then ok "(D) union of the degraded answer and the replay = the unpressured answer (as sorted rows)" + else no "(D) union differs from (A)"; fi + if isnum "$bgate" && isnum "$dgate" && isnum "$AGATE" && [ $(( bgate + dgate )) = "$AGATE" ]; then ok "(D) gating= adds up: $bgate + $dgate = $AGATE" + else no "(D) gating: B=$bgate D=$dgate A=$AGATE"; fi + [ "$( attr "$WORK/D.xml" skipped_why )" = selector ] && ok "(D) the replay says its kinds were selected (skipped_why=selector)" || no "(D) replay skipped_why=$( attr "$WORK/D.xml" skipped_why )" +else + no "(D) premise: no kinds_next= to replay" +fi + +# ── (E) P0: the guard cut the base commit's FULL ingest (crawl seam: the full tree has more entries than the changed set) ─ +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/E.xml"; cp "$WORK/e" "$WORK/E.err"; ERC=$RC +if [ -s "$WORK/E.xml" ] && [ "$( attr "$WORK/E.xml" kinds_skipped )" = "dead-code,defect-shape,duplication,new-clone-of-reused-helper" ] \ + && [ "$( attr "$WORK/E.xml" ingest_scope )" = changed-files ] && [ "$( ingests )" = 3 ] && { [ "$ERC" = 2 ] || [ "$ERC" = 5 ]; }; then + ok "(E) P0: the cut full ingest is never scored; the changed files answer (rc=$ERC); defect-shape skipped (a changed C++ file)" +else + no "(E) P0: rc=$ERC kinds_skipped=$( attr "$WORK/E.xml" kinds_skipped ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/E.err" | head -c 300 )" +fi +if [ -n "$( attr "$WORK/E.xml" kinds_skipped )" ]; then + for k in api-surface complexity error-masking nesting params placeholder verbosity; do + [ "$( rows "$WORK/E.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ] && ok "(E) P0 $k: rows identical" || no "(E) P0 $k: rows differ" + done + case "$( attr "$WORK/E.xml" memory_need )" in '>'*M) ok "(E) P0: memory_need= is a lower bound ('>' the limit) — the full need was never measured";; *) no "(E) P0 memory_need=$( attr "$WORK/E.xml" memory_need )";; esac +else + no "(E) P0: premise — not a degraded answer" +fi +# (E2) a Python-only change: no changed C++ file, so defect-shape runs on the changed files and equals the unpressured rows +FX2="$WORK/fx2"; mkfx "$FX2" +printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {} {}".format(name)\n' >"$FX2/py/mod.py"; g "$FX2" add -A; g "$FX2" commit -qm py +( cd "$FX2" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2a.xml" 2>/dev/null ) +( cd "$FX2" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2.xml" 2>/dev/null ) +if [ "$( attr "$WORK/E2.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ -n "$( rows "$WORK/E2a.xml" defect-shape )" ] \ + && [ "$( rows "$WORK/E2.xml" defect-shape )" = "$( rows "$WORK/E2a.xml" defect-shape )" ]; then + ok "(E2) P0, Python-only change: defect-shape runs on the changed files and its rows equal the unpressured ones" +else + no "(E2) kinds_skipped=$( attr "$WORK/E2.xml" kinds_skipped ) unpressured defect rows=$( rows "$WORK/E2a.xml" defect-shape | grep -c . )" +fi + +# ── (F) even the changed files do not fit ────────────────────────────────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=hard:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; FRC=$RC +if [ "$FRC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] \ + && grep -q '^ripwire: memory limit reached during the ingest of the base commit (limit .*changed files' "$WORK/e"; then + ok "(F) the changed files do not fit either: exit 5, nothing on stdout, one line naming the phase and the changed-files attempt" +else + no "(F) rc=$FRC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )" +fi + +# ── (G) near misses under project:1 ──────────────────────────────────────────────────────────────────────────── +nm(){ # $1 label, $2 repo; compares LOCAL rows with the unpressured run and requires the degraded header + local L="$1" R="$2" + ( cd "$R" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmA.xml" 2>/dev/null ) + ( cd "$R" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmB.xml" 2>"$WORK/nmB.err" ); local rc=$? + local loc; loc="$( echo "$LOCAL_KINDS" | tr ' ' ',' )" + if [ "$( attr "$WORK/nmB.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( rows "$WORK/nmB.xml" "$loc" )" = "$( rows "$WORK/nmA.xml" "$loc" )" ] \ + && ! grep -q 'could not materialize' "$WORK/nmB.err" && { [ "$rc" = 5 ] || [ "$rc" = 2 ]; }; then + ok "(G) $L: degraded answer (rc=$rc), LOCAL rows equal the unpressured run" + else + no "(G) $L: rc=$rc kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) stderr: $( head -c 300 "$WORK/nmB.err" )" + fi +} +FE="$WORK/fe"; mkfx "$FE"; g "$FE" commit -q --allow-empty -m empty; nm "empty diff" "$FE" +FR="$WORK/fr"; mkfx "$FR"; g "$FR" mv py/u1.py py/u1_moved.py; g "$FR" commit -qm rename; nm "rename-only diff" "$FR" +FN="$WORK/fn"; mkfx "$FN"; printf 'more notes\n' >>"$FN/NOTES.txt"; g "$FN" add -A; g "$FN" commit -qm notes; nm "non-code-only diff" "$FN" +( cd "$FX" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD..HEAD --no-cache >"$WORK/AA.xml" 2>"$WORK/AA.err" ); r1=$? +( cd "$FX" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD..HEAD --no-cache >"$WORK/AAp.xml" 2>"$WORK/AAp.err" ); r2=$? +if [ "$r1" = "$r2" ] && cmp -s "$WORK/AA.xml" "$WORK/AAp.xml" && [ -s "$WORK/AA.xml" ]; then ok "(G) A..A: one tree, nothing to project — answered in full, byte-identical (rc=$r1)" +else no "(G) A..A: rc $r1 vs $r2 or the answers differ"; fi + +# ── (H) --quality-kinds ─────────────────────────────────────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dupe --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q "dupe" "$WORK/e" && grep -q "new-clone-of-reused-helper" "$WORK/e" && grep -q "short-horizon-churn" "$WORK/e"; then + ok "(H) an unknown kind is refused (exit 1) naming it and the 12 kinds" +else no "(H) unknown kind: rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +qd "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --quality-ack --no-cache +if [ "$RC" = 1 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && grep -q -- '--quality-kinds.*--quality-ack\|--quality-ack.*--quality-kinds' "$WORK/e" && ! grep -q 'unknown flag' "$WORK/e"; then + ok "(H) --quality-kinds beside --quality-ack is refused (exit 1) naming both flags, no ledger written" +else no "(H) with --quality-ack rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +for k in api-surface complexity dead-code defect-shape duplication error-masking nesting new-clone-of-reused-helper params placeholder verbosity; do + qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$k" --no-cache + if [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/o" )" ] && [ "$( attr "$WORK/o" skipped_why )" = selector ]; then ok "(H) --quality-kinds=$k: exactly (A)'s $k rows" + else no "(H) --quality-kinds=$k: rc=$RC rows=$( rows "$WORK/o" | grep -c . ) want $( rows "$WORK/A.xml" "$k" | grep -c . ) why=$( attr "$WORK/o" skipped_why )"; fi +done +qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$ALL12" --no-cache +[ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml" && ok "(H) all 12 named: byte-identical to no selector" || no "(H) all 12 named: rc=$RC or output differs from (A)" + +# ── (I) --quality-ack under pressure ────────────────────────────────────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache +if [ "$RC" = 5 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && [ "$( memlines )" = 1 ]; then ok "(I) --quality-ack under pressure refuses (exit 5), the ledger untouched" +else no "(I) rc=$RC ledger=$( [ -e "$FX/.ripwire_quality_acks" ] && echo written || echo absent ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +rm -f "$FX/.ripwire_quality_acks" + +# ── (J) --scope naming an unchanged file under pressure ─────────────────────────────────────────────────────── +qd "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache; jA=$RC; cp "$WORK/o" "$WORK/JA.xml" +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache +if [ "$RC" != 1 ] && ! grep -q 'names nothing' "$WORK/e" && [ -n "$( attr "$WORK/o" kinds_skipped )" ] \ + && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/JA.xml" "$( echo "$LOCAL_KINDS" | tr ' ' ',' )" )" ]; then + ok "(J) --scope on an unchanged file under pressure: answered (rc=$RC, unpressured rc=$jA), not refused as naming nothing" +else no "(J) rc=$RC (unpressured $jA) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (K) legend and JSON ─────────────────────────────────────────────────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +missing="" +for a in kinds_skipped= skipped_why= ingest_scope= memory_need= also_skipped= kinds_next=; do + sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- "$a" || missing="$missing $a" +done +if [ -z "$missing" ] && python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$WORK/o" 2>/dev/null; then ok "(K) --legend=full defines every new attribute and parses as XML" +else no "(K) legend lacks:$missing (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache +if python3 - "$WORK/o" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +assert d["kinds_skipped"] == ["dead-code", "duplication", "new-clone-of-reused-helper"], d.get("kinds_skipped") +assert d["skipped_why"] == "memory" and d["ingest_scope"] == "changed-files" +assert d["also_skipped"] == ["config-warnings", "stale-acks"] and "stale" not in d +assert d["kinds_next"].startswith("--quality-delta=") and "memory_need" in d and "memory_limit" in d +PY +then ok "(K) --json carries the twins (kinds_skipped list, skipped_why, ingest_scope, also_skipped, kinds_next, memory_*)" +else no "(K) --json twins missing or wrong: $( head -c 300 "$WORK/o" )"; fi +qd "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache +grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"' "$WORK/o" && no "(K) an unpressured --json carries a degraded-only key" || ok "(K) an unpressured --json carries none of the degraded keys" + +# ── (L) guard: the projection seam past the run's one projection reading changes nothing ─────────────────────── +qd RIPWIRE_TEST_MEMGUARD=project:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml" && [ "$( grep -v '^ripwire: cache-stats ' "$WORK/e" )" = "$( grep -v '^ripwire: cache-stats ' "$WORK/A.err" )" ]; then + ok "(L) project:2 (past the one projection reading) is byte-identical to no seam (rc=$RC)" +else no "(L) project:2 rc=$RC (want $ARC) or the answer differs"; fi + +[ "$fail" = 0 ] && echo "qddegradecheck: ALL PASS" || echo "qddegradecheck: FAIL" +exit "$fail" diff --git a/test/regression.sh b/test/regression.sh index d5e21f5d4..59933d72b 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 0fa110f2afd077a59a5d94cf1c607323c611b300 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 15:18:53 -0400 Subject: [PATCH 036/162] test(ownerhopcheck): an apostrophe in the (R) comment broke the heredoc inside $( ) Co-Authored-By: Claude Opus 5.5 --- test/ownerhopcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index a2fe84a4b..8a5dae344 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -379,7 +379,7 @@ missing = [ident(r) for r in base if ident(r) not in hd or not same(calls(r), ca gained = [r['n'] for r in base if ident(r) in hd and len(calls(hd[ident(r)])) > len(calls(r))] moved = [r['n'] for r in off['rows'] if ident(r) in owners] order_ok = [ident(r) for r in head if ident(r) in {ident(b) for b in base}] == [ident(r) for r in base if ident(r) in hd] -# forbudget-soft (decision D-3; rule 4): the DEFAULT is no longer cut at its 1,000 B allowance, so the A/B handle's rows +# forbudget-soft (decision D-3; rule 4): the DEFAULT is no longer cut at its 1,000 B allowance, so the rows of the A/B handle # are already complete and a moved owner can free nothing — "gained" is then impossible by construction. The premise keeps # "an owner moved" and accepts either a gain or an A/B listing with no cut left to widen. base_complete = all((r.get('ca') or {}).get('capped') != '1' for r in base) From 831960176b64b2a4f845ee9e66e56141482c7db7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 15:27:54 -0400 Subject: [PATCH 037/162] =?UTF-8?q?test:=20qddegradecheck=20=E2=80=94=20ve?= =?UTF-8?q?rdicts=20through=20if/else,=20an=20absence=20read=20only=20off?= =?UTF-8?q?=20a=20run=20that=20succeeded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gateexitcheck (G1/G2): ok() now records a failed PASS write, and no arm reports through a one-line `&& ok || no` chain. The unpressured --json absence arm now requires the run's own rc and a non-empty answer before it reads "no key", and the --quality-kinds/--quality-ack refusal arm requires the refusal to name both flags (an unknown-flag exit 1 no longer passes it). Co-Authored-By: Claude Opus 5.5 --- test/qddegradecheck.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index c623f6649..07fc0e59d 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -45,7 +45,7 @@ ROOT="$( cd "$( dirname "$0" )/.." && pwd )" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" fail=0 -ok(){ printf ' PASS %s\n' "$*"; } +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } command -v git >/dev/null 2>&1 || { echo " SKIP qddegradecheck (git not available)"; exit 0; } @@ -174,8 +174,8 @@ if [ -n "$( attr "$WORK/B.xml" kinds_skipped )" ]; then if [ "$( rows "$WORK/B.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(C) P1 $k: rows identical to the unpressured answer" else no "(C) P1 $k: rows differ: $( diff <( rows "$WORK/A.xml" "$k" ) <( rows "$WORK/B.xml" "$k" ) | head -c 300 )"; fi done - [ -z "$( rows "$WORK/B.xml" "$WHOLE_KINDS" )" ] && ok "(C) P1: no row of a skipped kind" || no "(C) P1: a skipped kind has rows" - [ "$( attr "$WORK/B.xml" api-new-surface )" = "$( attr "$WORK/A.xml" api-new-surface )" ] && ok "(C) P1: api-new-surface= equal" || no "(C) api-new-surface differs" + if [ -z "$( rows "$WORK/B.xml" "$WHOLE_KINDS" )" ]; then ok "(C) P1: no row of a skipped kind"; else no "(C) P1: a skipped kind has rows"; fi + if [ "$( attr "$WORK/B.xml" api-new-surface )" = "$( attr "$WORK/A.xml" api-new-surface )" ]; then ok "(C) P1: api-new-surface= equal"; else no "(C) api-new-surface differs"; fi else no "(C) P1: premise — (B) is not a degraded answer (no kinds_skipped=), so per-kind identity proves nothing" fi @@ -191,7 +191,7 @@ if [ -n "$bnext" ]; then else no "(D) union differs from (A)"; fi if isnum "$bgate" && isnum "$dgate" && isnum "$AGATE" && [ $(( bgate + dgate )) = "$AGATE" ]; then ok "(D) gating= adds up: $bgate + $dgate = $AGATE" else no "(D) gating: B=$bgate D=$dgate A=$AGATE"; fi - [ "$( attr "$WORK/D.xml" skipped_why )" = selector ] && ok "(D) the replay says its kinds were selected (skipped_why=selector)" || no "(D) replay skipped_why=$( attr "$WORK/D.xml" skipped_why )" + if [ "$( attr "$WORK/D.xml" skipped_why )" = selector ]; then ok "(D) the replay says its kinds were selected (skipped_why=selector)"; else no "(D) replay skipped_why=$( attr "$WORK/D.xml" skipped_why )"; fi else no "(D) premise: no kinds_next= to replay" fi @@ -206,7 +206,7 @@ else fi if [ -n "$( attr "$WORK/E.xml" kinds_skipped )" ]; then for k in api-surface complexity error-masking nesting params placeholder verbosity; do - [ "$( rows "$WORK/E.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ] && ok "(E) P0 $k: rows identical" || no "(E) P0 $k: rows differ" + if [ "$( rows "$WORK/E.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(E) P0 $k: rows identical"; else no "(E) P0 $k: rows differ"; fi done case "$( attr "$WORK/E.xml" memory_need )" in '>'*M) ok "(E) P0: memory_need= is a lower bound ('>' the limit) — the full need was never measured";; *) no "(E) P0 memory_need=$( attr "$WORK/E.xml" memory_need )";; esac else @@ -269,7 +269,7 @@ for k in api-surface complexity dead-code defect-shape duplication error-masking else no "(H) --quality-kinds=$k: rc=$RC rows=$( rows "$WORK/o" | grep -c . ) want $( rows "$WORK/A.xml" "$k" | grep -c . ) why=$( attr "$WORK/o" skipped_why )"; fi done qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$ALL12" --no-cache -[ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml" && ok "(H) all 12 named: byte-identical to no selector" || no "(H) all 12 named: rc=$RC or output differs from (A)" +if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(H) all 12 named: byte-identical to no selector"; else no "(H) all 12 named: rc=$RC or output differs from (A)"; fi # ── (I) --quality-ack under pressure ────────────────────────────────────────────────────────────────────────── qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache @@ -305,7 +305,9 @@ PY then ok "(K) --json carries the twins (kinds_skipped list, skipped_why, ingest_scope, also_skipped, kinds_next, memory_*)" else no "(K) --json twins missing or wrong: $( head -c 300 "$WORK/o" )"; fi qd "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache -grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"' "$WORK/o" && no "(K) an unpressured --json carries a degraded-only key" || ok "(K) an unpressured --json carries none of the degraded keys" +if [ "$RC" = "$ARC" ] && [ -s "$WORK/o" ] && ! grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"' "$WORK/o"; then + ok "(K) an unpressured --json carries none of the degraded keys" +else no "(K) unpressured --json: rc=$RC (want $ARC), empty, or it carries a degraded-only key"; fi # ── (L) guard: the projection seam past the run's one projection reading changes nothing ─────────────────────── qd RIPWIRE_TEST_MEMGUARD=project:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache From 480bc90b2a3f8de6c1ffd25c96d38226541b9cfa Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 16:03:30 -0400 Subject: [PATCH 038/162] fix(for): scope the default's lego at the target cut by rank, not position (the docs reorder moves rows) ownerhopcheck (R/prefix): a listing cut only at the 16-name row cap cannot gain names either. Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 55 +++++++++++++++++++++++++++++++++---------- test/ownerhopcheck.sh | 5 ++-- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 3442896d0..b66214c0e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -8834,28 +8834,57 @@ inline std::vector> legoImplementorsOnSurface( const IngestR // every --for run whose rendered sigs happened to hit this path at all. Caught by legobundlecheck.sh going // red for the wrong reason (an empty lego, not a mis-narrowed one) rather than by a dedicated assertion — // worth a gate of its own if this class recurs. -// forbudget-soft: the text a DEFAULT answer's sections are scoped by — the rows up to the target cut, without the -// past_target= rows (the last K rows in emission order), so / narrow as they did when the target was a -// ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). -// The whole block on a soft target or an answer with nothing past its target. Emission order is rank order except where the -// docs reorder moved a doc row below the code rows (docs_after_code=): then the cut falls one row early or late — a scope -// over- or under-approximation by that row's file, never a wrong row. -inline std::string_view sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) noexcept +// forbudget-soft: the rows a DEFAULT answer's sections are scoped by — the rows at the target cut, without the +// past_target= rows (the K highest ranks: the rows are a rank prefix), so / narrow as they did when the target +// was a ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). +// Selected by each row's r=, not by emission position (the docs reorder, docs_after_code=, moves doc rows below the code rows). +// The whole block on a soft target or an answer with nothing past its target. Only … rows are kept (a file's +// notes ride its rows and are scanned for p= the same way). +inline std::string sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) { if( !cut.pastTargetDisclosed || cut.pastTarget == 0 || cut.pastTarget >= cut.shown ) { - return sigsRendered; + return std::string( sigsRendered ); } - const std::size_t keep = cut.shown - cut.pastTarget; - std::size_t seen = 0; + struct Row { std::uint32_t rank; std::string_view text; }; + std::vector rows; for( std::size_t at = sigsRendered.find( "", at ); + if( end == std::string_view::npos ) { - return sigsRendered.substr( 0, at ); + break; + } + const std::string_view text = sigsRendered.substr( at, end + 4 - at ); + const std::size_t rAt = text.find( " r=\"" ); + std::uint32_t rank = 0; + for( std::size_t i = rAt == std::string_view::npos ? text.size() : rAt + 4; i < text.size() && text[ i ] >= '0' && text[ i ] <= '9'; ++i ) + { + rank = rank * 10 + std::uint32_t( text[ i ] - '0' ); + } + rows.push_back( { rank, text } ); + } + if( rows.size() <= cut.pastTarget ) + { + return std::string( sigsRendered ); + } + std::vector ranks; + ranks.reserve( rows.size() ); + for( const Row& r : rows ) + { + ranks.push_back( r.rank ); + } + std::nth_element( ranks.begin(), ranks.begin() + std::ptrdiff_t( rows.size() - cut.pastTarget - 1 ), ranks.end() ); + const std::uint32_t keepUpTo = ranks[ rows.size() - cut.pastTarget - 1 ]; // the highest rank inside the target cut + std::string kept; + for( const Row& r : rows ) + { + if( r.rank <= keepUpTo ) + { + kept.append( r.text ); } } - return sigsRendered; + return kept; } inline bool narrowLegoToRenderedSigs( const IngestResult& ing, std::vector>& legoScoped, std::string_view sigsRendered, std::string_view rootPrefix = {} ) diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 8a5dae344..2b27ba3a6 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -381,8 +381,9 @@ moved = [r['n'] for r in off['rows'] if ident(r) in owners] order_ok = [ident(r) for r in head if ident(r) in {ident(b) for b in base}] == [ident(r) for r in base if ident(r) in hd] # forbudget-soft (decision D-3; rule 4): the DEFAULT is no longer cut at its 1,000 B allowance, so the rows of the A/B handle # are already complete and a moved owner can free nothing — "gained" is then impossible by construction. The premise keeps -# "an owner moved" and accepts either a gain or an A/B listing with no cut left to widen. -base_complete = all((r.get('ca') or {}).get('capped') != '1' for r in base) +# "an owner moved" and accepts either a gain or an A/B listing with no BUDGET-cut left to widen (a listing cut at the +# 16-name row cap cannot gain either: the cap is not the budget). +base_complete = all((r.get('ca') or {}).get('capped') != '1' or int((r.get('ca') or {}).get('shown', '0') or 0) >= 16 for r in base) # only the 16-name row cap left premise = bool(moved) and (bool(gained) or base_complete) if mode == 'prefix' else True print('OK' if not missing and order_ok and off['rows'] and premise else 'BAD missing=%s order_ok=%s base_rows=%d gained=%s moved=%s' % (missing[:3], order_ok, len(off['rows']), gained, moved)) From ba729c9589efb708f6906d20ead65999d98935a7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 16:14:25 -0400 Subject: [PATCH 039/162] =?UTF-8?q?fix(for):=20CI=20round=201=20=E2=80=94?= =?UTF-8?q?=20MCP=20descriptions=20under=20the=20manifest=20ceiling;=20cen?= =?UTF-8?q?sus=20and=20twin=20gates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MCP for/explore/from_trace budget_tokens descriptions shortened (tools/list 47,416 B <= the 47,420 B ceiling). - serialize.h comment no longer cites an unshipped plan doc (ripwirepubliccheck 8). - fixedbufsweep: sigsOpenTag's nb[96] gains past_target= (+1 site). - detailcheck / truncvocabcheck: the closed tag shapes admit past_target= and docs_after_code= (the same block). - forsectioncollapsecheck 13d/13e (rule 4): under a soft target an opted-in --sections= costs no row (was: charged in full under a hard ceiling). Co-Authored-By: Claude Opus 5.5 --- src/mcp.h | 6 +++--- src/serialize.h | 2 +- test/detailcheck.sh | 3 ++- test/fixedbufsweep.sh | 6 ++++-- test/forsectioncollapsecheck.sh | 19 +++++++++++-------- test/truncvocabcheck.sh | 2 +- 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/mcp.h b/src/mcp.h index 87d388968..3399e7de4 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1325,7 +1325,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "situational_awareness", pathIsRequired ) + "}," "{\"name\":\"mentions\",\"description\":\"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. " + std::string( kAtSeedRebindClause ) + "\"," + mcprefuse::toolMetadataFor( "mentions", pathIsRequired ) + "}," - "{\"name\":\"for\",\"description\":\"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional SOFT target over the ranked rows (the CLI --for --token-budget): rows stop once it is reached, never a byte promise; without it the whole ranked head ships. The header carries route= (which ranker answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + "{\"name\":\"for\",\"description\":\"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional soft row target (the CLI --for --token-budget). The header carries route= (which ranker answered, and why), confidence=/margin_pct= (how sharp the ranked head is — low means a starting point, not an answer) and lens= (the per-row columns this dialect does not measure).\"," + mcprefuse::toolMetadataFor( "for", pathIsRequired ) + "}," "{\"name\":\"lego\",\"description\":\"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base name, or file:name to disambiguate; " + std::string( kAtSeedShortClause ) + "\"," + mcprefuse::toolMetadataFor( "lego", pathIsRequired ) + "}," @@ -1368,9 +1368,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // L4 — the one-call orientation front door + B11 verb parity. `explore` is // the SAME handler as the CLI --pack-task; the older name `pack_task` still dispatches (tools/call // name=="pack_task" works) but is not separately advertised here — see mcp.h's kMcpVerbTable comment. - "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional soft target (sections stop at their shares; the default 6000 stays a hard ceiling); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional soft target (default 6000, hard); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + mcprefuse::toolMetadataFor( "explore", pathIsRequired ) + "}," - "{\"name\":\"from_trace\",\"description\":\"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional (a soft target).\"," + "{\"name\":\"from_trace\",\"description\":\"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional (soft).\"," + mcprefuse::toolMetadataFor( "from_trace", pathIsRequired ) + "}," "{\"name\":\"edit_check\",\"description\":\"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not a proof: call edges are matched by NAME, so a same-named callee this tool does not index can flag a caller that never touches the edited symbol at all, and a clean tree can carry a nonzero incompatible= with nothing edited. status is one of unchanged / new-symbol / contract-change, and callers= is itself a FLOOR — 'no incompatible caller' is not proof of safety either. Fast and targeted; for the same question over a WHOLE diff use quality_delta. symbol = the def name (file:name to disambiguate); at= names the commit compared against. " + std::string( kAtSeedShortClause ) + " limit/offset page the unflagged caller rows only. PRE-APPLY PREVIEW: pass new_body to ask the same question about a replacement that has NOT been written — it is spliced in memory, re-parsed, and the answer carries preview=1. Nothing is written; a payload that does not parse, or does not define the symbol, is refused.\"," + mcprefuse::toolMetadataFor( "edit_check", pathIsRequired ) + "}," diff --git a/src/serialize.h b/src/serialize.h index b66214c0e..b20d90259 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -931,7 +931,7 @@ inline constexpr std::size_t kForCapTailSigBytes = 96; // sections + ), far above a typical answer: hitting it is a BUG SIGNAL (a recursion, a dump, a head far wider than any // question needs), never a budget. A default answer that hits it discloses over_ceiling="1" on the root, (the continuation serves every row: an explicit target is never cut by this guard, see max(guard, T)) and -// kForRunawayGuardNote. The value is set by the pre-registered rule (forbudget-soft PLAN §G): roundup_1000( max( 4 x the +// forRunawayGuardNote(). The value is set by the lane's pre-registered guard rule (forbudget-soft, frozen before the code): roundup_1000( max( 4 x the // target, 2 x the largest complete default payload on the round-2 --for argv ) ) = max( 30,000, 2 x 12,519 ) = 30,000 B (44 // default-regime round-2 answers, measured on the provisional 32,000 build); verified with zero hits on the round-4-only (max // 13,005 B) and capsweep (max 12,094 B) argv. Gate: test/forsoftbudgetcheck.sh (D4, D4b, NX2). diff --git a/test/detailcheck.sh b/test/detailcheck.sh index 36371d773..0bc2e0d80 100755 --- a/test/detailcheck.sh +++ b/test/detailcheck.sh @@ -41,7 +41,8 @@ bodies(){ grep -o ' block, so the shape admits the attribute; the count it asserts is unchanged. -sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } +# forbudget-soft: the default answer's tag also carries past_target="K" (the rows past its 7.5 KB target) — the same block. +sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } # ── #1: --detail=0 == no --detail (byte-identical, golden-neutral) ────────────────────────────────────── "$BIN" src --for="$TASK" --no-cache >"$TMP/plain" 2>/dev/null diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index e4f04c655..cde22604c 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -280,7 +280,7 @@ NUMERIC_ONLY = { # "{}" of one std::uint32_t, ten digits worst case against 15 usable + NUL, no %s and # nothing escaped, so it does not join the string-interpolating population ( "src/serialize.h", "lineAttr" ): 1, - ( "src/serialize.h", "nb" ): 6, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. + ( "src/serialize.h", "nb" ): 7, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. # cut-fix lane A (2026-09-23): +2, sigsOpenTag's nb[96] — ' shown="{}" total="{}" capped="1"' # (31 B literal + two size_t at 20 digits = 71 B worst case, against 95 usable + NUL) and # ' docs_dropped="{}"' (16 B + 20 digits = 36 B). Counts only, no %s, nothing escaped. @@ -288,6 +288,8 @@ NUMERIC_ONLY = { # (19 B + 20 digits = 39 B). A count, no %s, nothing escaped. # knob-honesty-068 (2026-10-07): +2 in the same nb[96] — ' next_offset="{}"' (15 B + 20 # digits = 35 B) and ' next_budget_tokens="{}"' (22 B + 20 digits = 42 B). Counts only. + # forbudget-soft (2026-10-14): +1 in the same nb[96] — ' past_target="{}"' (15 B + 20 + # digits = 35 B). A count, no %s, nothing escaped. ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, ( "src/serialize.h", "rc" ): 1, @@ -620,7 +622,7 @@ if not bad: # 2026-10-08 (lane/cli-perf-068): -1 call/-1 mention/-1 site, rows unchanged (the lane's own delta on its base: # 351/241/241/105 -> 350/240/240/105). # train 26c (cli-perf merged onto the train's 363/248/248/109): re-derived on the merged tree. -EXPECTED = { "mentions": 362, "calls": 247, "sites": 247, "rows": 109, "widthforms": 0 } +EXPECTED = { "mentions": 363, "calls": 248, "sites": 248, "rows": 109, "widthforms": 0 } # forbudget-soft: +1 nb site (past_target=) # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the diff --git a/test/forsectioncollapsecheck.sh b/test/forsectioncollapsecheck.sh index 27eb844cd..4d23bc60e 100644 --- a/test/forsectioncollapsecheck.sh +++ b/test/forsectioncollapsecheck.sh @@ -427,11 +427,13 @@ else [[ "$LC_STUBTOTAL" =~ ^[0-9]+$ ]] && [ "$LC_IFACES" -eq "$(( LC_STUBTOTAL < 12 ? LC_STUBTOTAL : 12 ))" ] \ && ok "(13c) the stub's total=$LC_STUBTOTAL matches the restored ($LC_IFACES rows, display cap 12)" \ || no "(13c) the stub's total=\"$LC_STUBTOTAL\" vs $LC_IFACES restored rows (display cap 12)" - # (13d) NEAR-MISS: under an explicit ceiling, a section --sections= opts into is charged in full — the hard bound holds - # (its is then narrowed to the few rendered rows' files — here none of stages.h — so the bound is read off ) - [ "$LC_TBEST" -le 3000 ] && [ "$LC_TBREST" -lt "$LC_TB" ] \ - && ok "(13d) --token-budget=3000 --sections=lego,compose: est_tokens=$LC_TBEST <= 3000, $LC_TBREST < $LC_TB (the full was charged)" \ - || no "(13d) --token-budget=3000 --sections=lego,compose: est_tokens=$LC_TBEST, $LC_TBREST vs $LC_TB stubbed — the opted-in section was not charged in full under an explicit ceiling" + # (13d) forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget is a SOFT target charged against + # the rows only, so a section --sections= opts into is NOT charged to the rows any more — it rides on top (priced in + # est_tokens=) and costs no row (SEC); nothing is labelled over_ceiling. The old near-miss (the opted-in charged in + # full, shrinking) is the by-design break; what must hold now: the same rows with and without the opt-in. + [ "$LC_TBREST" -eq "$LC_TB" ] && ! grep -q 'over_ceiling="1"' "$TMP/lc_tbrest.xml" && grep -q ' costs no row ($LC_TBREST == $LC_TB), no over_ceiling (est_tokens=$LC_TBEST)" \ + || no "(13d) --token-budget=3000 --sections=lego,compose: $LC_TBREST vs $LC_TB without it (or over_ceiling, or no full ) — an opted-in section must not cost rows under a soft target" fi if command -v python3 >/dev/null 2>&1; then lc_mcp(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"test/legochargefix","task":"%s"%s}}}\n' "$LCQ" "$1" \ @@ -441,9 +443,10 @@ t = json.loads(sys.stdin.readline())['result']['content'][0]['text'] m = re.search(r' $LCM_DEF rows by default; under budget_tokens + sections $LCM_TBREST < $LCM_TB" \ - || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST (want >= 24, and the last below the middle)" + # forbudget-soft (rule 4): the CLI twin's rule — under budget_tokens (a soft target) the opted-in sections cost no row + [[ "$LCM_DEF" =~ ^[0-9]+$ && "$LCM_TB" =~ ^[0-9]+$ && "$LCM_TBREST" =~ ^[0-9]+$ ]] && [ "$LCM_DEF" -ge 24 ] && [ "$LCM_TBREST" -eq "$LCM_TB" ] \ + && ok "(13e) MCP for: $LCM_DEF rows by default; under budget_tokens the sections cost no row ($LCM_TBREST == $LCM_TB)" \ + || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST (want >= 24, and the last two equal)" else printf ' SKIP %s\n' "(13e) python3 absent: the MCP twin of the lego charge is not checked" fi diff --git a/test/truncvocabcheck.sh b/test/truncvocabcheck.sh index b8e013af8..8fc74d950 100755 --- a/test/truncvocabcheck.sh +++ b/test/truncvocabcheck.sh @@ -320,7 +320,7 @@ grep -q '' "$TMP/tiny_for.xml" \ || no "(C) --for on a two-symbol corpus is not the bare untrimmed " # knob-honesty-068: a trimmed tag also carries its continuation, next= (the call that serves it uncut) — the pair is # still asserted immediately after the tag name; only the closing `>` admits the trailing attributes the tag may carry. -grep -qE '' "$TMP/sweep_forbudget.xml" \ +grep -qE '' "$TMP/sweep_forbudget.xml" \ && ok "(C) --for trimmed by --token-budget: (rule 5, amended: the pair rides on a byte trim too)" \ || no "(C) --for --token-budget=800 did not mark its trimmed payload : $( grep -oE ']*>' "$TMP/sweep_forbudget.xml" | head -1 )" From feac850f5b6cd1024b09de664ad1eb66c523e89b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 16:37:23 -0400 Subject: [PATCH 040/162] refactor(serialize): the ladder's row-drop walks as two helpers; the tail next= inlined (qd) Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 105 ++++++++++++++++++++++++++++-------------------- src/verbs_for.h | 15 +++---- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index b20d90259..04e2c3a58 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4972,6 +4972,62 @@ inline std::size_t droppedPositiveCount( std::size_t candidatePositives, std::si // so subtract the old cost first and add the new one — never form `before - after` (it can be negative, // i.e. unsigned-overflow UB under G1's -fsanitize=integer). `total >= before` always holds (before is a // summand of total). +// Ladder step F's walk, LOWEST RANK first (P7, terminality round A, lane R, 2026-09-05: `entries` is in rank order — both callers +// sort it before the ladder runs — so the tail walk IS the rank walk; the old file-major form could drop a rank-5 row in the +// last file while a rank-39 row in the first file survived). A file's wrapBytes (its notes) go with the file's last live row. +// Rank 1..4 always survive (the floor). Returns the rows it dropped. +template +inline std::size_t sigLadderDropRows( std::vector& entries, std::vector& files, std::size_t& total, + std::size_t budget, CostFn entryCost ) +{ + std::size_t dropped = 0; + for( std::size_t k = entries.size(); k > 0 && total > budget; ) + { + EntryT& e = entries[ --k ]; + if( e.dropped || e.globalRank <= 4 ) + { + continue; + } + total -= entryCost( e ); + e.dropped = true; + ++dropped; + FileT& sf = files[ e.fileSlot ]; + if( --sf.liveCount == 0 ) + { + total -= sf.wrapBytes; + } + } + return dropped; +} +// forbudget-soft: the same walk as a DRY RUN on a copy of the live counts — how many rows step F would drop at `budget` and the +// block total it would leave, mutating nothing. +template +inline std::pair sigLadderDryDrop( const std::vector& entries, const std::vector& files, + std::size_t total, std::size_t budget, CostFn entryCost ) +{ + std::vector live( files.size() ); + for( std::size_t i = 0; i < files.size(); ++i ) + { + live[ i ] = files[ i ].liveCount; + } + std::size_t wouldDrop = 0; + for( std::size_t k = entries.size(); k > 0 && total > budget; ) + { + const EntryT& e = entries[ --k ]; + if( e.dropped || e.globalRank <= 4 ) + { + continue; + } + total -= entryCost( e ); + ++wouldDrop; + if( --live[ e.fileSlot ] == 0 ) + { + total -= files[ e.fileSlot ].wrapBytes; + } + } + return { wouldDrop, total }; +} + // // THE TARGET AND THE ROW-DROP BUDGET (forbudget-soft). Steps A..E aim at `effectiveBudget` (the TARGET: they shorten row text, // no row goes). Step F — the only step that removes a row — runs against `dropBudget` (>= the target; equal to it in the @@ -5032,54 +5088,17 @@ inline SigLadderOutcome trimSigLadder( std::vector& entries, std::vector shrinkSig( entries[k], kForTailSigBytes ); } } - // F: drop whole entries, LOWEST RANK first. P7 (terminality round A, lane R, 2026-09-05): `entries` is in - // rank order (both callers sort it before the ladder runs), so the tail walk IS the rank walk — the old - // file-major form ("tail file first, source order inside it") could drop a rank-5 row in the last file - // while a rank-39 row in the first file survived. A file's wrapBytes (its file notes now — the - // wrapper is gone) go with the file's last live row, exactly as before. - // F's dry run at the TARGET (only when the row-drop budget is wider): the same walk, on a copy of the live counts, so the - // rows it names are exactly the tail rows the real walk below keeps past the target. + // F: drop whole entries, lowest rank first (sigLadderDropRows), against the row-drop budget. + // F's dry run at the TARGET (only when the row-drop budget is wider) names exactly the tail rows the real walk keeps past it std::size_t wouldDrop = 0; out.targetCutTotal = total; if( dropBudget > effectiveBudget && !fits() ) { - std::vector live( files.size() ); - for( std::size_t i = 0; i < files.size(); ++i ) - { - live[ i ] = files[ i ].liveCount; - } - for( std::size_t k = entries.size(); k > 0 && out.targetCutTotal > effectiveBudget; ) - { - const EntryT& e = entries[ --k ]; - if( e.dropped || e.globalRank <= 4 ) - { - continue; - } - out.targetCutTotal -= entryCost( e ); - ++wouldDrop; - if( --live[ e.fileSlot ] == 0 ) - { - out.targetCutTotal -= files[ e.fileSlot ].wrapBytes; - } - } - } - const auto fitsDrop = [ & ] { return total <= dropBudget; }; - for( std::size_t k = entries.size(); k > 0 && !fitsDrop(); ) - { - EntryT& e = entries[ --k ]; - if( e.dropped || e.globalRank <= 4 ) - { - continue; // rank 1..4 always survive (the floor) - } - total -= entryCost( e ); - e.dropped = true; - ++out.droppedRows; - FileT& sf = files[ e.fileSlot ]; - if( --sf.liveCount == 0 ) - { - total -= sf.wrapBytes; // the file's notes go with its last live row - } + const auto [ rows, cutTotal ] = sigLadderDryDrop( entries, files, total, effectiveBudget, entryCost ); + wouldDrop = rows; + out.targetCutTotal = cutTotal; } + out.droppedRows = sigLadderDropRows( entries, files, total, dropBudget, entryCost ); if( dropBudget == effectiveBudget ) { out.targetCutTotal = total; diff --git a/src/verbs_for.h b/src/verbs_for.h index 94c12b36e..34af233a6 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2453,7 +2453,7 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR // residual the rendered bundle actually left; the shell's bytes were reserved ahead of the body walk // (kForFileTailShellReserve inside the enrichment's committed sum), so the disclosure always fits. // fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): under an explicit ceiling the cut tail carries `cutNextAttr` — -// the --for file page (forTailCutNextAttr) that lists every positive-score file — and the shell reserve the caller +// the --for file page (forTailNext in runForLensPass) that lists every positive-score file — and the shell reserve the caller // took (forTailShellReserve) includes it, so the disclosure still always fits. inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t tokenBudget, std::size_t bundleBudget, std::size_t spentBytes, std::string_view cutNextAttr = {} ) @@ -2468,14 +2468,6 @@ inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t t return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc, cutNextAttr ), esc, cutNextAttr ); } -// The tail's cut continuation: the --for file page (one row per positive-score file, pageable), the one call that lists the -// files the left out. forbudget-soft (decision D-4, PROCESS rule 5: every cut recoverable): in EVERY regime now — the -// default's 24-row cap was the one default cut with no next=; it moves every default answer whose tail is capped by this -// attribute alone (rendered only on a capped tail, renderFileTailXml). -inline std::string forTailCutNextAttr( const rw::Config& cfg ) -{ - return rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ); -} // R2-AF (round 2, S4): render the task's named-file/decl-impl-partner rows (rw::forNamedHeaderRows, // mention.h — the resolution rule and its rationale live there) as ``, root-relative like @@ -3381,7 +3373,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // the first --detail body's floor under a soft target (head-cut with next= when it is over a quarter of the target): the // target fills the rows first, and the floor keeps the one body the caller asked for from being cut to nothing const std::size_t detailFloorBudget = forSoftTarget ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; - const std::string forTailNext = forTailCutNextAttr( cfg ); // D-4: a capped 's next= (the file page), every regime + // D-4 (forbudget-soft; PROCESS rule 5: every cut recoverable): a capped carries next= — the --for file page (one row + // per positive-score file, pageable), the one call that lists the files the tail left out — in EVERY regime (the + // default's 24-row cap was the one default cut with no continuation; renderFileTailXml writes it on a capped tail only) + const std::string forTailNext = rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ); // THE TWO BUDGETS (forbudget-soft; serialize.h kForPayloadBudgetBytes / kForRunawayGuardBytes): the target the // text-shrink steps aim at, and where the row-drop step aims — the runaway guard (default), or the soft target's rows. const ForSigsBudgets sigsBudgets = forSigsBudgets( forSoftTarget ? bundleBudget : 0u, fixedBytes, From 1da6c315b5f0d8ab70af3e22b8c0c5df34732f54 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 16:51:05 -0400 Subject: [PATCH 041/162] feat(attrib): multi-definition answers say which definition each row belongs to A selector naming several same-named definitions answered with the union of their callers, callees or dependants; only defs= said there was more than one. The new src/defattrib.h is the one place that decides it, read by --callers/--callees and their MCP twins (callhierarchy.h computes it once), --safe-delete, --impact (CLI, MCP, the rf rollup) and the --path endpoints: - definition groups: a definition plus the declarations tied to it. C/C++/ObjC use the --edit-check identity rule itself (editCheckDeclFoldTargets, factored out of editCheckFoldDeclGroups with a parser-field pre-filter); TypeScript overload signatures join their implementation. A foreign declaration (another scope chain, a static definition elsewhere) is its own group; a declaration the rule can only call Unproven is silent unless a row ties to it. - rows (JSON/MCP def_table, columnar before ), every row of_def="K..": K proven edge, K~s split label, K~n by name, K~t a primary template whose specialisation is listed. Above 16 groups the table lists the groups the page names (defs_listed=). - impact ties are three reach bitsets over a dense index of the reach set. - Q1: a row (or --path hop) whose edge to the selector is a resolver split carries prov="split" on one-group answers too; --path drops to_p/from_p for an endpoint with several definitions that no proven hop bound. - importers_next= gains its full-legend reading (the import tier passed 40 rows on this tree once defattrib.h includes serialize.h). Gate attribmultidefcheck: base rc=1 (53 PASS / 87 FAIL), head 140 PASS. impactimportcheck / impactpartitioncheck read the new of_def= on their rows. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 19 + src/callhierarchy.h | 95 +-- src/columnar.h | 32 + src/compactlegend.h | 10 + src/defattrib.h | 948 +++++++++++++++++++++++ src/editcheck.h | 44 +- src/editcheckdecl.h | 15 +- src/graph.h | 2 + src/graphlegend.h | 32 + src/mcpverbs.h | 70 +- src/serialize.h | 8 +- src/verbs_navigate.h | 152 +++- test/attribmultidefcheck.sh | 23 +- test/attribmultideffix/cpp/eval_user.cc | 2 +- test/attribmultideffix/cpp/piece_user.cc | 2 +- test/impactimportcheck.sh | 12 +- test/impactpartitioncheck.sh | 4 +- 17 files changed, 1305 insertions(+), 165 deletions(-) create mode 100644 src/defattrib.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 017405813..3e07e5001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,25 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — a multi-definition answer says which definition each row belongs to + +A selector that names several same-named definitions (`--callers=empty` over two classes' `empty`) used to answer with the +union of their callers, callees or dependants, and only `defs=` said there was more than one. `--callers`, `--callees`, +`--safe-delete`, `--impact` (and its file rollup) and their MCP twins now list the definition GROUPS as +`` rows (JSON `def_table`) and tag every row with `of_def="K..."`: `K` a resolved edge +ties the row to group K, `K~s` its edge carries the resolver's `split` label, `K~n` by name alone, `K~t` a resolved edge into a +primary template while a specialisation of it is another group. A group is a definition with the declarations tied to it — the +C/C++/ObjC declaration identity `--edit-check` already uses, and a TypeScript implementation with its overload signatures; a +declaration that identity rule cannot tie stays silent unless a row names it. Above 16 groups the table lists the ones the +page's rows name (`defs_listed=`). Labels come from the call graph's own edge provenance; they are the resolver's binding, not +a type check, and a hedged token does not mean the edge is false. +Two by-design byte changes ride along: a row (or a `--path` hop) whose edge to the selector is one arm of a resolver split +carries `prov="split"` on every answer, single-definition ones included; and `--path` leaves out `to_p=` (`from_p=`) when that +endpoint has several definitions and no proven hop bound one. Measured base vs head on 217 CLI argv over 7 repositories and +16 MCP requests (single-definition selectors, prototype + definition pairs, every dialect): 212 and 16 byte-identical; the +other five are the split-arm break above (2) and one C++ prototype whose parameter types are spelled differently from its +definition (`std::string` vs `string`), which the declaration identity rule reads as another function (3). + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. diff --git a/src/callhierarchy.h b/src/callhierarchy.h index ed1d3220a..f20d23976 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -20,6 +20,7 @@ // surface-specific by contract (the CLI has a legend, the MCP arm speaks JSON-RPC errors), and folding them // in would trade one drift for another. +#include "defattrib.h" // the per-row definition attribution every surface of these rows serves (and tsContractSignatures) #include "filter.h" // pathTierIndexOver / compareTierThenPath — the tier-then-path row order both surfaces serve #include "graph.h" // resolveAllByNameQualified, the CSR, testSymbolForwardReach / countTestedIn / isTestedByReach #include "model.h" @@ -89,6 +90,7 @@ struct CallHierarchyRows std::size_t declinedIface = 0; // callers only: declinedIfaceCallsNaming below, the root's declined_iface= ValueRefRows valueRefs; // reference-as-value round: rows, never in `rows` or any count above StdMemberCallsMade stdCalls; // callees only: of declinedCalls, the C++ standard-member ones (graph.h stdMemberCallsMadeBy) + OneHopAttribution attr; // attrib-multidef (defattrib.h): the definition groups + each row's ties, parallel to `rows` }; // The callees answer's line (graph.h StdMemberGate, stdMemberCallsMadeBy), one spelling per dialect — the JSON keys @@ -141,94 +143,8 @@ inline std::string crossKindValue( const IngestResult& ing, const std::vector tsContractSignatures( const IngestResult& ing ) -{ - struct Span - { - std::uint32_t fileId; - std::uint32_t start; - std::uint32_t end; - NodeId id; - }; - std::vector owners; // every TS class/interface/struct span, by file then start - std::vector methods; - for( NodeId id = 0; id < ing.symbols.size(); ++id ) - { - const Symbol& s = ing.symbols[id]; - if( s.lang != Lang::TypeScript ) - { - continue; - } - if( s.kind == SymKind::Method ) - { - methods.push_back( id ); - } - else if( s.kind == SymKind::Class || s.kind == SymKind::Interface || isStructOrNamedType( s.kind ) ) - { - owners.push_back( Span{ s.fileId, s.sigStartByte, s.endByte, id } ); - } - } - std::sort( owners.begin(), owners.end(), []( const Span& a, const Span& b ) noexcept - { return a.fileId != b.fileId ? a.fileId < b.fileId : a.start < b.start; } ); - // The innermost owner span holding `s` in its own file; kNoNode for a method no owner span holds. - const auto ownerOf = [ & ]( const Symbol& s ) noexcept - { - const auto first = std::lower_bound( owners.begin(), owners.end(), s.fileId, - []( const Span& o, std::uint32_t f ) noexcept { return o.fileId < f; } ); - NodeId best = kNoNode; - std::uint32_t bestLen = 0xFFFFFFFFu; - for( auto it = first; it != owners.end() && it->fileId == s.fileId && it->start <= s.sigStartByte; ++it ) - { - if( s.sigStartByte < it->end && it->end - it->start < bestLen ) - { - best = it->id; - bestLen = it->end - it->start; - } - } - return best; - }; - struct Owned - { - NodeId owner; - std::uint32_t fileId; - std::string_view name; - }; - const auto byOwnerThenName = []( const Owned& a, const Owned& b ) noexcept - { - if( a.fileId != b.fileId ) - { - return a.fileId < b.fileId; - } - return a.owner != b.owner ? a.owner < b.owner : sortutil::svLess( a.name, b.name ); - }; - std::vector bodied; // (file, owner, name) of every bodied TS method - std::vector bodyless; - for( const NodeId id : methods ) - { - const Symbol& s = ing.symbols[id]; - if( s.sigEndByte == s.endByte ) - { - bodyless.push_back( id ); - } - else - { - bodied.push_back( Owned{ ownerOf( s ), s.fileId, s.name } ); - } - } - std::sort( bodied.begin(), bodied.end(), byOwnerThenName ); - std::erase_if( bodyless, [ & ]( NodeId id ) - { - const Symbol& s = ing.symbols[id]; - return std::binary_search( bodied.begin(), bodied.end(), Owned{ ownerOf( s ), s.fileId, s.name }, byOwnerThenName ); - } ); - return bodyless; -} +// tsContractSignatures (the TS contract-vs-overload-signature rule) lives in defattrib.h, beside the definition grouping that +// also reads it. // One declined list's verdict for declinedIfaceCallsNaming: TypeScript, its called name among `sigNames`, and either a // candidate in `isTarget` or the name among `targetSigNames`. A declined list is one called name's same-language @@ -413,6 +329,9 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph rankBeforeCap( ing, out.rows, [ & ]( NodeId r ) { return ing.symbols[r].fileId; }, [ & ]( NodeId r ) { return navRelevanceWeight( g, r ); } ); } + // attrib-multidef: which definition each row belongs to (defattrib.h), over the rows in their FINAL order — one pass over + // the matches' edges, read by every dialect of both surfaces (and their via="name": strongest tie by name alone). + out.attr = attributeOneHopAnswer( ing, g, out.matches, out.rows, wantCallers ); return out; } diff --git a/src/columnar.h b/src/columnar.h index d840ca923..d9847d89f 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -230,11 +230,17 @@ inline void emitColumnarAtColumns( std::FILE* out, const std::vector table printed before , `splitArm` the dense 0/1 `` column (a one-group answer's prov="split" rows, present +// only when some row carries it — columnarViaPresent's rule). struct ColumnarRowExtras { const std::vector* viaName; const std::vector>* atTokens; const std::vector* atTotals; + const std::vector* ofDef = nullptr; + const std::string* defRows = nullptr; + const std::vector* splitArm = nullptr; }; inline std::string columnarExtraFields( const ColumnarRowExtras& extras ) { @@ -243,8 +249,22 @@ inline std::string columnarExtraFields( const ColumnarRowExtras& extras ) { fields += columnarAtCut( *extras.atTokens, *extras.atTotals ) ? ",sites_at,sites_total" : ",sites_at"; } + fields += extras.ofDef != nullptr ? ",of_def" : ""; + fields += columnarViaPresent( extras.splitArm ) ? ",split" : ""; return fields; } +// the of_def column: each row's tokens, comma-separated rows (a token holds no comma) +inline void emitColumnarOfDefColumn( std::FILE* out, const std::vector& ofDef ) +{ + std::string column = ""; + for( std::size_t i = 0; i < ofDef.size(); ++i ) + { + column += i ? "," : ""; + column += ofDef[i]; + } + column += ""; + rw::emitTo( out, "{}", column ); +} // The optional columns a caller asked for, and the fields= suffix naming them, in ONE place: emitColumnarSymbolRows // reads each through these, so its own branch count does not grow per optional column. @@ -273,6 +293,14 @@ inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing { emitColumnarAtColumns( out, *extras.atTokens, *extras.atTotals ); } + if( extras.ofDef != nullptr ) + { + emitColumnarOfDefColumn( out, *extras.ofDef ); + } + if( columnarViaPresent( extras.splitArm ) ) + { + emitColumnarFlagColumn( out, "split", rows.size(), [ & ]( std::size_t i ) { return i < extras.splitArm->size() && ( *extras.splitArm )[ i ] != 0; } ); + } } // COLUMNAR form of a symbol-row verb (--callers/--callees/--impact/--pr-context symbols). `wrapperTag` is the @@ -307,6 +335,10 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( kColumnarLegend, out ); // §B1.5: once per output, before the element it describes rw::emitTo( out, "<{} {} format=\"columnar\">", wrapperTag, wrapperAttrs.c_str() ); emitPathTable( out, ing, uniqueFiles, esc, rootPrefix ); + if( extras.defRows != nullptr ) + { + rw::emitRaw( out, extras.defRows->c_str() ); // attrib-multidef: the table the of_def column's ix= name + } rw::emitTo( out, "", rows.size(), columnarOptionalFields( testReach != nullptr, depth != nullptr ), columnarExtraFields( extras ) ); diff --git a/src/compactlegend.h b/src/compactlegend.h index bfcb2316b..1bdcc939f 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -583,6 +583,16 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // FE-B: a map row merging N byte-identical via=name rows (serialize.h writeMapCalleeRows) carries x=N { "x", ": N same-named via=name rows merged; callees=FILE:SYM lists all", true, "c" }, { "via", ": that edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "e", MapHeaderRead::No, "name" }, + // attrib-multidef (defattrib.h; graphlegend.h kDefTableLegend): the definition table of a multi-group selector, its rows' + // ties, the cut table's count, and the columnar split column. Present-only, element-scoped. A row's prov="split" (Q1) reads + // through the shared prov= row above, which already fires on any prov= in the payload. + { "ix", ": the groups defs= forms (a definition + the declarations tied to it; bodyless=1 tied to none)", true, "def" }, + { "defs_listed", "defs_listed=N: above 16 groups, only the N this page's rows name are listed" }, + { "of_def", "of_def=K..: the def ix=K groups the row belongs to; K resolved edge, K~s edge labelled split, K~n by name alone, K~t into a primary template whose specialisation is listed; NOT a claim the edge is false", true, "s" }, + { "of_def", "c of_def=K..: the def ix=K groups that caller belongs to (K~s split label, K~n by name, K~t primary template); NOT a claim the edge is false", true, "c" }, + { "of_def", "rf of_def=K..: the def ix=K groups its file's reached symbols belong to, strongest tie per group", true, "rf" }, + { "fields", " column: each row's def ix=K groups (K~s split label, K~n by name, K~t primary template), space-separated", true, "cols", MapHeaderRead::No, "of_def" }, + { "fields", " column: 1 where the row's edge to of= carries the resolver's split label (may be another arm's)", true, "cols", MapHeaderRead::No, "split" }, { "fields", " column: 1 = via=name, the row's edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "cols", MapHeaderRead::No, "via" }, // DEPTH-LABELLED --impact (0.6.5): graph.h transitiveCallersDepth's hop per row. The row prints d= run-length // (graph.h depthRunAttrXml: the window's first row and each depth change), the columnar form a dense column, and diff --git a/src/defattrib.h b/src/defattrib.h new file mode 100644 index 000000000..ea5440d90 --- /dev/null +++ b/src/defattrib.h @@ -0,0 +1,948 @@ +#pragma once +// defattrib.h — THE one home of "which of the selector's definitions does this row belong to" (lane attrib-multidef). +// +// THE DEFECT CLASS (round 4, O1 false claims ninja-07 / nfpm-06 / ninja-26): a selector that names several same-named +// definitions (`--callers=empty` with StringPiece::empty and EvalString::empty) answers with the UNION of their callers, +// callees or dependants, and every row read as belonging to one definition. defs="2" said there were two; nothing said +// which row was whose. fix27 hedged the --impact file rollup only. Every verb that folds per-selector rows reads this file: +// --callers/--callees and their MCP twins (callhierarchy.h computes the rows once for both), --safe-delete's caller rows, +// --impact (CLI + MCP) and its rollup, and the --path endpoints. +// +// THE DEFINITION GROUP. A bodied definition plus the bodyless declarations that DECLARE it is one group; any other match is +// a group of its own. Who declares whom is never a new rule here: +// * C, C++, ObjC: the --edit-check identity rule (editcheckdecl.h editCheckTieDeclaration — same full scope chain, member +// qualifiers, parameter-type list, include proof, linkage), through the same fold (editcheck.h editCheckDeclFoldTargets). +// A declaration of `a::detail::f` never pairs with a definition of `b::detail::f` (Symbol::scope is only the innermost +// label — review M1 on that file), and a prototype never pairs with a `static` definition in another file. +// * TypeScript: an overload signature (a bodyless function, or a bodyless method that is not an interface/abstract/ambient +// contract — tsContractSignatures below) joins the implementation that follows it: the language requires the +// implementation to come next in the same declaration list, so the nearest following bodied same-name, same-kind, +// same-scope match in its file is it. +// * Every other language: each match is its own group. Their bodyless matches are contracts (a Java interface method, a +// Rust trait or Swift protocol requirement, a Go interface method), which are other definitions, not declarations of one. +// Python `@overload` stubs carry a `...` body, so each is its own group: a disclosed floor (the rows still attribute to +// whichever stub the resolver bound). +// +// THE ROW ATTRIBUTION. Labels come from the resolver's edges only (Graph::outNameOnly / outProv), never from text: +// K a proven edge ties the row to group K (the resolver's binding, not a type check) +// K~s only edges that are one arm of a call the resolver split between same-named definitions (outProv 3) +// K~n only edges bound by name alone (outNameOnly) +// K~t a proven edge into a PRIMARY template's member while a specialisation of it is another listed group (Q2: the parser +// labels them sc="Box" and sc="Box"; which one the call reaches is unproven). A hedge read off the two scope +// labels — a string relation, so it may only ever WEAKEN a token, never make one. +// callers: row → a member of K; callees: a member of K → row; impact: a chain of calls from the row to a member of K (any +// by-name hop makes the chain ~n, else any split hop ~s); the rollup row: the union of its file's symbols, strongest per K. +// A one-group answer (one definition, or one with its own declarations) carries no table and no of_def; its rows whose every +// non-by-name edge to the selector is a split arm carry prov="split" (the orchestrator's Q1 ruling: whatever defs= is). +// +// WHAT IT DOES NOT MEAN (checklist 3): K is the resolver's binding, not a compiler's — a proven edge can still be wrong (the +// Box case is why ~t exists); K~n / K~s / K~t do not say the edge is false; of_def says nothing about calls the resolver +// declined (declined_calls= stays that floor). + +#include "editcheck.h" // editCheckGroupKeys' fold (editCheckDeclFoldTargets) — the C-family declaration identity +#include "graph.h" +#include "model.h" +#include "pageview.h" +#include "sarif.h" // rootRelativeUri: the table's p= in the rows' own path dialect +#include "serialize.h" // escapeXml / jsonStr / symTag +#include "infra/sortutil.h" // svLess: tsContractSignatures' explicit byte order + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +// The table lists every group up to this many; above it, only the groups a row on the page names (defs_listed= says how +// many it listed). textual's `render` has 61 definitions and `compose` 1047: a full table there would outweigh the page. +inline constexpr std::size_t kDefTableCap = 16; + +// One row's tie to one group, weakest first, so the strongest of several edges is a max(). +enum class DefMark : std::uint8_t +{ + None = 0, + Name = 1, // K~n + Split = 2, // K~s + Proven = 3, // K (or K~t when the group is a primary template with a listed specialisation) +}; + +struct DefGroups +{ + std::vector ofMatch; // parallel to the matches: the group each one belongs to + std::vector rep; // per group: the bodied member (the table's t=/sc=/p=), else its first match + std::vector bodyless; // per group: no member has a body (a declaration that declares no listed definition) + std::vector primaryOfSpec; // per group: its sc= is the template base of another group's sc= (the ~t mark) + std::vector untied; // per group: C-family declarations only, which MAY declare a listed definition (the + // identity rule's Unproven: a template-qualified or macro-built declarator, no shared + // #include) — silent unless a row ties to it (defGroupsDropSilent) + + std::size_t size() const noexcept { return rep.size(); } + bool multi() const noexcept { return rep.size() >= 2; } +}; + +// The TypeScript methods with no body that are a CONTRACT — an interface member (method_signature), an abstract or an +// ambient (.d.ts) member — as their own symbol ids, minus overload signatures: those sit in the same OWNER (the innermost +// class or interface whose span holds them, in the same file) as a bodied method of the same name, which implements +// them, so their receiver is that class and not an interface. The owner is part of the key because TS methods carry no +// `scope`: keyed by file alone, an `interface Router { match(): void }` beside an unrelated `class Matcher { match() {} }` +// lost its signature (test/declinecheck.sh (I), the same-file arm). +inline std::vector tsContractSignatures( const IngestResult& ing ) +{ + struct Span + { + std::uint32_t fileId; + std::uint32_t start; + std::uint32_t end; + NodeId id; + }; + std::vector owners; // every TS class/interface/struct span, by file then start + std::vector methods; + for( NodeId id = 0; id < ing.symbols.size(); ++id ) + { + const Symbol& s = ing.symbols[id]; + if( s.lang != Lang::TypeScript ) + { + continue; + } + if( s.kind == SymKind::Method ) + { + methods.push_back( id ); + } + else if( s.kind == SymKind::Class || s.kind == SymKind::Interface || isStructOrNamedType( s.kind ) ) + { + owners.push_back( Span{ s.fileId, s.sigStartByte, s.endByte, id } ); + } + } + std::sort( owners.begin(), owners.end(), []( const Span& a, const Span& b ) noexcept + { return a.fileId != b.fileId ? a.fileId < b.fileId : a.start < b.start; } ); + // The innermost owner span holding `s` in its own file; kNoNode for a method no owner span holds. + const auto ownerOf = [ & ]( const Symbol& s ) noexcept + { + const auto first = std::lower_bound( owners.begin(), owners.end(), s.fileId, + []( const Span& o, std::uint32_t f ) noexcept { return o.fileId < f; } ); + NodeId best = kNoNode; + std::uint32_t bestLen = 0xFFFFFFFFu; + for( auto it = first; it != owners.end() && it->fileId == s.fileId && it->start <= s.sigStartByte; ++it ) + { + if( s.sigStartByte < it->end && it->end - it->start < bestLen ) + { + best = it->id; + bestLen = it->end - it->start; + } + } + return best; + }; + struct Owned + { + NodeId owner; + std::uint32_t fileId; + std::string_view name; + }; + const auto byOwnerThenName = []( const Owned& a, const Owned& b ) noexcept + { + if( a.fileId != b.fileId ) + { + return a.fileId < b.fileId; + } + return a.owner != b.owner ? a.owner < b.owner : sortutil::svLess( a.name, b.name ); + }; + std::vector bodied; // (file, owner, name) of every bodied TS method + std::vector bodyless; + for( const NodeId id : methods ) + { + const Symbol& s = ing.symbols[id]; + if( s.sigEndByte == s.endByte ) + { + bodyless.push_back( id ); + } + else + { + bodied.push_back( Owned{ ownerOf( s ), s.fileId, s.name } ); + } + } + std::sort( bodied.begin(), bodied.end(), byOwnerThenName ); + std::erase_if( bodyless, [ & ]( NodeId id ) + { + const Symbol& s = ing.symbols[id]; + return std::binary_search( bodied.begin(), bodied.end(), Owned{ ownerOf( s ), s.fileId, s.name }, byOwnerThenName ); + } ); + return bodyless; +} + +// TypeScript's overload-signature rule (see the header): the implementation a bodyless TS match `i` declares, or i itself. +inline std::size_t defGroupTsImplementation( const IngestResult& ing, std::span matches, std::size_t i, + std::span contracts ) +{ + const Symbol& d = ing.symbols[ matches[i] ]; + if( std::binary_search( contracts.begin(), contracts.end(), matches[i] ) ) + { + return i; // an interface / abstract / ambient member: a contract, not a declaration of a body + } + std::size_t best = i; + for( std::size_t j = 0; j < matches.size(); ++j ) + { + const Symbol& f = ing.symbols[ matches[j] ]; + if( j == i || f.lang != Lang::TypeScript || f.fileId != d.fileId || f.kind != d.kind || f.name != d.name || f.scope != d.scope + || !isDefinitionNotDeclaration( f ) || f.sigStartByte < d.endByte ) + { + continue; + } + if( best == i || f.sigStartByte < ing.symbols[ matches[best] ].sigStartByte ) + { + best = j; + } + } + return best; +} + +// The fold target of every match (its own index when it declares no other listed match), by the two rules above. +inline std::vector defGroupTargets( const IngestResult& ing, std::span matches ) +{ + std::vector target( matches.size() ); + bool anyCDecl = false; + bool anyTsDecl = false; + for( std::size_t i = 0; i < matches.size(); ++i ) + { + target[i] = i; + const Symbol& s = ing.symbols[ matches[i] ]; + const bool bodyless = !isDefinitionNotDeclaration( s ); + anyCDecl = anyCDecl || ( bodyless && langCompatible( s.lang, Lang::C ) ); + anyTsDecl = anyTsDecl || ( bodyless && s.lang == Lang::TypeScript && ( s.kind == SymKind::Function || s.kind == SymKind::Method ) ); + } + if( anyCDecl ) + { + // One editcheck group per match, so the fold's "definitions in two groups" rule reads "declares two definitions": + // such a declaration (one header, a posix.cpp and a win.cpp) stays its own group. + EditCheckGroupKeys gk; + for( std::size_t i = 0; i < matches.size(); ++i ) + { + gk.keys.emplace_back( ing.symbols[ matches[i] ].fileId, std::to_string( i ) ); + gk.members.push_back( { matches[i] } ); + } + target = editCheckDeclFoldTargets( ing, gk ); + } + if( anyTsDecl ) + { + std::vector contracts = tsContractSignatures( ing ); + std::sort( contracts.begin(), contracts.end() ); + for( std::size_t i = 0; i < matches.size(); ++i ) + { + const Symbol& s = ing.symbols[ matches[i] ]; + if( s.lang == Lang::TypeScript && !isDefinitionNotDeclaration( s ) && ( s.kind == SymKind::Function || s.kind == SymKind::Method ) ) + { + target[i] = defGroupTsImplementation( ing, matches, i, contracts ); + } + } + } + ENSURES( target.size() == matches.size() ); + return target; +} + +// Per group: 1 when every member is a C-family declaration and the identity rule answers Unproven (may declare, could not prove) +// for one of the listed definitions. A Different-from-all declaration (another scope chain, a static definition elsewhere) is a +// separate function and stays a group of its own; an Unproven one may be the same function, so it is listed only when a row +// actually ties to it — otherwise every template class's out-of-line member (`template void Box::grow()`, an +// unknown chain) and every local forward declaration would grow a table about a tie nobody can prove either way. +inline std::vector defGroupUntied( const IngestResult& ing, std::span matches, const DefGroups& groups ); + +// Q2: `base` is the template base of `spec` — spec spells base<…> (the parser's scope labels: sc="Box" vs sc="Box"). +inline bool defScopeIsTemplateBaseOf( std::string_view base, std::string_view spec ) noexcept +{ + return !base.empty() && spec.size() > base.size() + 2 && spec.starts_with( base ) && spec[ base.size() ] == '<' && spec.back() == '>'; +} + +inline DefGroups defGroupsOf( const IngestResult& ing, std::span matches ) +{ + DefGroups out; + if( matches.size() < 2 ) + { + out.ofMatch.assign( matches.size(), 0 ); + if( !matches.empty() ) + { + out.rep.push_back( matches.front() ); + out.bodyless.push_back( isDefinitionNotDeclaration( ing.symbols[ matches.front() ] ) ? 0 : 1 ); + out.primaryOfSpec.push_back( 0 ); + out.untied.push_back( 0 ); + } + return out; + } + const std::vector target = defGroupTargets( ing, matches ); + std::vector groupOfKept( matches.size(), UINT32_MAX ); + for( std::size_t i = 0; i < matches.size(); ++i ) // the resolver's match order: group ix follows the first member + { + const std::size_t t = target[i]; + ASSUME( t < matches.size() && target[t] == t, "a fold target is kept (it holds the body the declaration declares)" ); + if( groupOfKept[t] == UINT32_MAX ) + { + groupOfKept[t] = std::uint32_t( out.rep.size() ); + out.rep.push_back( matches[t] ); + out.bodyless.push_back( isDefinitionNotDeclaration( ing.symbols[ matches[t] ] ) ? 0 : 1 ); + } + } + out.ofMatch.resize( matches.size() ); + for( std::size_t i = 0; i < matches.size(); ++i ) + { + out.ofMatch[i] = groupOfKept[ target[i] ]; + } + out.primaryOfSpec.assign( out.rep.size(), 0 ); + for( std::size_t a = 0; a < out.rep.size(); ++a ) + { + const Symbol& p = ing.symbols[ out.rep[a] ]; + for( std::size_t b = 0; b < out.rep.size() && !out.primaryOfSpec[a]; ++b ) + { + const Symbol& q = ing.symbols[ out.rep[b] ]; + out.primaryOfSpec[a] = ( b != a && p.name == q.name && langCompatible( p.lang, q.lang ) && defScopeIsTemplateBaseOf( p.scope, q.scope ) ) ? 1 : 0; + } + } + out.untied = defGroupUntied( ing, matches, out ); + ENSURES( out.ofMatch.size() == matches.size() && out.rep.size() <= matches.size() && out.untied.size() == out.rep.size() ); + return out; +} + +inline std::vector defGroupUntied( const IngestResult& ing, std::span matches, const DefGroups& groups ) +{ + std::vector untied( groups.size(), 0 ); + std::vector declOnly( groups.size(), 1 ); + for( std::size_t i = 0; i < matches.size(); ++i ) + { + const Symbol& s = ing.symbols[ matches[i] ]; + declOnly[ groups.ofMatch[i] ] &= char( langCompatible( s.lang, Lang::C ) && !isDefinitionNotDeclaration( s ) ); + } + if( std::none_of( declOnly.begin(), declOnly.end(), []( char c ) { return c != 0; } ) ) + { + return untied; + } + EditCheckSources sources( ing, EditCheckSpliced{} ); + std::deque> defSigs; // each definition's signature, read once (stable refs) + for( std::size_t i = 0; i < matches.size(); ++i ) + { + const std::uint32_t grp = groups.ofMatch[i]; + if( !declOnly[grp] || untied[grp] ) + { + continue; + } + for( std::size_t j = 0; j < matches.size() && !untied[grp]; ++j ) + { + const Symbol& f = ing.symbols[ matches[j] ]; + if( !editCheckTieCandidate( ing.symbols[ matches[i] ], f ) ) + { + continue; // Different on parser fields alone: no source read + } + auto at = std::find_if( defSigs.begin(), defSigs.end(), [ & ]( const auto& e ) { return e.first == matches[j]; } ); + if( at == defSigs.end() ) + { + defSigs.emplace_back( matches[j], editCheckSignatureOf( sources, f ) ); + at = std::prev( defSigs.end() ); + } + untied[grp] = editCheckTieDeclaration( ing, sources, matches[i], matches[j], at->second ).tie == EditCheckTie::Unproven ? 1 : 0; + } + } + return untied; +} + +// ── the per-row tokens ────────────────────────────────────────────────────────────────────────────────────────────── +// A CSR over the rows (in the served order): row r's ties are tok[off[r] .. off[r+1]), each (group << 2) | DefMark, groups +// ascending. Built once per answer; every dialect and both transports read it. +struct RowDefTokens +{ + std::vector off; + std::vector tok; + + bool empty() const noexcept { return off.empty(); } + std::span of( std::size_t row ) const noexcept + { + return row + 1 < off.size() ? std::span( tok.data() + off[row], off[row + 1] - off[row] ) + : std::span(); + } + // the strongest tie the row has to any group (None when the row has none recorded) + DefMark strongest( std::size_t row ) const noexcept + { + std::uint32_t best = 0; + for( const std::uint32_t t : of( row ) ) + { + best = std::max( best, t & 3u ); + } + return DefMark( best ); + } +}; + +// One out-edge's tie, off its two parallel per-edge bytes (either may be unallocated: a graph with no such edge). +inline DefMark defMarkOfEdge( const Graph& g, std::size_t e ) noexcept +{ + if( e < g.outNameOnly.size() && g.outNameOnly[e] != 0 ) + { + return DefMark::Name; + } + return ( e < g.outProv.size() && g.outProv[e] == 3u ) ? DefMark::Split : DefMark::Proven; +} + +// The out-CSR position of from → to, or SIZE_MAX (ascending within a source: buildGraph sorts by (from, to)). +inline std::size_t defEdgeIndex( const Graph& g, NodeId from, NodeId to ) noexcept +{ + if( std::size_t( from ) + 1 >= g.outOff.size() ) + { + return SIZE_MAX; + } + const auto b = g.outTargets.begin() + g.outOff[ from ]; + const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; + const auto it = std::lower_bound( b, e, to ); + return ( it != e && *it == to ) ? std::size_t( it - g.outTargets.begin() ) : SIZE_MAX; +} + +// FE-B's two path predicates' sibling: the hop from → to is one arm of a resolver split (and not by name alone). +inline bool edgeSplitArm( const Graph& g, NodeId from, NodeId to ) noexcept +{ + const std::size_t e = defEdgeIndex( g, from, to ); + return e != SIZE_MAX && defMarkOfEdge( g, e ) == DefMark::Split; +} + +// (row node, group, mark) facts → the CSR over `rows`, strongest mark per (row, group). +struct DefTie +{ + NodeId node; + std::uint32_t group; + std::uint8_t mark; +}; +inline RowDefTokens rowDefTokensFrom( std::vector& ties, std::span rows ) +{ + std::sort( ties.begin(), ties.end(), []( const DefTie& a, const DefTie& b ) noexcept + { return a.node != b.node ? a.node < b.node : ( a.group != b.group ? a.group < b.group : a.mark > b.mark ); } ); + RowDefTokens out; + out.off.reserve( rows.size() + 1 ); + out.off.push_back( 0 ); + for( const NodeId r : rows ) + { + auto it = std::lower_bound( ties.begin(), ties.end(), r, []( const DefTie& t, NodeId n ) noexcept { return t.node < n; } ); + for( ; it != ties.end() && it->node == r; ++it ) + { + if( out.tok.size() > out.off.back() && ( out.tok.back() >> 2 ) == it->group ) + { + continue; // a weaker tie to the same group (sorted strongest first) + } + out.tok.push_back( ( it->group << 2 ) | it->mark ); + } + out.off.push_back( std::uint32_t( out.tok.size() ) ); + } + ENSURES( out.off.size() == rows.size() + 1 ); + return out; +} + +// --callers / --callees / --safe-delete: one pass over each match's edges (the walk that built the rows). `towardMatches`: +// the rows are callers (row → match); else callees (match → row). +inline RowDefTokens attributeOneHop( const Graph& g, const DefGroups& groups, std::span matches, + std::span rows, bool towardMatches ) +{ + EXPECTS( groups.ofMatch.size() == matches.size(), "the groups are defGroupsOf( matches )" ); + std::vector ties; + const auto* ro = g.inEdges.rowOffsets(); + const auto* ci = g.inEdges.colIndices(); + for( std::size_t mi = 0; mi < matches.size(); ++mi ) + { + const NodeId m = matches[mi]; + const std::uint32_t grp = groups.ofMatch[mi]; + if( std::size_t( m ) + 1 >= g.outOff.size() ) + { + continue; + } + if( towardMatches ) + { + for( std::uint32_t k = ro[m]; k < ro[m + 1]; ++k ) + { + const std::size_t e = defEdgeIndex( g, ci[k], m ); + if( e != SIZE_MAX ) + { + ties.push_back( DefTie{ ci[k], grp, std::uint8_t( defMarkOfEdge( g, e ) ) } ); + } + } + } + else + { + for( std::uint32_t k = g.outOff[m]; k < g.outOff[ std::size_t( m ) + 1 ]; ++k ) + { + ties.push_back( DefTie{ g.outTargets[k], grp, std::uint8_t( defMarkOfEdge( g, k ) ) } ); + } + } + } + return rowDefTokensFrom( ties, rows ); +} + +// --impact: the three reaches of every reached node, as bitsets over the groups — P (an all-proven chain), S (no by-name hop), +// A (any chain) — propagated from the seeds over the in-edges until nothing grows (monotone, so it ends; a cycle only re-adds +// bits it already holds). The bitsets live on a dense index of the reach set plus the seeds, never on every node (review S5: +// 1047 groups over all nodes would be megabytes per walk). Token: K from P, K~s from S\P, K~n from A\S. +inline RowDefTokens attributeReach( const Graph& g, const DefGroups& groups, std::span seeds, std::span reach, + std::span rows ) +{ + EXPECTS( groups.ofMatch.size() == seeds.size(), "the groups are defGroupsOf( seeds )" ); + std::vector dense( reach.begin(), reach.end() ); + dense.insert( dense.end(), seeds.begin(), seeds.end() ); + std::sort( dense.begin(), dense.end() ); + dense.erase( std::unique( dense.begin(), dense.end() ), dense.end() ); + const auto denseOf = [ & ]( NodeId n ) noexcept -> std::size_t + { + const auto it = std::lower_bound( dense.begin(), dense.end(), n ); + return ( it != dense.end() && *it == n ) ? std::size_t( it - dense.begin() ) : SIZE_MAX; + }; + const std::size_t words = ( groups.size() + 63 ) / 64; + std::vector pBits( dense.size() * words, 0 ), sBits( dense.size() * words, 0 ), aBits( dense.size() * words, 0 ); + std::vector queued( dense.size(), 0 ); + std::vector work; + for( std::size_t si = 0; si < seeds.size(); ++si ) + { + const std::size_t d = denseOf( seeds[si] ); + const std::size_t grp = groups.ofMatch[si]; + ASSUME( d != SIZE_MAX, "every seed is in the dense index (it was inserted)" ); + const std::uint64_t bit = std::uint64_t( 1 ) << ( grp % 64 ); + pBits[ d * words + grp / 64 ] |= bit; + sBits[ d * words + grp / 64 ] |= bit; + aBits[ d * words + grp / 64 ] |= bit; + if( !queued[d] ) + { + queued[d] = 1; + work.push_back( d ); + } + } + const auto* ro = g.inEdges.rowOffsets(); + const auto* ci = g.inEdges.colIndices(); + while( !work.empty() ) + { + const std::size_t u = work.back(); + work.pop_back(); + queued[u] = 0; + const NodeId un = dense[u]; + for( std::uint32_t k = ro[un]; k < ro[un + 1]; ++k ) + { + const std::size_t c = denseOf( ci[k] ); + const std::size_t e = defEdgeIndex( g, ci[k], un ); + if( c == SIZE_MAX || e == SIZE_MAX ) + { + continue; // outside the reach set (cannot happen for a full closure) or no out-edge twin + } + const DefMark mark = defMarkOfEdge( g, e ); + bool changed = false; + for( std::size_t w = 0; w < words; ++w ) + { + const std::uint64_t a = aBits[ c * words + w ] | aBits[ u * words + w ]; + const std::uint64_t s = mark >= DefMark::Split ? ( sBits[ c * words + w ] | sBits[ u * words + w ] ) : sBits[ c * words + w ]; + const std::uint64_t p = mark == DefMark::Proven ? ( pBits[ c * words + w ] | pBits[ u * words + w ] ) : pBits[ c * words + w ]; + changed = changed || a != aBits[ c * words + w ] || s != sBits[ c * words + w ] || p != pBits[ c * words + w ]; + aBits[ c * words + w ] = a; + sBits[ c * words + w ] = s; + pBits[ c * words + w ] = p; + } + if( changed && !queued[c] ) + { + queued[c] = 1; + work.push_back( c ); + } + } + } + std::vector ties; + for( const NodeId r : rows ) + { + const std::size_t d = denseOf( r ); + if( d == SIZE_MAX ) + { + continue; + } + for( std::size_t grp = 0; grp < groups.size(); ++grp ) + { + const std::uint64_t bit = std::uint64_t( 1 ) << ( grp % 64 ); + const std::size_t w = d * words + grp / 64; + const DefMark m = ( pBits[w] & bit ) ? DefMark::Proven : ( sBits[w] & bit ) ? DefMark::Split : ( aBits[w] & bit ) ? DefMark::Name : DefMark::None; + if( m != DefMark::None ) + { + ties.push_back( DefTie{ r, std::uint32_t( grp ), std::uint8_t( m ) } ); + } + } + } + return rowDefTokensFrom( ties, rows ); +} + +// The --impact rollup: per FILE, the union of its reached symbols' ties, the strongest per group. `files` are the +// rollup rows' file ids in their order; `reach` with `reachTok` (attributeReach over `reach` itself) supplies the symbols. +inline RowDefTokens attributeFiles( const IngestResult& ing, std::span files, std::span reach, + const RowDefTokens& reachTok ) +{ + std::vector ties; + for( std::size_t i = 0; i < reach.size(); ++i ) + { + for( const std::uint32_t t : reachTok.of( i ) ) + { + ties.push_back( DefTie{ NodeId( ing.symbols[ reach[i] ].fileId ), t >> 2, std::uint8_t( t & 3u ) } ); + } + } + std::vector fileRows( files.begin(), files.end() ); + return rowDefTokensFrom( ties, fileRows ); +} + +// ── the table and the rendering ────────────────────────────────────────────────────────────────────────────────────── +// Which groups the table lists: all of them up to kDefTableCap, else the ones a shown row names (`named`, by group). +struct DefTableListing +{ + std::vector listed; // ascending group indices + bool cut = false; +}; +inline void defTableMarkNamed( std::vector& named, const DefGroups& groups, const RowDefTokens& tokens, std::size_t begin, std::size_t end ) +{ + named.resize( groups.size(), 0 ); + for( std::size_t r = begin; r < end; ++r ) + { + for( const std::uint32_t t : tokens.of( r ) ) + { + named[ t >> 2 ] = 1; + } + } +} +inline DefTableListing defTableListing( const DefGroups& groups, const std::vector& named ) +{ + DefTableListing out; + const bool all = groups.size() <= kDefTableCap; + for( std::size_t k = 0; k < groups.size(); ++k ) + { + if( all || ( k < named.size() && named[k] ) ) + { + out.listed.push_back( std::uint32_t( k ) ); + } + } + out.cut = out.listed.size() < groups.size(); + return out; +} + +// One token: K (1-based, the table's ix=) and its mark. +inline void defTokenTo( std::string& out, const DefGroups& groups, std::uint32_t tok ) +{ + const std::uint32_t grp = tok >> 2; + const DefMark mark = DefMark( tok & 3u ); + out += std::to_string( grp + 1 ); + if( mark == DefMark::Name ) + { + out += "~n"; + } + else if( mark == DefMark::Split ) + { + out += "~s"; + } + else if( grp < groups.primaryOfSpec.size() && groups.primaryOfSpec[grp] ) + { + out += "~t"; + } +} +// A row's tokens, space-separated (the sites_at= convention). +inline void defTokensTo( std::string& out, const DefGroups& groups, std::span toks ) +{ + for( std::size_t i = 0; i < toks.size(); ++i ) + { + out += i ? " " : ""; + defTokenTo( out, groups, toks[i] ); + } +} +inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) +{ + if( !groups.multi() || tokens.empty() ) + { + return {}; + } + std::string out = " of_def=\""; + defTokensTo( out, groups, tokens.of( row ) ); + out += '"'; + return out; +} +inline std::string ofDefKeyJson( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) +{ + if( !groups.multi() || tokens.empty() ) + { + return {}; + } + std::string out = ",\"of_def\":["; + const std::span toks = tokens.of( row ); + for( std::size_t i = 0; i < toks.size(); ++i ) + { + out += i ? ",\"" : "\""; + defTokenTo( out, groups, toks[i] ); + out += '"'; + } + out += "]"; + return out; +} +// The columnar of_def column's value for one row: the tokens space-separated (the sites_at column's convention). +inline std::string ofDefColumnValue( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) +{ + std::string out; + defTokensTo( out, groups, tokens.of( row ) ); + return out; +} + +// Q1: a row of a ONE-group answer whose every non-by-name edge to the selector is a split arm says so, in the map's per-edge +// vocabulary. Never on a multi-group answer (its tokens carry ~s), never on a by-name row (via="name" already hedges it). +inline bool rowSplitArm( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) noexcept +{ + return !groups.multi() && !tokens.empty() && tokens.strongest( row ) == DefMark::Split; +} +inline const char* splitArmAttrXml( bool on ) noexcept { return on ? " prov=\"split\"" : ""; } +inline const char* splitArmKeyJson( bool on ) noexcept { return on ? ",\"prov\":\"split\"" : ""; } + +// The rows (XML; the columnar form prints the same rows before its ). +inline std::string defTableXml( const IngestResult& ing, const DefGroups& groups, const DefTableListing& listing, std::string_view rootPrefix ) +{ + if( !groups.multi() ) + { + return {}; + } + // An empty listing is reachable (above the cap, a page past the last row names no group): it prints no rows, and the root's + // defs_listed="0" says the table was cut to nothing. + std::vector esc; + std::string out; + for( const std::uint32_t k : listing.listed ) + { + const Symbol& s = ing.symbols[ groups.rep[k] ]; + const std::string_view raw = ing.files[ s.fileId ]; + const std::string_view rel = rootPrefix.empty() ? raw : sarif::rootRelativeUri( raw, rootPrefix ); + out += "" : "/>"; + } + return out; +} +// The CLI JSON dialect: "def_table":[{"ix","t","sc","p"[,"bodyless"]}] (the XML attribute names); `mcp` spells the MCP row +// vocabulary instead ({"ix","kind","scope","file","line"[,"bodyless"]}), the keys find_symbol's own rows use. +inline std::string defTableJson( const IngestResult& ing, const DefGroups& groups, const DefTableListing& listing, std::string_view rootPrefix, bool mcp ) +{ + if( !groups.multi() ) + { + return {}; + } + std::string out = ",\"def_table\":["; + for( std::size_t i = 0; i < listing.listed.size(); ++i ) + { + const std::uint32_t k = listing.listed[i]; + const Symbol& s = ing.symbols[ groups.rep[k] ]; + const std::string_view raw = ing.files[ s.fileId ]; + const std::string_view rel = rootPrefix.empty() ? raw : sarif::rootRelativeUri( raw, rootPrefix ); + out += ( i ? ",{\"ix\":" : "{\"ix\":" ) + std::to_string( k + 1 ); + out += mcp ? ",\"kind\":\"" : ",\"t\":\""; + out += symTag( s.kind ); + out += '"'; + if( !s.scope.empty() ) + { + out += ( mcp ? ",\"scope\":\"" : ",\"sc\":\"" ) + jsonStr( s.scope ) + "\""; + } + out += mcp ? ",\"file\":\"" + jsonStr( rel ) + "\",\"line\":" + std::to_string( s.line ) + : ",\"p\":\"" + jsonStr( rel ) + ":" + std::to_string( s.line ) + "\""; + out += groups.bodyless[k] ? ",\"bodyless\":1}" : "}"; + } + out += "]"; + if( listing.cut ) + { + out += ",\"defs_listed\":" + std::to_string( listing.listed.size() ); + } + return out; +} +inline std::string defsListedAttrXml( const DefGroups& groups, const DefTableListing& listing ) +{ + return ( groups.multi() && listing.cut ) ? " defs_listed=\"" + std::to_string( listing.listed.size() ) + "\"" : std::string(); +} + +// An answer's groups once its ties are known: an untied group (defGroupUntied) no row names is dropped, the rest renumbered in +// order, and the tokens rewritten to the new numbers. Fewer than two groups left: a one-group answer (no table, no of_def). +inline void defGroupsDropSilent( DefGroups& groups, RowDefTokens& tokens ) +{ + if( !groups.multi() || std::none_of( groups.untied.begin(), groups.untied.end(), []( char c ) { return c != 0; } ) ) + { + return; + } + std::vector named( groups.size(), 0 ); + for( const std::uint32_t t : tokens.tok ) + { + named[ t >> 2 ] = 1; + } + std::vector renum( groups.size(), UINT32_MAX ); + DefGroups kept; + for( std::size_t k = 0; k < groups.size(); ++k ) + { + if( groups.untied[k] && !named[k] ) + { + continue; + } + renum[k] = std::uint32_t( kept.rep.size() ); + kept.rep.push_back( groups.rep[k] ); + kept.bodyless.push_back( groups.bodyless[k] ); + kept.primaryOfSpec.push_back( groups.primaryOfSpec[k] ); + kept.untied.push_back( groups.untied[k] ); + } + kept.ofMatch.reserve( groups.ofMatch.size() ); + for( const std::uint32_t g : groups.ofMatch ) + { + kept.ofMatch.push_back( renum[g] ); // UINT32_MAX: a silent group's member (no row ties to it) + } + for( std::uint32_t& t : tokens.tok ) + { + ASSUME( renum[ t >> 2 ] != UINT32_MAX, "a group a row names is kept" ); + t = ( renum[ t >> 2 ] << 2 ) | ( t & 3u ); + } + groups = std::move( kept ); +} + +// ── one answer's attribution, computed once ────────────────────────────────────────────────────────────────────────── +// What a 1-hop answer (callers / callees / safe-delete) carries. Computed only when it can say something: a multi-group +// selector, or a graph that holds split arms (Q1) or by-name edges (the rows' via="name" reads strongest() == Name). +struct OneHopAttribution +{ + DefGroups groups; + RowDefTokens tokens; // parallel to the rows; empty when nothing was computed + + bool nameOnly( std::size_t row ) const noexcept { return !tokens.empty() && tokens.strongest( row ) == DefMark::Name; } + bool splitArm( std::size_t row ) const noexcept { return rowSplitArm( groups, tokens, row ); } +}; +inline OneHopAttribution attributeOneHopAnswer( const IngestResult& ing, const Graph& g, std::span matches, + std::span rows, bool towardMatches ) +{ + OneHopAttribution out; + out.groups = defGroupsOf( ing, matches ); + if( out.groups.multi() || !g.outProv.empty() || !g.outNameOnly.empty() ) + { + out.tokens = attributeOneHop( g, out.groups, matches, rows, towardMatches ); + defGroupsDropSilent( out.groups, out.tokens ); + } + return out; +} + +// What an --impact answer carries (CLI and MCP): the groups, and each reached node's ties through the chains of calls. +// Computed only on a multi-group selector — a one-group answer keeps its bytes (its rows' via="name" is provenCallerReach's). +struct ReachAttribution +{ + DefGroups groups; + std::span reach; // ascending (transitiveCallersDepth sorts it); the caller keeps the vector alive + RowDefTokens tokens; // parallel to `reach`; empty unless groups.multi() + + std::size_t indexOf( NodeId n ) const noexcept + { + const auto it = std::lower_bound( reach.begin(), reach.end(), n ); + return ( it != reach.end() && *it == n ) ? std::size_t( it - reach.begin() ) : SIZE_MAX; + } + std::string ofDefXml( NodeId n ) const + { + const std::size_t i = indexOf( n ); + return i == SIZE_MAX ? std::string() : ofDefAttrXml( groups, tokens, i ); + } + std::string ofDefJson( NodeId n ) const + { + const std::size_t i = indexOf( n ); + return i == SIZE_MAX ? std::string() : ofDefKeyJson( groups, tokens, i ); + } + void markNamed( std::vector& named, NodeId n ) const + { + if( const std::size_t i = indexOf( n ); i != SIZE_MAX ) + { + defTableMarkNamed( named, groups, tokens, i, i + 1 ); + } + } +}; +inline ReachAttribution attributeReachAnswer( const IngestResult& ing, const Graph& g, std::span seeds, std::span reach ) +{ + ReachAttribution out; + out.groups = defGroupsOf( ing, seeds ); + out.reach = reach; + if( out.groups.multi() ) + { + out.tokens = attributeReach( g, out.groups, seeds, reach, reach ); + defGroupsDropSilent( out.groups, out.tokens ); + if( !out.groups.multi() ) + { + out.tokens = RowDefTokens{}; // one group left: the answer keeps its bytes (impact carries no one-group mark) + } + } + return out; +} +// The rollup rows' of_def (the union of each listed file's reached symbols, strongest per group), rendered once for both +// dialects onto the rows the page shows, and the groups they name marked for the table. +template +inline void attachRollupOfDef( RollupT& rollup, const IngestResult& ing, const ReachAttribution& a, std::vector& named ) +{ + if( !a.groups.multi() || rollup.rows.empty() ) + { + return; + } + std::vector files; + for( std::size_t i = 0; i < rollup.shown; ++i ) + { + files.push_back( rollup.rows[i].fileId ); + } + const RowDefTokens fileTok = attributeFiles( ing, files, a.reach, a.tokens ); + defTableMarkNamed( named, a.groups, fileTok, 0, files.size() ); + for( std::size_t i = 0; i < rollup.shown; ++i ) + { + rollup.rows[i].ofDefXml = ofDefAttrXml( a.groups, fileTok, i ); + rollup.rows[i].ofDefJson = ofDefKeyJson( a.groups, fileTok, i ); + } +} + +// ── --path endpoints ───────────────────────────────────────────────────────────────────────────────────────────────── +// from_p= / to_p= name "the definition the endpoint was bound to". With several definitions that claim needs a binding: +// the path's own start (from=) or a PROVEN hop into its end (to=). Unreached, or reached through a split arm or a by-name +// hop, the endpoint was not bound and the attribute is left out (the hop row's prov=/via= says why). +// `srcDefs` / `dstDefs` are endpointDefinitionCount's, not the raw match counts. +struct PathEndpointBinding +{ + bool from = true; + bool to = true; +}; +// How many definitions an endpoint really names: its groups (a definition with its declarations is one), less the untied +// declarations (defGroupUntied) — a prototype the identity rule cannot prove is the same function is no second candidate here, +// exactly as it is silent on a 1-hop answer no row ties it to. +inline std::size_t endpointDefinitionCount( const IngestResult& ing, std::span defs ) +{ + if( defs.size() < 2 ) + { + return defs.size(); + } + const DefGroups groups = defGroupsOf( ing, defs ); + const std::size_t untied = std::size_t( std::count( groups.untied.begin(), groups.untied.end(), char( 1 ) ) ); + return std::max( 1, groups.size() - untied ); +} +inline PathEndpointBinding pathEndpointBinding( const Graph& g, std::size_t srcDefs, std::size_t dstDefs, std::span path ) noexcept +{ + PathEndpointBinding b; + b.from = srcDefs <= 1 || !path.empty(); + if( dstDefs > 1 ) + { + if( path.size() < 2 ) + { + b.to = path.size() == 1; // from= is to= (a zero-hop path binds it); unreached binds nothing + } + else + { + const std::size_t e = defEdgeIndex( g, path[ path.size() - 2 ], path.back() ); + b.to = e != SIZE_MAX && defMarkOfEdge( g, e ) == DefMark::Proven; + } + } + return b; +} +// a hop row (i > 0) whose edge into it is a split arm (Q1: on every path, whatever the endpoints' defs=) +inline bool pathHopSplitArm( const Graph& g, std::span path, std::size_t i ) noexcept +{ + return i > 0 && i < path.size() && edgeSplitArm( g, path[ i - 1 ], path[i] ); +} +inline bool pathHasSplitHop( const Graph& g, std::span path ) noexcept +{ + for( std::size_t i = 1; i < path.size(); ++i ) + { + if( pathHopSplitArm( g, path, i ) ) + { + return true; + } + } + return false; +} + +} // namespace rw diff --git a/src/editcheck.h b/src/editcheck.h index 077c6c3c7..843b922be 100644 --- a/src/editcheck.h +++ b/src/editcheck.h @@ -184,12 +184,23 @@ inline std::size_t editCheckDeclTargetGroup( const IngestResult& ing, const Edit return target; } -inline void editCheckFoldDeclGroups( const IngestResult& ing, EditCheckGroupKeys& gk ) +// The fold's verdict for every group: the index of the group it folds into, or its own index when it is kept. ONE rule +// with two readers: editCheckFoldDeclGroups below drops the folded groups (a prototype and its definition are one +// contract), and defattrib.h's definition groups (the per-row attribution of --callers/--callees/--impact) join a folded +// declaration to the definition it declares — so "which declarations are this definition" cannot mean two things. +// A pair is tried only when editCheckTieCandidate (parser fields alone) admits it, so a definition's signature is read +// only for a declaration that could declare it. +inline std::vector editCheckDeclFoldTargets( const IngestResult& ing, const EditCheckGroupKeys& gk ) { - const std::size_t groupCount = gk.keys.size(); + const std::size_t groupCount = gk.keys.size(); + std::vector target( groupCount ); + for( std::size_t groupIndex = 0; groupIndex < groupCount; ++groupIndex ) + { + target[ groupIndex ] = groupIndex; + } if( groupCount < 2 ) { - return; + return target; } EditCheckSources sources( ing, EditCheckSpliced{} ); std::deque> defSigs; // each definition's signature, read once (a deque: stable refs) @@ -205,13 +216,34 @@ inline void editCheckFoldDeclGroups( const IngestResult& ing, EditCheckGroupKeys }; const auto declares = [ & ]( NodeId decl, NodeId def ) { - return editCheckTieDeclaration( ing, sources, decl, def, defSigOf( def ) ).tie == EditCheckTie::Declares; + return editCheckTieCandidate( ing.symbols[ decl ], ing.symbols[ def ] ) + && editCheckTieDeclaration( ing, sources, decl, def, defSigOf( def ) ).tie == EditCheckTie::Declares; }; + for( std::size_t groupIndex = 0; groupIndex < groupCount; ++groupIndex ) + { + if( editCheckDeclOnlyGroup( ing, gk.members[ groupIndex ] ) ) + { + const std::size_t into = editCheckDeclTargetGroup( ing, gk, groupIndex, declares ); + target[ groupIndex ] = into < groupCount ? into : groupIndex; + } + } + const bool targetsKept = std::all_of( target.begin(), target.end(), [ & ]( std::size_t t ) { return t < groupCount && target[ t ] == t; } ); + ENSURES( targetsKept, "a folded group names a kept one (a fold target holds a definition, so it never folds itself)" ); + return target; +} - EditCheckGroupKeys kept; +inline void editCheckFoldDeclGroups( const IngestResult& ing, EditCheckGroupKeys& gk ) +{ + const std::size_t groupCount = gk.keys.size(); + if( groupCount < 2 ) + { + return; + } + const std::vector target = editCheckDeclFoldTargets( ing, gk ); + EditCheckGroupKeys kept; for( std::size_t groupIndex = 0; groupIndex < groupCount; ++groupIndex ) { - if( editCheckDeclOnlyGroup( ing, gk.members[ groupIndex ] ) && editCheckDeclTargetGroup( ing, gk, groupIndex, declares ) < groupCount ) + if( target[ groupIndex ] != groupIndex ) { continue; // folded into the definition group it declares } diff --git a/src/editcheckdecl.h b/src/editcheckdecl.h index b5efc30d8..6bb4213ec 100644 --- a/src/editcheckdecl.h +++ b/src/editcheckdecl.h @@ -745,16 +745,23 @@ struct EditCheckTieResult bool mayDefault; // on Unproven: the declaration may carry a default (what defaults_untied= counts) }; +// The identity rule's parser-field half, read off the two symbols alone (no source read): a pair failing it is Different. +// Callers that test many pairs (editcheck.h editCheckDeclFoldTargets) ask it FIRST, so a definition's signature is read +// only for a declaration that could declare it (checklist 30(c): the source reads stay bounded by the candidate pairs). +inline bool editCheckTieCandidate( const Symbol& d, const Symbol& f ) noexcept +{ + return d.name == f.name && d.scope == f.scope && langCompatible( d.lang, Lang::C ) && langCompatible( f.lang, Lang::C ) + && !isDefinitionNotDeclaration( d ) && isDefinitionNotDeclaration( f ) && d.params == f.params + && ( f.internalLinkage == 0 || d.fileId == f.fileId ); +} + // THE IDENTITY RULE, for one (declaration, definition) pair. `defSig` is the definition's own signature, read once. inline EditCheckTieResult editCheckTieDeclaration( const IngestResult& ing, EditCheckSources& sources, NodeId decl, NodeId def, const EditCheckSignature& defSig ) { const Symbol& d = ing.symbols[ decl ]; const Symbol& f = ing.symbols[ def ]; - const bool candidate = d.name == f.name && d.scope == f.scope && langCompatible( d.lang, Lang::C ) && langCompatible( f.lang, Lang::C ) - && !isDefinitionNotDeclaration( d ) && isDefinitionNotDeclaration( f ) && d.params == f.params - && ( f.internalLinkage == 0 || d.fileId == f.fileId ); - if( !candidate ) + if( !editCheckTieCandidate( d, f ) ) { return { EditCheckTie::Different, 0, false }; } diff --git a/src/graph.h b/src/graph.h index 285e20d26..f5ff4a92a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -11606,6 +11606,8 @@ struct ImpactFileRow std::uint32_t syms = 0; std::uint32_t minDepth = 0; std::uint32_t nameOnly = 0; // fix27: of syms, the via="name" rows (impactRowNameOnly) — counted only on a multi-def selector + std::string ofDefXml; // attrib-multidef (defattrib.h attachRollupOfDef): the row's of_def=, "" on a one-group selector + std::string ofDefJson; // ... and its JSON key }; struct ImpactFileRollup diff --git a/src/graphlegend.h b/src/graphlegend.h index d2cbddbfc..5fab48f42 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -480,6 +480,13 @@ inline constexpr const char* kImpactLegendOpen = inline constexpr const char* kImpactImportTierLegend = "importers= is a SECOND, weaker reach: the files that directly include/import a file defining SYM, as rows after the symbol rows — not call reach, never added to reaches= (different units, files vs symbols; an importer may use a different symbol from that file, or none at all). DIRECT (one hop), never the transitive include cone. lazy=\"1\" means every one of that importer's edges into SYM's file is written INSIDE A CLOSURE (a TS/JS require()/import() in a function body, a Ruby constant receiver or argument in a method/lambda/block, a Ruby autoload or rescue class), firing only if and when it runs; lazy=\"0\" means at least one edge is load-time. shown_importers=/importers_capped= disclose that listing's own truncation (importers= stays the full count); most-imported first; limit= sizes it, offset= windows the symbol rows only. "; +// cut-fix E's importers_next= (graph.h sizeImportTier), present-only: it rides a CUT import listing alone, and the full +// legend never defined it (legendcoveragecheck (A) found it once SYM's file passed 40 importers on this tree). +inline const char* importersNextLegend( bool on ) noexcept +{ + return on ? "importers_next= (a cut importer listing only) is the call that lists every importer. " : ""; +} + // The columnar form re-serializes the SYMBOL rows as parallel arrays and has no row shape for a second // listing, so it carries importers= alone. Said in band rather than left as a shape difference a reader // has to notice: a count with no rows beside it otherwise reads as a bug. @@ -904,6 +911,31 @@ inline std::string crossKindKeyJson( const std::string& value ) return value.empty() ? std::string() : ",\"cross_kind\":\"" + value + "\""; } +// ── attrib-multidef (src/defattrib.h): the definition table, a row's of_def=, the split-arm prov=, an unbound --path endpoint ── +// Each clause rides exactly when its attribute is in the answer (present-only), on the callers/callees/safe-delete/impact and +// path legends, CLI and MCP alike. No double hyphen anywhere: every one lands inside an XML comment (checklist 25). +inline constexpr const char* kDefTableLegend = + " rows: the GROUPS the defs= definitions form, a definition with the declarations tied to it " + "(C family: the edit-check identity rule; TypeScript: its overload signatures); bodyless=1 a declaration tied to none of them " + "(another function, or the tie is unproven). of_def=K... on a row: the groups it belongs to, by ix. K = a resolved edge (the " + "resolver's binding, not a type check); K~s = its edge carries prov=split (a call along it was one arm of a call the resolver split " + "between same-named definitions); K~n = every call along it bound by name alone; K~t = a resolved edge into a primary template " + "while a specialisation of it is another group (which one the call reaches is unproven). On impact the tie is a chain of calls (a " + "by-name hop makes it ~n, else a split hop ~s) and an rf row unions its file's symbols. ~n/~s/~t do NOT mean the edge is false. "; +inline constexpr const char* kDefsListedLegend = "defs_listed=N: above 16 groups the table lists only the N that this page's rows name. "; +inline std::string defTableLegend( bool table, bool cut ) +{ + return table ? std::string( kDefTableLegend ) + ( cut ? kDefsListedLegend : "" ) : std::string(); +} +inline constexpr const char* kSplitArmRowLegend = + "prov=\"split\" on a row: its edge to of= (on a path, the hop into it) carries the resolver's split label, a call along it was one " + "arm of a call split between same-named definitions, so the row may be another arm's; NOT a claim the edge is false. "; +inline const char* splitArmLegend( bool on ) noexcept { return on ? kSplitArmRowLegend : ""; } +inline constexpr const char* kPathUnboundLegend = + "from_p=/to_p= are left out when that endpoint has several definitions and nothing bound one: unreached, or (to_p) reached " + "only through a split or by-name hop. "; +inline const char* pathUnboundLegend( bool on ) noexcept { return on ? kPathUnboundLegend : ""; } + // ── issue #60: the module-scope owner's clause, emitted exactly when a t="modscope" ROW is in the answer ── // A file-scope call (a top-level statement, or a call inside an anonymous callback body) used to have no // caller node, so --callers/--impact answered with a row short. It now has one, and the row's t= is a kind diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f3e720b51..1f58ebdbf 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -731,7 +731,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& bool anyVia = false; // FE-B: an entry carried "via":"name" — the answer then carries via_note, its reading RowCallSites calledBySites{}; // CALLSITE-AT: the calledBy window's sites (set once the window is known, below) - const auto symObj = [ & ]( NodeId id, bool via ) -> std::string + const auto symObj = [ & ]( NodeId id, bool via, const std::string& attrib = std::string() ) -> std::string { anyVia = anyVia || via; const Symbol& s = ing.symbols[id]; @@ -746,17 +746,25 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + "\",\"line\":" + std::to_string( s.line ) + ",\"handle\":\"" + mcpdetail::jsonEscape( handleFor( ix, id ) ) + "\"" + ( rw::isTestedByReach( ing, chTested.testReach, id ) ? ",\"tested\":true" : "" ) - + ( via ? ",\"via\":\"name\"" : "" ) // FE-B: the CLI row's via="name", the same edge bit (graph.h rowNameOnly) + + ( via ? ",\"via\":\"name\"" : "" ) // FE-B: the CLI row's via="name", the same edge bit (defattrib.h, one pass) + + attrib // attrib-multidef: the CLI row's of_def= / prov="split" + "}"; }; + // attrib-multidef (defattrib.h): calledBy rows are callers (the callers computation's attribution), calls rows callees + const OneHopAttribution& calledByAttr = referencingOnly ? chRows.attr : chCallers.attr; + const OneHopAttribution& callsAttr = chRows.attr; + bool anySplit = false; // a row carried "prov":"split" — the answer then carries split_note, its reading const auto rowArray = [ & ]( const std::vector& ids, const PageWindow& w, bool callersSide ) -> std::string { + const OneHopAttribution& at = callersSide ? calledByAttr : callsAttr; std::string a = "["; for( std::size_t i = w.begin; i < w.end; ++i ) { if( i != w.begin ) { a += ","; } + anySplit = anySplit || at.splitArm( i ); // CALLSITE-AT: a calledBy row carries its call sites (the CLI --callers row's sites_at=); a calls row never does. - a += withRowSites( symObj( ids[i], rowNameOnly( g, chRows.matches, ids[i], callersSide ) ), calledBySites, i - w.begin, callersSide ); + a += withRowSites( symObj( ids[i], at.nameOnly( i ), ofDefKeyJson( at.groups, at.tokens, i ) + splitArmKeyJson( at.splitArm( i ) ) ), + calledBySites, i - w.begin, callersSide ); } a += "]"; return a; @@ -815,6 +823,22 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // which is where the disclosure has to be — the same posture bodyless_defs= already holds here. out += unprovenDefsKeyJson( chRows.unprovenDefs ); out += crossKindKeyJson( chRows.crossKind ); // hono-07: the CLI root's cross_kind=, self-named like unproven_defs above + // attrib-multidef: the CLI root's table, in this surface's row vocabulary, listing the groups either window names (all of + // them up to the cap). The `symbol` object above stays the first definition — def_table[0] — and this table rides beside it. + { + std::vector named; + defTableMarkNamed( named, chRows.attr.groups, calledByAttr.tokens, ( referencingOnly ? pwPrimary : pwSecond ).begin, ( referencingOnly ? pwPrimary : pwSecond ).end ); + if( !referencingOnly ) + { + defTableMarkNamed( named, chRows.attr.groups, callsAttr.tokens, pwPrimary.begin, pwPrimary.end ); + } + const DefTableListing listing = defTableListing( chRows.attr.groups, named ); + out += defTableJson( ing, chRows.attr.groups, listing, sqRootPrefix, /*mcp=*/true ); + if( chRows.attr.groups.multi() ) + { + out += nextFieldJson( defTableLegend( true, listing.cut ), "def_note" ); // its reading, only when the table rides + } + } out += pageDisclosure( pab, sizeof( pab ), pwPrimary.end - pwPrimary.begin, rowTotal, pwPrimary.end, page.limit, page.offset, discloseCap, kJsonPageSyntax ); if( !referencingOnly ) @@ -830,6 +854,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { out += rw::viaNameNoteJson(); // FE-B: the reading of "via":"name", only in an answer that carries it } + out += nextFieldJson( splitArmLegend( anySplit ), "split_note" ); // attrib-multidef Q1: likewise for "prov":"split" out += nextFieldJson( callSitesAtLegend( calledBySites.any, calledBySites.anyCut, kCallSitesPerRowCap ), "sites_note" ); // CALLSITE-AT: its reading, only when a row carries it (nextFieldJson: "" -> no key, JSON-escaped) // Reference-as-value round: the CLI's window, the same rows (callhierarchy.h computed them once): valueRefs = // where the symbol is USED AS A VALUE (the --callers side), valueCallees = what it stores/passes and may call @@ -2985,12 +3010,21 @@ inline std::optional impactText( const std::string& root, const std const ValueRefRows imValueRefs = valueRefCallerRows( ing, valueRefIndexOf( ix ), seeds ); const PageWindow ipw = pageWindow( show.size(), effectiveRowCap( page.limit, 40 ), page.offset ); // idea #6: the CLI arm's rollup, under the same rule — it rides only beside a CUT symbol window - const ImpactFileRollup imFiles = impactFileRollup( ing, reach, depth, page.limit, symbol, ipw.end - ipw.begin < show.size(), - seeds.size() > 1 ? proven : kNoImpactHedge ); // fix27, as on the CLI + ImpactFileRollup imFiles = impactFileRollup( ing, reach, depth, page.limit, symbol, ipw.end - ipw.begin < show.size(), + seeds.size() > 1 ? proven : kNoImpactHedge ); // fix27, as on the CLI + // attrib-multidef (defattrib.h): the CLI --impact's attribution, by the same calls + const ReachAttribution imAttr = attributeReachAnswer( ing, g, seeds, reach ); + std::vector imNamed; + attachRollupOfDef( imFiles, ing, imAttr, imNamed ); + for( std::size_t i = ipw.begin; i < ipw.end && imAttr.groups.multi(); ++i ) + { + imAttr.markNamed( imNamed, show[ i ] ); + } + const DefTableListing imListing = defTableListing( imAttr.groups, imNamed ); const bool imHasVia = std::any_of( show.begin() + ipw.begin, show.begin() + ipw.end, [ & ]( NodeId n ) { return impactRowNameOnly( proven, n ); } ); rw::emitTo( mem, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}-->", kImpactLegendOpen, kPageRaiseCapClause, reach.empty() ? "" : kImpactDepthLegend, // 0.6.5: exactly when d=/by_depth= ride, as on the CLI - kImpactImportTierLegend, + ( std::string( kImpactImportTierLegend ) + importersNextLegend( !imports.next.empty() ) ).c_str(), // cut-fix E, as on the CLI ( impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them, as on the CLI + viaNameLegend( imHasVia, true ) ).c_str(), // FE-B: exactly when a shown row is via="name", as on the CLI kTestedRowLegend, kImpactTestedPartitionLegend, // A6 @@ -3000,7 +3034,8 @@ inline std::optional impactText( const std::string& root, const std declinedIfaceLegend( declinedIface > 0 ), // likewise declined_iface=, as on the CLI ( std::string( valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs=, as on the CLI + valueRefsDepthLegendFor( imValueRefs ) // ... and the depth disclosure, as on the CLI - + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ) ) ).c_str(), // idea #6, as on the CLI + + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ) ) // idea #6, as on the CLI + + defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), // attrib-multidef, as on the CLI graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), renderDisclosure( prD, DiscloseAs::LegendClause ).c_str() ); // r27-emitters §P2.1: the listing is capped at 40 by rank. Without shown=/capped= a 40-row answer to // "is it safe to change X?" reads as the WHOLE blast radius when it can be 3% of it. Same attributes, @@ -3016,21 +3051,22 @@ inline std::optional impactText( const std::string& root, const std const std::string imRootPrefix = imSingleRoot ? sarif::rootPrefixOf( root ) : std::string(); const std::string imRootAttr = imSingleRoot ? ( " root=\"" + ex( root ) + "\"" ) : std::string(); rw::emitTo( mem, "", - ex( symbol ).c_str(), seeds.size(), reach.size(), unprovenDefsAttrXml( unprovenDefs ).c_str(), // H1: where the CLI root carries it + ex( symbol ).c_str(), seeds.size(), reach.size(), unprovenDefsAttrXml( unprovenDefs ) + defsListedAttrXml( imAttr.groups, imListing ), // H1: where the CLI root carries it; attrib-multidef byDepthAttrXml( byDepth ), // 0.6.5: the CLI root's by_depth= imports.xmlAttrs.c_str(), radiusTested, radiusUntested, ( declinedCallsAttrXml( declinedCalls ) + declinedIfaceAttrXml( declinedIface ) ).c_str(), ( valueRefsCountAttrXml( imValueRefs.rows.size() ) + valueRefsDepthAttrXml( ing, imValueRefs.depthCut, VrRender{ imSingleRoot, imRootPrefix } ) ), imRootAttr.c_str(), pageDisclosure( ipab, sizeof( ipab ), shownRows, show.size(), ipw.end, page.limit, page.offset, true ), graphCountFloorAttrXml( g ).c_str(), renderDisclosure( prD, DiscloseAs::XmlAttrs ).c_str(), // M15: gauge + marker nextAttrXml( nextFlag( "--safe-delete=", symbol ) ).c_str() ); // P3 (L7): the CLI twin's next=, same root attribute set (mcpclidiffcheck) + rw::emitRaw( mem, defTableXml( ing, imAttr.groups, imListing, imSingleRoot ? std::string_view( imRootPrefix ) : std::string_view() ).c_str() ); // attrib-multidef emitImpactFileRollupXml( mem, ing, imFiles, imSingleRoot ? std::string_view( imRootPrefix ) : std::string_view() ); // idea #6, as on the CLI for( std::size_t i = ipw.begin; i < ipw.end; ++i ) { const Symbol& s = ing.symbols[ show[i] ]; const std::string_view rp = imSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], imRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= as on the CLI row. - rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, depthRunAttrXml( show, depth, i, ipw.begin ), isTestedByReach( ing, impTestReach, show[i] ) ? " tested=\"1\"" : "", - viaNameAttr( impactRowNameOnly( proven, show[i] ) ) ); } // FE-B (D3) + viaNameAttr( impactRowNameOnly( proven, show[i] ) ), imAttr.ofDefXml( show[i] ) ); } // FE-B (D3); attrib-multidef // the import tier's rows, after the symbol rows and under their own tag — a different unit, so a // different element (see the CLI arm and kImpactImportTierLegend for why they are never one number). emitImportRowsXml( mem, ing, std::span( imports.files ).first( imports.shown ), imRootPrefix, @@ -3461,13 +3497,17 @@ inline std::optional pathText( const std::string& root, const std:: // PATH-GAP: the CLI --path's gap clause, by the same analysis and emitter (src/pathgaps.h). const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI + const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI + const bool ptHasSplit = pathHasSplitHop( g, pth ); rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) ).c_str(), + "graph holds none. {}{}{}{}-->{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) + + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) ).c_str(), ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), pathGapsLegend( ptGaps.any() ), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); - rw::emitTo( mem, " pathText( const std::string& root, const std:: for( std::size_t i = 0; i < pth.size(); ++i ) { const Symbol& s = ing.symbols[ pth[i] ]; const std::string_view rp = ptSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], ptRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); - rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, - viaNameAttr( i > 0 && edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ) ) ); } // FE-B: as on the CLI + rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + viaNameAttr( i > 0 && edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ) ), splitArmAttrXml( pathHopSplitArm( g, pth, i ) ) ); } // FE-B; attrib-multidef: as on the CLI rw::emitRaw( mem, "" ); return mcpAnswerText( stream ); // nullopt = the buffer lost bytes (dispatch answers -32603), "" stays not-found } diff --git a/src/serialize.h b/src/serialize.h index 830ed426e..20c05d57e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -9659,8 +9659,8 @@ inline void emitImpactFileRollupXml( std::FILE* out, const IngestResult& ing, co { const std::string_view raw = ing.files[ r.rows[i].fileId ]; const std::string_view rel = rootPrefix.empty() ? raw : rw::sarif::rootRelativeUri( raw, rootPrefix ); - rw::emitTo( out, "", std::string( escapeXml( rel, esc ) ).c_str(), r.rows[i].syms, r.rows[i].minDepth, - impactRollupHedgeXml( r.rows[i] ) ); // fix27: the symbol rows' via="name", kept through the fold + rw::emitTo( out, "", std::string( escapeXml( rel, esc ) ).c_str(), r.rows[i].syms, r.rows[i].minDepth, + impactRollupHedgeXml( r.rows[i] ), r.rows[i].ofDefXml ); // fix27: the symbol rows' via="name", kept through the fold; attrib-multidef } rw::emitRaw( out, "" ); } @@ -9680,8 +9680,8 @@ inline void emitImpactFileRollupJson( std::FILE* out, const IngestResult& ing, c { const std::string_view raw = ing.files[ r.rows[i].fileId ]; const std::string_view rel = rootPrefix.empty() ? raw : rw::sarif::rootRelativeUri( raw, rootPrefix ); - rw::emitTo( out, "{}{{\"p\":\"{}\",\"syms\":{},\"d\":{}{}}}", i ? "," : "", jsonStr( rel ).c_str(), r.rows[i].syms, r.rows[i].minDepth, - impactRollupHedgeJson( r.rows[i] ) ); // fix27: as the XML row + rw::emitTo( out, "{}{{\"p\":\"{}\",\"syms\":{},\"d\":{}{}{}}}", i ? "," : "", jsonStr( rel ).c_str(), r.rows[i].syms, r.rows[i].minDepth, + impactRollupHedgeJson( r.rows[i] ), r.rows[i].ofDefJson ); // fix27: as the XML row; attrib-multidef } rw::emitRaw( out, "]}" ); } diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index cbcc9acdb..b6900b1ca 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -214,6 +214,17 @@ std::optional runCallHierarchy( const MainDispatch& d ) const rw::RowCallSites chSites = wantCallers ? rw::rowCallSites( ing, matches, chPage, rw::VrRender{ chSingleRoot, chRootPrefix } ) : rw::RowCallSites{}; + // attrib-multidef (defattrib.h): the definition table this page lists (every group up to the cap, else the ones its rows + // name) and whether a row on it is a split arm of a one-group answer (Q1) — computed once for every dialect below. + const rw::OneHopAttribution& chAttr = chRows.attr; + std::vector chNamed; + rw::defTableMarkNamed( chNamed, chAttr.groups, chAttr.tokens, pw.begin, pw.end ); + const rw::DefTableListing chListing = rw::defTableListing( chAttr.groups, chNamed ); + bool chHasSplit = false; + for( std::size_t i = pw.begin; i < pw.end && !chHasSplit; ++i ) + { + chHasSplit = chAttr.splitArm( i ); + } // §H4 §3.4: the FIRST legend these two verbs have ever shipped (0 bytes before — which is why every // one of their root attributes sits in test/legendcoverage_baseline.txt), and the floor marker that @@ -239,7 +250,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) bool chHasVia = false; for( std::size_t i = pw.begin; i < pw.end && !chHasVia; ++i ) { - chHasVia = rowNameOnly( g, matches, result[ i ], wantCallers ); + chHasVia = chAttr.nameOnly( i ); // FE-B's via="name": every edge to the matches by name (defattrib.h, one pass) } rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, @@ -249,7 +260,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) + rw::stdMemberCallsLegend( chRows.stdCalls.calls > 0 && !cfg.columnar ), // and when the line is (XML only) ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + rw::crossKindLegend( !chRows.crossKind.empty() ) // hono-07: likewise for cross_kind= - + rw::viaNameLegend( chHasVia, false, cfg.columnar ) ).c_str(), // FE-B: likewise for via="name" + + rw::viaNameLegend( chHasVia, false, cfg.columnar ) // FE-B: likewise for via="name" + + rw::defTableLegend( chAttr.groups.multi(), chListing.cut ) + rw::splitArmLegend( chHasSplit ) ).c_str(), // attrib-multidef: likewise ( std::string( rw::modScopeLegend( chHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is + rw::callSitesAtLegend( chSites.any, chSites.anyCut, rw::kCallSitesPerRowCap ) ).c_str(), // CALLSITE-AT: likewise for sites_at=/sites_total= ( rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ) + rw::valueRefsLegend( !chRows.valueRefs.rows.empty(), wantCallers ) @@ -286,6 +298,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) + ( !wantCallers && bodylessDefsCount > 0 ? " bodyless_defs=\"" + std::to_string( bodylessDefsCount ) + "\"" : "" ) + chUnprovenAttr // H1: the decl→def residue, on BOTH directions + rw::crossKindAttrXml( chRows.crossKind ) // hono-07: defs of 2+ kinds, beside defs= + + rw::defsListedAttrXml( chAttr.groups, chListing ) // attrib-multidef: a cut definition table says so + chTested.xmlAttr // A6: hop_tested=/hop_untested=, the same partition on every dialect + chDeclinedAttr // the tier-3 declines, beside the count they are not in + chValueRefsAttr // value uses, beside the count they are not in (rows: the XML form) @@ -294,13 +307,22 @@ std::optional runCallHierarchy( const MainDispatch& d ) cfg.pageLimit, cfg.pageOffset, chDiscloseCap ) + rw::graphCountFloorAttrXml( g ) // §H4 §3.4 — every dialect carries the marker + chNextAttr; // P3 (L7): the same next= on the columnar root - std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it + std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it + std::vector pageSplit( page.size(), 0 ); // attrib-multidef Q1: likewise for a one-group answer's split arms + std::vector pageOfDef; // attrib-multidef: of_def per row, only on a multi-group answer for( std::size_t i = 0; i < page.size(); ++i ) { - pageVia[ i ] = rowNameOnly( g, matches, page[ i ], wantCallers ) ? 1 : 0; + pageVia[ i ] = chAttr.nameOnly( pw.begin + i ) ? 1 : 0; + pageSplit[ i ] = chAttr.splitArm( pw.begin + i ) ? 1 : 0; + if( chAttr.groups.multi() ) + { + pageOfDef.push_back( rw::ofDefColumnValue( chAttr.groups, chAttr.tokens, pw.begin + i ) ); + } } + const std::string chDefRows = rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ); emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, - rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals } ); // CALLSITE-AT: sites_at only when a row has sites + rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals, // CALLSITE-AT: sites_at only when a row has sites + chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit } ); return 0; } @@ -315,8 +337,9 @@ std::optional runCallHierarchy( const MainDispatch& d ) { rw::emitTo( stdout, ",\"bodyless_defs\":{}", bodylessDefsCount ); } - rw::emitTo( stdout, "{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin - rw::crossKindKeyJson( chRows.crossKind ).c_str() ); // hono-07: absent unless defs span 2+ kinds + rw::emitTo( stdout, "{}{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin + rw::crossKindKeyJson( chRows.crossKind ).c_str(), // hono-07: absent unless defs span 2+ kinds + rw::defTableJson( ing, chAttr.groups, chListing, chRootPrefix, /*mcp=*/false ) ); // attrib-multidef: the table // R-E: the JSON twin of the XML root= below — right after the leading identifying fields. if( chSingleRoot ) { rw::emitTo( stdout, ",\"root\":\"{}\"", jsonStr( cfg.roots[0] ).c_str() ); } rw::emitTo( stdout, ",\"hop_tested\":{},\"hop_untested\":{}{}{}", chTested.tested, chTested.untested, @@ -331,11 +354,12 @@ std::optional runCallHierarchy( const MainDispatch& d ) std::vector chAtKeys( result.size() ); // CALLSITE-AT: the rows' sites_at/sites_total keys, absent when none for( std::size_t i = pw.begin; i < pw.end; ++i ) { - chVia[ i ] = rowNameOnly( g, matches, result[ i ], wantCallers ) ? 1 : 0; + chVia[ i ] = chAttr.nameOnly( i ) ? 1 : 0; if( chSites.any ) { chAtKeys[ i ] = rw::callSitesKeyJson( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ); } + chAtKeys[ i ] += rw::ofDefKeyJson( chAttr.groups, chAttr.tokens, i ) + rw::splitArmKeyJson( chAttr.splitArm( i ) ); // attrib-multidef } printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia, &chAtKeys ); rw::emitTo( stdout, "]{}}}", rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); @@ -349,22 +373,25 @@ std::optional runCallHierarchy( const MainDispatch& d ) { rw::emitTo( stdout, " bodyless_defs=\"{}\"", bodylessDefsCount ); } - rw::emitTo( stdout, "{}{}{}{}{}", chUnprovenAttr.c_str(), rw::crossKindAttrXml( chRows.crossKind ).c_str(), // H1's residue; hono-07's cross_kind= + rw::emitTo( stdout, "{}{}{}{}{}{}", chUnprovenAttr.c_str(), rw::crossKindAttrXml( chRows.crossKind ).c_str(), // H1's residue; hono-07's cross_kind= + rw::defsListedAttrXml( chAttr.groups, chListing ), // attrib-multidef: a cut table says so chTested.xmlAttr.c_str(), chDeclinedAttr.c_str(), chValueRefsAttr ); // then A6's partition, the declines, the value uses rw::emitTo( stdout, "{}{}{}>", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ), rw::graphCountFloorAttrXml( g ).c_str(), chNextAttr.c_str() ); // P3 (L7): callers → the SITES (--uses=SELECTOR, spelling mirrored); callees → the body (--expand=SYM) rw::writeMultiRootTable( stdout, ing ); // M12: the roots list this element's own root= cannot carry + rw::emitRaw( stdout, rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ).c_str() ); // attrib-multidef: the groups the rows name, by ix= for( std::size_t i = pw.begin; i < pw.end; ++i ) { const Symbol& s = ing.symbols[ result[i] ]; const std::string_view rp = chSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ s.fileId ], chRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — the same absence-meaningful convention tested= // already follows everywhere else (serialize.h/verbs_for.h), so an untested row costs 0 bytes. - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, macroRoleAttr( s.kind ), rw::isTestedByReach( ing, chTested.testReach, result[i] ) ? " tested=\"1\"" : "", - viaNameAttr( rowNameOnly( g, matches, result[i], wantCallers ) ), // FE-B: a name-only edge says so on its row + viaNameAttr( chAttr.nameOnly( i ) ), // FE-B: a name-only edge says so on its row + rw::splitArmAttrXml( chAttr.splitArm( i ) ), rw::ofDefAttrXml( chAttr.groups, chAttr.tokens, i ), // attrib-multidef chSites.any ? rw::callSitesAttrXml( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ) : std::string() ); // CALLSITE-AT } rw::emitTo( stdout, "{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows @@ -906,6 +933,9 @@ struct SafeDeleteLegendFlags bool hasSitesAt = false; // a row is on this page (CALLSITE-AT) bool sitesCut = false; // a row is on this page: its sites_at= list was cut at kCallSitesPerRowCap bool valueRefDepthCut = false; // the depth disclosure is on the root: the value walk stopped at its depth cap + bool defTable = false; // attrib-multidef: the table + of_def= rows (a multi-group selector) + bool defTableCut = false; // ... and the table lists only the groups this page's rows name (defs_listed=) + bool splitArms = false; // a row is on this page (a one-group selector's split arm, Q1) }; // count-floor (countfloor.h) + CALLSITE-AT: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and @@ -941,6 +971,7 @@ inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, st { clause += "next= is that uses call. "; // runSafeDelete names it exactly then (sdNextAttr) } + clause += rw::defTableLegend( flags.defTable, flags.defTableCut ) + rw::splitArmLegend( flags.splitArms ); // attrib-multidef return clause; } @@ -1102,6 +1133,9 @@ std::optional runSafeDelete( const MainDispatch& d ) return sa.line != sb.line ? sa.line < sb.line : sa.name < sb.name; } ); + // attrib-multidef (defattrib.h): which definition each caller row belongs to — the callers verb's own computation. + const rw::OneHopAttribution sdAttr = rw::attributeOneHopAnswer( ing, g, defs, callerIds, /*towardMatches=*/true ); + // transitive blast radius: the --impact walk, verbatim. const std::vector reach = rw::transitiveCallers( g, defs ); @@ -1229,12 +1263,21 @@ std::optional runSafeDelete( const MainDispatch& d ) // Designated (train 27): two lanes each inserted a bool here at a different position; named fields keep two adjacent // bools from trading places silently in a positional aggregate. + std::vector sdNamed; + rw::defTableMarkNamed( sdNamed, sdAttr.groups, sdAttr.tokens, sdLw.begin, sdLw.end ); + const rw::DefTableListing sdListing = rw::defTableListing( sdAttr.groups, sdNamed ); + bool sdHasSplit = false; + for( std::size_t i = sdLw.begin; i < sdLw.end && !sdHasSplit; ++i ) + { + sdHasSplit = sdAttr.splitArm( i ); + } emitSafeDeleteLegend( defs.size(), sdUnprovenDefs, ambiguousCallers, risk, SafeDeleteLegendFlags{ .singleRoot = sdSingleRoot, .hasUnindexed = rw::graphGaugeClauses( g ), .hasModScope = anyModuleScopeRow( ing, std::span( callerIds ).subspan( sdLw.begin, sdLw.end - sdLw.begin ) ), .declinedCalls = sdDeclinedCalls, .gateDeclined = g.gateDeclinedCalls > 0, .stdDeclined = g.stdMemberDeclinedCalls > 0, .valueRefs = sdValueRefs.rows.size(), .callersFloor = sdFloor.isFloor, .usesFloor = usesFloor, - .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0 } ); + .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0, + .defTable = sdAttr.groups.multi(), .defTableCut = sdListing.cut, .splitArms = sdHasSplit } ); const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified walks ascending id — defs[0] is the // lowest, same convention --impact/--uses/--callers's of=/defs= @@ -1253,7 +1296,7 @@ std::optional runSafeDelete( const MainDispatch& d ) : rw::nextFlag( "--uses=", cfg.safeDeleteSym ) ); rw::emitTo( stdout, "", + "dead_code_candidate=\"{}\" risk=\"{}\"{}{}{}{}{}{}{}{}>", ex( cfg.safeDeleteSym ).c_str(), symTag( lead.kind ), ex( sdPathRel( lead.fileId ) ).c_str(), lead.line, defs.size(), callerIds.size(), sdCallersFloorAttr, ambiguousCallers, reach.size(), sites.size(), sdUsesFloorAttr, testedSelf ? 1 : 0, radiusTested, radiusUntested, deadCodeCandidate ? 1 : 0, risk, @@ -1261,7 +1304,9 @@ std::optional runSafeDelete( const MainDispatch& d ) rw::declinedCallsAttrXml( sdDeclinedCalls ).c_str(), // beside risk= too: declined calls that may reach it; absent at zero ( rw::valueRefsCountAttrXml( sdValueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, sdValueRefs.depthCut, sdVr ) ), // value uses: in uses=, never in callers=/impact_reaches=; absent at zero pageDisclosure( cab, sizeof( cab ), cw.end - cw.begin, callerIds.size(), cw.end, cfg.pageLimit, cfg.pageOffset, true ), - rw::graphCountFloorAttrXml( g ).c_str(), sdRootAttr.c_str(), sdNextAttr.c_str() ); + rw::graphCountFloorAttrXml( g ).c_str(), sdRootAttr.c_str(), sdNextAttr.c_str(), + rw::defsListedAttrXml( sdAttr.groups, sdListing ) ); // attrib-multidef: a cut table says so + rw::emitRaw( stdout, rw::defTableXml( ing, sdAttr.groups, sdListing, sdSingleRoot ? std::string_view( sdRootPrefix ) : std::string_view() ).c_str() ); for( std::size_t i = cw.begin; i < cw.end; ++i ) { const NodeId callerId = callerIds[i]; @@ -1273,7 +1318,7 @@ std::optional runSafeDelete( const MainDispatch& d ) { rw::emitTo( stdout, " amb=\"{}\"", g.ambOut[ callerId ] ); // M15: the same COUNT a map row's amb= prints — one meaning, one unit } - rw::emitTo( stdout, "/>" ); + rw::emitTo( stdout, "{}{}/>", rw::splitArmAttrXml( sdAttr.splitArm( i ) ), rw::ofDefAttrXml( sdAttr.groups, sdAttr.tokens, i ) ); // attrib-multidef } rw::emitTo( stdout, "{}", rw::valueRefsXml( ing, sdValueRefs, true, sdVr, "--uses=" + std::string( cfg.safeDeleteSym ) ) ); @@ -2326,13 +2371,18 @@ std::optional runPath( const MainDispatch& d ) const rw::ToValueRefs pthToValueRefs = path.empty() ? rw::toValueRefs( ing, true, dstDefs, &*pthValueIdx ) : rw::ToValueRefs{}; const rw::PathSearchGaps pthGaps = path.empty() ? rw::pathSearchGaps( ing, g, srcDefs, *pthValueIdx ) : rw::PathSearchGaps{}; const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" + // attrib-multidef (defattrib.h): from_p=/to_p= only for an endpoint the path bound; prov="split" on a split-arm hop + const rw::PathEndpointBinding pthBound = rw::pathEndpointBinding( g, rw::endpointDefinitionCount( ing, srcDefs ), rw::endpointDefinitionCount( ing, dstDefs ), path ); + const bool pthHasSplit = rw::pathHasSplitHop( g, path ); rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) ).c_str(), + "graph holds none. {}{}{}{}-->{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) ).c_str(), ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), rw::pathGapsLegend( pthGaps.any() ), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); - rw::emitTo( stdout, " runPath( const MainDispatch& d ) const Symbol& s = ing.symbols[ path[i] ]; const std::string_view rp = pthSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ s.fileId ], pthRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // FE-B: a hop whose edge INTO it is name-only says so on its row (the first row is no hop) - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, - viaNameAttr( i > 0 && edgeNameOnly( g, path[ i - 1 ], path[ i ] ) ) ); + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + viaNameAttr( i > 0 && edgeNameOnly( g, path[ i - 1 ], path[ i ] ) ), rw::splitArmAttrXml( rw::pathHopSplitArm( g, path, i ) ) ); } rw::emitTo( stdout, "" ); return 0; @@ -2483,8 +2533,21 @@ struct ImpactView const rw::ValueRefRows& valueRefs; // reference-as-value round: SYM's binding sites — never in reaches= const std::vector& provenReach; // FE-B (D3): graph.h provenCallerReach — a row outside it is via="name" const rw::ImpactFileRollup& fileRollup; // idea #6: graph.h impactFileRollup over the FULL reach set + const rw::ReachAttribution& attrib; // attrib-multidef (defattrib.h): the definition groups + each reached row's ties + const rw::DefTableListing& defListing; // ... and the groups this page's table lists }; +// attrib-multidef: the window's of_def JSON keys in row order (indexed like `show`), "" where none +inline std::vector impactOfDefKeys( const ImpactView& v ) +{ + std::vector keys( v.show.size() ); + for( std::size_t i = v.page.begin; i < v.page.end && v.attrib.groups.multi(); ++i ) + { + keys[ i ] = v.attrib.ofDefJson( v.show[ i ] ); + } + return keys; +} + // FE-B: the window's hedge bits in row order (impactRowNameOnly), for the dialects that carry them per entry or per column inline std::vector impactViaColumn( const ImpactView& v, std::size_t begin, std::size_t end ) { @@ -2514,6 +2577,7 @@ int emitImpactColumnar( const ImpactView& v ) const std::string attr = "of=\"" + std::string( escapeXml( v.sym, esc ) ) + "\" defs=\"" + std::to_string( v.defs ) + "\" reaches=\"" + std::to_string( v.reaches ) + "\"" + rw::unprovenDefsAttrXml( v.unprovenDefs ) // H1: where the XML root carries it + + rw::defsListedAttrXml( v.attrib.groups, v.defListing ) // attrib-multidef: a cut table says so + rw::byDepthAttrXml( v.byDepth ) // 0.6.5: the XML root's, same place + " importers=\"" + std::to_string( v.imports.files.size() ) + "\"" + rw::importsUnresolvedAttrXml( v.imports.importsUnresolved ) // #220: the XML root's, absent at 0 @@ -2542,7 +2606,15 @@ int emitImpactColumnar( const ImpactView& v ) + rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ); // P3 (L7): the XML root's next=, same set const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); const std::vector rowVia( via.begin() + v.page.begin, via.begin() + v.page.end ); // FE-B: in `rows` order - emitColumnarSymbolRows( stdout, v.ing, "impact", attr.c_str(), rows, v.rootPrefix, v.testReach, &v.depth, rw::ColumnarRowExtras{ &rowVia, nullptr, nullptr } ); + std::vector rowOfDef; // attrib-multidef: of_def per row, only on a multi-group selector + for( std::size_t i = v.page.begin; i < v.page.end && v.attrib.groups.multi(); ++i ) + { + const std::size_t at = v.attrib.indexOf( v.show[ i ] ); + rowOfDef.push_back( at == SIZE_MAX ? std::string() : rw::ofDefColumnValue( v.attrib.groups, v.attrib.tokens, at ) ); + } + const std::string defRows = rw::defTableXml( v.ing, v.attrib.groups, v.defListing, v.rootPrefix ); + emitColumnarSymbolRows( stdout, v.ing, "impact", attr.c_str(), rows, v.rootPrefix, v.testReach, &v.depth, + rw::ColumnarRowExtras{ &rowVia, nullptr, nullptr, v.attrib.groups.multi() ? &rowOfDef : nullptr, &defRows, nullptr } ); return 0; } @@ -2558,8 +2630,9 @@ int emitImpactJson( const ImpactView& v ) using namespace rw; char ipab[ kPageDisclosureCap ]; const std::size_t shownRows = v.page.end - v.page.begin; - rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"reaches\":{}{}{}", jsonStr( v.sym ).c_str(), v.defs, v.reaches, + rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"reaches\":{}{}{}{}", jsonStr( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsKeyJson( v.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin + rw::defTableJson( v.ing, v.attrib.groups, v.defListing, v.rootPrefix, /*mcp=*/false ), // attrib-multidef: the table rw::byDepthKeyJson( v.byDepth ) ); // 0.6.5: the XML root's by_depth=, as an array rw::emitTo( stdout, ",\"importers\":{},\"shown_importers\":{},\"importers_capped\":{}", v.imports.files.size(), v.imports.shown, v.imports.capped ? "true" : "false" ); @@ -2578,8 +2651,9 @@ int emitImpactJson( const ImpactView& v ) v.pageLimit, v.pageOffset, true, kJsonPageSyntax ), rw::graphCountFloorAttrJson( v.g ).c_str(), // §H4 §3.4 rw::renderDisclosure( v.prD, rw::DiscloseAs::JsonKeys ).c_str() ); // W2-F: ONE keyset - const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); // FE-B: the rows' hedge, per entry - printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth, &via ); + const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); // FE-B: the rows' hedge, per entry + const std::vector keys = impactOfDefKeys( v ); // attrib-multidef: each row's of_def + printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth, &via, &keys ); rw::emitTo( stdout, "],\"import_reach\":[" ); rw::emitImportRowsJson( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitRaw( stdout, "]" ); @@ -2601,7 +2675,7 @@ int emitImpactXml( const ImpactView& v ) char ipab[ kPageDisclosureCap ]; const std::size_t shownRows = v.page.end - v.page.begin; rw::emitTo( stdout, "", - ex( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsAttrXml( v.unprovenDefs ).c_str(), // H1: beside the reaches= it qualifies + ex( v.sym ).c_str(), v.defs, v.reaches, rw::unprovenDefsAttrXml( v.unprovenDefs ) + rw::defsListedAttrXml( v.attrib.groups, v.defListing ), // H1: beside the reaches= it qualifies; attrib-multidef rw::byDepthAttrXml( v.byDepth ), // 0.6.5: partitions reaches= by hop depth v.imports.xmlAttrs.c_str(), v.radiusTested, v.radiusUntested, ( rw::declinedCallsAttrXml( v.declinedCalls ) + rw::declinedIfaceAttrXml( v.declinedIface ) ).c_str(), // tier-3 declines into the radius @@ -2611,6 +2685,7 @@ int emitImpactXml( const ImpactView& v ) v.pageLimit, v.pageOffset, true ), rw::graphCountFloorAttrXml( v.g ).c_str(), rw::renderDisclosure( v.prD, rw::DiscloseAs::XmlAttrs ).c_str(), rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ).c_str() ); // P3 (L7): "is it safe to change" → "can it go" + rw::emitRaw( stdout, rw::defTableXml( v.ing, v.attrib.groups, v.defListing, v.rootPrefix ).c_str() ); // attrib-multidef: the groups, by ix= rw::emitImpactFileRollupXml( stdout, v.ing, v.fileRollup, v.rootPrefix ); // idea #6: before the symbol window for( std::size_t i = v.page.begin; i < v.page.end; ++i ) { @@ -2618,10 +2693,11 @@ int emitImpactXml( const ImpactView& v ) const std::string_view rp = v.singleRoot ? rw::sarif::rootRelativeUri( v.ing.files[ s.fileId ], v.rootPrefix ) : std::string_view( v.ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= the row's hop depth, run-length (graph.h depthRunAttrXml). - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, rw::depthRunAttrXml( v.show, v.depth, i, v.page.begin ), rw::isTestedByReach( v.ing, *v.testReach, v.show[i] ) ? " tested=\"1\"" : "", - viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ) ); // FE-B (D3): reached only through a name-only edge + viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ), // FE-B (D3): reached only through a name-only edge + v.attrib.ofDefXml( v.show[i] ) ); // attrib-multidef } rw::emitImportRowsXml( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitTo( stdout, "{}", rw::valueRefsXml( v.ing, v.valueRefs, true, rw::VrRender{ v.singleRoot, v.rootPrefix }, @@ -2693,8 +2769,18 @@ std::optional runImpact( const MainDispatch& d ) const rw::PageWindow imPage = pageWindow( show.size(), effectiveRowCap( cfg.pageLimit, rw::kCallHierarchyRowCap ), cfg.pageOffset ); // idea #6: the rollup rides only beside a CUT symbol window (graph.h impactFileRollup) // fix27: on a multi-definition selector the rows keep the symbol rows' via="name" hedge through the fold - const rw::ImpactFileRollup imFiles = rw::impactFileRollup( ing, reach, imDepth, cfg.pageLimit, cfg.impactSym, - imPage.end - imPage.begin < show.size(), seeds.size() > 1 ? imProven : rw::kNoImpactHedge ); + rw::ImpactFileRollup imFiles = rw::impactFileRollup( ing, reach, imDepth, cfg.pageLimit, cfg.impactSym, + imPage.end - imPage.begin < show.size(), seeds.size() > 1 ? imProven : rw::kNoImpactHedge ); + // attrib-multidef (defattrib.h): which definition each reached row (and rollup file) is a dependant of, and the table + // this page lists — the groups its rows and rollup rows name, all of them up to the cap. + const rw::ReachAttribution imAttr = rw::attributeReachAnswer( ing, g, seeds, reach ); + std::vector imNamed; + rw::attachRollupOfDef( imFiles, ing, imAttr, imNamed ); + for( std::size_t i = imPage.begin; i < imPage.end && imAttr.groups.multi(); ++i ) + { + imAttr.markNamed( imNamed, show[ i ] ); + } + const rw::DefTableListing imListing = rw::defTableListing( imAttr.groups, imNamed ); // Reference-as-value round: SYM's own binding sites, beside the radius they are not in. const rw::ValueRefIndex imVri( ing ); const rw::ValueRefRows imValueRefs = rw::valueRefCallerRows( ing, imVri, seeds ); @@ -2712,7 +2798,8 @@ std::optional runImpact( const MainDispatch& d ) const bool imHasModScope = anyModuleScopeRow( ing, std::span( show ).subspan( imPage.begin, imPage.end - imPage.begin ) ); rw::emitTo( stdout, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}{}-->", rw::kImpactLegendOpen, rw::kPageRaiseCapClause, reach.empty() ? "" : rw::impactDepthLegend( cfg.columnar ), // 0.6.5: exactly when rows (d=) and by_depth= exist - cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend, + ( std::string( cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend ) + + rw::importersNextLegend( !imports.next.empty() && !cfg.columnar ) ).c_str(), // cut-fix E: exactly when the root carries it ( rw::impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them + rw::viaNameLegend( std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, // FE-B: exactly when a shown row is via="name" [ & ]( NodeId n ) { return rw::impactRowNameOnly( imProven, n ); } ), true, cfg.columnar ) ).c_str(), @@ -2722,7 +2809,8 @@ std::optional runImpact( const MainDispatch& d ) rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( imDeclinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::modScopeLegend( imHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is - + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ) ) ).c_str(), // idea #6; fix27 + + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ) ) // idea #6; fix27 + + rw::defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), // attrib-multidef: exactly when the table rides ( std::string( rw::valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs= + rw::valueRefsDepthLegendFor( imValueRefs ) ).c_str(), // ... and the depth disclosure rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str() ); @@ -2736,7 +2824,7 @@ std::optional runImpact( const MainDispatch& d ) imports, importPage, importLazyPage, prD, imSingleRoot, imRootPrefix, imRootAttr, imSingleRoot ? cfg.roots[0] : std::string_view(), cfg.pageLimit, cfg.pageOffset, &imTestReach, imRadiusTested, imRadiusUntested, imDeclinedCalls, imDeclinedIface, g, imDepth, imByDepth, imValueRefs, - imProven, imFiles }; + imProven, imFiles, imAttr, imListing }; if( cfg.columnar ) { return emitImpactColumnar( view ); } if( cfg.json ) { return emitImpactJson( view ); } diff --git a/test/attribmultidefcheck.sh b/test/attribmultidefcheck.sh index 4d178e1a4..b85075c9f 100755 --- a/test/attribmultidefcheck.sh +++ b/test/attribmultidefcheck.sh @@ -29,7 +29,7 @@ # it is a split arm or by name: dropped); a from= with several definitions carries no from_p= when unreached. # Corpus test/attribmultideffix (in-tree, outside test/fixture); the stand-in dir per known-items row in [brackets]: -# cpp/ [ninja-07] StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, RunPiece/RunEval at depth 2 +# cpp/ [ninja-07] StringPiece::empty vs EvalString::empty (UsesPiece/UsesEval/UsesBoth, DrivePiece/DriveEval at depth 2 # in their own files; UsesStd, a std::string::empty confounder), [ninja-26] ManifestParser::ParseDefault (decl + # def) calling the CanonicalizePath overload pair (a split call), near misses in fmt.{h,cc}: Fmt::put overloads in # ONE class (split), outer::run vs outer::inner::run (nested namespace), Box::get vs the Box @@ -286,9 +286,14 @@ if ans b_cpp2 cpp callers s xml -- --callers=CanonicalizePath; then [ "$( groups b_cpp2 )" = 2 ]; say $? "[N] b_cpp2: two groups (each util.h declaration folds into its util.cc definition)" \ "[N] b_cpp2: rows=$( groups b_cpp2 ), want 2" hedged P b_cpp2 ParseDefault "util.cc:2 util.cc:6" - hedged P b_cpp2 CanonicalizePath "util.cc:2 util.cc:6" + # the string* overload's own call passes three arguments: the resolver binds it to the 3-parameter overload, proven + expect N b_cpp2 CanonicalizePath util.cc:6 fi if ans b_rp cpp callers s xml -- --callers=ReadPath; then plain N b_rp; fi +# UsesPiece is declared in piece_user.cc with no #include that ties it to parser.cc's definition: the identity rule answers +# Unproven (may declare it, cannot prove it), and no row ties to that declaration (DrivePiece's call binds the definition), so +# the declaration is silent and the answer stays one group — the tmux/ninja prototype shape (a variadic or template-qualified +# declarator is Unproven the same way). A FOREIGN declaration (c_tal, c_hlp below: provably another function) is always listed. if ans b_up cpp callers s xml -- --callers=UsesPiece; then plain N b_up; fi if ans b_q cpp callers s xml -- --callers=string_piece.h:empty; then plain N b_q; fi # Q1 (orchestrator ruling): the split-arm hedge rides single-definition answers too (by-design byte change on them) @@ -366,9 +371,9 @@ else no "c_for: --for=empty failed"; fi echo "(D) C++ --impact=empty" if ans d_imp cpp impact s xml -- --impact=empty; then expect P d_imp UsesPiece "$SP"; expect P d_imp UsesEval "$EV"; expect P d_imp UsesBoth "$EV $SP" - expect P d_imp RunPiece "$SP"; expect P d_imp RunEval "$EV"; expect P d_imp ParseDefault "$EV" + expect P d_imp DrivePiece "$SP"; expect P d_imp DriveEval "$EV"; expect P d_imp ParseDefault "$EV" invariant P d_imp - rowset C d_imp 6 "ParseDefault RunEval RunPiece UsesBoth UsesEval UsesPiece" + rowset C d_imp 6 "DriveEval DrivePiece ParseDefault UsesBoth UsesEval UsesPiece" fi if ans d_rf cpp impact rf xml -- --impact=empty --limit=2; then expect P d_rf parser.cc "$EV $SP" @@ -379,9 +384,9 @@ if ans d_impj cpp - reach json -- --impact=empty --json; then # the row array key of the JSON dialect is read off the answer, never assumed key="$( python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); print(next((k for k,v in j.items() if isinstance(v,list) and v and isinstance(v[0],dict) and "n" in v[0]), "-"))' "$DIR/d_impj.out" 2>/dev/null )" python3 "$DIR/amd.py" "$DIR/d_impj.out" json - "$key" >"$DIR/d_impj.rows" - expect P d_impj RunPiece "$SP" + expect P d_impj DrivePiece "$SP" fi -if mcp d_mcp cpp s impact '"symbol":"empty"'; then expect P d_mcp RunEval "$EV"; fi +if mcp d_mcp cpp s impact '"symbol":"empty"'; then expect P d_mcp DriveEval "$EV"; fi if ans d_one cpp impact s xml -- --impact=string_piece.h:empty; then plain N d_one; fi # ── (E) --path endpoints ────────────────────────────────────────────────────────────────────────────────────────────── @@ -392,7 +397,7 @@ if ans e_un cpp path s xml -- --path=Clear,empty; then [ "$( rootf e_un from_p )" = "eval_env.h:4" ]; say $? "[N] e_un: the single-definition from= keeps from_p" \ "[N] e_un: from_p='$( rootf e_un from_p )'" fi -if ans e_re cpp path s xml -- --path=RunPiece,empty; then +if ans e_re cpp path s xml -- --path=DrivePiece,empty; then [ "$( rootf e_re to_p )" = "$SP" ]; say $? "[C] e_re: a reached multi-definition to= names the bound one ($SP)" \ "[C] e_re: to_p='$( rootf e_re to_p )'" fi @@ -434,7 +439,9 @@ if ans f_py py callers s xml -- --callers=render; then expect P f_py draw_widget widgets.py:2 expect P f_py draw_canvas widgets.py:6 expect P f_py draw_both "widgets.py:2 widgets.py:6" - expect P f_py draw_mixed "widgets.py:2 widgets.py:6~n" + # draw_mixed calls Widget.render twice: w.render() (proven) and thing.render() (split between the two by name). The graph keeps + # ONE provenance per edge (resolve.h edgeProvenance: split outranks plain), so the Widget edge reads ~s — a hedge, never a claim + expect P f_py draw_mixed "widgets.py:2~s widgets.py:6~n" rowset C f_py 4 "draw_both draw_canvas draw_mixed draw_widget" fi if ans f_ts ts callers s xml -- --callers=area; then diff --git a/test/attribmultideffix/cpp/eval_user.cc b/test/attribmultideffix/cpp/eval_user.cc index 800f3ca63..43444e94e 100644 --- a/test/attribmultideffix/cpp/eval_user.cc +++ b/test/attribmultideffix/cpp/eval_user.cc @@ -1,7 +1,7 @@ // impact depth 2: reaches EvalString::empty only, through UsesEval #include "eval_env.h" bool UsesEval(EvalString ev); -bool RunEval() { +bool DriveEval() { EvalString ev{0}; return UsesEval(ev); } diff --git a/test/attribmultideffix/cpp/piece_user.cc b/test/attribmultideffix/cpp/piece_user.cc index 4f687da8e..ae40daf74 100644 --- a/test/attribmultideffix/cpp/piece_user.cc +++ b/test/attribmultideffix/cpp/piece_user.cc @@ -1,7 +1,7 @@ // impact depth 2: reaches StringPiece::empty only, through UsesPiece #include "string_piece.h" bool UsesPiece(StringPiece sp); -bool RunPiece() { +bool DrivePiece() { StringPiece sp{"", 0}; return UsesPiece(sp); } diff --git a/test/impactimportcheck.sh b/test/impactimportcheck.sh index 239e22e92..c76585365 100755 --- a/test/impactimportcheck.sh +++ b/test/impactimportcheck.sh @@ -164,7 +164,8 @@ SHOWN="$( attr shown "$OUT_W" )" && ok "--impact=Widget: shown=1 and exactly 1 row — import rows are outside the paged listing" \ || no "--impact=Widget: rows=$S_ROWS vs shown=$SHOWN (expected 1 and 1)" # 0.6.5: the listing's first row states its hop depth (d="1": build calls Widget directly) — test/impactdepthcheck.sh. -printf '%s' "$OUT_W" | grep -qE '' \ +# attrib-multidef: Widget names several definitions, so the row also says which one it depends on (of_def=, defattrib.h) +printf '%s' "$OUT_W" | grep -qE '' \ && ok "--impact=Widget: user.js appears as the SYMBOL build in the call tier" \ || no "--impact=Widget: the call-reach row for build (lib/user.js:7) is gone" @@ -437,10 +438,11 @@ for LG in py ts go; do no "multidef $LG: the single-definition corpus has no via=name symbol row — its near-miss arm is inert" fi O_RF="$( rfrow "$M_OUT" 'o_00\.[a-z]+' )"; X_RF="$( rfrow "$M_OUT" 'mixed\.[a-z]+' )" - printf '%s' "$O_RF" | grep -qE 'syms="1" d="1" via="name"/>' \ + # attrib-multidef: the row's of_def= (which definitions its symbols depend on) rides after the fix27 hedge + printf '%s' "$O_RF" | grep -qE 'syms="1" d="1" via="name"( of_def="[^"]*")?/>' \ && ok "multidef $LG: an all-by-name file keeps the hedge: $O_RF" \ || no "multidef $LG: the o_00 rf row lost the symbol rows' via=\"name\": '$O_RF'" - { printf '%s' "$X_RF" | grep -qE 'syms="2" d="1" by_name="1"/>' && ! printf '%s' "$X_RF" | grep -q 'via='; } \ + { printf '%s' "$X_RF" | grep -qE 'syms="2" d="1" by_name="1"( of_def="[^"]*")?/>' && ! printf '%s' "$X_RF" | grep -q 'via='; } \ && ok "multidef $LG: a mixed file counts its hedged symbols: $X_RF" \ || no "multidef $LG: the mixed rf row: '$X_RF' (want syms=2 by_name=1, no via=)" if [ $LG != ts ]; then @@ -452,7 +454,7 @@ for LG in py ts go; do printf '%s' "$M_OUT" | grep -qF 'rf via="name": of= matched several definitions' \ && ok "multidef $LG: the rf hedge sentence rides with the hedged rows" \ || no "multidef $LG: legend lacks the rf via=/by_name= sentence" - perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$SY --no-cache --json 2>/dev/null | grep -qE '"syms":1,"d":1,"via":"name"\}' \ + perl -e 'alarm 30; exec @ARGV' "$BIN" "$MD" --impact=$SY --no-cache --json 2>/dev/null | grep -qE '"syms":1,"d":1,"via":"name"(,"of_def":\[[^]]*\])?\}' \ && ok "multidef $LG: --json carries the same hedge" \ || no "multidef $LG: --json rollup rows lack \"via\":\"name\"" for NM in single qualified; do @@ -472,7 +474,7 @@ for LG in py ts go; do [ $LG = py ] && { MCP_M="$( printf '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"impact","arguments":{"path":"%s","symbol":"arrange"}}}\n' "$MD" \ | perl -e 'alarm 60; exec @ARGV' "$BIN" --mcp 2>/dev/null | tail -1 )" - printf '%s' "$MCP_M" | grep -qF 'syms=\"1\" d=\"1\" via=\"name\"/>' && printf '%s' "$MCP_M" | grep -qF 'by_name=\"1\"' \ + printf '%s' "$MCP_M" | grep -qE 'syms=\\"1\\" d=\\"1\\" via=\\"name\\"( of_def=\\"[^\\]*\\")?/>' && printf '%s' "$MCP_M" | grep -qF 'by_name=\"1\"' \ && ok "multidef py: the MCP impact twin keeps the same hedge" \ || no "multidef py: MCP impact rollup rows lack via=/by_name=" } diff --git a/test/impactpartitioncheck.sh b/test/impactpartitioncheck.sh index 5e2eb59f4..9f20d145d 100755 --- a/test/impactpartitioncheck.sh +++ b/test/impactpartitioncheck.sh @@ -155,7 +155,9 @@ for name in seed_names: # row COUNT this invariance reads is unchanged by it (test/impactdepthcheck.sh gates the depths themselves). # FE-B: a row reached only through a by-name edge carries via="name" after tested= — it DISCLOSES how the row was # reached, never whether it is one, so the row count this invariance reads must take it like d= above. - rows = re.findall(r'', doc) + # attrib-multidef: a row of a multi-definition selector carries of_def= (which definition it is a dependant of) last — it + # attributes the row, never adds or removes one, so the count this invariance reads takes it like via= above. + rows = re.findall(r'', doc) reaches = int(attrs.get("reaches", "-1")) if len(rows) != reaches: row_count_ok = False From 51f12b8eab8f53e0fbba9ab081688858a22161e5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:03:44 -0400 Subject: [PATCH 042/162] refactor(attrib): one edge-index helper, smaller reach walk, no dead rowNameOnly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quality-delta follow-up on the attribution commit: graph.h edgeIndex is the one out-CSR position lookup (edgeExists reads it; defattrib.h no longer carries a copy); rowNameOnly is gone — OneHopAttribution::nameOnly replaced its every caller; attributeReach splits into ReachBits (dense bitsets, the fold, the mark) and the token pass; token rendering returns a string instead of appending through a helper. Output unchanged (attribmultidefcheck 140 PASS, impactimportcheck, callerscheck). Co-Authored-By: Claude Opus 5.5 --- src/callhierarchy.h | 3 +- src/defattrib.h | 182 +++++++++++++++++++++----------------------- src/graph.h | 44 ++++------- src/graphlegend.h | 8 +- 4 files changed, 109 insertions(+), 128 deletions(-) diff --git a/src/callhierarchy.h b/src/callhierarchy.h index f20d23976..4c24779e2 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -329,8 +329,7 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph rankBeforeCap( ing, out.rows, [ & ]( NodeId r ) { return ing.symbols[r].fileId; }, [ & ]( NodeId r ) { return navRelevanceWeight( g, r ); } ); } - // attrib-multidef: which definition each row belongs to (defattrib.h), over the rows in their FINAL order — one pass over - // the matches' edges, read by every dialect of both surfaces (and their via="name": strongest tie by name alone). + // attrib-multidef (defattrib.h): each row's definition, over the FINAL row order, for every dialect of both surfaces out.attr = attributeOneHopAnswer( ing, g, out.matches, out.rows, wantCallers ); return out; } diff --git a/src/defattrib.h b/src/defattrib.h index ea5440d90..3d287d164 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -382,23 +382,10 @@ inline DefMark defMarkOfEdge( const Graph& g, std::size_t e ) noexcept return ( e < g.outProv.size() && g.outProv[e] == 3u ) ? DefMark::Split : DefMark::Proven; } -// The out-CSR position of from → to, or SIZE_MAX (ascending within a source: buildGraph sorts by (from, to)). -inline std::size_t defEdgeIndex( const Graph& g, NodeId from, NodeId to ) noexcept -{ - if( std::size_t( from ) + 1 >= g.outOff.size() ) - { - return SIZE_MAX; - } - const auto b = g.outTargets.begin() + g.outOff[ from ]; - const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; - const auto it = std::lower_bound( b, e, to ); - return ( it != e && *it == to ) ? std::size_t( it - g.outTargets.begin() ) : SIZE_MAX; -} - // FE-B's two path predicates' sibling: the hop from → to is one arm of a resolver split (and not by name alone). inline bool edgeSplitArm( const Graph& g, NodeId from, NodeId to ) noexcept { - const std::size_t e = defEdgeIndex( g, from, to ); + const std::size_t e = edgeIndex( g, from, to ); return e != SIZE_MAX && defMarkOfEdge( g, e ) == DefMark::Split; } @@ -454,7 +441,7 @@ inline RowDefTokens attributeOneHop( const Graph& g, const DefGroups& groups, st { for( std::uint32_t k = ro[m]; k < ro[m + 1]; ++k ) { - const std::size_t e = defEdgeIndex( g, ci[k], m ); + const std::size_t e = edgeIndex( g, ci[k], m ); if( e != SIZE_MAX ) { ties.push_back( DefTie{ ci[k], grp, std::uint8_t( defMarkOfEdge( g, e ) ) } ); @@ -476,37 +463,68 @@ inline RowDefTokens attributeOneHop( const Graph& g, const DefGroups& groups, st // A (any chain) — propagated from the seeds over the in-edges until nothing grows (monotone, so it ends; a cycle only re-adds // bits it already holds). The bitsets live on a dense index of the reach set plus the seeds, never on every node (review S5: // 1047 groups over all nodes would be megabytes per walk). Token: K from P, K~s from S\P, K~n from A\S. -inline RowDefTokens attributeReach( const Graph& g, const DefGroups& groups, std::span seeds, std::span reach, - std::span rows ) +struct ReachBits +{ + std::vector dense; // ascending: the reach set plus the seeds + std::size_t words = 0; + std::vector p, s, a; // dense.size() * words each + + std::size_t denseOf( NodeId n ) const noexcept + { + const auto [ first, last ] = std::equal_range( dense.begin(), dense.end(), n ); + return first == last ? SIZE_MAX : std::size_t( first - dense.begin() ); + } + // fold u's reaches into its caller c across one edge of tie `mark`; true when c gained a bit + bool foldInto( std::size_t c, std::size_t u, DefMark mark ) noexcept + { + bool changed = false; + for( std::size_t w = 0; w < words; ++w ) + { + const std::size_t cw = c * words + w; + const std::size_t uw = u * words + w; + const std::uint64_t na = a[cw] | a[uw]; + const std::uint64_t ns = mark >= DefMark::Split ? ( s[cw] | s[uw] ) : s[cw]; + const std::uint64_t np = mark == DefMark::Proven ? ( p[cw] | p[uw] ) : p[cw]; + changed = changed || na != a[cw] || ns != s[cw] || np != p[cw]; + a[cw] = na; + s[cw] = ns; + p[cw] = np; + } + return changed; + } + DefMark markOf( std::size_t d, std::size_t grp ) const noexcept + { + const std::uint64_t bit = std::uint64_t( 1 ) << ( grp % 64 ); + const std::size_t w = d * words + grp / 64; + return ( p[w] & bit ) ? DefMark::Proven : ( s[w] & bit ) ? DefMark::Split : ( a[w] & bit ) ? DefMark::Name : DefMark::None; + } +}; + +inline ReachBits reachBitsets( const Graph& g, const DefGroups& groups, std::span seeds, std::span reach ) { EXPECTS( groups.ofMatch.size() == seeds.size(), "the groups are defGroupsOf( seeds )" ); - std::vector dense( reach.begin(), reach.end() ); - dense.insert( dense.end(), seeds.begin(), seeds.end() ); - std::sort( dense.begin(), dense.end() ); - dense.erase( std::unique( dense.begin(), dense.end() ), dense.end() ); - const auto denseOf = [ & ]( NodeId n ) noexcept -> std::size_t - { - const auto it = std::lower_bound( dense.begin(), dense.end(), n ); - return ( it != dense.end() && *it == n ) ? std::size_t( it - dense.begin() ) : SIZE_MAX; - }; - const std::size_t words = ( groups.size() + 63 ) / 64; - std::vector pBits( dense.size() * words, 0 ), sBits( dense.size() * words, 0 ), aBits( dense.size() * words, 0 ); - std::vector queued( dense.size(), 0 ); - std::vector work; + ReachBits bits; + bits.dense.assign( reach.begin(), reach.end() ); + bits.dense.insert( bits.dense.end(), seeds.begin(), seeds.end() ); + std::sort( bits.dense.begin(), bits.dense.end() ); + bits.dense.erase( std::unique( bits.dense.begin(), bits.dense.end() ), bits.dense.end() ); + bits.words = ( groups.size() + 63 ) / 64; + bits.p.assign( bits.dense.size() * bits.words, 0 ); + bits.s = bits.p; + bits.a = bits.p; + std::vector queued( bits.dense.size(), 0 ); + std::vector work; for( std::size_t si = 0; si < seeds.size(); ++si ) { - const std::size_t d = denseOf( seeds[si] ); - const std::size_t grp = groups.ofMatch[si]; + const std::size_t d = bits.denseOf( seeds[si] ); ASSUME( d != SIZE_MAX, "every seed is in the dense index (it was inserted)" ); - const std::uint64_t bit = std::uint64_t( 1 ) << ( grp % 64 ); - pBits[ d * words + grp / 64 ] |= bit; - sBits[ d * words + grp / 64 ] |= bit; - aBits[ d * words + grp / 64 ] |= bit; - if( !queued[d] ) - { - queued[d] = 1; - work.push_back( d ); - } + const std::size_t w = d * bits.words + groups.ofMatch[si] / 64; + const std::uint64_t bit = std::uint64_t( 1 ) << ( groups.ofMatch[si] % 64 ); + bits.p[w] |= bit; + bits.s[w] |= bit; + bits.a[w] |= bit; + work.push_back( d ); + queued[d] = 1; } const auto* ro = g.inEdges.rowOffsets(); const auto* ci = g.inEdges.colIndices(); @@ -515,48 +533,32 @@ inline RowDefTokens attributeReach( const Graph& g, const DefGroups& groups, std const std::size_t u = work.back(); work.pop_back(); queued[u] = 0; - const NodeId un = dense[u]; - for( std::uint32_t k = ro[un]; k < ro[un + 1]; ++k ) + for( std::uint32_t k = ro[ bits.dense[u] ]; k < ro[ bits.dense[u] + 1 ]; ++k ) { - const std::size_t c = denseOf( ci[k] ); - const std::size_t e = defEdgeIndex( g, ci[k], un ); - if( c == SIZE_MAX || e == SIZE_MAX ) - { - continue; // outside the reach set (cannot happen for a full closure) or no out-edge twin - } - const DefMark mark = defMarkOfEdge( g, e ); - bool changed = false; - for( std::size_t w = 0; w < words; ++w ) - { - const std::uint64_t a = aBits[ c * words + w ] | aBits[ u * words + w ]; - const std::uint64_t s = mark >= DefMark::Split ? ( sBits[ c * words + w ] | sBits[ u * words + w ] ) : sBits[ c * words + w ]; - const std::uint64_t p = mark == DefMark::Proven ? ( pBits[ c * words + w ] | pBits[ u * words + w ] ) : pBits[ c * words + w ]; - changed = changed || a != aBits[ c * words + w ] || s != sBits[ c * words + w ] || p != pBits[ c * words + w ]; - aBits[ c * words + w ] = a; - sBits[ c * words + w ] = s; - pBits[ c * words + w ] = p; - } - if( changed && !queued[c] ) + const std::size_t c = bits.denseOf( ci[k] ); + const std::size_t e = edgeIndex( g, ci[k], bits.dense[u] ); + // outside the reach set (cannot happen for a full closure) or no out-edge twin: nothing to fold + if( c != SIZE_MAX && e != SIZE_MAX && bits.foldInto( c, u, defMarkOfEdge( g, e ) ) && !queued[c] ) { queued[c] = 1; work.push_back( c ); } } } + return bits; +} + +inline RowDefTokens attributeReach( const Graph& g, const DefGroups& groups, std::span seeds, std::span reach, + std::span rows ) +{ + const ReachBits bits = reachBitsets( g, groups, seeds, reach ); std::vector ties; for( const NodeId r : rows ) { - const std::size_t d = denseOf( r ); - if( d == SIZE_MAX ) + const std::size_t d = bits.denseOf( r ); + for( std::size_t grp = 0; grp < groups.size() && d != SIZE_MAX; ++grp ) { - continue; - } - for( std::size_t grp = 0; grp < groups.size(); ++grp ) - { - const std::uint64_t bit = std::uint64_t( 1 ) << ( grp % 64 ); - const std::size_t w = d * words + grp / 64; - const DefMark m = ( pBits[w] & bit ) ? DefMark::Proven : ( sBits[w] & bit ) ? DefMark::Split : ( aBits[w] & bit ) ? DefMark::Name : DefMark::None; - if( m != DefMark::None ) + if( const DefMark m = bits.markOf( d, grp ); m != DefMark::None ) { ties.push_back( DefTie{ r, std::uint32_t( grp ), std::uint8_t( m ) } ); } @@ -635,13 +637,15 @@ inline void defTokenTo( std::string& out, const DefGroups& groups, std::uint32_t } } // A row's tokens, space-separated (the sites_at= convention). -inline void defTokensTo( std::string& out, const DefGroups& groups, std::span toks ) +inline std::string defTokens( const DefGroups& groups, std::span toks ) { + std::string text; for( std::size_t i = 0; i < toks.size(); ++i ) { - out += i ? " " : ""; - defTokenTo( out, groups, toks[i] ); + text += i ? " " : ""; + defTokenTo( text, groups, toks[i] ); } + return text; } inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { @@ -649,10 +653,7 @@ inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& to { return {}; } - std::string out = " of_def=\""; - defTokensTo( out, groups, tokens.of( row ) ); - out += '"'; - return out; + return " of_def=\"" + defTokens( groups, tokens.of( row ) ) + "\""; } inline std::string ofDefKeyJson( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { @@ -674,16 +675,14 @@ inline std::string ofDefKeyJson( const DefGroups& groups, const RowDefTokens& to // The columnar of_def column's value for one row: the tokens space-separated (the sites_at column's convention). inline std::string ofDefColumnValue( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { - std::string out; - defTokensTo( out, groups, tokens.of( row ) ); - return out; + return defTokens( groups, tokens.of( row ) ); } // Q1: a row of a ONE-group answer whose every non-by-name edge to the selector is a split arm says so, in the map's per-edge // vocabulary. Never on a multi-group answer (its tokens carry ~s), never on a by-name row (via="name" already hedges it). inline bool rowSplitArm( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) noexcept { - return !groups.multi() && !tokens.empty() && tokens.strongest( row ) == DefMark::Split; + return !groups.multi() && tokens.strongest( row ) == DefMark::Split; // None when no tie was recorded } inline const char* splitArmAttrXml( bool on ) noexcept { return on ? " prov=\"split\"" : ""; } inline const char* splitArmKeyJson( bool on ) noexcept { return on ? ",\"prov\":\"split\"" : ""; } @@ -801,7 +800,7 @@ struct OneHopAttribution DefGroups groups; RowDefTokens tokens; // parallel to the rows; empty when nothing was computed - bool nameOnly( std::size_t row ) const noexcept { return !tokens.empty() && tokens.strongest( row ) == DefMark::Name; } + bool nameOnly( std::size_t row ) const noexcept { return tokens.strongest( row ) == DefMark::Name; } // None when none recorded bool splitArm( std::size_t row ) const noexcept { return rowSplitArm( groups, tokens, row ); } }; inline OneHopAttribution attributeOneHopAnswer( const IngestResult& ing, const Graph& g, std::span matches, @@ -827,8 +826,8 @@ struct ReachAttribution std::size_t indexOf( NodeId n ) const noexcept { - const auto it = std::lower_bound( reach.begin(), reach.end(), n ); - return ( it != reach.end() && *it == n ) ? std::size_t( it - reach.begin() ) : SIZE_MAX; + const auto [ first, last ] = std::equal_range( reach.begin(), reach.end(), n ); + return first == last ? SIZE_MAX : std::size_t( first - reach.begin() ); } std::string ofDefXml( NodeId n ) const { @@ -922,7 +921,7 @@ inline PathEndpointBinding pathEndpointBinding( const Graph& g, std::size_t srcD } else { - const std::size_t e = defEdgeIndex( g, path[ path.size() - 2 ], path.back() ); + const std::size_t e = edgeIndex( g, path[ path.size() - 2 ], path.back() ); b.to = e != SIZE_MAX && defMarkOfEdge( g, e ) == DefMark::Proven; } } @@ -935,14 +934,7 @@ inline bool pathHopSplitArm( const Graph& g, std::span path, std:: } inline bool pathHasSplitHop( const Graph& g, std::span path ) noexcept { - for( std::size_t i = 1; i < path.size(); ++i ) - { - if( pathHopSplitArm( g, path, i ) ) - { - return true; - } - } - return false; + return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeSplitArm( g, from, to ); } ) != path.end(); } } // namespace rw diff --git a/src/graph.h b/src/graph.h index f5ff4a92a..88e8f8815 100644 --- a/src/graph.h +++ b/src/graph.h @@ -377,45 +377,29 @@ inline bool pathHasNameOnlyHop( const Graph& g, std::span path ) n return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ) != path.end(); } -// FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) -inline bool edgeExists( const Graph& g, NodeId from, NodeId to ) noexcept +// The out-CSR position of the call edge from → to, or SIZE_MAX when there is none (ascending within a source: buildGraph sorts +// by (from, to)). The per-edge bytes (outNameOnly, outProv) are read at it; defattrib.h's row attribution reads both. +inline std::size_t edgeIndex( const Graph& g, NodeId from, NodeId to ) noexcept { if( std::size_t( from ) + 1 >= g.outOff.size() ) { - return false; + return SIZE_MAX; } - const auto b = g.outTargets.begin() + g.outOff[ from ]; - const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; - return std::binary_search( b, e, to ); + const auto b = g.outTargets.begin() + g.outOff[ from ]; + const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; + const auto it = std::lower_bound( b, e, to ); + return ( it != e && *it == to ) ? std::size_t( it - g.outTargets.begin() ) : SIZE_MAX; } -// FE-B: a neighbour row of a selector that matched several definitions is via="name" iff EVERY edge joining it to one -// of them is name-only — one proven edge makes the row proven. `towardMatches`: the row is a CALLER (row → match); -// else a callee (match → row). false when no edge joins them, and on a graph with no name-only edge. -inline bool rowNameOnly( const Graph& g, std::span matches, NodeId row, bool towardMatches ) noexcept +// FE-B: does the call edge from → to exist at all +inline bool edgeExists( const Graph& g, NodeId from, NodeId to ) noexcept { - if( g.outNameOnly.empty() ) - { - return false; - } - bool any = false; - for( const NodeId m : matches ) - { - const NodeId from = towardMatches ? row : m; - const NodeId to = towardMatches ? m : row; - if( !edgeExists( g, from, to ) ) - { - continue; - } - if( !edgeNameOnly( g, from, to ) ) - { - return false; - } - any = true; - } - return any; + return edgeIndex( g, from, to ) != SIZE_MAX; } +// FE-B's per-row via="name" (EVERY edge joining a row to the selector's definitions is name-only) is defattrib.h's +// OneHopAttribution::nameOnly now: the strongest tie the one attribution pass recorded is by name alone. + // The via="name" spelling every XML row of a name-only edge carries (" via=\"name\"", 11 bytes), "" otherwise. inline const char* viaNameAttr( bool nameOnly ) noexcept { diff --git a/src/graphlegend.h b/src/graphlegend.h index 5fab48f42..5d4d6775d 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -925,7 +925,13 @@ inline constexpr const char* kDefTableLegend = inline constexpr const char* kDefsListedLegend = "defs_listed=N: above 16 groups the table lists only the N that this page's rows name. "; inline std::string defTableLegend( bool table, bool cut ) { - return table ? std::string( kDefTableLegend ) + ( cut ? kDefsListedLegend : "" ) : std::string(); + std::string clause; + if( table ) + { + clause = kDefTableLegend; + clause += cut ? kDefsListedLegend : ""; + } + return clause; } inline constexpr const char* kSplitArmRowLegend = "prov=\"split\" on a row: its edge to of= (on a path, the hop into it) carries the resolver's split label, a call along it was one " From 0c397b6fce869e64327ba8964b9aebb020b88e37 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:13:14 -0400 Subject: [PATCH 043/162] refactor(attrib): one path-hop walk for via= and prov=, edgeExists folded into edgeIndex quality-delta follow-up: pathHasHopWhere is the walk behind pathHasNameOnlyHop and pathHasSplitHop; edgeExists lost its last caller with rowNameOnly. Output unchanged. Co-Authored-By: Claude Opus 5.5 --- src/callhierarchy.h | 3 +-- src/defattrib.h | 18 +++++++++++++----- src/graph.h | 19 ++++++++++--------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/callhierarchy.h b/src/callhierarchy.h index 4c24779e2..0b1735e5a 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -329,8 +329,7 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph rankBeforeCap( ing, out.rows, [ & ]( NodeId r ) { return ing.symbols[r].fileId; }, [ & ]( NodeId r ) { return navRelevanceWeight( g, r ); } ); } - // attrib-multidef (defattrib.h): each row's definition, over the FINAL row order, for every dialect of both surfaces - out.attr = attributeOneHopAnswer( ing, g, out.matches, out.rows, wantCallers ); + out.attr = attributeOneHopAnswer( ing, g, out.matches, out.rows, wantCallers ); // attrib-multidef, over the FINAL row order return out; } diff --git a/src/defattrib.h b/src/defattrib.h index 3d287d164..3f6e988dd 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -640,10 +640,14 @@ inline void defTokenTo( std::string& out, const DefGroups& groups, std::uint32_t inline std::string defTokens( const DefGroups& groups, std::span toks ) { std::string text; - for( std::size_t i = 0; i < toks.size(); ++i ) + for( const std::uint32_t tok : toks ) + { + defTokenTo( text, groups, tok ); + text += ' '; + } + if( !text.empty() ) { - text += i ? " " : ""; - defTokenTo( text, groups, toks[i] ); + text.pop_back(); // the separator after the last token } return text; } @@ -682,7 +686,11 @@ inline std::string ofDefColumnValue( const DefGroups& groups, const RowDefTokens // vocabulary. Never on a multi-group answer (its tokens carry ~s), never on a by-name row (via="name" already hedges it). inline bool rowSplitArm( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) noexcept { - return !groups.multi() && tokens.strongest( row ) == DefMark::Split; // None when no tie was recorded + if( groups.multi() ) + { + return false; // its tokens carry ~s instead + } + return tokens.strongest( row ) == DefMark::Split; // None when no tie was recorded } inline const char* splitArmAttrXml( bool on ) noexcept { return on ? " prov=\"split\"" : ""; } inline const char* splitArmKeyJson( bool on ) noexcept { return on ? ",\"prov\":\"split\"" : ""; } @@ -934,7 +942,7 @@ inline bool pathHopSplitArm( const Graph& g, std::span path, std:: } inline bool pathHasSplitHop( const Graph& g, std::span path ) noexcept { - return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeSplitArm( g, from, to ); } ) != path.end(); + return pathHasHopWhere( path, [ & ]( NodeId from, NodeId to ) { return edgeSplitArm( g, from, to ); } ); } } // namespace rw diff --git a/src/graph.h b/src/graph.h index 88e8f8815..88ac4adf4 100644 --- a/src/graph.h +++ b/src/graph.h @@ -370,11 +370,18 @@ inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept return edgeNameOnlyInCsr( g.outOff, g.outTargets, g.outNameOnly, from, to ); } -// FE-B: does any hop of a directed call path (path[i-1] → path[i]) run through a name-only edge? --path and MCP -// path_between ask it once to decide their via="name" legend sentence (one spelling for both transports). +// Does any hop of a directed call path (path[i-1] → path[i]) satisfy `edgeHas( from, to )`? The one walk behind the path +// legends' present-only questions (via="name" here, defattrib.h's prov="split"). +template +inline bool pathHasHopWhere( std::span path, EdgePred&& edgeHas ) noexcept +{ + return std::adjacent_find( path.begin(), path.end(), edgeHas ) != path.end(); +} +// FE-B: does any hop run through a name-only edge? --path and MCP path_between ask it once to decide their via="name" +// legend sentence (one spelling for both transports). inline bool pathHasNameOnlyHop( const Graph& g, std::span path ) noexcept { - return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ) != path.end(); + return pathHasHopWhere( path, [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ); } // The out-CSR position of the call edge from → to, or SIZE_MAX when there is none (ascending within a source: buildGraph sorts @@ -391,12 +398,6 @@ inline std::size_t edgeIndex( const Graph& g, NodeId from, NodeId to ) noexcept return ( it != e && *it == to ) ? std::size_t( it - g.outTargets.begin() ) : SIZE_MAX; } -// FE-B: does the call edge from → to exist at all -inline bool edgeExists( const Graph& g, NodeId from, NodeId to ) noexcept -{ - return edgeIndex( g, from, to ) != SIZE_MAX; -} - // FE-B's per-row via="name" (EVERY edge joining a row to the selector's definitions is name-only) is defattrib.h's // OneHopAttribution::nameOnly now: the strongest tie the one attribution pass recorded is by name alone. From 30b6b89df34c38cdc9500cdfa74f49870cd577ec Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:21:30 -0400 Subject: [PATCH 044/162] refactor(attrib): split-hop count instead of a second path walk wrapper; attribution hook on the return quality-delta follow-up: pathHasNameOnlyHop keeps its own body; the split-hop legend question counts pathHopSplitArm; callHierarchyRows returns through withOneHopAttribution. Output unchanged (attribmultidefcheck 140 PASS). Co-Authored-By: Claude Opus 5.5 --- src/callhierarchy.h | 11 +++++++++-- src/defattrib.h | 10 ++++++++-- src/graph.h | 13 +++---------- src/mcpverbs.h | 2 +- src/verbs_navigate.h | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/callhierarchy.h b/src/callhierarchy.h index 0b1735e5a..b6e90a26a 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -244,6 +244,14 @@ inline std::pair callHierarchyNextSelector( const Ingest return { name, true }; } +// attrib-multidef (defattrib.h): which definition each row belongs to, over the rows in their FINAL order — computed here, +// once, so every dialect of both surfaces (the CLI verbs and the MCP twins) reads one attribution. +inline CallHierarchyRows withOneHopAttribution( const IngestResult& ing, const Graph& g, CallHierarchyRows rows, bool wantCallers ) +{ + rows.attr = attributeOneHopAnswer( ing, g, rows.matches, rows.rows, wantCallers ); + return rows; +} + inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph& g, std::string_view selector, bool wantCallers, const ValueRefIndex* valueIndex = nullptr ) // the MCP server passes its cached one { @@ -329,8 +337,7 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph rankBeforeCap( ing, out.rows, [ & ]( NodeId r ) { return ing.symbols[r].fileId; }, [ & ]( NodeId r ) { return navRelevanceWeight( g, r ); } ); } - out.attr = attributeOneHopAnswer( ing, g, out.matches, out.rows, wantCallers ); // attrib-multidef, over the FINAL row order - return out; + return withOneHopAttribution( ing, g, std::move( out ), wantCallers ); } } // namespace rw diff --git a/src/defattrib.h b/src/defattrib.h index 3f6e988dd..5d265da4a 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -940,9 +940,15 @@ inline bool pathHopSplitArm( const Graph& g, std::span path, std:: { return i > 0 && i < path.size() && edgeSplitArm( g, path[ i - 1 ], path[i] ); } -inline bool pathHasSplitHop( const Graph& g, std::span path ) noexcept +// How many hops of the path are split arms — both path emitters ask whether any is, for the legend clause (present-only). +inline std::size_t pathSplitHopCount( const Graph& g, std::span path ) noexcept { - return pathHasHopWhere( path, [ & ]( NodeId from, NodeId to ) { return edgeSplitArm( g, from, to ); } ); + std::size_t hops = 0; + for( std::size_t i = 1; i < path.size(); ++i ) + { + hops += pathHopSplitArm( g, path, i ) ? 1u : 0u; + } + return hops; } } // namespace rw diff --git a/src/graph.h b/src/graph.h index 88ac4adf4..0c4bb09a5 100644 --- a/src/graph.h +++ b/src/graph.h @@ -370,18 +370,11 @@ inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept return edgeNameOnlyInCsr( g.outOff, g.outTargets, g.outNameOnly, from, to ); } -// Does any hop of a directed call path (path[i-1] → path[i]) satisfy `edgeHas( from, to )`? The one walk behind the path -// legends' present-only questions (via="name" here, defattrib.h's prov="split"). -template -inline bool pathHasHopWhere( std::span path, EdgePred&& edgeHas ) noexcept -{ - return std::adjacent_find( path.begin(), path.end(), edgeHas ) != path.end(); -} -// FE-B: does any hop run through a name-only edge? --path and MCP path_between ask it once to decide their via="name" -// legend sentence (one spelling for both transports). +// FE-B: does any hop of a directed call path (path[i-1] → path[i]) run through a name-only edge? --path and MCP +// path_between ask it once to decide their via="name" legend sentence (one spelling for both transports). inline bool pathHasNameOnlyHop( const Graph& g, std::span path ) noexcept { - return pathHasHopWhere( path, [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ); + return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ) != path.end(); } // The out-CSR position of the call edge from → to, or SIZE_MAX when there is none (ascending within a source: buildGraph sorts diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 1f58ebdbf..ee5c3ab41 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3498,7 +3498,7 @@ inline std::optional pathText( const std::string& root, const std:: const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI - const bool ptHasSplit = pathHasSplitHop( g, pth ); + const bool ptHasSplit = pathSplitHopCount( g, pth ) > 0; rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) ).c_str(), diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index b6900b1ca..0f7dd4a54 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2373,7 +2373,7 @@ std::optional runPath( const MainDispatch& d ) const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" // attrib-multidef (defattrib.h): from_p=/to_p= only for an endpoint the path bound; prov="split" on a split-arm hop const rw::PathEndpointBinding pthBound = rw::pathEndpointBinding( g, rw::endpointDefinitionCount( ing, srcDefs ), rw::endpointDefinitionCount( ing, dstDefs ), path ); - const bool pthHasSplit = rw::pathHasSplitHop( g, path ); + const bool pthHasSplit = rw::pathSplitHopCount( g, path ) > 0; rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) ).c_str(), From b8383fea67a64025c770e25bff955aef9f29c7f4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:29:52 -0400 Subject: [PATCH 045/162] refactor(attrib): the path's split-hop bit rides PathEndpointBinding quality-delta follow-up: one walk over the path decides the endpoints' binding and whether a hop row carries prov="split". Output unchanged (attribmultidefcheck 140 PASS). Co-Authored-By: Claude Opus 5.5 --- src/defattrib.h | 29 ++++++++++++----------------- src/mcpverbs.h | 2 +- src/verbs_navigate.h | 2 +- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/defattrib.h b/src/defattrib.h index 5d265da4a..9763e436e 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -901,8 +901,9 @@ inline void attachRollupOfDef( RollupT& rollup, const IngestResult& ing, const R // `srcDefs` / `dstDefs` are endpointDefinitionCount's, not the raw match counts. struct PathEndpointBinding { - bool from = true; - bool to = true; + bool from = true; + bool to = true; + bool splitHop = false; // some hop's edge into it is a split arm (its row carries prov="split"; the legend clause rides) }; // How many definitions an endpoint really names: its groups (a definition with its declarations is one), less the untied // declarations (defGroupUntied) — a prototype the identity rule cannot prove is the same function is no second candidate here, @@ -917,10 +918,19 @@ inline std::size_t endpointDefinitionCount( const IngestResult& ing, std::span( 1, groups.size() - untied ); } +// a hop row (i > 0) whose edge into it is a split arm (Q1: on every path, whatever the endpoints' defs=) +inline bool pathHopSplitArm( const Graph& g, std::span path, std::size_t i ) noexcept +{ + return i > 0 && i < path.size() && edgeSplitArm( g, path[ i - 1 ], path[i] ); +} inline PathEndpointBinding pathEndpointBinding( const Graph& g, std::size_t srcDefs, std::size_t dstDefs, std::span path ) noexcept { PathEndpointBinding b; b.from = srcDefs <= 1 || !path.empty(); + for( std::size_t i = 1; i < path.size() && !b.splitHop; ++i ) + { + b.splitHop = pathHopSplitArm( g, path, i ); + } if( dstDefs > 1 ) { if( path.size() < 2 ) @@ -935,20 +945,5 @@ inline PathEndpointBinding pathEndpointBinding( const Graph& g, std::size_t srcD } return b; } -// a hop row (i > 0) whose edge into it is a split arm (Q1: on every path, whatever the endpoints' defs=) -inline bool pathHopSplitArm( const Graph& g, std::span path, std::size_t i ) noexcept -{ - return i > 0 && i < path.size() && edgeSplitArm( g, path[ i - 1 ], path[i] ); -} -// How many hops of the path are split arms — both path emitters ask whether any is, for the legend clause (present-only). -inline std::size_t pathSplitHopCount( const Graph& g, std::span path ) noexcept -{ - std::size_t hops = 0; - for( std::size_t i = 1; i < path.size(); ++i ) - { - hops += pathHopSplitArm( g, path, i ) ? 1u : 0u; - } - return hops; -} } // namespace rw diff --git a/src/mcpverbs.h b/src/mcpverbs.h index ee5c3ab41..f30d5e5e4 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3498,7 +3498,7 @@ inline std::optional pathText( const std::string& root, const std:: const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI - const bool ptHasSplit = pathSplitHopCount( g, pth ) > 0; + const bool ptHasSplit = ptBound.splitHop; rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) ).c_str(), diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 0f7dd4a54..6c966fe67 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2373,7 +2373,7 @@ std::optional runPath( const MainDispatch& d ) const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" // attrib-multidef (defattrib.h): from_p=/to_p= only for an endpoint the path bound; prov="split" on a split-arm hop const rw::PathEndpointBinding pthBound = rw::pathEndpointBinding( g, rw::endpointDefinitionCount( ing, srcDefs ), rw::endpointDefinitionCount( ing, dstDefs ), path ); - const bool pthHasSplit = rw::pathSplitHopCount( g, path ) > 0; + const bool pthHasSplit = pthBound.splitHop; rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) ).c_str(), From 7340fd70233a989781af71e263531c1e7e0f1f01 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:47:43 -0400 Subject: [PATCH 046/162] feat(for): a --for row of a resolver-split call carries prov="split" (Q1) The orchestrator's Q1 ruling extends the split-arm hedge to --for's bundle: a body's or a hop's row whose edge is one arm of a resolver split carries prov="split", the map's own per-edge label. Defined in --for's first-screen legend on the same over-approximated bit as via="name" (serialize.h splitArmsOutAny over the same ids), else as a comment beside the rows (packBodies/packHops, charged like via's); a merged names-only row merges only rows of the same split bit. Every other caller of packBodies/packHops (--expand, --pack-task, --from-trace, --exemplar) passes no outProv and keeps its bytes. Kept separate on purpose: it edits the --for header and section emitters lane forbudget-soft reworks, so the train can land it after that lane, rebased and re-measured. Gate attribmultidefcheck (I): red on base (2 FAIL), head 144 PASS. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + src/graphlegend.h | 12 ++++ src/serialize.h | 110 ++++++++++++++++++++++++++++++------ src/verbs_for.h | 35 ++++++++---- test/attribmultidefcheck.sh | 19 +++++++ 5 files changed, 150 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e07e5001..2909d172a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ endpoint has several definitions and no proven hop bound one. Measured base vs h 16 MCP requests (single-definition selectors, prototype + definition pairs, every dialect): 212 and 16 byte-identical; the other five are the split-arm break above (2) and one C++ prototype whose parameter types are spelled differently from its definition (`std::string` vs `string`), which the declaration identity rule reads as another function (3). +On `--for`, a body's or a hop's `` row whose edge is a resolver split arm carries the same `prov="split"`, defined in the +bundle's first-screen legend when any listed callee edge can carry it (the `via="name"` rule's over-approximation). ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) diff --git a/src/graphlegend.h b/src/graphlegend.h index 5d4d6775d..31a872acb 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -1016,6 +1016,18 @@ inline std::string forViaNameClause() { return std::string( kForViaNameClause ); } +// attrib-multidef Q1: a row whose edge is one arm of a resolver split carries prov="split" — the same per-edge label +// the map prints — on --for's bodies and hops. Defined in --for's FIRST-SCREEN legend on an over-approximated bit (serialize.h +// splitArmsOutAny), as via="name" is, and as a comment beside the rows on a bundle whose head did not define it. +inline constexpr std::string_view kForCompactSplitClause = + "; c prov=split: that callee's edge carries the resolver's split label, so it may be another arm's (NOT a claim the edge is false)"; +inline constexpr std::string_view kForSplitClause = + " prov=\"split\" on a row: that callee's edge carries the resolver's split label (a call along it was one arm of a call split " + "between same-named definitions), so it may be another arm's. It does NOT mean the edge is false."; +inline std::string splitArmCallsLegendComment() +{ + return ""; +} // …as its own comment node, for the surfaces whose legend is written beside the rows (a / block's ) inline std::string viaNameLegendComment() { diff --git a/src/serialize.h b/src/serialize.h index 20c05d57e..f1e13d510 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2745,6 +2745,26 @@ inline bool namesOnlyOutAny( const std::vector& outOff, const std return false; } +// attrib-multidef Q1: may a row under one of `nodes` carry prov="split"? True when any of their out-edges is one arm of a +// resolver split and not by name alone (a by-name row carries via="name" instead) — namesOnlyOutAny's twin question, over +// the same nodes, for the same first-screen legend. +inline bool splitArmsOutAny( const std::vector& outOff, const std::vector& outNameOnly, + const std::vector& outProv, std::span nodes ) noexcept +{ + for( const NodeId n : nodes ) + { + for( std::uint32_t e = ( std::size_t( n ) + 1 < outOff.size() ) ? outOff[ n ] : 0u; + std::size_t( n ) + 1 < outOff.size() && e < outOff[ n + 1 ] && e < outProv.size(); ++e ) + { + if( outProv[ e ] == 3u && !( e < outNameOnly.size() && outNameOnly[ e ] != 0 ) ) + { + return true; + } + } + } + return false; +} + // THE HOP-SLOT RULE as one predicate (gate test/forsigspancheck.sh (H)): a candidate earns a packHops slot only with // at least one callee edge that is NOT name-only (empty bits: a graph with no name-only edge, so every edge is proven). // packHops' slot test and --for's header question "may a hop row carry via=name?" (verbs_for.h) both read it, so a hop @@ -6481,6 +6501,10 @@ struct CalleeCallsSink // every pre-existing caller is byte-identical. `cappedNext` is already XML-escaped and written only when cut. std::uint32_t rowCap = 16; std::string_view cappedNext; + // attrib-multidef Q1: Graph::outProv (parallel to outTargets), so a callee row whose edge is a split arm carries prov="split"; + // nullptr / empty ⇒ no row is marked. `splitLegendCharged`: as viaLegendCharged, for its comment (kForSplitClause). + const std::vector* prov = nullptr; + bool* splitLegendCharged = nullptr; }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` @@ -6625,17 +6649,18 @@ struct MergedCalleeNameRow std::string_view name; // a view into ing.symbols — stable for the emitter's lifetime std::vector lines; // every definition line of that name; joined ascending at append time bool via = false; // FE-B: these edges are name-only — merged only with rows of the same bit + bool split = false; // attrib-multidef Q1: these edges are split arms — likewise }; inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, - std::size_t& used, const CalleeCallsSink& sink, bool via = false ) + std::size_t& used, const CalleeCallsSink& sink, bool via = false, bool split = false ) { char lb[ 16 ]; rw::formatTo( lb, sizeof( lb ), "{}", cs.line ); bool merged = false; for( MergedCalleeNameRow& r : rows ) { - if( r.name == cs.name && r.via == via ) + if( r.name == cs.name && r.via == via && r.split == split ) { r.lines.push_back( cs.line ); merged = true; break; @@ -6643,10 +6668,11 @@ inline void collectCalleeNameRow( std::vector& rows, const } if( !merged ) { - rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via } ); + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via, split } ); } - // the comma and the digits a merge appends, or the whole row it opens (FE-B: plus its 11-byte via="name") - used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( via ? 11u : 0u ); + // the comma and the digits a merge appends, or the whole row it opens (FE-B: plus its 11-byte via="name"; Q1: its 13-byte + // prov="split") + used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( via ? 11u : 0u ) + ( split ? 13u : 0u ); if( sink.recorded ) { sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, std::string() } ); // §H5: no sig to record @@ -6674,6 +6700,7 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& outOff, const std::v return via; } +// attrib-multidef Q1: the sink a section passes, with the split-arm bytes it reads and the flag its comment charge sets +inline CalleeCallsSink withSplitArms( CalleeCallsSink sink, const std::vector* prov, bool* splitLegendCharged ) noexcept +{ + sink.prov = prov; + sink.splitLegendCharged = splitLegendCharged; + return sink; +} + +// attrib-multidef Q1: is the row id → cid a split arm (and not by name alone: that row says via="name")? The first +// such row of an answer charges the comment that defines it to `used`, before the row, when the head did not define it. +inline bool calleeRowSplit( const std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, bool via, + std::size_t& used, const CalleeCallsSink& sink ) +{ + if( via || sink.prov == nullptr || sink.prov->empty() || std::size_t( id ) + 1 >= outOff.size() ) + { + return false; + } + const auto b = outTargets.begin() + outOff[ id ]; + const auto e = outTargets.begin() + outOff[ std::size_t( id ) + 1 ]; + const auto it = std::lower_bound( b, e, cid ); // ascending within a source (buildGraph sorts by (from, to)) + const bool split = it != e && *it == cid && ( *sink.prov )[ std::size_t( it - outTargets.begin() ) ] == 3u; + if( split && sink.splitLegendCharged != nullptr && !*sink.splitLegendCharged ) + { + used += splitArmCallsLegendComment().size(); + *sink.splitLegendCharged = true; + } + return split; +} + // §P10.1: the disclosed block // for one body's 1-hop callee signatures — extracted out of packBodies so the disclosure logic doesn't // inflate packBodies' own complexity/LOC. `total` is outOff[id+1]-outOff[id] — outTargets is deduped-per- @@ -6730,11 +6786,13 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } const Symbol& cs = ing.symbols[cid]; const std::string_view viaAttr = viaNameAttr( calleeRowVia( outOff, outTargets, id, cid, used, sink ) ); // FE-B: via="name" or "" + const bool split = calleeRowSplit( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); // Q1 + const std::string_view splitAttr = split ? std::string_view( " prov=\"split\"" ) : std::string_view(); // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) { - collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty() ); + collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty(), split ); ++shown; continue; } @@ -6749,10 +6807,10 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector { continue; } - char hb[ 48 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}>", cs.line, viaAttr ); + char hb[ 64 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}{}>", cs.line, viaAttr, splitAttr ); callsBody += "* outProv = nullptr, // attrib-multidef Q1: Graph::outProv — the rows' + // prov="split"; nullptr (every caller but --for) ⇒ none + bool splitLegendInHead = false ) // Q1: viaLegendInHead's twin (serialize.h splitArmsOutAny) { bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) + bool splitLegendCharged = splitLegendInHead; // attrib-multidef Q1: likewise for the prov="split" comment // budgetBytes == 0 ⇒ UNLIMITED (A3-F2): the MCP `exemplar` verb has no byte budget, and 0 must never // mean "cap at zero bytes" (the cap fired before the first body and emitted a bare ). // Matches buildRecall's "0 = no cap" convention; the CLI always passes a real budget (default 64 KB). @@ -7430,7 +7492,8 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // total=/shown=/capped= block — see emitCalleeCallsBlock above; `calleeRank` decides which // rows survive when it CUTS one, which is far from rare here (CalleeCallsSink::rank). emitCalleeCallsBlock( piece, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, &viaLegendCharged } ); + withSplitArms( CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, + &viaLegendCharged }, outProv, &splitLegendCharged ) ); const std::string bodyNotes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 piece += bodyNotes; used += bodyNotes.size(); // W3-N2: same charge-never-trim rule @@ -7506,6 +7569,10 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect { w.write( viaNameLegendComment() ); // FE-B: the same rule, for a row's via="name" } + if( !splitLegendInHead && children.find( " prov=\"split\"" ) != std::string::npos ) + { + w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: the same rule, for a row's prov="split" + } w.write( children ); w.write( "" ); w.flush(); @@ -7560,7 +7627,8 @@ inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector const std::vector& outOff, const std::vector& outTargets, const std::vector& outNameOnly, RedactCounts* redact, const std::vector* rank, std::string_view rootArg, std::size_t rankedBudget, bool rankedVia, - PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc ) + PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc, + const std::vector* outProv = nullptr, bool* splitLegendCharged = nullptr ) // Q1 { OwnerHopRows out; out.rankedBudget = rankedBudget; @@ -7599,8 +7667,8 @@ inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector } used += rowFixed; emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budget, - CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, - kForOwnerHopNameGuard, next } ); + withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, + kForOwnerHopNameGuard, next }, outProv, splitLegendCharged ) ); out.xml += row; out.xml += ""; ++out.shown; @@ -7675,9 +7743,12 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing std::string_view rootArg = {}, // R-E: same single-root-only root= every verb takes bool viaLegendInHead = false, // FE-B: packBodies' parameter of the same name - const HopOwners* owners = nullptr ) // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) + const HopOwners* owners = nullptr, // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) + const std::vector* outProv = nullptr, // attrib-multidef Q1: packBodies' parameter of the same name + bool splitLegendInHead = false ) // Q1: likewise { bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) + bool splitLegendCharged = splitLegendInHead; // attrib-multidef Q1: likewise for the prov="split" comment if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention { budgetBytes = SIZE_MAX; @@ -7722,7 +7793,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // owner-hop: the leading owner rows first (THE QUESTION'S OWNER ROWS above; nothing when `owners` is null), then the // ranked rows exactly as before, from the node the owners end at, on the budget and via charge the owners leave them const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, outNameOnly, redact, rank, rootArg, - budgetBytes, viaLegendCharged, pathRel, contentOf, esc ); + budgetBytes, viaLegendCharged, pathRel, contentOf, esc, outProv, &splitLegendCharged ); std::string children = own.xml; std::size_t requestedCount = own.shown; std::size_t shownCount = own.shown; @@ -7773,7 +7844,8 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // the 1-hop callee signatures — the identical block a body carries, charged against the same // running `used` so the row identity bytes and the edge bytes share one budget. emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged } ); + withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged }, + outProv, &splitLegendCharged ) ); row += ""; children += row; ++shownCount; @@ -7789,6 +7861,10 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector { w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" } + if( !splitLegendInHead && children.find( " prov=\"split\"" ) != std::string::npos ) + { + w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: exactly when a hop's row carries prov="split" + } w.write( openTag ); w.write( children ); w.write( "" ); diff --git a/src/verbs_for.h b/src/verbs_for.h index a14ab77a9..bc38fb347 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -561,6 +561,8 @@ struct ForLensHeaderParts bool viaByOwnerOnly = false; // owner-hop: viaPresent holds ONLY because the owner rows joined the via question // (the six ranked hop ids alone answer no) — the via clause is then the owner // rows' disclosure, exempt from the sig trim exactly as the qword clause is + bool splitPresent = false; // attrib-multidef Q1: may a row carry prov="split"? viaPresent's twin + // question over the same ids (serialize.h splitArmsOutAny); set after construction // ── THE DROPPABLE LEGEND, as ONE bit ────────────────────────────────────────────────────────────── // confidenceNote / tailLegend / idRouteLegend moved in lock step at every read and every write, and the @@ -960,6 +962,7 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, { p.modScopePresent, rw::kForCompactModScopeClause }, // #60 { p.endLinePresent, rw::kForCompactEndLineLegend }, { p.viaPresent, rw::kForCompactViaNameClause }, // FE-B + { p.splitPresent, rw::kForCompactSplitClause }, // attrib-multidef Q1 }; for( const auto& [ on, clause ] : kPresentOnly ) { @@ -1118,6 +1121,10 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout { h.append( rw::forViaNameClause() ); // FE-B: present-only, on the same bit the compact strip reads } + if( p.splitPresent ) + { + h.append( rw::kForSplitClause ); // attrib-multidef Q1: likewise + } if( p.legendDropped ) { // L1: the droppable clauses went to the ceiling — say so, and name exactly the ones THIS document had @@ -2119,7 +2126,7 @@ std::vector computeAutoBodyCandidateIds( const rw::IngestResult& ing ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector& lensSurfaceIds, const std::vector& lensRank, std::size_t committedBytes, std::size_t bundleBudget, rw::RedactCounts* redactPtr, - const std::vector& anchorDefs, bool viaLegendInHead ) + const std::vector& anchorDefs, bool viaLegendInHead, bool splitLegendInHead = false ) { ForAutoBodiesResult out; // R-E (2026-08-17 harvest): same single-root condition every other verb's root= uses (sarif.h). @@ -2184,7 +2191,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR out.section = rw::chargeSection( [ & ]( std::FILE* f ) { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, nullptr, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead, &g.outProv, splitLegendInHead ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) { @@ -2206,7 +2213,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR out.section = rw::chargeSection( [ & ]( std::FILE* f ) { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &autoEmitted, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead, &g.outProv, splitLegendInHead ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) @@ -2383,6 +2390,7 @@ struct ForSectionPromises { bool viaLegendInHead = false; const ForOwnerPlan* owners = nullptr; // nullptr ⇒ no owner rows (an empty plan reads the same) + bool splitLegendInHead = false; // attrib-multidef Q1: the header defines prov="split" (splitPresent) }; // ASCII fold of one name or word, leading _ # $ @ removed (the owner match key) @@ -2713,7 +2721,7 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest out.section = rw::chargeSection( [ & ]( std::FILE* f ) { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, g.outNameOnly, redactPtr, &lensRank, fcRootArg, - viaLegendInHead, &owners ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) + viaLegendInHead, &owners, &g.outProv, promised.splitLegendInHead ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) // MARKUP rate, not the body rate — and this is an honesty choice, not a copy-paste slip. The body // rate (3.80 B/tok) prices SOURCE TEXT; the compact section contains none, only tags, identifiers // and line numbers, which tokenize like the rest of the bundle. Charging structured markup at the @@ -2753,7 +2761,8 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR const std::size_t committed = committedBytes + plan.attrReserve; ForAutoBodiesResult out = plan.compact ? buildForCompactHops( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, promised ) - : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, promised.viaLegendInHead ); + : buildForAutoBodies( cfg, ing, g, lensSurfaceIds, lensRank, committed, bundleBudget, redactPtr, anchorDefs, promised.viaLegendInHead, + promised.splitLegendInHead ); if( plan.compact ) { out.markupBytes = out.section.xml.size(); @@ -3331,7 +3340,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co : ForOwnerPlan{}; bool forViaPresent = false; bool forViaByOwnerOnly = false; // owner-hop: the six ranked hop ids alone answer no, the owner rows switch it on - if( !g.outNameOnly.empty() ) + bool forSplitPresent = false; // attrib-multidef Q1: the same question for prov="split", over the same ids + if( !g.outNameOnly.empty() || !g.outProv.empty() ) { std::vector viaIds; if( autoBundleMode && plan.compact ) @@ -3355,7 +3365,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { viaIds = forDetailIds( ing, lensRank, cfg.detail, forTopN ); } - forViaPresent = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); + forViaPresent = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); + forSplitPresent = rw::splitArmsOutAny( g.outOff, g.outNameOnly, g.outProv, viaIds ); } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, @@ -3365,6 +3376,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co /*hdrLegend=*/!forHdrRows.empty(), forScPresent, forComposePresent, forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent, forViaPresent, ownerPlan.any(), forViaByOwnerOnly }; + headerParts.splitPresent = forSplitPresent; // attrib-multidef Q1 (a trailing member: the aggregate above keeps its order) const auto buildForHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { return forLensHeaderText( headerParts, withRouteAttr, withTaskEcho, extraNotes ); }; std::string headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); @@ -3726,7 +3738,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co trailingReserve += rw::chargeSection( [ & ]( std::FILE* f ) { packBodies( f, ing, detailIds, detailFloorBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, floorRedact ? &*floorRedact : nullptr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); }, + /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); }, rw::kBytesPerTokenBody ).xml.size(); // a degraded measure is 0 bytes: the reserve falls back to the graph alone } const std::string forTailNext = forTailCutNextAttr( cfg ); // fix round 4: the explicit-budget tail's cut next= (empty otherwise) @@ -4052,7 +4064,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co detailSection = rw::chargeSection( [ & ]( std::FILE* f ) { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); }, // L3: --detail bodies surface notes too (part of the --for bundle) + /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); }, // L3: --detail bodies surface notes too (part of the --for bundle) rw::kBytesPerTokenBody ); if( !detailSection.isRendered ) { @@ -4079,11 +4091,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // reserved ahead of the body walk (the kAutoAttrReserve pattern) so // the disclosure always fits; the DEFAULT regime reserves nothing — // the tail rides on top there and the bodies stay byte-identical. - bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan } ); + bundleBudget, ForSectionPromises{ headerParts.viaPresent, &ownerPlan, headerParts.splitPresent } ); if( enrich.surfaceOff || enrich.legendOff ) { headerParts.autoBundle = headerParts.compactBundle = false; headerParts.viaPresent = false; // FE-B: the section that could carry it is gone (autoBundleMode excludes --detail) + headerParts.splitPresent = false; // attrib-multidef Q1: likewise headerParts.ownerPresent = false; // owner-hop: the same section headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } @@ -4423,7 +4436,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent ); } ); + /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); } ); } else if( autoSection.isRendered && !autoSection.xml.empty() ) { diff --git a/test/attribmultidefcheck.sh b/test/attribmultidefcheck.sh index b85075c9f..c65f99d6c 100755 --- a/test/attribmultidefcheck.sh +++ b/test/attribmultidefcheck.sh @@ -509,6 +509,25 @@ if ans h_p1 capfix callers s xml -- --callers=go --limit=5; then fi FIX="$FIXSAVE" +# ── (I) Q1 on --for: a body's row (and a hop's) whose edge is a resolver split arm carries prov="split", defined in the +# bundle's first-screen legend; a proven callee row does not ────────────────────────────────────────────────────────────────── +echo "(I) Q1: --for rows of a split call" +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=PutInt ) >"$DIR/i_for.out" 2>/dev/null && grep -q ']* prov="split"' "$DIR/i_for.out" | wc -l | tr -d ' ' )" + [ "$n_split" = 2 ]; say $? "[P] i_for: both put arms in PutInt's carry prov=\"split\"" \ + "[P] i_for: $n_split of 2 put rows carry prov=\"split\"" + sed 's/-->.*//' "$DIR/i_for.out" | grep -q 'prov=split'; say $? "[P] i_for: the first-screen legend defines prov=split" \ + "[P] i_for: prov=split is not defined in the --for header" +else no "i_for: --for=PutInt failed"; fi +if ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=UsesBoth ) >"$DIR/i_for2.out" 2>/dev/null && grep -q '/dev/null 2>&1; then + ( cd "$FIX/cpp" && "$BIN" . --no-cache --for=PutInt --legend=full ) >"$DIR/i_forf.out" 2>/dev/null && xmllint --noout "$DIR/i_forf.out" 2>/dev/null + say $? "[C] i_forf: --for=PutInt --legend=full well-formed (the prov=split clause rides)" "[C] i_forf: --for=PutInt --legend=full malformed or failed" +fi + # ── (G) well-formedness and determinism ─────────────────────────────────────────────────────────────────────────────── echo "(G) well-formedness, determinism" if command -v xmllint >/dev/null 2>&1; then From ca3c45c7c841424bd31fe51f3c6e50b220cb28f3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 17:57:44 -0400 Subject: [PATCH 047/162] refactor(for): Q1's split-arm bytes ride the existing packBodies/packHops parameters quality-delta follow-up: CalleeEdgeBytes (outNameOnly + optional outProv) and CalleeLegendsInHead (via + split) replace the two parameters each function grew, both built implicitly from what every other caller already passes; the callee row reads its prov="split" attribute in one call. Output unchanged (attribmultidefcheck 144 PASS). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 80 ++++++++++++++++++++++++++++++------------------- src/verbs_for.h | 24 +++++++-------- 2 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index f1e13d510..d97b8d43c 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -6719,6 +6719,27 @@ inline bool calleeRowVia( const std::vector& outOff, const std::v return via; } +// attrib-multidef Q1: the per-edge bytes a section's rows read — Graph::outNameOnly (via="name") and, where the caller +// asks for it, Graph::outProv (prov="split"), with the flag its prov comment's one-time charge sets (packHops passes its own to +// the owner rows). Built implicitly from outNameOnly alone, so every caller that marks no split arm passes what it always did. +struct CalleeEdgeBytes +{ + const std::vector& nameOnly; + const std::vector* prov = nullptr; + bool* splitLegendCharged = nullptr; + CalleeEdgeBytes( const std::vector& nameOnlyBytes, const std::vector* provBytes = nullptr, // NOLINT: implicit + bool* charged = nullptr ) noexcept + : nameOnly( nameOnlyBytes ), prov( provBytes ), splitLegendCharged( charged ) {} +}; +// …and which of their readings the caller's own first-screen legend already carries (FE-B's via="name", Q1's prov="split"), so +// the section neither repeats nor charges them. Built implicitly from the via bit alone, as every pre-Q1 caller passes it. +struct CalleeLegendsInHead +{ + bool via = false; + bool split = false; + CalleeLegendsInHead( bool viaInHead = false, bool splitInHead = false ) noexcept : via( viaInHead ), split( splitInHead ) {} // NOLINT: implicit +}; + // attrib-multidef Q1: the sink a section passes, with the split-arm bytes it reads and the flag its comment charge sets inline CalleeCallsSink withSplitArms( CalleeCallsSink sink, const std::vector* prov, bool* splitLegendCharged ) noexcept { @@ -6729,12 +6750,12 @@ inline CalleeCallsSink withSplitArms( CalleeCallsSink sink, const std::vector row id → cid a split arm (and not by name alone: that row says via="name")? The first // such row of an answer charges the comment that defines it to `used`, before the row, when the head did not define it. -inline bool calleeRowSplit( const std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, bool via, - std::size_t& used, const CalleeCallsSink& sink ) +inline std::string_view calleeRowSplitAttr( const std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, + bool via, std::size_t& used, const CalleeCallsSink& sink ) { if( via || sink.prov == nullptr || sink.prov->empty() || std::size_t( id ) + 1 >= outOff.size() ) { - return false; + return {}; } const auto b = outTargets.begin() + outOff[ id ]; const auto e = outTargets.begin() + outOff[ std::size_t( id ) + 1 ]; @@ -6745,7 +6766,7 @@ inline bool calleeRowSplit( const std::vector& outOff, const std: used += splitArmCallsLegendComment().size(); *sink.splitLegendCharged = true; } - return split; + return split ? std::string_view( " prov=\"split\"" ) : std::string_view(); } // §P10.1: the disclosed block @@ -6786,13 +6807,12 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } const Symbol& cs = ing.symbols[cid]; const std::string_view viaAttr = viaNameAttr( calleeRowVia( outOff, outTargets, id, cid, used, sink ) ); // FE-B: via="name" or "" - const bool split = calleeRowSplit( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); // Q1 - const std::string_view splitAttr = split ? std::string_view( " prov=\"split\"" ) : std::string_view(); + const std::string_view splitAttr = calleeRowSplitAttr( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); // Q1: prov="split" or "" // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) { - collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty(), split ); + collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty(), !splitAttr.empty() ); ++shown; continue; } @@ -7167,7 +7187,7 @@ inline void regroupEmittedRecord( EmittedBodies* out, const std::vector& nodes, std::size_t budgetBytes, const std::vector& outOff, const std::vector& outTargets, - const std::vector& outNameOnly, // FE-B: Graph::outNameOnly — the rows' via="name" + const CalleeEdgeBytes& outEdges, // FE-B: Graph::outNameOnly — the rows' via="name" (+ Q1's outProv) bool compress = false, RedactCounts* redact = nullptr, const HashMap* ranges = nullptr, const notes::NoteIndex* noteIndex = nullptr, // L3: field notes — surfaces children on each @@ -7188,14 +7208,14 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // on --for/--pack-task/--from-trace, calleeNameSpecificity on // --expand; nullptr (--around/--exemplar) ⇒ node-id order. // See CalleeCallsSink::rank. - bool viaLegendInHead = false, // FE-B: the caller's own first-screen legend already defines via="name" - // (namesOnlyOutAny above), so this section neither repeats nor charges it - const std::vector* outProv = nullptr, // attrib-multidef Q1: Graph::outProv — the rows' - // prov="split"; nullptr (every caller but --for) ⇒ none - bool splitLegendInHead = false ) // Q1: viaLegendInHead's twin (serialize.h splitArmsOutAny) + CalleeLegendsInHead legendsInHead = {} ) // FE-B: the caller's own first-screen legend already defines via="name" + // (namesOnlyOutAny above), so this section neither repeats nor charges + // it; Q1: likewise prov="split" (splitArmsOutAny) { + const std::vector& outNameOnly = outEdges.nameOnly; + const bool viaLegendInHead = legendsInHead.via; bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) - bool splitLegendCharged = splitLegendInHead; // attrib-multidef Q1: likewise for the prov="split" comment + bool splitLegendCharged = legendsInHead.split; // attrib-multidef Q1: likewise for the prov="split" comment // budgetBytes == 0 ⇒ UNLIMITED (A3-F2): the MCP `exemplar` verb has no byte budget, and 0 must never // mean "cap at zero bytes" (the cap fired before the first body and emitted a bare ). // Matches buildRecall's "0 = no cap" convention; the CLI always passes a real budget (default 64 KB). @@ -7493,7 +7513,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // rows survive when it CUTS one, which is far from rare here (CalleeCallsSink::rank). emitCalleeCallsBlock( piece, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, withSplitArms( CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, - &viaLegendCharged }, outProv, &splitLegendCharged ) ); + &viaLegendCharged }, outEdges.prov, &splitLegendCharged ) ); const std::string bodyNotes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 piece += bodyNotes; used += bodyNotes.size(); // W3-N2: same charge-never-trim rule @@ -7569,7 +7589,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect { w.write( viaNameLegendComment() ); // FE-B: the same rule, for a row's via="name" } - if( !splitLegendInHead && children.find( " prov=\"split\"" ) != std::string::npos ) + if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) { w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: the same rule, for a row's prov="split" } @@ -7625,11 +7645,11 @@ struct OwnerHopRows template inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector& nodes, const HopOwners* owners, const std::vector& outOff, const std::vector& outTargets, - const std::vector& outNameOnly, RedactCounts* redact, const std::vector* rank, + const CalleeEdgeBytes& outEdges, RedactCounts* redact, const std::vector* rank, std::string_view rootArg, std::size_t rankedBudget, bool rankedVia, - PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc, - const std::vector* outProv = nullptr, bool* splitLegendCharged = nullptr ) // Q1 + PathRelFn&& pathRel, ContentOfFn&& contentOf, std::vector& esc ) { + const std::vector& outNameOnly = outEdges.nameOnly; OwnerHopRows out; out.rankedBudget = rankedBudget; out.rankedVia = rankedVia; @@ -7668,7 +7688,7 @@ inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector used += rowFixed; emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budget, withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, - kForOwnerHopNameGuard, next }, outProv, splitLegendCharged ) ); + kForOwnerHopNameGuard, next }, outEdges.prov, outEdges.splitLegendCharged ) ); out.xml += row; out.xml += ""; ++out.shown; @@ -7738,17 +7758,17 @@ inline std::string hopsOpenTag( std::size_t shown, std::size_t requested, std::s inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector& nodes, std::size_t budgetBytes, const std::vector& outOff, const std::vector& outTargets, - const std::vector& outNameOnly, // FE-B: Graph::outNameOnly — the rows' via="name" + const CalleeEdgeBytes& outEdges, // FE-B: Graph::outNameOnly — the rows' via="name" (+ Q1's outProv) RedactCounts* redact = nullptr, const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing std::string_view rootArg = {}, // R-E: same single-root-only root= every verb takes - bool viaLegendInHead = false, // FE-B: packBodies' parameter of the same name - const HopOwners* owners = nullptr, // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) - const std::vector* outProv = nullptr, // attrib-multidef Q1: packBodies' parameter of the same name - bool splitLegendInHead = false ) // Q1: likewise + CalleeLegendsInHead legendsInHead = {}, // FE-B (+ Q1): packBodies' parameter of the same name + const HopOwners* owners = nullptr ) // owner-hop: the leading owner rows; nullptr ⇒ none (byte-identical) { + const std::vector& outNameOnly = outEdges.nameOnly; + const bool viaLegendInHead = legendsInHead.via; bool viaLegendCharged = viaLegendInHead; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) - bool splitLegendCharged = splitLegendInHead; // attrib-multidef Q1: likewise for the prov="split" comment + bool splitLegendCharged = legendsInHead.split; // attrib-multidef Q1: likewise for the prov="split" comment if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention { budgetBytes = SIZE_MAX; @@ -7792,8 +7812,8 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // owner-hop: the leading owner rows first (THE QUESTION'S OWNER ROWS above; nothing when `owners` is null), then the // ranked rows exactly as before, from the node the owners end at, on the budget and via charge the owners leave them - const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, outNameOnly, redact, rank, rootArg, - budgetBytes, viaLegendCharged, pathRel, contentOf, esc, outProv, &splitLegendCharged ); + const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, CalleeEdgeBytes( outNameOnly, outEdges.prov, &splitLegendCharged ), + redact, rank, rootArg, budgetBytes, viaLegendCharged, pathRel, contentOf, esc ); std::string children = own.xml; std::size_t requestedCount = own.shown; std::size_t shownCount = own.shown; @@ -7845,7 +7865,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // running `used` so the row identity bytes and the edge bytes share one budget. emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged }, - outProv, &splitLegendCharged ) ); + outEdges.prov, &splitLegendCharged ) ); row += ""; children += row; ++shownCount; @@ -7861,7 +7881,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector { w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" } - if( !splitLegendInHead && children.find( " prov=\"split\"" ) != std::string::npos ) + if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) { w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: exactly when a hop's row carries prov="split" } diff --git a/src/verbs_for.h b/src/verbs_for.h index bc38fb347..8b328ae55 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2189,9 +2189,9 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR // shell the "budget" branch below already gets for free, rather than a second hand-rolled tag. out.attr = " bundle=\"auto\" bodies=\"0\" reason=\"no_candidates\""; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, nullptr, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead, &g.outProv, splitLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) { @@ -2211,9 +2211,9 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR const std::size_t autoBodyBudget = std::max( 1, std::min( leftBytes, cfg.packBudgetBytes ) ); rw::EmittedBodies autoEmitted; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &autoEmitted, /*truncateOversizedFirst=*/false, - /*withFileContext=*/false, fabRootArg, &lensRank, viaLegendInHead, &g.outProv, splitLegendInHead ); }, + /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); if( !out.section.isRendered ) @@ -2720,8 +2720,8 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest const rw::HopOwners owners = forHopOwners( cfg, ing, ownerPlan, leftBytes, fcRootArg ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, g.outNameOnly, redactPtr, &lensRank, fcRootArg, - viaLegendInHead, &owners, &g.outProv, promised.splitLegendInHead ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) + { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), redactPtr, &lensRank, fcRootArg, + rw::CalleeLegendsInHead( viaLegendInHead, promised.splitLegendInHead ), &owners ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) // MARKUP rate, not the body rate — and this is an honesty choice, not a copy-paste slip. The body // rate (3.80 B/tok) prices SOURCE TEXT; the compact section contains none, only tags, identifiers // and line numbers, which tokenize like the rest of the bundle. Charging structured markup at the @@ -3736,9 +3736,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // real render redacts is still counted once std::optional floorRedact = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; trailingReserve += rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailFloorBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, floorRedact ? &*floorRedact : nullptr, + { packBodies( f, ing, detailIds, detailFloorBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, floorRedact ? &*floorRedact : nullptr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); }, + /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); }, rw::kBytesPerTokenBody ).xml.size(); // a degraded measure is 0 bytes: the reserve falls back to the graph alone } const std::string forTailNext = forTailCutNextAttr( cfg ); // fix round 4: the explicit-budget tail's cut next= (empty otherwise) @@ -4062,9 +4062,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co detailBodyBudget = std::min( detailBodyBudget, std::max( leftBytes, detailFloorBudget ) ); } detailSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); }, // L3: --detail bodies surface notes too (part of the --for bundle) + /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); }, // L3: --detail bodies surface notes too (part of the --for bundle) rw::kBytesPerTokenBody ); if( !detailSection.isRendered ) { @@ -4433,10 +4433,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // directly at the SAME budget — uncharged for that one run, with an alert, never a fabricated number. if( cfg.detail > 0 ) { - rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, + rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, flRootArg, &lensRank, headerParts.viaPresent, &g.outProv, headerParts.splitPresent ); } ); + /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); } ); } else if( autoSection.isRendered && !autoSection.xml.empty() ) { From 86a28e6ade25339f5de4957310cd11f2ff2b938c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 18:13:29 -0400 Subject: [PATCH 048/162] fix(for): scope the default's sections by the past-target rows' shown ranks; estchargecheck runtime cut The docs reorder (docs_after_code=) renumbers the shown rows, so an r= threshold cannot find the rank tail the target cut kept: a doc row of global rank 28 is shown as r="35". The ladder now records the r= values the past_target rows are shown with (sigsCutRecordPastTargetRanks, after the reorder), and sigsRowsAtTarget drops exactly those. On the 108-argv size check the one default-regime section that still shrank (starlette, lego full -> stub) keeps its full lego. estchargecheck: per-verb-class caches instead of --no-cache where an arm tests neither the cache nor a degrade path (#7 now asserts cached == cold), exact duplicate calls reused, the A7 sweep coarsened to the first and last budget of every row count (66 rungs, counted). No assertion weakened. Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 63 ++++++---- test/estchargecheck.sh | 258 +++++++++++++++++++++++++---------------- 2 files changed, 197 insertions(+), 124 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 04e2c3a58..7cbf1c4ef 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5189,6 +5189,9 @@ struct SigsCutReport std::size_t pastTargetBytes = 0; // OUT: the block bytes those rows add over the target cut (rows, their file notes, // the tag's attribute bytes) — what the caller's trailing sections must not pay for bool rowDropCut = false; // OUT: step F dropped rows against rowDropBudgetBytes (the guard / the soft target) + std::vector pastTargetRanks; // OUT: the r= values the past_target rows are SHOWN with, ascending — the K + // lowest-ranked shown rows by their global rank, read after the docs reorder + // renumbered them (sigsRowsAtTarget scopes the default's sections by it) std::size_t shown = 0; std::size_t total = 0; std::size_t docsDropped = 0; @@ -5349,6 +5352,35 @@ inline void sigsCutRecordLadder( SigsCutReport& cut, const SigLadderOutcome& lad ENSURES( cut.pastTarget <= cut.shown, "sigsCutRecordLadder: more rows past the target than rows shown" ); // they are shown rows } +// forbudget-soft: which shown rows are the past_target= ones, by the r= they are shown with. The ladder's past-target rows are +// the rank TAIL of the shown set (the K highest global ranks); the docs reorder (reorderDocsAfterCode) then renumbers the shown +// rows, so the tail cannot be read back off the rendered r= (a doc row of global rank 28 is shown as r="35"). Recorded from the +// entries after the reorder, before the block renders. EntryT: the XML SigEntry or the JSON sibling (dropped, globalRank, displayRank). +template< class EntryT > +inline void sigsCutRecordPastTargetRanks( SigsCutReport& cut, const std::vector& entries ) +{ + cut.pastTargetRanks.clear(); + if( cut.pastTarget == 0 ) + { + return; + } + std::vector> live; // (globalRank, displayRank) of the shown rows + for( const EntryT& e : entries ) + { + if( !e.dropped ) + { + live.emplace_back( e.globalRank, e.displayRank ); + } + } + std::sort( live.begin(), live.end(), []( const auto& a, const auto& b ) { return a.first > b.first; } ); + for( std::size_t k = 0; k < live.size() && k < cut.pastTarget; ++k ) + { + cut.pastTargetRanks.push_back( live[ k ].second ); + } + std::sort( cut.pastTargetRanks.begin(), cut.pastTargetRanks.end() ); + ENSURES( cut.pastTargetRanks.size() == cut.pastTarget, "sigsCutRecordPastTargetRanks: a past_target row is not a shown row" ); +} + // What the finished block shows, from its rows after the gate and the ladder: shown rows, and the shown rows whose doc // comment was removed (hadDoc, and no doc at emission) — the docs_dropped= value. template @@ -5954,6 +5986,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } cut.continuationRequest = cutNext; sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft + sigsCutRecordPastTargetRanks( cut, entries ); // forbudget-soft: after the docs reorder renumbered them sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -8856,17 +8889,17 @@ inline std::vector> legoImplementorsOnSurface( const IngestR // forbudget-soft: the rows a DEFAULT answer's sections are scoped by — the rows at the target cut, without the // past_target= rows (the K highest ranks: the rows are a rank prefix), so / narrow as they did when the target // was a ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). -// Selected by each row's r=, not by emission position (the docs reorder, docs_after_code=, moves doc rows below the code rows). +// Selected by the r= values the ladder recorded for the past-target rows (cut.pastTargetRanks), not by emission position nor +// by an r= threshold: the docs reorder (docs_after_code=) moves doc rows below the code rows AND renumbers them. // The whole block on a soft target or an answer with nothing past its target. Only … rows are kept (a file's // notes ride its rows and are scanned for p= the same way). inline std::string sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) { - if( !cut.pastTargetDisclosed || cut.pastTarget == 0 || cut.pastTarget >= cut.shown ) + if( !cut.pastTargetDisclosed || cut.pastTargetRanks.empty() || cut.pastTarget >= cut.shown ) { return std::string( sigsRendered ); } - struct Row { std::uint32_t rank; std::string_view text; }; - std::vector rows; + std::string kept; for( std::size_t at = sigsRendered.find( "", at ); @@ -8881,26 +8914,9 @@ inline std::string sigsRowsAtTarget( std::string_view sigsRendered, const SigsCu { rank = rank * 10 + std::uint32_t( text[ i ] - '0' ); } - rows.push_back( { rank, text } ); - } - if( rows.size() <= cut.pastTarget ) - { - return std::string( sigsRendered ); - } - std::vector ranks; - ranks.reserve( rows.size() ); - for( const Row& r : rows ) - { - ranks.push_back( r.rank ); - } - std::nth_element( ranks.begin(), ranks.begin() + std::ptrdiff_t( rows.size() - cut.pastTarget - 1 ), ranks.end() ); - const std::uint32_t keepUpTo = ranks[ rows.size() - cut.pastTarget - 1 ]; // the highest rank inside the target cut - std::string kept; - for( const Row& r : rows ) - { - if( r.rank <= keepUpTo ) + if( !std::binary_search( cut.pastTargetRanks.begin(), cut.pastTargetRanks.end(), rank ) ) { - kept.append( r.text ); + kept.append( text ); } } return kept; @@ -11077,6 +11093,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } cut.continuationRequest = cutNext; sigsCutRecordLadder( cut, ladder, totalAfterLadder, rowDropBudgetIn, pastTargetDisclosed ); // forbudget-soft + sigsCutRecordPastTargetRanks( cut, entries ); // forbudget-soft: after the docs reorder renumbered them sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for if( cutOut ) { diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index d19f588fd..4d2fcbc3a 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -54,6 +54,13 @@ skip(){ printf ' SKIP %s\n' "$*"; } cd "$ROOT" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +# RUNTIME (CI kills a gate at 1200 s; this one measured ~2065 s on a loaded Mac, every src/ call cold): one incremental cache +# per verb class and root, under $TMP (never the shared per-root TMPDIR cache), instead of --no-cache wherever an arm tests +# neither the cache nor a degrade path. A cached document is byte-identical to a cold one (measured on the map and --for), and +# #7 below compares each cached #1 shape against a cold --no-cache rerun, so that transparency is asserted, not assumed. The +# degrade arms (#14), the git-less fixture corpora (tiny, cold by design) and the flavour probe stay --no-cache. +CM="--cache=$TMP/c_map"; CF="--cache=$TMP/c_for"; CP="--cache=$TMP/c_pack"; CT="--cache=$TMP/c_trace"; CX="--cache=$TMP/c_expand" +CH="--cache=$TMP/c_handoff" # the --handoff arm's root is "." (the checkout), not src: its own cache # ── BUILD-FLAVOUR / DEGRADE-OBSERVABILITY PROBE (read by #14 below) ─────────────────────────────────── # CI runs the whole suite TWICE: once against a Release binary (catches optimizer-only bugs, e.g. the @@ -124,7 +131,7 @@ declare -a SHAPES=( for entry in "${SHAPES[@]}"; do label="${entry%%:*}"; rest="${entry#*:}"; hi="${rest%%:*}"; args="${rest#*:}" # shellcheck disable=SC2086 - "$BIN" src $args --no-cache >"$TMP/$label.out" 2>/dev/null + "$BIN" src $args "$CM" >"$TMP/$label.out" 2>/dev/null B="$( bytes_of "$TMP/$label.out" )" E="$( est_of "$TMP/$label.out" )" if [ -z "$E" ] || [ "$E" -le 0 ] 2>/dev/null; then @@ -144,7 +151,7 @@ done # ranked rows leave out), and a payload verb's ride-along map does not — so "the same map" is the bare map WITHOUT that # tier. --no-inventory is that map (the same rows, no tier; #2-rows proves both premises). The old comparison is kept # as #2-tier: the bare map with its tier must charge at least what the rows alone do. -"$BIN" src --top-k=10 --no-inventory --no-cache >"$TMP/map_rows.out" 2>/dev/null +"$BIN" src --top-k=10 --no-inventory "$CM" >"$TMP/map_rows.out" 2>/dev/null E_MAP="$( est_of "$TMP/map_rows.out" )" E_TIER="$( est_of "$TMP/map.out" )" rows_of(){ sed 's/"$TMP/tb_map.out" 2>"$TMP/tb_map.err" +"$BIN" src --top-k=10 --no-inventory --token-budget=$TB "$CM" >"$TMP/tb_map.out" 2>"$TMP/tb_map.err" rc_map=$? [ "$rc_map" -eq 0 ] \ && ok "#3 control: the bare map fits --token-budget=$TB (exit 0)" \ @@ -179,7 +186,7 @@ rc_map=$? for entry in "sigs:--pack-signatures" "src:--pack-top-n=3" "bodies:--expand=$SYM" "outline:--outline=$SYM"; do label="${entry%%:*}"; args="${entry#*:}" # shellcheck disable=SC2086 - "$BIN" src --top-k=10 $args --token-budget=$TB --no-cache >"$TMP/tb_$label.out" 2>"$TMP/tb_$label.err" + "$BIN" src --top-k=10 $args --token-budget=$TB "$CM" >"$TMP/tb_$label.out" 2>"$TMP/tb_$label.err" rc=$? OB="$( bytes_of "$TMP/tb_$label.out" )" if [ "$rc" -eq 3 ] && [ "$OB" -lt 2048 ]; then @@ -192,14 +199,14 @@ done # ── #4 (§B7.8): the WITHHELD artifact uses the withheld_est_tokens vocabulary --recall already renamed # this exact semantic to (recall.h). `est_tokens=` on a withheld artifact names a number that describes # something the caller did NOT receive. XML and JSON both. ───────────────────────────────────────────── -"$BIN" src --token-budget=1 --no-cache >"$TMP/wh.xml" 2>"$TMP/wh.err" +"$BIN" src --token-budget=1 "$CM" >"$TMP/wh.xml" 2>"$TMP/wh.err" grep -aq 'withheld_est_tokens="' "$TMP/wh.xml" \ && ok "#4 XML withheld record uses withheld_est_tokens=\"…\"" \ || no "#4 XML withheld record does not use withheld_est_tokens= (got: $( head -c 200 "$TMP/wh.xml" ))" grep -aqE '"$TMP/wh.json" 2>"$TMP/whj.err" +"$BIN" src --token-budget=1 --json "$CM" >"$TMP/wh.json" 2>"$TMP/whj.err" grep -aq '"withheld_est_tokens":' "$TMP/wh.json" \ && ok "#4 JSON withheld record uses \"withheld_est_tokens\"" \ || no "#4 JSON withheld record does not use \"withheld_est_tokens\" (got: $( head -c 200 "$TMP/wh.json" ))" @@ -228,7 +235,7 @@ grep -aq 'withheld_est_tokens=[0-9]* > budget=' "$TMP/wh.err" \ # L1 (2026-09-19): the CLI default legend is compact; #5's legend arms read the FULL legend's max_tokens=/over_ceiling= prose # from this sweep, so it asks for the full legend (byte-identical to the old default, and the larger envelope). for N in 100 300 400 450 500 600 800 1200 1500 3000 6000; do - "$BIN" src --max-tokens=$N --no-cache --legend=full >"$TMP/mt$N.out" 2>"$TMP/mt$N.err" + "$BIN" src --max-tokens=$N "$CM" --legend=full >"$TMP/mt$N.out" 2>"$TMP/mt$N.err" MB="$( bytes_of "$TMP/mt$N.out" )" ME="$( est_of "$TMP/mt$N.out" )" LIM="$( awk "BEGIN{printf \"%d\", $N*2.36*0.90}" )" @@ -327,7 +334,7 @@ PY for entry in "mapdiff:3000:--map-diff" "mapdiff2:12000:--map-diff" "churn:800:--rank-by=churn" "churn2:1200:--rank-by=churn" "payload:6000:--pack-signatures" "payload2:6000:--expand=$SYM"; do label="${entry%%:*}"; rest="${entry#*:}"; N="${rest%%:*}"; args="${rest#*:}" # shellcheck disable=SC2086 - "$BIN" src --max-tokens=$N $args --no-cache >"$TMP/p_$label.out" 2>/dev/null + "$BIN" src --max-tokens=$N $args "$CM" >"$TMP/p_$label.out" 2>/dev/null PB="$( mapbytes_of "$TMP/p_$label.out" )" LIM="$( awk "BEGIN{printf \"%d\", $N*2.36*0.90}" )" OVER=0; head -c "$PB" "$TMP/p_$label.out" | grep -aq 'over_ceiling=1' && OVER=1 @@ -415,7 +422,8 @@ grep -aq 'max_tokens=.*2\.36.*0\.90\|max_tokens=.*conservative' "$TMP/mt1500.out || no "#5 legend clause for max_tokens=/fit_bytes= missing or does not name the rate + headroom" # and the DEFAULT map must be byte-identical to a run without --max-tokens support (golden neutrality: # no --max-tokens ⇒ no marker, no legend clause) -"$BIN" src --top-k=10 --no-cache >"$TMP/neutral.out" 2>/dev/null +# RUNTIME: the default map is #1's "map" shape (src --top-k=10), already captured — not re-run. +cp "$TMP/map.out" "$TMP/neutral.out" grep -aq 'max_tokens=\|fit_bytes=' "$TMP/neutral.out" \ && no "#5 the default map leaked a max_tokens=/fit_bytes= marker (must be --max-tokens-only)" \ || ok "#5 the default map carries no max_tokens=/fit_bytes= marker (opt-in, golden-neutral)" @@ -423,7 +431,7 @@ grep -aq 'max_tokens=\|fit_bytes=' "$TMP/neutral.out" \ # ── #6: the THIRD estimator (--partition's , partition.h) is flat bytes/2.36 — exact # and conservative, therefore honest, but a DIFFERENT currency from the map's language-weighted one. # It must say which, in its own legend. ───────────────────────────────────────────────────────────── -"$BIN" src --pack-task="estimate tokens" --partition=2 --no-cache >"$TMP/part.out" 2>/dev/null +"$BIN" src --pack-task="estimate tokens" --partition=2 "$CP" >"$TMP/part.out" 2>/dev/null if grep -aq ']*est_tokens=' "$TMP/part.out"; then # NON-VACUOUS (L1 fix round, the iter1a measurement): the rate must be stated in the answer's own COMMENTS. A document-wide # grep passed on a ranked body that merely quoted "2.36" in its CDATA, while the compact legend never said it. @@ -452,8 +460,9 @@ fi det=1 for entry in "${SHAPES[@]}"; do label="${entry%%:*}"; rest="${entry#*:}"; args="${rest#*:}" - # shellcheck disable=SC2086 - "$BIN" src $args --no-cache >"$TMP/d1.out" 2>/dev/null + # RUNTIME: run 1 is #1's capture of this shape (warm per-class cache), run 2 a cold --no-cache rerun — two runs as + # before, and the comparison now also asserts that the cache never changes a byte of the document + cp "$TMP/$label.out" "$TMP/d1.out" # shellcheck disable=SC2086 "$BIN" src $args --no-cache >"$TMP/d2.out" 2>/dev/null A="$( est_of "$TMP/d1.out" )"; C="$( est_of "$TMP/d2.out" )" @@ -480,7 +489,7 @@ fi # ── #9: XML/JSON parity on the charged number — the two serializations must report the SAME est_tokens # for the same request (the sibling emitter has its own header writer; a fix applied to one only is # exactly how the §H5 dialect divergences are born). ─────────────────────────────────────────────────── -"$BIN" src --top-k=10 --metrics --json --no-cache >"$TMP/j.json" 2>/dev/null +"$BIN" src --top-k=10 --metrics --json "$CM" >"$TMP/j.json" 2>/dev/null JE="$( grep -aoE '"est_tokens":[0-9]+' "$TMP/j.json" | head -1 | grep -oE '[0-9]+' )" XE="$( est_of "$TMP/metrics.out" )" JB="$( bytes_of "$TMP/j.json" )" @@ -584,7 +593,7 @@ PYX for entry in "for:320:" "for_graph:320:--with-graph" "for_detail:420:--detail=20" "for_both:420:--detail=20 --with-graph"; do label="${entry%%:*}"; rest="${entry#*:}"; hi="${rest%%:*}"; args="${rest#*:}" # shellcheck disable=SC2086 - "$BIN" src --for="$FOR_TASK" $args --no-cache >"$TMP/f_$label.out" 2>/dev/null + "$BIN" src --for="$FOR_TASK" $args "$CF" >"$TMP/f_$label.out" 2>/dev/null B="$( bytes_of "$TMP/f_$label.out" )" E="$( forest_of "$TMP/f_$label.out" )" if [ -z "$E" ] || [ "$E" -le 0 ] 2>/dev/null; then @@ -649,20 +658,36 @@ def rb( p ): rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) rows.sort() return len( inner.encode() ), len( inner.encode() ) + elided, rows -a = rb( sys.argv[ 1 ] ); T = int( int( sys.argv[ 2 ] ) * 2.36 * 0.90 ) -if a is None: print( "BAD no " ); sys.exit( 1 ) -pb, ab, ar = a; k = len( ar ) -fits = pb <= T or k <= 4 -if len( sys.argv ) < 4: - print( "%s fits=%d rows=%d bytes=%d T=%d" % ( "OK" if fits else "BAD", fits, k, pb, T ) ); sys.exit( 0 if fits else 1 ) -ref = rb( sys.argv[ 3 ] ) -if ref is None: print( "BAD no in the reference answer" ); sys.exit( 1 ) -rr = ref[ 2 ] -if k >= len( rr ): - print( "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) ); sys.exit( 0 if fits else 1 ) -maximal = ab + rr[ k ][ 1 ] > T - 160 -print( "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T ) ) -sys.exit( 0 if fits and maximal else 1 ) +def row_ids( p ): # soft_rows.py's list, for an in-process caller (the sweep) + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return [] + rows = [] + for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) + rows.sort() + return [ x[ 1 ] for x in rows ] +def verdict( path, n, refpath=None ): + a = rb( path ); T = int( int( n ) * 2.36 * 0.90 ) + if a is None: return False, "BAD no " + pb, ab, ar = a; k = len( ar ) + fits = pb <= T or k <= 4 + if refpath is None: + return fits, "%s fits=%d rows=%d bytes=%d T=%d" % ( "OK" if fits else "BAD", fits, k, pb, T ) + ref = rb( refpath ) + if ref is None: return False, "BAD no in the reference answer" + rr = ref[ 2 ] + if k >= len( rr ): + return fits, "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) + maximal = ab + rr[ k ][ 1 ] > T - 160 + return fits and maximal, "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d" % ( + "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T ) +if __name__ == "__main__": + good, msg = verdict( sys.argv[ 1 ], sys.argv[ 2 ], sys.argv[ 3 ] if len( sys.argv ) > 3 else None ) + print( msg ); sys.exit( 0 if good else 1 ) PY soft_rows(){ python3 "$TMP/soft_rows.py" "$1"; } soft_st(){ python3 "$TMP/soft_st.py" "$@"; } @@ -682,12 +707,12 @@ SOFT_CLAUSE_FULL='[budget_tokens=: the soft token target; rows stop once it is r # 10 / 11 rows with --detail=20 / --with-graph against the bare run's 14 — the reserve the soft target deleted. TBF=2000 ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" # still read by the no-budget --detail arm below -"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --no-cache >"$TMP/f_tbp.out" 2>/dev/null -"$BIN" src --for="$FOR_TASK" --token-budget=5000 --no-cache >"$TMP/f_tbp5.out" 2>/dev/null +"$BIN" src --for="$FOR_TASK" --token-budget=$TBF "$CF" >"$TMP/f_tbp.out" 2>/dev/null +"$BIN" src --for="$FOR_TASK" --token-budget=5000 "$CF" >"$TMP/f_tbp5.out" 2>/dev/null for tw in "2000:--detail=20:f_tbp:f_for_detail" "2000:--with-graph:f_tbp:f_for_graph" "5000:--detail=20 --with-graph:f_tbp5:f_for_both"; do tw_n="${tw%%:*}"; tw_rest="${tw#*:}"; tw_args="${tw_rest%%:*}"; tw_rest="${tw_rest#*:}"; tw_bare="${tw_rest%%:*}"; tw_ref="${tw_rest#*:}" # shellcheck disable=SC2086 - "$BIN" src --for="$FOR_TASK" --token-budget=$tw_n $tw_args --no-cache >"$TMP/f_tw.out" 2>/dev/null + "$BIN" src --for="$FOR_TASK" --token-budget=$tw_n $tw_args "$CF" >"$TMP/f_tw.out" 2>/dev/null rc_tw=$? tw_root="$( ctx_root "$TMP/f_tw.out" )" soft_rows "$TMP/f_tw.out" >"$TMP/tw.rows" 2>/dev/null; soft_rows "$TMP/$tw_bare.out" >"$TMP/tw_bare.rows" 2>/dev/null @@ -706,7 +731,7 @@ done # invariant and is kept: --detail=20 --with-graph at 2000 keeps every row of the flagless 2000 answer, and its rows are # the uncapped answer's top rows in rank order (no higher-ranked row missing). Plus NOLBL, both trailing sections present, and # a cut first body still carries the next= that serves the rest. -"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --no-cache >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" +"$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph "$CF" >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? a7b_root="$( ctx_root "$TMP/f_tb.out" )" soft_rows "$TMP/f_tb.out" >"$TMP/a7b.rows" 2>/dev/null; soft_rows "$TMP/f_tbp.out" >"$TMP/a7b.plain" 2>/dev/null; soft_rows "$TMP/f_for.out" >"$TMP/a7b.unc" 2>/dev/null @@ -727,7 +752,7 @@ a7b_b1="$( grep -aoE ']*>' "$TMP/f_tb.out" | head -1 )" # spelling broke xmllint). The note is gone; the disclosure that now rides the header comment of a soft answer is the reading # of budget_tokens= as a soft target, compact and full dialect. Both answers must carry their clause and stay well-formed XML. if command -v xmllint >/dev/null 2>&1; then - "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full --no-cache >"$TMP/f_tbl.out" 2>/dev/null + "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full "$CF" >"$TMP/f_tbl.out" 2>/dev/null a7x_bad="" grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/f_tb.out" || a7x_bad="$a7x_bad f_tb:no-soft-target-clause" grep -aqF "$SOFT_CLAUSE_FULL" "$TMP/f_tbl.out" || a7x_bad="$a7x_bad f_tbl:no-soft-target-clause" @@ -746,7 +771,7 @@ fi # shape now pins HL: the root keeps task= VERBATIM (all 700 chars) and route=, nothing is dropped "(ceiling)", no task_echo / # legend-clause drop note, no last-rung note, no over_ceiling=; the rank 1..4 floor ships and the cut carries next=. A7N_TASK="$( python3 -c 'w="serialize the map rows into the output buffer while keeping the row order stable and flushing the writer "; print((w*10)[:700].strip())' )" -"$BIN" src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph --no-cache >"$TMP/f_tbn.out" 2>/dev/null +"$BIN" src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph "$CF" >"$TMP/f_tbn.out" 2>/dev/null rc_a7n=$? a7n_root="$( ctx_root "$TMP/f_tbn.out" )" a7n_sigs="$( grep -aoE ']*>' "$TMP/f_tbn.out" | head -1 )" @@ -778,9 +803,9 @@ python3 -c 'import re,sys; m=re.search(r" task=\"([^\"]*)\"", sys.argv[1]); sys. # NX (a with shown < total carries next=). The byte-exactness the old arm protected lives on in est_tokens' # identities (A9/A10, §C1, #19): est_tokens prices the delivered document, whatever its size. # RE-ANCHORED (forbudget-soft): the --detail=20 --with-graph spec ran 2880..3080, the old late-label band; on the soft target -# that range serves all 32 rows (complete), so it now sweeps 300..2500 step 50, where the rows run from the 4-row floor to -# complete (d099ec21: 4 rows at 300, 28 at 2000, 32 at 2500). The default spec keeps 760..1500 step 10 (8..20 rows); the -# one-row named task keeps 400..600 (step 20) — its document prices at est_tokens=719, past every N there: the soft overshoot. +# that range serves all 32 rows (complete), so it now sweeps 300..2500, where the rows run from the 4-row floor to complete +# (d099ec21: 4 rows at 300, 28 at 2000, 32 at 2500). The default spec keeps 760..1500 (8..20 rows); the one-row named task +# keeps 400..600 — its document prices at est_tokens=719, past every N there: the soft overshoot. A7S="$TMP/a7sweep" mkdir -p "$A7S/corpus" python3 - "$A7S/corpus" <<'PYG' @@ -800,43 +825,67 @@ for i in range( 4 ): fh.write( "\n".join( lines ) ) PYG a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_over=0; a7s_cut=0 -for spec in "default:760:1500:10:serialize the map:" "detail_graph:300:2500:50:serialize the map:--detail=20 --with-graph" \ - "named_one_row:400:600:20:serializeRow0_3:"; do - s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_rest="${s_rest#*:}" - s_step="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" +# RUNTIME (CI's 1200 s per-gate limit): the sweep keeps only the rungs where the served row set CHANGES — the first and the +# last budget of every distinct row count, plus the band edges — and judges all of them in ONE python pass after the runs +# (it was 131 rungs with three interpreter starts each, ~266 s of this gate's wall time). Measured on the 480bc90b binary at +# step 10 / 50 / 20: default 760..1500 serves 8..20 rows (13 counts over 75 rungs), --detail=20 --with-graph 300..2500 serves +# 4..32 (29 counts over 45 rungs), the named task 1 row everywhere. A rung dropped here served exactly the rows of a kept +# neighbour on both sides, so every ST fit (tightest at a count's FIRST budget) and every maximality (tightest at its LAST) +# is still asserted, and RP still compares each adjacent pair of distinct row sets. +A7S_RUNGS_default="760 770 780 840 850 900 910 960 970 1020 1030 1090 1100 1150 1160 1210 1220 1270 1280 1330 1340 1400 1410 1460 1470 1500" +A7S_RUNGS_detail_graph="300 500 550 600 650 700 750 800 850 900 950 1000 1050 1100 1150 1200 1250 1300 1350 1400 1450 1500 1550 1600 1650 1700 1750 1800 1850 1900 1950 2000 2050 2100 2150 2200 2250 2500" +A7S_RUNGS_named_one_row="400 600" +: >"$A7S/manifest" +for spec in "default:serialize the map:" "detail_graph:serialize the map:--detail=20 --with-graph" "named_one_row:serializeRow0_3:"; do + s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" + eval "s_rungs=\$A7S_RUNGS_$s_label" # shellcheck disable=SC2086 - ( cd "$A7S" && "$BIN" corpus --for="$s_task" $s_args --no-cache ) >"$A7S/ref.xml" 2>/dev/null # the spec's complete answer - : >"$A7S/prev.rows" - for (( N = s_from; N <= s_to; N += s_step )); do + ( cd "$A7S" && "$BIN" corpus --for="$s_task" $s_args --no-cache ) >"$A7S/ref_$s_label.xml" 2>/dev/null # the spec's complete answer + for N in $s_rungs; do # shellcheck disable=SC2086 - ( cd "$A7S" && "$BIN" corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o.xml" 2>/dev/null - s_rc=$? - a7s_runs=$(( a7s_runs + 1 )) - s_root="$( ctx_root "$A7S/o.xml" )" - s_sigs="$( grep -aoE ']*>' "$A7S/o.xml" | head -1 )" - s_sh="$( printf '%s' "$s_sigs" | sed -nE 's/.* shown="([0-9]+)".*/\1/p' )"; s_tot="$( printf '%s' "$s_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" - s_est="$( printf '%s' "$s_root" | sed -nE 's/.* est_tokens="([0-9]+)".*/\1/p' )" - soft_rows "$A7S/o.xml" >"$A7S/cur.rows" 2>/dev/null - s_why="" - { [ "$s_rc" -eq 0 ] && [ -n "$s_root" ] && [ -n "$s_est" ]; } || s_why="$s_why exit-$s_rc/no-root-or-est" - { [ "${s_root#* over_ceiling=}" = "$s_root" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml"; } || s_why="$s_why labelled" - s_st="$( soft_st "$A7S/o.xml" "$N" "$A7S/ref.xml" )" || s_why="$s_why ST:[$s_st]" - [ ! -s "$A7S/prev.rows" ] || rows_prefix "$A7S/prev.rows" "$A7S/cur.rows" || s_why="$s_why not-monotone" - if [ -n "$s_sh" ] && [ -n "$s_tot" ] && [ "$s_sh" -lt "$s_tot" ] 2>/dev/null; then - a7s_cut=$(( a7s_cut + 1 )) - [ "${s_sigs#* next=\"--for=}" != "$s_sigs" ] || s_why="$s_why cut-without-next" - fi - [ -n "$s_est" ] && [ "$s_est" -gt "$N" ] 2>/dev/null && a7s_over=$(( a7s_over + 1 )) - if [ -n "$s_why" ]; then - a7s_badn=$(( a7s_badn + 1 )) - [ "$a7s_badn" -le 6 ] && a7s_bad="$a7s_bad $s_label@$N:[${s_why# }]" - fi - cp "$A7S/cur.rows" "$A7S/prev.rows" + ( cd "$A7S" && "$BIN" corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o_${s_label}_$N.xml" 2>/dev/null + printf '%s %s %s %s %s\n' "$s_label" "$N" "$?" "$A7S/o_${s_label}_$N.xml" "$A7S/ref_$s_label.xml" >>"$A7S/manifest" done done -[ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep (forbudget-soft): $a7s_runs budgets over a git-less corpus (default 760..1500 step 10, --detail=20 --with-graph 300..2500 step 50, one-row named task 400..600 step 20) — every answer exit 0, unlabelled, its rows fit T or the floor and a cut is maximal, rows monotone in N, every cut carries next=" \ - || no "#11 A7 sweep (forbudget-soft): $a7s_badn of $a7s_runs budgets break the soft target (first:$a7s_bad)" +# one pass over every rung: exit 0 + root + est_tokens; NOLBL (no root over_ceiling=, no last-rung note); ST (soft_st's verdict); +# RP (each rung's rows a rank prefix of the next rung's, same spec); NX (shown < total carries next="--for=). Prints +# "runs badn cut over", then up to six "label@N:[why]" lines. +a7s_out="$( python3 - "$A7S/manifest" "$TMP" <<'PYS' +import re, sys +sys.path.insert( 0, sys.argv[ 2 ] ) +import soft_st +runs = badn = cut = over = 0; bad = []; prev = {} +for line in open( sys.argv[ 1 ] ): + label, n, rc, f, ref = line.split() + n = int( n ); runs += 1; why = [] + d = open( f, encoding='utf-8', errors='replace' ).read() + root = re.search( r'^]*>', d ); root = root.group( 0 ) if root else '' + est = re.search( r' est_tokens="(\d+)"', root ) + if rc != '0' or not root or not est: why.append( 'exit-%s/no-root-or-est' % rc ) + if ' over_ceiling=' in root or '[over_ceiling= is 1 on the root: ' in d: why.append( 'labelled' ) + good, msg = soft_st.verdict( f, n, ref ) + if not good: why.append( 'ST:[%s]' % msg ) + rows = soft_st.row_ids( f ) + if not rows or ( label in prev and rows[ :len( prev[ label ] ) ] != prev[ label ] ): why.append( 'not-monotone' ) + prev[ label ] = rows + sigs = re.search( r']*>', d ); sigs = sigs.group( 0 ) if sigs else '' + sh = re.search( r' shown="(\d+)"', sigs ); tot = re.search( r' total="(\d+)"', sigs ) + if sh and tot and int( sh.group( 1 ) ) < int( tot.group( 1 ) ): + cut += 1 + if ' next="--for=' not in sigs: why.append( 'cut-without-next' ) + if est and int( est.group( 1 ) ) > n: over += 1 + if why: + badn += 1 + if len( bad ) < 6: bad.append( '%s@%d:[%s]' % ( label, n, ' '.join( why ) ) ) +print( runs, badn, cut, over ) +for b in bad: print( b ) +PYS +)" +read -r a7s_runs a7s_badn a7s_cut a7s_over <<<"$( printf '%s\n' "$a7s_out" | head -1 )" +a7s_bad="$( printf '%s\n' "$a7s_out" | tail -n +2 | tr '\n' ' ' )" +{ [ -n "$a7s_runs" ] && [ "$a7s_runs" -eq 66 ] && [ "$a7s_badn" -eq 0 ]; } 2>/dev/null \ + && ok "#11 A7 sweep (forbudget-soft): $a7s_runs budgets over a git-less corpus (the first and last budget of every row count: default 760..1500, --detail=20 --with-graph 300..2500, one-row named task 400/600) — every answer exit 0, unlabelled, its rows fit T or the floor and a cut is maximal, rows monotone in N, every cut carries next=" \ + || no "#11 A7 sweep (forbudget-soft): ${a7s_badn:-?} of ${a7s_runs:-?} budgets break the soft target, or the sweep did not judge its 66 rungs (first: $a7s_bad)" # control (REPLACED, forbudget-soft (rule 4); was ">= 1 budget labelled over_ceiling INSIDE the allowance", a band the soft # target deleted): the sweep must actually exercise both halves of the contract — rows CUT at the target (else ST and NX ran # on complete answers only) and an answer whose est_tokens exceeds its N (the soft overshoot, which NOLBL then pins unlabelled). @@ -1140,7 +1189,7 @@ PY } # T3 (contract update, same wave): the bare --for is terminal by default and carries auto BODIES at the # 3.80 body rate, so the markup-only 2.50 identity now lives on the --signatures-only shape… -"$BIN" src --for="$FOR_TASK" --signatures-only --no-cache >"$TMP/f_sigonly.out" 2>/dev/null +"$BIN" src --for="$FOR_TASK" --signatures-only "$CF" >"$TMP/f_sigonly.out" 2>/dev/null if out="$( selfconsistent "$TMP/f_sigonly.out" )"; then ok "#11 A9/A10 --for --signatures-only: est_tokens is self-consistent — bytes/est/expected = $out (its own attributes are charged)" else @@ -1168,7 +1217,7 @@ PY # multi-word conceptual query — serve instead of by default, so the AUTO-BODY shape this # arm is about now lives behind --auto-bodies. Re-anchored there rather than deleted: the mixed-rate # identity is the only arm that can catch a mis-rated body span. -"$BIN" src --for="$FOR_TASK" --auto-bodies --no-cache >"$TMP/f_forauto.out" 2>/dev/null +"$BIN" src --for="$FOR_TASK" --auto-bodies "$CF" >"$TMP/f_forauto.out" 2>/dev/null if out="$( mixedconsistent "$TMP/f_forauto.out" )"; then ok "#11 A9/A10 --for --auto-bodies: est_tokens matches markup@2.50 + bodies@3.80 — bytes/span/est/expected = $out" else @@ -1199,7 +1248,7 @@ grep -aq ', markup-rate like the rest of that document, # so the FLAT identity is the right one here now (mixedconsistent would exit on the absent ). -"$BIN" src --for="zzqqxx" --no-cache >"$TMP/f_weak.out" 2>/dev/null +"$BIN" src --for="zzqqxx" "$CF" >"$TMP/f_weak.out" 2>/dev/null if grep -aq 'weak="1"' "$TMP/f_weak.out"; then if out="$( selfconsistent "$TMP/f_weak.out" )"; then ok "#11 A9 --for with weak=\"1\": est_tokens self-consistent — bytes/est/expected = $out (the 9 B attr + the R9 shell are both charged)" @@ -1224,7 +1273,8 @@ fi # to sit above it. If no budget in the ladder qualifies, that is a real failure and it says so. PT_TB="" for _tb in 800 1000 1200 1500 2000 3000; do - "$BIN" src --pack-task="$FOR_TASK" --token-budget=$_tb --no-cache >"$TMP/pt_probe.out" 2>/dev/null + "$BIN" src --pack-task="$FOR_TASK" --token-budget=$_tb "$CP" >"$TMP/pt_probe.out" 2>/dev/null + cp "$TMP/pt_probe.out" "$TMP/pt_probe_$_tb.out" # RUNTIME: reused below (the bare bundle at PT_TB, #15's 1500) _b="$( bytes_of "$TMP/pt_probe.out" )" _a="$( awk "BEGIN{printf \"%d\", $_tb*2.36*1.15}" )" if [ "$_b" -le "$_a" ] 2>/dev/null && ! grep -aqE 'over_ceiling(="1"|:)' "$TMP/pt_probe.out"; then PT_TB=$_tb; break; fi @@ -1234,8 +1284,9 @@ if [ -z "$PT_TB" ]; then PT_TB=3000 fi PT_ALLOW="$( awk "BEGIN{printf \"%d\", $PT_TB*2.36*1.15}" )" -"$BIN" src --pack-task="$FOR_TASK" --token-budget=$PT_TB --no-cache >"$TMP/pt_bare.out" 2>/dev/null -"$BIN" src --pack-task="$FOR_TASK" --token-budget=$PT_TB --with-graph --no-cache >"$TMP/pt_graph.out" 2>/dev/null +if [ -s "$TMP/pt_probe_$PT_TB.out" ]; then cp "$TMP/pt_probe_$PT_TB.out" "$TMP/pt_bare.out" # RUNTIME: the probe's own document +else "$BIN" src --pack-task="$FOR_TASK" --token-budget=$PT_TB "$CP" >"$TMP/pt_bare.out" 2>/dev/null; fi +"$BIN" src --pack-task="$FOR_TASK" --token-budget=$PT_TB --with-graph "$CP" >"$TMP/pt_graph.out" 2>/dev/null PTB="$( bytes_of "$TMP/pt_bare.out" )"; PTG="$( bytes_of "$TMP/pt_graph.out" )" if [ "$PTG" -le "$PT_ALLOW" ] 2>/dev/null; then ok "#11 A11 --pack-task --token-budget=$PT_TB --with-graph: $PTG B within the $PT_ALLOW B allowance" @@ -1251,7 +1302,7 @@ fi # A12 — --around's appended /. Pick a focus whose ego-graph is SMALL, so the sibling blocks # are a large fraction of the document and an uncharged block moves the rate hard (base: 3.07 B/tok). -"$BIN" src --around=Config --no-cache >"$TMP/around.out" 2>/dev/null +"$BIN" src --around=Config "$CM" >"$TMP/around.out" 2>/dev/null AB="$( bytes_of "$TMP/around.out" )" AE="$( est_of "$TMP/around.out" )" if grep -aq '/dev/null; then @@ -1271,12 +1322,12 @@ fi # ── #12 (bucket B of the enumeration): the two DELIBERATE non-conventions, pinned so a future change to them # is a decision rather than a drift. Neither is a defect; both are things a reader could mistake for one. ─ -"$BIN" src --top-k=10 --stable --pack-signatures --no-cache >"$TMP/stable.out" 2>/dev/null +"$BIN" src --top-k=10 --stable --pack-signatures "$CM" >"$TMP/stable.out" 2>/dev/null grep -aqE '"$TMP/k0.out" 2>"$TMP/k0.err" +"$BIN" src --top-k=0 --pack-top-n=3 --token-budget=600 "$CM" >"$TMP/k0.out" 2>"$TMP/k0.err" rc_k0=$? { [ "$rc_k0" -eq 3 ] && [ "$( bytes_of "$TMP/k0.out" )" -lt 2048 ]; } 2>/dev/null \ && ok "#12 B4 --top-k=0 --pack-top-n=3 --token-budget=600: exit 3, payload withheld (mapEstTokens = payloadTokens)" \ @@ -1316,6 +1367,8 @@ fi # arm is unobservable BY DESIGN and the plain-flavour CI leg is what proves it (→ SKIP, reason # named), or the seam regressed on a flavour that CAN see alerts (→ FAILURE). ───────────────────── # L1 (2026-09-19): the compact legend spells inside its comment, so #14c's find("$TMP/dg.out" 2>"$TMP/dg.err" rc_dg=$? if grep -aq 'chargeSection: open_memstream failed' "$TMP/dg.err"; then @@ -1331,7 +1384,7 @@ if grep -aq 'chargeSection: open_memstream failed' "$TMP/dg.err"; then # (c) THE BYTES ARE STILL COMPLETE AND CORRECT. The whole point of the degrade: the caller loses the # charge, never the content. Byte-compare the payload against the undegraded run. - "$BIN" src --top-k=10 --pack-signatures --no-cache --legend=full >"$TMP/dg_ctl.out" 2>/dev/null + "$BIN" src --top-k=10 --pack-signatures "$CM" --legend=full >"$TMP/dg_ctl.out" 2>/dev/null if python3 - "$TMP/dg_ctl.out" "$TMP/dg.out" <<'PY' import sys ctl = open( sys.argv[1], 'rb' ).read() @@ -1408,7 +1461,7 @@ PY # The negative assertion is paired with a POSITIVE CONTROL on the identical shape, because "no # est_tokens in the header region" is also what an extractor pointed at the wrong bytes reports: the # UNDEGRADED run of the same command must find one there, or the anchor is dead and the arm is vacuous. - "$BIN" src --for="$FOR_TASK" --detail=5 --no-cache >"$TMP/dgf_ctl.out" 2>/dev/null + "$BIN" src --for="$FOR_TASK" --detail=5 "$CF" >"$TMP/dgf_ctl.out" 2>/dev/null FCTL="$( forest_of "$TMP/dgf_ctl.out" )" { [ -n "$FCTL" ] && [ "$FCTL" -gt 0 ]; } 2>/dev/null \ && ok "#14d anchor control: the UNDEGRADED --for --detail=5 header does carry est_tokens=\"$FCTL\" (the probe is live)" \ @@ -1428,7 +1481,7 @@ PY # control (`$TMP/dg_ctl.out`, no switch in the environment) is the reference. g4fail=0 for badval in 10 1x 1000000 11 '1 ' 0 true ''; do - RIPWIRE_FAULT_CHARGE_BUFFER="$badval" "$BIN" src --top-k=10 --pack-signatures --no-cache --legend=full >"$TMP/dg_g4.out" 2>"$TMP/dg_g4.err" + RIPWIRE_FAULT_CHARGE_BUFFER="$badval" "$BIN" src --top-k=10 --pack-signatures "$CM" --legend=full >"$TMP/dg_g4.out" 2>"$TMP/dg_g4.err" if grep -aq 'chargeSection: open_memstream failed' "$TMP/dg_g4.err"; then no "#14e RIPWIRE_FAULT_CHARGE_BUFFER='$badval' INJECTED the fault — only the exact value \"1\" may (prefix test, verifier G4)" g4fail=1 @@ -1476,7 +1529,7 @@ if grep -aq 'chargeSection: the charge buffer did not finish whole' "$TMP/mf.err grep -aq 'serialize: the charge buffer did not finish whole .* MODELLED bytes' "$TMP/mf.err" \ && ok "#14f(a) the serialize finish alert fired and says est_tokens reports the MODELLED bytes" \ || no "#14f(a) serialize's finish degraded without its alert (or the alert lost the MODELLED clause)" - "$BIN" src --top-k=10 --pack-signatures --no-cache >"$TMP/mf_ctl.out" 2>/dev/null + "$BIN" src --top-k=10 --pack-signatures "$CM" >"$TMP/mf_ctl.out" 2>/dev/null if [ -s "$TMP/mf_ctl.out" ] && cmp -s <( mask_est "$TMP/mf_ctl.out" ) <( mask_est "$TMP/mf.out" ); then ok "#14f(b) the degraded document is byte-identical to the undegraded one outside est_tokens ($( bytes_of "$TMP/mf.out" ) B) — rendered again, whole" else @@ -1502,7 +1555,7 @@ if grep -aq 'chargeSection: the charge buffer did not finish whole' "$TMP/mf.err && ok "#14f(d) degraded est_tokens=$MFE is the MODELLED number, below the charged $MFC" \ || no "#14f(d) degraded est_tokens=$MFE vs charged $MFC — the modelled fallback is not observable" INFRA_FAULT_MEMSTREAM_FINISH=1 "$BIN" src --top-k=10 --json --no-cache >"$TMP/mfj.out" 2>"$TMP/mfj.err" - "$BIN" src --top-k=10 --json --no-cache >"$TMP/mfj_ctl.out" 2>/dev/null + "$BIN" src --top-k=10 --json "$CM" >"$TMP/mfj_ctl.out" 2>/dev/null if grep -aq 'serializeJson: the charge buffer did not finish whole' "$TMP/mfj.err" && [ -s "$TMP/mfj_ctl.out" ] \ && cmp -s <( mask_est "$TMP/mfj_ctl.out" ) <( mask_est "$TMP/mfj.out" ) \ && python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$TMP/mfj.out" 2>/dev/null; then @@ -1708,7 +1761,8 @@ JSON_TASK="add retry to the http client" jsonEnvelopeCase() { local label="$1"; shift - "$BIN" src --for="$JSON_TASK" --json --no-cache "$@" > "$TMP/fj.out" 2>/dev/null || true + "$BIN" src --for="$JSON_TASK" --json "$CF" "$@" > "$TMP/fj.out" 2>/dev/null || true + [ "$label" = "--token-budget=200" ] && cp "$TMP/fj.out" "$TMP/fj200.out" # RUNTIME: the §C2 control reads this capture python3 - "$TMP/fj.out" "$label" <<'PYEOF' import json, sys raw = open( sys.argv[1], "rb" ).read() @@ -1749,7 +1803,7 @@ jsonEnvelopeCase "--token-budget=16000" --token-budget=16000 # survives only at the default regime's runaway guard). The control now pins both halves of that at the same call: the # overshoot is REAL (est_tokens > 200 — the §C1 identity above already proved that number prices the whole document) and the # document carries NO over_ceiling key (NOLBL). Base d4325a8c: "over_ceiling":true here. -"$BIN" src --for="$JSON_TASK" --json --no-cache --token-budget=200 > "$TMP/fj200.out" 2>/dev/null || true +# (RUNTIME: $TMP/fj200.out is the --token-budget=200 capture jsonEnvelopeCase made above — the same call, not re-run.) c2_est="$( python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("est_tokens","")); sys.exit(0 if "over_ceiling" not in d else 1)' "$TMP/fj200.out" 2>/dev/null )"; c2_rc=$? { [ "$c2_rc" -eq 0 ] && [ -n "$c2_est" ] && [ "$c2_est" -gt 200 ]; } 2>/dev/null \ && ok "§C2 control (forbudget-soft): --for --json --token-budget=200 prices past its target (est_tokens=$c2_est) with no over_ceiling key — a soft target is never labelled" \ @@ -1768,7 +1822,7 @@ c2_est="$( python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d. # as an implication over a sweep rather than pinned to an N, because the crossover moves with the corpus. c4any=0; c4bad=0 for n in 500 1000 3000 6000 6500 10000 20000; do - "$BIN" src --max-tokens="$n" --json --no-cache > "$TMP/c4.json" 2>/dev/null || true + "$BIN" src --max-tokens="$n" --json "$CM" > "$TMP/c4.json" 2>/dev/null || true verdict="$( python3 - "$TMP/c4.json" <<'PYEOF' import json, sys raw = open( sys.argv[1], "rb" ).read() @@ -1795,7 +1849,7 @@ done # and the XML dialect must be untouched by all of this: its verdict already measured its own rendering. for n in 200 1000 6000; do - "$BIN" src --max-tokens="$n" --no-cache > "$TMP/c4.xml" 2>/dev/null || true + "$BIN" src --max-tokens="$n" "$CM" > "$TMP/c4.xml" 2>/dev/null || true xb="$( wc -c < "$TMP/c4.xml" | tr -d ' ' )" # the XML states its fit inside the header COMMENT (serialize.h writes ` fit_bytes=N`, unquoted prose, # not an attribute) — and the legend line also carries the literal `fit_bytes=honoured:`, so take the @@ -1830,7 +1884,8 @@ band15(){ # $1 label $2 file $3 root-element $4 hi*100 [$5 arm tag, default #1 || no "$tag $label: $B B / est_tokens=$E = $(( R / 100 )).$(( R % 100 )) B/tok — OUTSIDE the band (the root's number does not price the delivered document)" } # (a) --pack-task: a comfortable budget prices; a tiny one labels over_ceiling on the ROOT, not only in prose -"$BIN" src --pack-task="$FOR_TASK" --token-budget=1500 --no-cache >"$TMP/p15_pt.xml" 2>/dev/null +if [ -s "$TMP/pt_probe_1500.out" ]; then cp "$TMP/pt_probe_1500.out" "$TMP/p15_pt.xml" # RUNTIME: A11's probe made this exact call +else "$BIN" src --pack-task="$FOR_TASK" --token-budget=1500 "$CP" >"$TMP/p15_pt.xml" 2>/dev/null; fi band15 "--pack-task --token-budget=1500" "$TMP/p15_pt.xml" ctx 420 [ "$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --pack-task: root budget_tokens=\"1500\" — the ceiling in the SAME unit as est_tokens=" \ @@ -1840,8 +1895,8 @@ band15 "--pack-task --token-budget=1500" "$TMP/p15_pt.xml" ctx 420 # header floor exceeds the ceiling). Now NOLBL + HL at the same call: no over_ceiling= on the root, task= verbatim and route= # kept, no "(ceiling)" / task-echo drop; the full dialect reads budget_tokens= as the soft target (packtask.h # kPackTaskSoftBudgetSentence). The overshoot stays real and priced: est_tokens > 50. -"$BIN" src --pack-task="$FOR_TASK" --token-budget=50 --no-cache >"$TMP/p15_pt50.xml" 2>/dev/null -"$BIN" src --pack-task="$FOR_TASK" --token-budget=50 --legend=full --no-cache >"$TMP/p15_pt50f.xml" 2>/dev/null +"$BIN" src --pack-task="$FOR_TASK" --token-budget=50 "$CP" >"$TMP/p15_pt50.xml" 2>/dev/null +"$BIN" src --pack-task="$FOR_TASK" --token-budget=50 --legend=full "$CP" >"$TMP/p15_pt50f.xml" 2>/dev/null pt50_why="" [ -n "$( perl -0pe 's###gs' "$TMP/p15_pt50.xml" | grep -oE ']*)?>' | head -1 )" ] || pt50_why="$pt50_why no-root" [ -z "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" ] || pt50_why="$pt50_why over_ceiling-on-a-soft-target" @@ -1857,7 +1912,7 @@ E50="$( root_est "$TMP/p15_pt50.xml" ctx )" || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the 50-token target (the floor cannot fit 50 tokens)" # (b) --from-trace: the same two shapes on a name-resolved frame printf 'Traceback (most recent call last):\n File "src/graph.h", line 1, in rankGraphTeleport\n' >"$TMP/p15_trace.txt" -"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=1500 --no-cache >"$TMP/p15_ft.xml" 2>/dev/null +"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=1500 "$CT" >"$TMP/p15_ft.xml" 2>/dev/null band15 "--from-trace --token-budget=1500" "$TMP/p15_ft.xml" ctx 420 [ "$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --from-trace: root budget_tokens=\"1500\"" \ @@ -1866,8 +1921,8 @@ band15 "--from-trace --token-budget=1500" "$TMP/p15_ft.xml" ctx 420 # over_ceiling= only against max_tokens=. Was: root over_ceiling="1" at 50. Now at the same call: est_tokens > 50 (the overshoot # is priced), no over_ceiling= (no max_tokens= was passed), the echo of the trace file ships, and the full dialect # reads budget_tokens= as the soft target (tracelocus.h). -"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --no-cache >"$TMP/p15_ft50.xml" 2>/dev/null -"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --legend=full --no-cache >"$TMP/p15_ft50f.xml" 2>/dev/null +"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 "$CT" >"$TMP/p15_ft50.xml" 2>/dev/null +"$BIN" src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --legend=full "$CT" >"$TMP/p15_ft50f.xml" 2>/dev/null FT50="$( root_est "$TMP/p15_ft50.xml" ctx )" ft50_why="" { [ -n "$FT50" ] && [ "$FT50" -gt 50 ]; } 2>/dev/null || ft50_why="$ft50_why est_tokens='${FT50:-absent}'-not-past-50" @@ -1892,7 +1947,7 @@ grep -aqF 'budget_tokens= is the soft token target you passed' "$TMP/p15_ft50f.x # d099ec21: 4 rows at 100..600, 5 at 800, 10 at 1200, 20 at 2000, every rung unlabelled with est_tokens > N. f15_prev="" for N in 100 300 400 600 800 1200 1500 1600 1700 2000 2500 3000; do - "$BIN" src --for="$FOR_TASK" --token-budget=$N --no-cache >"$TMP/p15_for$N.xml" 2>/dev/null + "$BIN" src --for="$FOR_TASK" --token-budget=$N "$CF" >"$TMP/p15_for$N.xml" 2>/dev/null FB="$( bytes_of "$TMP/p15_for$N.xml" )" FE="$( root_est "$TMP/p15_for$N.xml" ctx )" FO="$( root_attr "$TMP/p15_for$N.xml" ctx over_ceiling )" @@ -1927,8 +1982,8 @@ perl -0pe 's##\1#gs' "$TMP/p15_for3000.xml" | grep -qE '(^|[^[:alnum && ok "#15 --for --token-budget=100 (forbudget-soft): the legend defines budget_tokens= as the soft row target the rows stop at" \ || no "#15 --for --token-budget=100 (forbudget-soft): budget_tokens= rides the root but the legend does not read it as a soft target" # (c) --handoff: priced, labelled, and withheld= is a boolean with the count beside it -"$BIN" . --handoff --token-budget=100 --no-cache >"$TMP/p15_ho100.xml" 2>/dev/null -"$BIN" . --handoff --token-budget=100000 --no-cache >"$TMP/p15_hobig.xml" 2>/dev/null +"$BIN" . --handoff --token-budget=100 "$CH" >"$TMP/p15_ho100.xml" 2>/dev/null +"$BIN" . --handoff --token-budget=100000 "$CH" >"$TMP/p15_hobig.xml" 2>/dev/null band15 "--handoff --token-budget=100000" "$TMP/p15_hobig.xml" handoff 320 [ "$( root_attr "$TMP/p15_ho100.xml" handoff over_ceiling )" = "1" ] \ && ok "#15 --handoff --token-budget=100: root over_ceiling=\"1\" (the verified floor exceeds 100 tokens)" \ @@ -1949,11 +2004,11 @@ fi && ok "#15 --handoff --token-budget=100000: no over_ceiling= (absent = inside the ceiling, the map's convention)" \ || no "#15 --handoff --token-budget=100000: over_ceiling=\"$( root_attr "$TMP/p15_hobig.xml" handoff over_ceiling )\" on a packet well inside its budget" # (d) --expand --top-k=0: the root prices the bodies-only document, and that number IS the one --token-budget gates on -"$BIN" src --expand=pageWindow --top-k=0 --no-cache >"$TMP/p15_ex.xml" 2>/dev/null +"$BIN" src --expand=pageWindow --top-k=0 "$CX" >"$TMP/p15_ex.xml" 2>/dev/null band15 "--expand=pageWindow --top-k=0" "$TMP/p15_ex.xml" ctx 420 EX="$( root_est "$TMP/p15_ex.xml" ctx )" if [ -n "$EX" ] && [ "$EX" -gt 1 ] 2>/dev/null; then - "$BIN" src --expand=pageWindow --top-k=0 --no-cache --token-budget=$(( EX - 1 )) >/dev/null 2>"$TMP/p15_ex.err"; rc=$? + "$BIN" src --expand=pageWindow --top-k=0 "$CX" --token-budget=$(( EX - 1 )) >/dev/null 2>"$TMP/p15_ex.err"; rc=$? grep -q "withheld_est_tokens=$EX " "$TMP/p15_ex.err" && [ "$rc" -ne 0 ] \ && ok "#15 --expand --top-k=0: the root's est_tokens=$EX is the number --token-budget gates on (withheld_est_tokens=$EX, exit $rc) — one estimator" \ || no "#15 --expand --top-k=0: root est_tokens=$EX but the gate says '$( head -1 "$TMP/p15_ex.err" | cut -c1-120 )' — two counters" @@ -1999,8 +2054,9 @@ sys.stdout.write( r["result"]["content"][0]["text"] if "result" in r else "" ) ' } LEGARG="" -[ -n "$( mcp_for_text "rank graph teleport" ',"legend":"full"' )" ] && LEGARG=',"legend":"full"' -mcp_for_text "rank graph teleport" "$LEGARG" >"$TMP/f5_mcp.xml" +# RUNTIME: the probe's own answer is the bundle when the server takes the arg; re-asked without it only when it does not +mcp_for_text "rank graph teleport" ',"legend":"full"' >"$TMP/f5_mcp.xml" +if [ -s "$TMP/f5_mcp.xml" ]; then LEGARG=',"legend":"full"'; else mcp_for_text "rank graph teleport" "" >"$TMP/f5_mcp.xml"; fi if [ ! -s "$TMP/f5_mcp.xml" ]; then no "#16 MCP for returned nothing (legend arg '${LEGARG:-none}') — the arm cannot measure what it cannot fetch" else From 635a01c9abe761473cd149a9847d20d955ff3b44 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 18:41:22 -0400 Subject: [PATCH 049/162] test(compactlegendcheck): the P arms assert the soft target (rule 4) (P1-S), (P1-S2) and the (P4) floor twin asserted the retired paid/unpaid continuation and the over_ceiling label on an explicit --for budget. They now assert, in both dialects: compact rows == full rows, the rows fit T or the rank 1..4 floor and are maximal, a rank prefix monotone in N, no over_ceiling, every cut carries a next= that runs, and no header clause or task echo is dropped. Green on HEAD, red on BASE (13 / 2 / 2 budgets). Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 292 ++++++++++++++++++++++++------------- 1 file changed, 189 insertions(+), 103 deletions(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index b850b2d50..dd0aaf794 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -936,6 +936,11 @@ echo "=== (P) L1: compact BUYS rows and never loses one; a compact answer is PRI # is covered, not lost (the tail lists files OUTSIDE the sigs, so a bigger sigs section legitimately shortens it). # Posture-generic: it compares the default against --legend=full, whatever the default is. The arm must also BIND — # at least one argv where the default carries strictly more rows — or it would pass on a binary whose default is full. +# forbudget-soft (2026-10-14): an explicit --token-budget on --for is now a SOFT TARGET charged against the rows only, +# never the header, so the two dialects serve the SAME rows and there is no ceiling ladder left to buy one with. This arm and +# (P1-B) still hold as written (the superset is implied by equality; ruling B's exemption has no live case — p1Exempt needs a +# compact answer whose rows paid for its next=, and nothing is paid from rows any more); the equality itself, CF, is pinned +# strictly by the replaced (P1-S)/(P1-S2)/(P4 floor twin) arms below. cat > "$TMP/rows.py" <<'PY' import re, sys def rows( path ): @@ -1100,82 +1105,139 @@ else fi [ "$p1bbad" -eq 0 ] && ok "(P1-B) the ruled exemption is pinned: its comparison, the short-root fixed arms, the scope twin and the MCP sentinel" -# (P1-S) THE SWEEP (train 27). The pinned 1500 above is ONE point of a band, and the band moved under it: lego-charge (train 26c, -# bisected to its merge 7601d3ac) charged a stubbed / at its served size, which removed the slack that had been -# absorbing the compact header's excess over full's (the L1 cap charges the compact rows no more header than full's), so the -# default answer landed past budgets the full answer met (1300..1500, 1600..1700 on the merged tree), each with a last-rung note -# blaming "the header floor" while the header was about half the budget. Over --token-budget=1100..1700 step 50, on the same -# short relative root, both dialects, every budget must hold: -# FIT the default fits its budget wherever full does (so P4 — over the budget AND over full's — fires only where full is over too); -# NOTE in each dialect a last-rung reason is TRUE: "the header floor ... exceeds" only on a header (the root open tag and the -# comments before the payload) past the allowance (budget x 2.36 x 1.15), "the header alone fits" only on one inside it; -# P1 a row full keeps and the default lost is the ruled exemption (p1Exempt above), as on the fixed arms. -# …and it BINDS: some budget where full fits and the default paid rows for it (an exempt row), and some where both are over and -# the true reason rides (the note arm reads a real last rung, not an absence). -p1sbad=0; p1sn=0; p1sexempt=0; p1sbothover=0; p1sfirst="" +# ── forbudget-soft (rule 4): (P1-S) and (P1-S2) REPLACED; the (P4 floor twin) below uses the same reader ───────────────────── +# Owner decisions 2026-10-14: an explicit --token-budget=N on --for is a SOFT TARGET. T = N x 2.36 x 0.90 bytes is charged +# against the rows inside ONLY (the header and the disclosures are never charged), rows are added in rank order while +# they fit T, rank 1..4 always ship, the cut carries next=, nothing is labelled over_ceiling= and no header clause, task echo +# or route= is dropped "(ceiling)". The paid/unpaid continuation (knob-honesty-068 C3) is gone: one render, nothing paid from +# rows. What the two retired arms pinned is therefore gone BY DESIGN: (P1-S) needed an exempt row (a row the compact answer +# paid for its next=) and a both-over budget whose last-rung note told the true reason; (P1-S2) needed the full answer to fit +# 1300 / 1200 (its premise — the soft lens's est_tokens may exceed N by the uncharged header: HEAD 2331 / 2249) and the +# compact answer to fit or be labelled over_ceiling="1" at the floor. The core of what P1 protects — compact BUYS rows and +# never loses one — is now exact, because the rows are charged and the header is not, and is pinned as these invariants +# (one python3 per answer, the reader below; every field present and numeric before it is compared, CHECKLIST 14): +# CF the compact and full dialects serve the SAME rows (p:l:n in rank order) — equality, not the old superset; +# ST the rows' bytes inside (+ the zero cx=/ccx=/in= the lean spelling elides, which serialize.h's entryCost +# charges — fornotesbudgetcheck's soft.py reads them the same way) fit T, or only the rank 1..4 floor ships, the floor +# is never short; a cut is MAXIMAL: the next row of the same dialect's no-budget answer would cross T (160 B slack); +# RP the rows are a rank prefix of that dialect's no-budget rows; +# NOLBL no over_ceiling= on the root, and neither old over_ceiling clause ("[over_ceiling= is 1 on the root" / "[over_ceiling=1 +# also when the sigs next= rides unpaid") in the legend; +# NX a cut (rows < total) is capped="1" and carries next= on , and its next_offset= (when printed) is the row count; +# HL nothing is dropped "(ceiling)", no "task_echo: dropped", the root keeps route= and task= verbatim. +# softp.py FILE N TASK ROWSOUT [REF] prints ONE line "OK|BAD rows= total= charged= T= est= over= next= fits= maximal= rp= hl="; +# exit 3 = no root or no block (a missing premise: the caller FAILs it, never skips). ROWSOUT gets the rows. +cat > "$TMP/softp.py" <<'PY' +import html, re, sys +def read( path ): + try: s = open( path, encoding = "utf-8", errors = "replace" ).read() + except OSError: return None + root = re.search( r"]*>", s ); m = re.search( r"]*)>(.*?)", s, re.S ) + if not root or not m: return None + inner = m.group( 2 ); charged = len( inner.encode() ); rows = [] + for x in re.finditer( r"]*)>.*?|]*)/>", inner, re.S ): + a = x.group( 1 ) or x.group( 2 ) + g = lambda k: ( re.search( r"\b" + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + elided = sum( n for k, n in ( ( "cx", 7 ), ( "ccx", 8 ), ( "in", 7 ) ) if not re.search( r"\b" + k + r'="', a ) ) + charged += elided; r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ), len( x.group( 0 ).encode() ) + elided ) ) + rows.sort(); sa = m.group( 1 ); rt = root.group( 0 ) + t = re.search( r'\btotal="(\d+)"', sa ); nx = re.search( r'\bnext="[^"]+"', sa ); no = re.search( r'\bnext_offset="(\d+)"', sa ) + e = re.search( r'\best_tokens="(\d+)"', rt ); tk = re.search( r'\btask="([^"]*)"', rt ) + return dict( s = s, rows = rows, charged = charged, total = int( t.group( 1 ) ) if t else len( rows ), capped = ' capped="1"' in sa, + next = nx is not None, nextoff = int( no.group( 1 ) ) if no else None, est = e.group( 1 ) if e else "-", + over = re.search( r'\bover_ceiling="', rt ) is not None or "[over_ceiling= is 1 on the root" in s + or "[over_ceiling=1 also when the sigs next= rides unpaid" in s, + hl = "(ceiling)" not in s and "task_echo: dropped" not in s and ' route="' in rt + and tk is not None and html.unescape( tk.group( 1 ) ) == sys.argv[ 3 ] ) +a = read( sys.argv[ 1 ] ) +if a is None: sys.exit( 3 ) +N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ); k = len( a[ "rows" ] ) +open( sys.argv[ 4 ], "w" ).write( "".join( x[ 1 ] + "\n" for x in a[ "rows" ] ) ) +cut = k < a[ "total" ] +fits = ( a[ "charged" ] <= X or k <= 4 ) and k >= min( 4, a[ "total" ] ) +nxt = not cut or ( a[ "capped" ] and a[ "next" ] and a[ "nextoff" ] in ( None, k ) ) +maximal = rp = True +if len( sys.argv ) > 5: + ref = read( sys.argv[ 5 ] ) + if ref is None: sys.exit( 3 ) + if cut and k < len( ref[ "rows" ] ): maximal = a[ "charged" ] + ref[ "rows" ][ k ][ 2 ] > X - 160 + rp = k > 0 and [ x[ 1 ] for x in a[ "rows" ] ] == [ x[ 1 ] for x in ref[ "rows" ] ][ :k ] +ok = a[ "est" ] != "-" and fits and maximal and rp and nxt and not a[ "over" ] and a[ "hl" ] +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d next=%d fits=%d maximal=%d rp=%d hl=%d" + % ( "OK" if ok else "BAD", k, a[ "total" ], a[ "charged" ], X, a[ "est" ], int( a[ "over" ] ), int( nxt ), int( fits ), + int( maximal ), int( rp ), int( a[ "hl" ] ) ) ) +PY +spfield(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # spfield LINE KEY +# spRef LAB DIR ROOTARG TASK — the no-budget answer of both dialects (the ST/RP reference), and its rows in "$TMP/sp.ref.LAB.DIAL.rows". +spRef(){ + local d; for d in compact full; do + rm -f "$TMP/sp.ref.$1.$d.rows" + ( cd "$2" && "$BIN" "$3" --for="$4" --legend=$d ) >"$TMP/sp.ref.$1.$d" 2>/dev/null + python3 "$TMP/softp.py" "$TMP/sp.ref.$1.$d" 0 "$4" "$TMP/sp.ref.$1.$d.rows" >/dev/null + done +} +# softPair LAB DIR ROOTARG TASK N — both dialects at --token-budget=N (each asked by name, so it reads the same dialect on any +# binary); appends every broken invariant to $spWhy (empty = all hold) and leaves each dialect's verdict in $spLine_compact / +# $spLine_full and its rows in "$TMP/sp.DIAL.rows". +softPair(){ + local lab="$1" dir="$2" rootarg="$3" task="$4" tb="$5" d line rc nc nf + spWhy=""; spLine_compact=""; spLine_full="" + for d in compact full; do + rm -f "$TMP/sp.$d.rows" + ( cd "$dir" && "$BIN" "$rootarg" --for="$task" --token-budget="$tb" --legend=$d ) >"$TMP/sp.$d" 2>/dev/null; rc=$? + if [ "$rc" -ne 0 ]; then spWhy="$spWhy $d: exit $rc;"; continue; fi + line="$( python3 "$TMP/softp.py" "$TMP/sp.$d" "$tb" "$task" "$TMP/sp.$d.rows" "$TMP/sp.ref.$lab.$d" )" + case "$line" in OK*) ;; *) spWhy="$spWhy $d: ${line:-no / in the answer or its no-budget reference};" ;; esac + eval "spLine_$d=\"\$line\"" + done + nc="$( wc -l <"$TMP/sp.compact.rows" 2>/dev/null | tr -d ' ' )"; nf="$( wc -l <"$TMP/sp.full.rows" 2>/dev/null | tr -d ' ' )" + { [ "${nc:-0}" -gt 0 ] && cmp -s "$TMP/sp.compact.rows" "$TMP/sp.full.rows"; } \ + || spWhy="$spWhy CF: compact rows (${nc:-?}) are not full's (${nf:-?});" +} + +# (P1-S) THE SWEEP — REPLACED, forbudget-soft (rule 4). Was (train 27): over --token-budget=1100..1700 step 50 the default fits +# wherever full does, every last-rung over_ceiling reason is true, every lost row is ruling B's exemption, and it bound on an +# exempt row + a both-over budget with the true reason riding. Under the soft target there is no exemption, no last rung and +# no over_ceiling= to be truthful about (HEAD 480bc90b: 13 of 13 read, 0 exempt, 0 both-over — the arm's binding premise is the +# deleted contract). Now, on the same short relative root and band, both dialects: CF + ST + RP + NOLBL + NX + HL at every +# budget, the rows a rank prefix that grows with N (RP monotone), and it BINDS: at least one budget cuts the head past the +# rank 1..4 floor (so ST's fit and maximality are read on a real cut, not a floor or a complete answer). +spRef pr "$ROOT" src "rank symbols by pagerank" +p1sbad=0; p1sn=0; p1scut=0; p1sfirst=""; p1sprev=""; p1slo=""; p1shi="" p1sfail(){ p1sbad=$(( p1sbad + 1 )); [ "$p1sbad" -le 6 ] && p1sfirst="$p1sfirst [$1]"; } for tb in $( seq 1100 50 1700 ); do - ( cd "$ROOT" && "$BIN" src --for="rank symbols by pagerank" --token-budget="$tb" ) >"$TMP/p1s.def" 2>/dev/null; rd=$? - ( cd "$ROOT" && "$BIN" src --for="rank symbols by pagerank" --token-budget="$tb" --legend=full ) >"$TMP/p1s.full" 2>/dev/null; rf=$? - allow="$( awk "BEGIN{printf \"%d\", $tb*2.36*1.15}" )" - set -- $( python3 "$TMP/p1d.py" "$TMP/p1s.def" "$TMP/p1s.full" ) - if [ "$rd" -ne 0 ] || [ "$rf" -ne 0 ] || [ $# -ne 6 ] || ! printf '%s' "$1$2$3$4$allow" | grep -qE '^[0-9]+$'; then - p1sfail "@$tb: unreadable answers (exit $rd/$rf, facts '$*')"; continue - fi - dh="$1"; fh="$2"; de="$3"; fe="$4"; p1sn=$(( p1sn + 1 )) - [ "$fe" -le "$tb" ] && [ "$de" -gt "$tb" ] && p1sfail "@$tb FIT: default est_tokens $de over the budget, full fits at $fe" - [ "$de" -gt "$tb" ] && [ "$fe" -gt "$tb" ] && p1sbothover=$(( p1sbothover + 1 )) - for dial in def full; do - f="$TMP/p1s.$dial"; h="$dh"; [ "$dial" = full ] && h="$fh" - if grep -qF '[over_ceiling= is 1 on the root: the header floor' "$f"; then - [ $(( h + 6 )) -gt "$allow" ] || p1sfail "@$tb NOTE($dial): blames the header floor, but the header is $h B of a $allow B allowance" - fi - if grep -qF '[over_ceiling= is 1 on the root: the header alone fits this budget' "$f"; then - [ $(( h + 6 )) -le "$allow" ] || p1sfail "@$tb NOTE($dial): says the header alone fits, but it is $h B of a $allow B allowance" - [ "$dial" = def ] && [ "$de" -gt "$tb" ] && [ "$fe" -gt "$tb" ] && p1sbothnote=$(( ${p1sbothnote:-0} + 1 )) - fi - grep -qF '[over_ceiling= is 1 on the root: the ranked rows are kept' "$f" \ - && p1sfail "@$tb NOTE($dial): the kept-rows reason (the --detail + --with-graph shape) on a bare --for" - done - set -- $( python3 "$TMP/rows.py" "$TMP/p1s.def" "$TMP/p1s.full" | tr -d "[](),'" ) - if [ "${3:-1}" -ne 0 ]; then - if p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" "$tb"; then - [ "$fe" -le "$tb" ] && p1sexempt=$(( p1sexempt + 1 )) - else - p1sfail "@$tb P1: lost ${3:-?} row(s) full keeps (default ${1:-?}, full ${2:-?}), not the ruled exemption ($p1facts)" - fi + softPair pr "$ROOT" src "rank symbols by pagerank" "$tb" + r="$( spfield "$spLine_compact" rows )"; t="$( spfield "$spLine_compact" total )" + if ! printf '%s' "$r$t" | grep -qE '^[0-9]+$'; then p1sfail "@$tb unreadable:$spWhy"; continue; fi + p1sn=$(( p1sn + 1 )); [ -z "$p1slo" ] && p1slo="$r"; p1shi="$r" + [ -n "$spWhy" ] && p1sfail "@$tb$spWhy" + [ "$r" -lt "$t" ] && [ "$r" -gt 4 ] && p1scut=$(( p1scut + 1 )) + if [ -n "$p1sprev" ]; then + head -n "$( wc -l <"$TMP/p1s.prev" | tr -d ' ' )" "$TMP/sp.compact.rows" | cmp -s - "$TMP/p1s.prev" \ + || p1sfail "@$tb RP: the rows at $p1sprev are not a prefix of the rows at $tb" fi + cp "$TMP/sp.compact.rows" "$TMP/p1s.prev" 2>/dev/null; p1sprev="$tb" done -if [ "$p1sbad" -eq 0 ] && [ "$p1sn" -eq 13 ] && [ "$p1sexempt" -gt 0 ] && [ "${p1sbothnote:-0}" -gt 0 ]; then - ok "(P1-S) --for='rank symbols by pagerank' on root src, --token-budget=1100..1700 step 50, both dialects: the default fits wherever full does, every last-rung reason is true, every lost row is the exemption (binds: $p1sexempt exempt row budget(s) where full fits, $p1sbothover where both are over with the true reason riding ${p1sbothnote:-0}x)" +if [ "$p1sbad" -eq 0 ] && [ "$p1sn" -eq 13 ] && [ "$p1scut" -gt 0 ]; then + ok "(P1-S) --for='rank symbols by pagerank' on root src, --token-budget=1100..1700 step 50: compact rows == full rows at every budget, each fits T or is the floor and stops only where the next row would cross it, no over_ceiling=, every cut carries next=, no (ceiling) drop; rows $p1slo -> $p1shi, a growing rank prefix (binds: $p1scut budget(s) cut past the floor)" else - no "(P1-S) the budget sweep ($p1sn of 13 budgets read, $p1sexempt exempt where full fits, ${p1sbothnote:-0} both-over with the true reason):$p1sfirst ($p1sbad failure(s))" + no "(P1-S) the soft-target sweep ($p1sn of 13 budgets read, $p1scut cut past the floor):$p1sfirst ($p1sbad failure(s))" fi -# (P1-S2) THE OVERRUN PAYMENT, and what is left when it cannot pay. The sweep above fits on its own at every budget; the -# compact dialect's last payment (verbs_for.h runForLens: a paid answer still over pays its MEASURED overrun from the rows) -# is what fits "serialize the map" at 1300 (a mutant without it serves 6 rows at est_tokens 1457, over, while full fits at -# 1294): the default must fit there, with full fitting too (the premise). At 1200 the same task is the case the payment -# cannot reach — both dialects at the rank 1..4 floor, full fits by a token, the compact header is larger — so the default -# goes over, and then it must say so truthfully (rule 5): over_ceiling="1", the header-fits reason with the header inside the -# allowance, and the next= that serves the rows. -p1s2bad="" -for spec in "1300:fit" "1200:disclose"; do - tb="${spec%%:*}"; want="${spec#*:}" - ( cd "$ROOT" && "$BIN" src --for="serialize the map" --token-budget="$tb" ) >"$TMP/p1s2.def" 2>/dev/null - ( cd "$ROOT" && "$BIN" src --for="serialize the map" --token-budget="$tb" --legend=full ) >"$TMP/p1s2.full" 2>/dev/null - set -- $( python3 "$TMP/p1d.py" "$TMP/p1s2.def" "$TMP/p1s2.full" ) - allow="$( awk "BEGIN{printf \"%d\", $tb*2.36*1.15}" )" - if [ $# -ne 6 ] || ! printf '%s' "$1$3$4" | grep -qE '^[0-9]+$'; then p1s2bad="$p1s2bad [@$tb unreadable: '$*']"; continue; fi - [ "$4" -le "$tb" ] || p1s2bad="$p1s2bad [@$tb premise: full est_tokens $4 is over the budget — re-anchor]" - if [ "$want" = fit ]; then - [ "$3" -le "$tb" ] || p1s2bad="$p1s2bad [@$tb FIT: default est_tokens $3 over the budget, full fits at $4]" - elif [ "$3" -gt "$tb" ]; then - { grep -q '^]* over_ceiling="1"' "$TMP/p1s2.def" && grep -qF '[over_ceiling= is 1 on the root: the header alone fits this budget' "$TMP/p1s2.def" \ - && [ $(( $1 + 6 )) -le "$allow" ] && grep -qE ']* next="--for=' "$TMP/p1s2.def"; } \ - || p1s2bad="$p1s2bad [@$tb DISCLOSE: default over ($3) without over_ceiling + the true reason (header $1 B of $allow B) + sigs next=]" - fi +# (P1-S2) REPLACED, forbudget-soft (rule 4). Was: 'serialize the map' FITS 1300 where full fits (the compact overrun payment), +# and at 1200 (the floor the payment cannot reach) any overshoot is labelled over_ceiling="1" with the header-fits reason and +# the sigs next=. Both halves assert the hard ceiling (HEAD 480bc90b: full est_tokens 2331 / 2249 — "over" N by the header the +# soft target never charges, compact 1996 / 1914, no label). The task was chosen because its compact and full headers sat on +# either side of the budget; that is exactly where CF could break if a header were still charged, so the same two budgets keep +# the invariants of the sweep above: CF + ST + RP + NOLBL + NX + HL in both dialects. +spRef sm "$ROOT" src "serialize the map" +p1s2bad=""; p1s2facts="" +for tb in 1300 1200; do + softPair sm "$ROOT" src "serialize the map" "$tb" + [ -n "$spWhy" ] && p1s2bad="$p1s2bad [@$tb$spWhy]" + p1s2facts="$p1s2facts @$tb: $( spfield "$spLine_compact" rows ) of $( spfield "$spLine_compact" total ) rows, est $( spfield "$spLine_compact" est ) (full $( spfield "$spLine_full" est ));" done -[ -z "$p1s2bad" ] && ok "(P1-S2) 'serialize the map' on root src: fits at 1300 where full fits (the overrun payment), and where the floor cannot pay (1200) any overshoot is labelled with its true reason and the sigs next=" \ +[ -z "$p1s2bad" ] && ok "(P1-S2) 'serialize the map' on root src at 1300 and 1200: compact rows == full rows, each fits T or is the floor, maximal, no over_ceiling=, the cut carries next=, no (ceiling) drop ($p1s2facts)" \ || no "(P1-S2)$p1s2bad" # (P2) the default map's --token-budget gate decides on the price the default PRINTS. It used to decide on the full @@ -1235,49 +1297,73 @@ for tb in 850 1000 1100 1200; do fi done [ "$p4bad" -eq 0 ] && ok "(P4) at --token-budget 850/1000/1100/1200 the compact --for answer is inside its budget or no further over it than the full one" -# (P4 floor twin — knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5) A capped under a hard ceiling -# names the call that recovers its cut (next=) and pays for it from its rows where that makes the answer fit. At 500 tokens -# this fixture's block is at its rank 1..4 floor in BOTH dialects and the answer is past its ceiling paid or not: no row is -# dropped for the handle (ruling C3), it still ships, unpaid, and the overshoot is DISCLOSED — root over_ceiling="1" with the -# unpaid clause defining it, est_tokens pricing the bytes delivered (P3's rule), and the P4 claim itself (compact no further -# over than full). Near miss at 750: paying is what makes it fit — est_tokens <= budget in both dialects, next= rides, no -# over_ceiling= and no unpaid clause (a label riding every capped answer would pass the 500 half alone). -# RE-PINNED 2026-10-10 (train 26c): the near miss moves 750 -> 900. lego-charge charges / at their served size -# and #362's rides the bundle, so this fixture's 4-row floor answer prices est_tokens=918 compact / 963 full -# (was 814 / 859): at 750 both dialects are still at the floor, unpaid and labelled; the paid next= first fits in both -# dialects at 850 (784 / 829) and at 900 with margin (855 / 868). The 500 half is unchanged (4 rows, over, the clause). -p4floor_bad=0 +# (P4 floor twin) REPLACED, forbudget-soft (rule 4). Was (knob-honesty-068; rulings 2026-10-07; re-pinned 750 -> 900 in train +# 26c): at 500 both dialects ship next= UNPAID at the rank 1..4 floor, labelled over_ceiling="1" with the unpaid clause; +# at 900 the PAID next= fits (est_tokens <= 900) with no label. Both halves pin the retired hard ceiling and its C3 payment. +# HEAD 480bc90b on this fixture: at 500 the compact and full dialects serve the same 7 of 12 rows (989 of T = 1,062 B charged; +# the 8th row's 133 B would cross it), est 1105 / 1644, no label — the arm read "shown=7 over_ceiling=0 clause=0"; at 900 both +# serve all 12 rows of the head in a complete (no shown=/total=/capped=/next= on it, as on the no-budget answer: 1,655 of +# T = 1,911 B), which the old arm misread as "a capped block without next=" — there is no cut there to recover. Now: +# 500 the soft cut: CF + ST + RP + NOLBL + NX + HL (softPair above), the cut's next= RUNS on this fixture and serves the whole +# head (the no-budget answer's rows) uncut, the cut's rows its prefix; est_tokens prices the bytes delivered (P3's rule, +# kept from the old arm); +# 900 stop when complete: a target wider than the answer serves exactly the no-budget answer's rows, uncapped, no next=, +# no label — in both dialects; +# both the P4 claim itself, kept: compact est_tokens is inside N or no further over it than full's. +spRef p4 "$P4" . "widget routine dispatcher" +p4floor_bad=0; p4facts="" for tb in 500 900; do + softPair p4 "$P4" . "widget routine dispatcher" "$tb" + [ -n "$spWhy" ] && { p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb:$spWhy"; } for lg in compact full; do - ( cd "$P4" && "$BIN" . --for="widget routine dispatcher" --token-budget=$tb --legend=$lg 2>/dev/null ) >"$TMP/p4f.$lg" - done - pc="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.compact" | head -1 | tr -dc '0-9' )"; pf="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.full" | head -1 | tr -dc '0-9' )" - for lg in compact full; do - f="$TMP/p4f.$lg"; pb="$( wc -c <"$f" | tr -d ' ' )"; pe="$( grep -o 'est_tokens="[0-9]*"' "$f" | head -1 | tr -dc '0-9' )" - root="$( grep -oE '^]*>' "$f" | head -1 )"; sigs="$( grep -oE ']*>' "$f" | tail -1 )" - hasnext=0; case "$sigs" in *' capped="1"'*' next="--for='*) hasnext=1;; esac - over=0; [ "${root#* over_ceiling=\"1\"}" != "$root" ] && over=1 - clause=0; grep -qF '[over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]' "$f" && clause=1 - if [ -z "$pe" ] || [ "$hasnext" -ne 1 ]; then - p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: est_tokens=${pe:-?}, next= present=$hasnext — a capped block must carry its next=" - continue + f="$TMP/sp.$lg"; pb="$( wc -c <"$f" | tr -d ' ' )"; pe="$( grep -oE '^]*>' "$f" | head -1 | grep -oE ' est_tokens="[0-9]+"' | tr -dc '0-9' )" + sigs="$( grep -oE ']*)?>' "$f" | head -1 )" + eval "line=\"\$spLine_$lg\""; r="$( spfield "$line" rows )"; t="$( spfield "$line" total )" + if ! printf '%s' "$pe$pb$r$t" | grep -qE '^[0-9]+$'; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: unreadable (est_tokens='$pe' rows='$r' total='$t')"; continue + fi + truth=$(( pb * 100 / 250 )); floor=$(( pb * 100 / 255 )); diff=$(( pe - truth )); [ "$diff" -lt 0 ] && diff=$(( -diff )) + if [ $(( diff * 100 )) -gt $(( truth * 12 )) ] || [ "$pe" -lt "$floor" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: est_tokens=$pe does not price the $pb B delivered (bytes/2.50 = $truth, floor $floor)" fi if [ "$tb" -eq 500 ]; then - truth=$(( pb * 100 / 250 )); floor=$(( pb * 100 / 255 )); diff=$(( pe - truth )); [ "$diff" -lt 0 ] && diff=$(( -diff )) - shown="$( printf '%s' "$sigs" | grep -oE ' shown="[0-9]+"' | tr -dc '0-9' )" - if [ "$over" -ne 1 ] || [ "$clause" -ne 1 ] || [ "${shown:-99}" -gt 4 ] || [ $(( diff * 100 )) -gt $(( truth * 12 )) ] || [ "$pe" -lt "$floor" ]; then - p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500 --legend=$lg: over_ceiling=$over clause=$clause shown=${shown:-?} est_tokens=$pe for $pb B — the floor's unpaid next= must ride labelled and honestly priced" + if [ "$r" -ge "$t" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500 --legend=$lg: premise — $r of $t rows, nothing cut; the arm reads no soft cut" + elif [ "$lg" = compact ]; then + # NX: run the cut's next= (entity-decoded, shell-split) on the same root; it must serve the whole head uncut. + nargv="$( printf '%s' "$sigs" | python3 -c 'import sys,re,html,shlex; m=re.search(r"\snext=\"([^\"]+)\"",sys.stdin.read()); print("\n".join(shlex.split(html.unescape(m.group(1)))) if m else "")' )" + if [ -z "$nargv" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500: the cut's carries no next= to run" + else + nb=0; nbr="$( printf '%s\n' "$nargv" | sed -n 's/^--token-budget=\([0-9]*\)$/\1/p' | head -1 )"; [ -n "$nbr" ] && nb="$nbr" + ( cd "$P4" && printf '%s\n' "$nargv" | tr '\n' '\0' | xargs -0 "$BIN" . ) >"$TMP/p4n.out" 2>/dev/null + nl="$( python3 "$TMP/softp.py" "$TMP/p4n.out" "$nb" "widget routine dispatcher" "$TMP/p4n.rows" )" + nr="$( spfield "$nl" rows )"; nt="$( spfield "$nl" total )" + if printf '%s' "$nr$nt" | grep -qE '^[0-9]+$' && [ "$nr" -eq "$nt" ] && cmp -s "$TMP/p4n.rows" "$TMP/sp.ref.p4.compact.rows" \ + && head -n "$r" "$TMP/p4n.rows" | cmp -s - "$TMP/sp.compact.rows"; then + p4facts="$p4facts the 500 cut's next= ($( printf '%s' "$nargv" | tr '\n' ' ')) serves $nr of $nt rows, the no-budget head;" + else + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500: the cut's next= ($( printf '%s' "$nargv" | tr '\n' ' ')) served '${nl:-no }' — not the whole no-budget head with the cut's rows as its prefix" + fi + fi + fi + else + case "$sigs" in *' capped="1"'*|*' next="'*) capd=1 ;; *) capd=0 ;; esac + if [ "$r" -ne "$t" ] || [ "$capd" -ne 0 ] || ! cmp -s "$TMP/sp.$lg.rows" "$TMP/sp.ref.p4.$lg.rows"; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=900 --legend=$lg: $r of $t rows, capped/next=$capd — a target wider than the answer must serve the no-budget answer's rows uncut" fi - elif [ "$pe" -gt "$tb" ] || [ "$over" -ne 0 ] || [ "$clause" -ne 0 ]; then - p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) near miss --token-budget=$tb --legend=$lg: est_tokens=$pe over_ceiling=$over clause=$clause — a paid next= must fit with no label" fi + p4facts="$p4facts $tb/$lg: $r of $t rows est $pe;" done - lim=$tb; [ "${pf:-0}" -gt "$lim" ] && lim=$pf - if [ -z "$pc" ] || [ "$pc" -gt "$lim" ]; then - p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=${pc:-?} over the budget AND over the full answer's ${pf:-?}" + pc="$( spfield "$spLine_compact" est )"; pf="$( spfield "$spLine_full" est )" + if printf '%s' "$pc$pf" | grep -qE '^[0-9]+$'; then + lim=$tb; [ "$pf" -gt "$lim" ] && lim=$pf + [ "$pc" -gt "$lim" ] && { p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=$pc over the budget AND over the full answer's $pf"; } + else + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: est_tokens unreadable (compact '$pc', full '$pf')" fi done -[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= unpaid at the rank 1..4 floor with over_ceiling=\"1\", the unpaid clause and an honest est_tokens, the 900 run's paid next= fits with no label, compact never further over than full" +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) soft target on the twelve-routine fixture: at 500 both dialects serve the same maximal rank prefix within T, unlabelled, its next= serving the whole head; at 900 the complete head ships uncut; est_tokens honest; compact never further over than full ($p4facts)" # (P5) the BUDGET LEDGER survives compaction (orchestrator rule, METHODOLOGY §9.3/§9.4: never cut silently). --pack-task's # full legend ends with "budget=N bytes (T-token target, ceiling C) | ranking: … | bodies: … | callers: … | notes: … | tests: From 66a9e574ac6b009e1b80a1d94642a6c7843f42ca Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 18:44:59 -0400 Subject: [PATCH 050/162] =?UTF-8?q?test:=20qddegradecheck=20round=202=20?= =?UTF-8?q?=E2=80=94=20the=20gates=20review's=20R1-R9,=20exit=206,=20B23?= =?UTF-8?q?=20by-design=20break=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gates before code, round 2 (early gates review GO-WITH-CHANGES): - Exit codes exact (R8): a degraded answer exits 2 when a row of a kind that ran gates, else 6 (owner 2026-10-14: a partial answer with no gating row); (V) pins 6 with rows on stdout. 5 stays "refused, nothing". - R1 (M): a new defect-shape site whose text also sits in an unchanged file keeps its gating row under P1 and P0. - R2 (N): an unchanged symlink, an export-ignore flip and an export-subst tree refuse by name; an unchanged export-ignore attribute and a CMakeCache.txt-pruned directory answer like the full run. - R3 (O): legacy-scheme acks — a name two files define suppresses the same rows; a name one file defines names ack-healing as skipped. - R4 (P): the degraded path with the cache on leaves every blob unchanged. - R5 (Q): a stop in the target's full ingest (new ingest:N seam) degrades. - R7: memory_stop= + memory_need="unmeasured" after a stop; memory_peak= on a full answer over the limit (peak:N seam); the replay is accepted and not degraded again; a whole-tree-only selection under pressure refuses (exit 5). - R9 (S): --quality-kinds=complexity reports no false finding-gone for a dead-code ack. S2 (T): 2x the measured peak answers in full. S3: the selector is refused on the bare form. S5: rename-only compares new-symbol= and api-new-surface=. - memguardcheck B23: a disclosed by-design break (owner decision 4) — a cut base ingest is released unscored and the changed files answer (1 cut + 2 changed-file ingests, no row outside the diff), with a twin that still asserts the exit-5 refusal when even the changed files are cut. B22 is unchanged. Red on the base build b274e0c3: qddegradecheck 7 PASS (guards and premises) / 50 FAIL; memguardcheck B23 3 FAIL (B21 also fails there, a timing arm, pre-existing). Co-Authored-By: Claude Opus 5.5 --- test/memguardcheck.sh | 30 +++- test/qddegradecheck.sh | 324 ++++++++++++++++++++++++++++++++++------- 2 files changed, 297 insertions(+), 57 deletions(-) diff --git a/test/memguardcheck.sh b/test/memguardcheck.sh index f6a5d3bab..dd21b647f 100755 --- a/test/memguardcheck.sh +++ b/test/memguardcheck.sh @@ -677,17 +677,35 @@ if [ "$rc" = "$refRc" ] && [ -s "$TMP/b22ref.out" ] && cmp -s "$TMP/b22.out" "$T else no "(B22) hard:7 rc=$rc (want $refRc) or its answer differs from the unseamed run" fi -# (B23) #387: a commit ingest the memory guard cut is never scored. The range form refuses (exit 5, one line naming the -# stop and the verb, no stdout) instead of answering from a partial tree with the backstop line after it (0.6.7). +# (B23) #387: a commit ingest the memory guard cut is never scored. Until qd-memory-degrade the range form refused here +# (exit 5, one line, no stdout). BY-DESIGN BREAK (qd-memory-degrade, owner decision 4, disclosed in CHANGELOG): the +# cut tree is released unscored and BOTH commits' changed files answer the kinds that need only them — one cut +# ingest + two changed-file ingests, a degraded header (ingest_scope="changed-files", memory_stop= naming the stop), +# exit 2 if a row gates else 6, and no row names a file outside the diff (a/extra.c). test/qddegradecheck.sh pins +# the degraded answer's contents; this arm pins that nothing of the cut full tree reaches it. for trip in parse:3 crawl:20; do qr_run "$trip"; rc=$? - if [ "$rc" = 5 ] && [ ! -s "$TMP/b22.out" ] && grep -q '^ripwire: the memory guard stopped the .*; --quality-delta=A..B cannot answer from a partial index' "$TMP/b22.err" \ - && [ "$( grep -c '^ripwire: [^c]' "$TMP/b22.err" )" = 1 ] && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 1 ]; then - ok "(B23) $trip cuts the base commit's ingest: the range form refuses before the target is ingested (exit 5, one line)" + gt="$( sed -n 's/.*]* gating="\([0-9]*\)".*/\1/p' "$TMP/b22.out" | head -n 1 )" + want=6; [ -n "$gt" ] && [ "$gt" -gt 0 ] && want=2 + outside="$( grep -o ' p="[^"]*"' "$TMP/b22.out" | grep -vc ' p="a/extra\.c:' )" + if [ -n "$gt" ] && [ "$rc" = "$want" ] && grep -q ']* ingest_scope="changed-files"' "$TMP/b22.out" \ + && grep -q "]* memory_stop=\"${trip%%:*}\"" "$TMP/b22.out" && [ "$outside" = 0 ] \ + && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 3 ] && [ "$( grep -c '^ripwire: memory limit ' "$TMP/b22.err" )" = 1 ]; then + ok "(B23) $trip cuts the base commit's ingest: the cut tree is never scored — 1 cut + 2 changed-file ingests answer (exit $rc, degraded header)" else - no "(B23) $trip rc=$rc stdout=$( wc -c <"$TMP/b22.out" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" + no "(B23) $trip rc=$rc (want $want) gating=$gt rows-outside-the-diff=$outside ingests=$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" ) stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" fi done +# (B23 twin, rule 4) the refusal still holds when even the changed files are cut: crawl:1 trips every ingest at its first +# crawl entry, so the target's changed-file ingest (a/, a/extra.c) is cut too — exit 5, no stdout, ONE line naming +# the stop, that the verb cannot answer from a partial index, and that the changed files were tried. +qr_run "crawl:1"; rc=$? +if [ "$rc" = 5 ] && [ ! -s "$TMP/b22.out" ] && grep -q '^ripwire: the memory guard stopped the .*; --quality-delta=A..B cannot answer from a partial index' "$TMP/b22.err" \ + && grep -q 'changed files' "$TMP/b22.err" && [ "$( grep -c '^ripwire: [^c]' "$TMP/b22.err" )" = 1 ]; then + ok "(B23) crawl:1 cuts the changed files too: the range form refuses (exit 5, one line naming the stop and the changed-files attempt)" +else + no "(B23) crawl:1 rc=$rc stdout=$( wc -c <"$TMP/b22.out" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" +fi # (B11) a verb's non-zero verdict from a partial ingest still gets the line; --html and --index-out never pass silently run_trip crawl:30 "$FX" --no-cache --html >/dev/null 2>"$TMP/b11.err"; rc=$? diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index 07fc0e59d..84c150629 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -7,36 +7,58 @@ # THE CONTRACT these arms pin. Why a kind is LOCAL: every per-symbol baseline key is path-qualified (quality.h qualityKey) # and so is the canonical id, so an UNCHANGED file yields the same keys and metrics in both trees and no row of such a kind: # * The 12 kinds split by input scope. LOCAL kinds (api-surface, complexity, error-masking, nesting, params, -# placeholder, verbosity) need only the changed files' trees. defect-shape is LOCAL except its format-arity scan of a -# C++ file, which reads the tree's C-family macro NAMES (the neighbourhood input). WHOLE-TREE kinds (dead-code, -# duplication, new-clone-of-reused-helper) need every file. short-horizon-churn is unavailable in the range form. -# * When the base commit fit but the target phase is projected past the limit (P1), or a commit's full ingest was -# stopped by the guard (P0), the range form ingests only the changed files of both commits and runs the kinds that -# can run there. Every kind it does not run is NAMED (kinds_skipped=, skipped_why="memory"), with the limit -# (memory_limit=), the measured or lower-bound need (memory_need=), and a replay (kinds_next=) that runs exactly the -# skipped kinds with enough memory. -# * Exit: 2 when a row of a kind that ran gates; otherwise 5 (never 0 — a partial answer is not a pass). -# * Exit 5 with nothing on stdout only when even the changed files do not fit. -# * When memory suffices the degraded path is unreachable: output byte-identical to the build before it (L). -# * --quality-kinds=K[,K] runs exactly those kinds (the replay's selector), header kinds_skipped= skipped_why="selector". +# placeholder, verbosity) need only the changed files' trees. defect-shape is LOCAL when the changed files define +# the same C-family macro names on both sides and either the base's whole macro set was read (P1) or no changed +# file is C++ (P0); otherwise it is skipped. WHOLE-TREE kinds (dead-code, duplication, new-clone-of-reused-helper) +# need every file. short-horizon-churn is unavailable in the range form. +# * When the base commit fit but the target phase is projected past the limit (P1), when the guard stopped a commit's +# full ingest (P0), or when it stopped the target's ingest or graph after the projection said it fits (P1-late), +# the range form ingests only the changed files of BOTH commits (never through a cache) and runs the kinds that can +# run there. Every kind it does not run is NAMED (kinds_skipped=, skipped_why="memory"), with the limit +# (memory_limit=), the projected need (memory_need="NM") or, after a stop, the stop (memory_stop=) and +# memory_need="unmeasured", the whole-answer facts not computed (also_skipped=), and a replay (kinds_next=) that +# runs exactly the skipped kinds. +# * Exit: 2 when a row of a kind that ran gates (the skipped kinds can only add rows); otherwise 6 = a partial answer +# with no gating row (never 0). Exit 5 with nothing on stdout only when even the changed files do not fit, or when +# the changed files cannot stand in for the trees (a symlink, an export attribute) — named on one line. +# * When memory suffices the degraded path is unreachable: output byte-identical to the build before it (L, T). +# * --quality-kinds=K[,K] (range form only) runs exactly those kinds (the replay's selector): kinds_skipped= +# skipped_why="selector"; stale acks are checked for the selected kinds only. # -# Arms (RED on the build before this lane, except (A) the premise and (L) the guard): +# SEAMS (RIPWIRE_TEST_MEMGUARD, memguard.h): crawl:N and parse:N count per INGEST (each ingest's own Watch), so (E) depends +# on the changed-file subsets staying under 12 crawl entries, directories included; project:N = the Nth projection +# reading says "does not fit" (one per range run); ingest:N = the Nth ingest of the process is cut at its first crawl +# entry; peak:N = the Nth post-answer peak reading reads as over the limit; hard:N = the Nth and later hard-line readings. +# +# Arms (RED on the build before this lane, except the guards (A), (G) A..A, (K) unpressured --json, (L) and (T)): # (A) premise: the fixture's unpressured answer has a row of each of the 11 kinds the range form measures. -# (B) P1 forced (RIPWIRE_TEST_MEMGUARD=project:1): the degraded answer's header, exit code, stderr line, ingests. +# (B) P1 forced (project:1): the degraded answer's header, exit code, stderr line, three ingests. # (C) P1: each LOCAL kind's rows equal the same kind's rows of (A); no row of a skipped kind. -# (D) the kinds_next= replay yields exactly the skipped kinds' rows; union with (B) = (A); gating= adds up. -# (E) P0 (crawl seam cuts the base's FULL ingest): answers from the changed files; defect-shape is skipped because a -# changed file is C++; (E2) a Python-only change keeps defect-shape and its rows equal (A)'s. -# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5, nothing -# on stdout, ONE line that names the phase AND that the changed files alone did not fit. +# (D) the kinds_next= replay yields exactly the skipped kinds' rows, not degraded again; union with (B) = (A). +# (E) P0 (crawl seam cuts the base's FULL ingest): answers from the changed files; defect-shape skipped (a changed +# C++ file and no whole macro set); memory_stop=, memory_need="unmeasured"; (E2) a Python-only change keeps it. +# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5. # (G) near misses under project:1: an empty diff, a rename-only diff, a non-code-only diff, and A..A. -# (H) --quality-kinds: unknown kind refused naming the 12; refused beside --quality-ack; each single kind = (A)'s rows -# of that kind; all 12 named = byte-identical to no flag. +# (H) --quality-kinds: unknown kind refused naming the 12; refused beside --quality-ack and on the bare form; each +# single kind = (A)'s rows of that kind; all 12 named = byte-identical to no flag. # (I) --quality-ack under pressure refuses (exit 5) and leaves the ledger untouched. # (J) --scope naming an unchanged file under pressure is not refused as "names nothing indexed". -# (K) legend + JSON: --legend=full defines every new attribute and parses as XML; --json carries the twins; an -# unpressured --json carries none of them. +# (K) legend + JSON: --legend=full defines every new attribute and parses as XML; --json carries the twins. # (L) guard: a projection seam index past the run's one projection reading changes nothing (byte-identical). +# --- round 2 (gates review R1-R9) --- +# (M) R1 defect-shape: a new site whose text also sits in an UNCHANGED file keeps its gating row under P1 and P0. +# (N) R2 the changed set: an unchanged symlink, an export-ignore flip and an export-subst tree refuse by name (exit 5); +# an unchanged export-ignore attribute and a CMakeCache.txt-pruned directory answer like the full run. +# (O) R3 acks: a legacy-scheme ack on a name two files define (one changed) suppresses the same rows as the full run; +# on a name one file defines, the degraded answer names ack-healing as skipped and shows the row. +# (P) R4 cache isolation: P1 with the cache on leaves the HEAD ingest-cache blob untouched; a cached run after it is +# byte-identical to (A). +# (Q) R5 the target-phase stop (ingest:2 cuts the target's full ingest): a degraded answer, four ingests. +# (R) R7b a peak over the limit (peak:1) is disclosed on a full answer, rc and rows unchanged; R7c a replay whose +# selection holds only whole-tree kinds refuses under pressure (exit 5), never an empty pass. +# (S) R9 --quality-kinds=complexity reports no false finding-gone for a dead-code ack. +# (T) S2 fits near the line: --max-memory at 2x the measured full peak answers in full, byte-identical (guard). +# (V) R8 a degraded answer with rows and no gating row exits 6, not 2 and not 0. # # Usage: bash test/qddegradecheck.sh [BIN] set -u @@ -55,6 +77,7 @@ mkdir -p "$WORK/tmp" echo "qddegradecheck: BIN=$BIN" LOCAL_KINDS="api-surface complexity defect-shape error-masking nesting params placeholder verbosity" +LOCAL_CSV="$( echo "$LOCAL_KINDS" | tr ' ' ',' )" WHOLE_KINDS="dead-code,duplication,new-clone-of-reused-helper" ALL12="api-surface,complexity,dead-code,defect-shape,duplication,error-masking,nesting,new-clone-of-reused-helper,params,placeholder,short-horizon-churn,verbosity" @@ -119,6 +142,8 @@ FX="$WORK/fx"; mkfx "$FX"; mktarget "$FX" BASE="$( git -C "$FX" rev-parse HEAD~1 )"; TGT="$( git -C "$FX" rev-parse HEAD )" # run [ENV=VAL ...] -- ARGS: stdout/stderr to $WORK/o, $WORK/e; rc in $RC qd(){ ( cd "$FX" && env TMPDIR="$WORK/tmp" RIPWIRE_CACHE_STATS=1 "$@" >"$WORK/o" 2>"$WORK/e" ); RC=$?; } +# the same in another repo: qdin REPO [ENV=VAL ...] BIN ARGS +qdin(){ local R="$1"; shift; ( cd "$R" && env TMPDIR="$WORK/tmp" RIPWIRE_CACHE_STATS=1 "$@" >"$WORK/o" 2>"$WORK/e" ); RC=$?; } # rows: every and element on its own line, sorted (the union rule: rows compare as SORTED sets) rows(){ python3 - "$1" "${2:-}" <<'PY' import re, sys @@ -137,6 +162,16 @@ hasattr(){ grep -q "]* $2=\"" "$1"; } ingests(){ grep -c '^ripwire: cache-stats ' "$WORK/e"; } memlines(){ grep -c '^ripwire: memory limit ' "$WORK/e"; } isnum(){ case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; } +# R8: the exact exit code of a degraded answer — 2 when a row gates, else 6 (a partial answer with no gating row). +# Read only off a document whose gating= is a number; anything else is no exit code at all. +degrc(){ local gt; gt="$( attr "$1" gating )"; isnum "$gt" || { echo "none"; return; }; [ "$gt" -gt 0 ] && echo 2 || echo 6; } +validxml(){ python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$1" 2>/dev/null; } +fnv(){ python3 -c ' +import sys +h = 0xcbf29ce484222325 +for b in sys.argv[1].encode(): + h ^= b; h = (h * 0x100000001b3) & 0xFFFFFFFFFFFFFFFF +print("%016x" % h)' "$1"; } # ── (A) the premise ─────────────────────────────────────────────────────────────────────────────────────────── qd "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/A.xml"; cp "$WORK/e" "$WORK/A.err"; ARC=$RC @@ -151,21 +186,21 @@ AGATE="$( attr "$WORK/A.xml" gating )" # ── (B) P1 forced: the base fit, the target phase is projected past the limit ─────────────────────────────────── qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/B.xml"; cp "$WORK/e" "$WORK/B.err"; BRC=$RC; BING="$( ingests )" bgate="$( attr "$WORK/B.xml" gating )"; need="$( attr "$WORK/B.xml" memory_need )"; needn="${need%M}" -wantrc=5; isnum "$bgate" && [ "$bgate" -gt 0 ] && wantrc=2 +wantrc="$( degrc "$WORK/B.xml" )" bnext="$( attr "$WORK/B.xml" kinds_next )" -if [ -s "$WORK/B.xml" ] && python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$WORK/B.xml" 2>/dev/null \ +if [ -s "$WORK/B.xml" ] && validxml "$WORK/B.xml" \ && [ "$( attr "$WORK/B.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/B.xml" skipped_why )" = memory ] \ && [ "$( attr "$WORK/B.xml" ingest_scope )" = changed-files ] && hasattr "$WORK/B.xml" memory_limit \ - && isnum "$needn" && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ - && ! hasattr "$WORK/B.xml" register-macro-excluded && isnum "$bgate" && [ "$BRC" = "$wantrc" ]; then - ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need}, also_skipped, no stale=/dead-code counts" + && isnum "$needn" && [ "$needn" -ge 64 ] && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ + && ! hasattr "$WORK/B.xml" memory_stop && ! hasattr "$WORK/B.xml" register-macro-excluded && isnum "$bgate" && [ "$BRC" = "$wantrc" ]; then + ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need} (>= the 64M floor), also_skipped, no stale=/dead-code counts" else no "(B) P1: rc=$BRC (want $wantrc) gating=$bgate kinds_skipped=$( attr "$WORK/B.xml" kinds_skipped ) why=$( attr "$WORK/B.xml" skipped_why ) scope=$( attr "$WORK/B.xml" ingest_scope ) need=$need stdout=$( wc -c <"$WORK/B.xml" | tr -d ' ' )B" fi -if [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=$need" ]; then ok "(B) kinds_next= replays exactly the skipped kinds at the measured need" +if [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=$need" ]; then ok "(B) kinds_next= replays exactly the skipped kinds at the projected need" else no "(B) kinds_next='$bnext'"; fi -if [ "$( memlines )" = 1 ] && grep -q "^ripwire: memory limit .*dead-code, duplication, new-clone-of-reused-helper" "$WORK/B.err" && [ "$BING" = 2 ]; then - ok "(B) one stderr line names the skipped kinds; two ingests (the base in full, then the target's changed files)" +if [ "$( memlines )" = 1 ] && grep -q "^ripwire: memory limit .*dead-code, duplication, new-clone-of-reused-helper" "$WORK/B.err" && [ "$BING" = 3 ]; then + ok "(B) one stderr line names the skipped kinds; three ingests (the base in full, then both commits' changed files)" else no "(B) memory lines=$( memlines ) ingests=$BING stderr: $( grep '^ripwire: [^c]' "$WORK/B.err" | head -c 300 )"; fi # ── (C) P1: LOCAL kinds equal the unpressured answer, kind by kind ───────────────────────────────────────────── @@ -192,6 +227,12 @@ if [ -n "$bnext" ]; then if isnum "$bgate" && isnum "$dgate" && isnum "$AGATE" && [ $(( bgate + dgate )) = "$AGATE" ]; then ok "(D) gating= adds up: $bgate + $dgate = $AGATE" else no "(D) gating: B=$bgate D=$dgate A=$AGATE"; fi if [ "$( attr "$WORK/D.xml" skipped_why )" = selector ]; then ok "(D) the replay says its kinds were selected (skipped_why=selector)"; else no "(D) replay skipped_why=$( attr "$WORK/D.xml" skipped_why )"; fi + # R7c: the replay's --max-memory clears the parser floor and is above the projection that degraded — it is accepted + # (not exit 1) and answers in full (no ingest_scope=), with the exit code its own rows decide + dwant=0; isnum "$dgate" && [ "$dgate" -gt 0 ] && dwant=2 + if [ "$DRC" = "$dwant" ] && [ -s "$WORK/D.xml" ] && ! hasattr "$WORK/D.xml" ingest_scope && ! hasattr "$WORK/D.xml" memory_stop; then + ok "(D) the replay is accepted at its --max-memory and is not degraded again (rc=$DRC)" + else no "(D) the replay rc=$DRC (want $dwant) or it degraded again: scope=$( attr "$WORK/D.xml" ingest_scope ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi else no "(D) premise: no kinds_next= to replay" fi @@ -199,16 +240,19 @@ fi # ── (E) P0: the guard cut the base commit's FULL ingest (crawl seam: the full tree has more entries than the changed set) ─ qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/E.xml"; cp "$WORK/e" "$WORK/E.err"; ERC=$RC if [ -s "$WORK/E.xml" ] && [ "$( attr "$WORK/E.xml" kinds_skipped )" = "dead-code,defect-shape,duplication,new-clone-of-reused-helper" ] \ - && [ "$( attr "$WORK/E.xml" ingest_scope )" = changed-files ] && [ "$( ingests )" = 3 ] && { [ "$ERC" = 2 ] || [ "$ERC" = 5 ]; }; then + && [ "$( attr "$WORK/E.xml" ingest_scope )" = changed-files ] && [ "$( ingests )" = 3 ] && [ "$ERC" = "$( degrc "$WORK/E.xml" )" ]; then ok "(E) P0: the cut full ingest is never scored; the changed files answer (rc=$ERC); defect-shape skipped (a changed C++ file)" else - no "(E) P0: rc=$ERC kinds_skipped=$( attr "$WORK/E.xml" kinds_skipped ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/E.err" | head -c 300 )" + no "(E) P0: rc=$ERC (want $( degrc "$WORK/E.xml" )) kinds_skipped=$( attr "$WORK/E.xml" kinds_skipped ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/E.err" | head -c 300 )" fi if [ -n "$( attr "$WORK/E.xml" kinds_skipped )" ]; then for k in api-surface complexity error-masking nesting params placeholder verbosity; do if [ "$( rows "$WORK/E.xml" "$k" )" = "$( rows "$WORK/A.xml" "$k" )" ]; then ok "(E) P0 $k: rows identical"; else no "(E) P0 $k: rows differ"; fi done - case "$( attr "$WORK/E.xml" memory_need )" in '>'*M) ok "(E) P0: memory_need= is a lower bound ('>' the limit) — the full need was never measured";; *) no "(E) P0 memory_need=$( attr "$WORK/E.xml" memory_need )";; esac + # R7a: a soft stop does not show the need exceeds the limit — the stop is named, the need is not claimed + if [ "$( attr "$WORK/E.xml" memory_stop )" = crawl ] && [ "$( attr "$WORK/E.xml" memory_need )" = unmeasured ]; then + ok "(E) P0: memory_stop=\"crawl\" names the stop; memory_need=\"unmeasured\" (a crawl stop does not measure the need)" + else no "(E) P0 memory_stop=$( attr "$WORK/E.xml" memory_stop ) memory_need=$( attr "$WORK/E.xml" memory_need )"; fi else no "(E) P0: premise — not a degraded answer" fi @@ -216,12 +260,12 @@ fi FX2="$WORK/fx2"; mkfx "$FX2" printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {} {}".format(name)\n' >"$FX2/py/mod.py"; g "$FX2" add -A; g "$FX2" commit -qm py ( cd "$FX2" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2a.xml" 2>/dev/null ) -( cd "$FX2" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2.xml" 2>/dev/null ) +( cd "$FX2" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/E2.xml" 2>/dev/null ); e2rc=$? if [ "$( attr "$WORK/E2.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ -n "$( rows "$WORK/E2a.xml" defect-shape )" ] \ - && [ "$( rows "$WORK/E2.xml" defect-shape )" = "$( rows "$WORK/E2a.xml" defect-shape )" ]; then - ok "(E2) P0, Python-only change: defect-shape runs on the changed files and its rows equal the unpressured ones" + && [ "$( rows "$WORK/E2.xml" defect-shape )" = "$( rows "$WORK/E2a.xml" defect-shape )" ] && [ "$e2rc" = "$( degrc "$WORK/E2.xml" )" ]; then + ok "(E2) P0, Python-only change: defect-shape runs on the changed files and its rows equal the unpressured ones (rc=$e2rc)" else - no "(E2) kinds_skipped=$( attr "$WORK/E2.xml" kinds_skipped ) unpressured defect rows=$( rows "$WORK/E2a.xml" defect-shape | grep -c . )" + no "(E2) rc=$e2rc (want $( degrc "$WORK/E2.xml" )) kinds_skipped=$( attr "$WORK/E2.xml" kinds_skipped ) unpressured defect rows=$( rows "$WORK/E2a.xml" defect-shape | grep -c . )" fi # ── (F) even the changed files do not fit ────────────────────────────────────────────────────────────────────── @@ -234,16 +278,17 @@ else fi # ── (G) near misses under project:1 ──────────────────────────────────────────────────────────────────────────── -nm(){ # $1 label, $2 repo; compares LOCAL rows with the unpressured run and requires the degraded header +nm(){ # $1 label, $2 repo; compares LOCAL rows with the unpressured run and requires the degraded header and the exact rc local L="$1" R="$2" ( cd "$R" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmA.xml" 2>/dev/null ) ( cd "$R" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmB.xml" 2>"$WORK/nmB.err" ); local rc=$? - local loc; loc="$( echo "$LOCAL_KINDS" | tr ' ' ',' )" - if [ "$( attr "$WORK/nmB.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( rows "$WORK/nmB.xml" "$loc" )" = "$( rows "$WORK/nmA.xml" "$loc" )" ] \ - && ! grep -q 'could not materialize' "$WORK/nmB.err" && { [ "$rc" = 5 ] || [ "$rc" = 2 ]; }; then - ok "(G) $L: degraded answer (rc=$rc), LOCAL rows equal the unpressured run" + if [ "$( attr "$WORK/nmB.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( rows "$WORK/nmB.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/nmA.xml" "$LOCAL_CSV" )" ] \ + && ! grep -q 'could not materialize' "$WORK/nmB.err" && [ "$rc" = "$( degrc "$WORK/nmB.xml" )" ] \ + && [ "$( attr "$WORK/nmB.xml" new-symbol )" = "$( attr "$WORK/nmA.xml" new-symbol )" ] \ + && [ "$( attr "$WORK/nmB.xml" api-new-surface )" = "$( attr "$WORK/nmA.xml" api-new-surface )" ]; then + ok "(G) $L: degraded answer (rc=$rc), LOCAL rows, new-symbol= and api-new-surface= equal the unpressured run" else - no "(G) $L: rc=$rc kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) stderr: $( head -c 300 "$WORK/nmB.err" )" + no "(G) $L: rc=$rc (want $( degrc "$WORK/nmB.xml" )) kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) new-symbol $( attr "$WORK/nmA.xml" new-symbol )/$( attr "$WORK/nmB.xml" new-symbol ) stderr: $( head -c 300 "$WORK/nmB.err" )" fi } FE="$WORK/fe"; mkfx "$FE"; g "$FE" commit -q --allow-empty -m empty; nm "empty diff" "$FE" @@ -263,6 +308,11 @@ qd "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --quality-ac if [ "$RC" = 1 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && grep -q -- '--quality-kinds.*--quality-ack\|--quality-ack.*--quality-kinds' "$WORK/e" && ! grep -q 'unknown flag' "$WORK/e"; then ok "(H) --quality-kinds beside --quality-ack is refused (exit 1) naming both flags, no ledger written" else no "(H) with --quality-ack rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi +# S3: the selector is the range form's (the bare form's MCP twin has no such field yet) — refused by name on the bare form +qd "$BIN" . --quality-delta --quality-kinds=complexity --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q -- '--quality-kinds' "$WORK/e" && grep -q 'A\.\.B' "$WORK/e" && ! grep -q 'unknown flag' "$WORK/e"; then + ok "(H) --quality-kinds on the bare (working-tree) form is refused (exit 1) naming the range form" +else no "(H) bare form: rc=$RC stderr: $( head -c 300 "$WORK/e" )"; fi for k in api-surface complexity dead-code defect-shape duplication error-masking nesting new-clone-of-reused-helper params placeholder verbosity; do qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$k" --no-cache if [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" "$k" )" ] && [ -n "$( rows "$WORK/o" )" ] && [ "$( attr "$WORK/o" skipped_why )" = selector ]; then ok "(H) --quality-kinds=$k: exactly (A)'s $k rows" @@ -273,17 +323,21 @@ if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(H) all 12 nam # ── (I) --quality-ack under pressure ────────────────────────────────────────────────────────────────────────── qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache -if [ "$RC" = 5 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && [ "$( memlines )" = 1 ]; then ok "(I) --quality-ack under pressure refuses (exit 5), the ledger untouched" +if [ "$RC" = 5 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && [ "$( memlines )" = 1 ] && [ ! -s "$WORK/o" ]; then ok "(I) --quality-ack under pressure refuses (exit 5), the ledger untouched" else no "(I) rc=$RC ledger=$( [ -e "$FX/.ripwire_quality_acks" ] && echo written || echo absent ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi rm -f "$FX/.ripwire_quality_acks" # ── (J) --scope naming an unchanged file under pressure ─────────────────────────────────────────────────────── qd "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache; jA=$RC; cp "$WORK/o" "$WORK/JA.xml" qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache -if [ "$RC" != 1 ] && ! grep -q 'names nothing' "$WORK/e" && [ -n "$( attr "$WORK/o" kinds_skipped )" ] \ - && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/JA.xml" "$( echo "$LOCAL_KINDS" | tr ' ' ',' )" )" ]; then +if [ "$RC" != 1 ] && ! grep -q 'names nothing\|matches no indexed path' "$WORK/e" && [ -n "$( attr "$WORK/o" kinds_skipped )" ] \ + && [ "$RC" = "$( degrc "$WORK/o" )" ] && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/JA.xml" "$LOCAL_CSV" )" ]; then ok "(J) --scope on an unchanged file under pressure: answered (rc=$RC, unpressured rc=$jA), not refused as naming nothing" -else no "(J) rc=$RC (unpressured $jA) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +else no "(J) rc=$RC (want $( degrc "$WORK/o" ), unpressured $jA) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +# the near miss: a scope naming NO path of the target commit is still refused under pressure (a typo is not a measurement) +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/nosuch.py --no-cache +if [ "$RC" = 1 ] && [ ! -s "$WORK/o" ] && grep -q 'matches no indexed path' "$WORK/e"; then ok "(J) a scope naming no path of the target commit is still refused under pressure (exit 1)" +else no "(J) --scope=py/nosuch.py under pressure: rc=$RC stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi # ── (K) legend and JSON ─────────────────────────────────────────────────────────────────────────────────────── qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache @@ -291,21 +345,24 @@ missing="" for a in kinds_skipped= skipped_why= ingest_scope= memory_need= also_skipped= kinds_next=; do sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- "$a" || missing="$missing $a" done -if [ -z "$missing" ] && python3 -c 'import sys,xml.dom.minidom as m; m.parse(sys.argv[1])' "$WORK/o" 2>/dev/null; then ok "(K) --legend=full defines every new attribute and parses as XML" +if [ -z "$missing" ] && validxml "$WORK/o"; then ok "(K) --legend=full defines every new attribute and parses as XML" else no "(K) legend lacks:$missing (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_stop=' && validxml "$WORK/o"; then ok "(K) --legend=full over a stop defines memory_stop= and parses as XML" +else no "(K) the P0 legend lacks memory_stop= (or invalid XML)"; fi qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache if python3 - "$WORK/o" <<'PY' import json, sys d = json.load(open(sys.argv[1])) assert d["kinds_skipped"] == ["dead-code", "duplication", "new-clone-of-reused-helper"], d.get("kinds_skipped") assert d["skipped_why"] == "memory" and d["ingest_scope"] == "changed-files" -assert d["also_skipped"] == ["config-warnings", "stale-acks"] and "stale" not in d +assert d["also_skipped"] == ["config-warnings", "stale-acks"] and "stale" not in d and "register-macro-excluded" not in d assert d["kinds_next"].startswith("--quality-delta=") and "memory_need" in d and "memory_limit" in d PY then ok "(K) --json carries the twins (kinds_skipped list, skipped_why, ingest_scope, also_skipped, kinds_next, memory_*)" else no "(K) --json twins missing or wrong: $( head -c 300 "$WORK/o" )"; fi qd "$BIN" . --quality-delta=HEAD~1..HEAD --json --no-cache -if [ "$RC" = "$ARC" ] && [ -s "$WORK/o" ] && ! grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"' "$WORK/o"; then +if [ "$RC" = "$ARC" ] && [ -s "$WORK/o" ] && ! grep -q '"kinds_skipped"\|"ingest_scope"\|"kinds_next"\|"also_skipped"\|"memory_' "$WORK/o"; then ok "(K) an unpressured --json carries none of the degraded keys" else no "(K) unpressured --json: rc=$RC (want $ARC), empty, or it carries a degraded-only key"; fi @@ -315,5 +372,170 @@ if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml" && [ "$( grep -v '^ripwi ok "(L) project:2 (past the one projection reading) is byte-identical to no seam (rc=$RC)" else no "(L) project:2 rc=$RC (want $ARC) or the answer differs"; fi +# ══ round 2 — the gates review's R1-R9 ═══════════════════════════════════════════════════════════════════════════ + +# ── (M) R1: a new defect site whose text also sits in an UNCHANGED file ───────────────────────────────────────── +# defect-shape counts a site as new by its REPO-WIDE excess. Pairing a full base with a changed-files target would net the +# new copy against the unchanged one and lose a gating row; both sides' changed files cancel the unchanged copy exactly. +FM="$WORK/fm"; mkfx "$FM" +printf 'def old():\n return "x {} {}".format(1)\n' >"$FM/py/old.py"; g "$FM" add -A; g "$FM" commit -qm old +printf 'def other():\n return 1\n\ndef greet(name):\n return "hello {}".format(name)\n\ndef dup():\n return "x {} {}".format(1)\n' >"$FM/py/mod.py" +g "$FM" add -A; g "$FM" commit -qm dup +qdin "$FM" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/MA.xml"; mrc=$RC +if [ "$mrc" = 2 ] && rows "$WORK/MA.xml" defect-shape | grep -q 'gating="1"'; then + for seam in project:1 crawl:12; do + qdin "$FM" RIPWIRE_TEST_MEMGUARD=$seam "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ -n "$( attr "$WORK/o" kinds_skipped )" ] && [ "$RC" = 2 ] && [ "$( rows "$WORK/o" defect-shape )" = "$( rows "$WORK/MA.xml" defect-shape )" ]; then + ok "(M) $seam: the new site's gating defect-shape row survives beside an unchanged copy of its text (rc=2)" + else no "(M) $seam: rc=$RC (want 2) kinds_skipped=$( attr "$WORK/o" kinds_skipped ) defect rows: $( rows "$WORK/o" defect-shape | head -c 200 )"; fi + done +else + no "(M) premise: the unpressured answer has no gating defect-shape row (rc=$mrc)" +fi + +# ── (N) R2: paths git diff does not list but an archive or a crawl treats differently ────────────────────────── +refused_by(){ # $1 label, $2 repo, $3 the word the one line must name + qdin "$2" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q "^ripwire: .*$3" "$WORK/e" && grep -q 'changed files' "$WORK/e"; then + ok "(N) $1: the changed files cannot stand in for the trees — refused by name (exit 5, one line naming $3)" + else no "(N) $1: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +} +same_as_full(){ # $1 label, $2 repo: the degraded LOCAL rows equal the unpressured ones + ( cd "$2" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nfA.xml" 2>/dev/null ) + qdin "$2" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache + if [ -n "$( attr "$WORK/o" kinds_skipped )" ] && [ "$RC" = "$( degrc "$WORK/o" )" ] && [ -n "$( rows "$WORK/nfA.xml" "$LOCAL_CSV" )" ] \ + && [ "$( rows "$WORK/o" "$LOCAL_CSV" )" = "$( rows "$WORK/nfA.xml" "$LOCAL_CSV" )" ]; then + ok "(N) $1: degraded (rc=$RC), LOCAL rows equal the unpressured run" + else no "(N) $1: rc=$RC kinds_skipped=$( attr "$WORK/o" kinds_skipped ) diff: $( diff <( rows "$WORK/nfA.xml" "$LOCAL_CSV" ) <( rows "$WORK/o" "$LOCAL_CSV" ) | head -c 300 ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 200 )"; fi +} +# an UNCHANGED symlink to a CHANGED file: the crawl follows it, so the full answer rows the link's path too +FS="$WORK/fs"; mkfx "$FS"; ln -s py/mod.py "$FS/link.py"; g "$FS" add -A; g "$FS" commit -qm link +printf 'def other():\n # TODO: link\n return 1\n' >"$FS/py/mod.py"; g "$FS" add -A; g "$FS" commit -qm todo +refused_by "an unchanged symlink to a changed file" "$FS" "symlink" +# an export-ignore FLIP: the target deletes .gitattributes, so gen/x.py (unchanged) appears in the target archive +FA="$WORK/fa"; mkfx "$FA"; mkdir -p "$FA/gen"; printf 'gen/** export-ignore\n' >"$FA/.gitattributes" +printf 'def gen():\n # TODO: generated\n return 1\n' >"$FA/gen/x.py"; g "$FA" add -A; g "$FA" commit -qm attrs +g "$FA" rm -q .gitattributes; printf 'def other():\n # TODO: flip\n return 1\n' >"$FA/py/mod.py"; g "$FA" add -A; g "$FA" commit -qm flip +refused_by "an export-ignore flip (.gitattributes deleted)" "$FA" ".gitattributes" +# export-subst rewrites archived bytes of files git calls unchanged +FU="$WORK/fu"; mkfx "$FU"; printf 'py/*.py export-subst\n' >"$FU/.gitattributes"; g "$FU" add -A; g "$FU" commit -qm subst +printf 'def other():\n # TODO: subst\n return 1\n' >"$FU/py/mod.py"; g "$FU" add -A; g "$FU" commit -qm todo +refused_by "an export-subst attribute" "$FU" "export-subst" +# the near miss: an UNCHANGED export-ignore attribute excludes the same paths from both sides — no refusal +FI="$WORK/fi"; mkfx "$FI"; mkdir -p "$FI/gen"; printf 'gen/** export-ignore\n' >"$FI/.gitattributes" +printf 'def gen():\n # TODO: generated\n return 1\n' >"$FI/gen/x.py"; g "$FI" add -A; g "$FI" commit -qm attrs +printf 'def gen():\n # TODO: generated\n # TODO: again\n return 1\n' >"$FI/gen/x.py" +printf 'def other():\n # TODO: kept\n return 1\n' >"$FI/py/mod.py"; g "$FI" add -A; g "$FI" commit -qm todo +same_as_full "an unchanged export-ignore attribute (a changed export-ignored file)" "$FI" +# a directory the crawl prunes because it holds CMakeCache.txt (unchanged), with a changed file inside it +FC="$WORK/fc"; mkfx "$FC"; mkdir -p "$FC/bld"; printf 'CMAKE_BUILD_TYPE:STRING=Debug\n' >"$FC/bld/CMakeCache.txt" +printf 'def built():\n return 1\n' >"$FC/bld/x.py"; g "$FC" add -A; g "$FC" commit -qm bld +printf 'def built():\n # TODO: pruned\n return 1\n' >"$FC/bld/x.py" +printf 'def other():\n # TODO: seen\n return 1\n' >"$FC/py/mod.py"; g "$FC" add -A; g "$FC" commit -qm todo +same_as_full "a changed file in a CMakeCache.txt-pruned directory" "$FC" +if grep -q 'bld/x.py' "$WORK/nfA.xml" || grep -q 'bld/x.py' "$WORK/o"; then no "(N) CMakeCache premise/result: a row names the pruned bld/x.py" +else ok "(N) CMakeCache: neither answer rows the pruned bld/x.py"; fi + +# ── (O) R3: ack identity healing reads tree-wide uniqueness, which the changed files cannot see ───────────────── +# Legacy-scheme ack key of a scope-less name = fnv1a64( bare name ) (quality.h addSchemeAliases: canonicalId degrades to the +# bare name). The full run re-keys it only when ONE file defines the name; two definitions poison it. +mkacks(){ printf '# ripwire quality acks v1\nack placeholder %s 5 legacy-scheme row\n' "$( fnv dupname )" >"$1/.ripwire_quality_acks"; } +FO="$WORK/fo"; mkfx "$FO"; printf 'def dupname():\n return 1\n' >"$FO/py/a.py"; printf 'def dupname():\n return 2\n' >"$FO/py/b.py" +g "$FO" add -A; g "$FO" commit -qm two; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO/py/a.py"; g "$FO" add -A; g "$FO" commit -qm todo; mkacks "$FO" +qdin "$FO" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/OA.xml" +qdin "$FO" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ -n "$( rows "$WORK/OA.xml" placeholder )" ] && [ "$( rows "$WORK/o" placeholder )" = "$( rows "$WORK/OA.xml" placeholder )" ] \ + && [ "$( attr "$WORK/o" acked )" = "$( attr "$WORK/OA.xml" acked )" ] && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac; then + ok "(O) a legacy ack on a name two files define (one changed): the same placeholder rows and acked= as the full run; ack-healing named as skipped" +else no "(O) two definitions: rows full=$( rows "$WORK/OA.xml" placeholder | grep -c . ) degraded=$( rows "$WORK/o" placeholder | grep -c . ) acked $( attr "$WORK/OA.xml" acked )/$( attr "$WORK/o" acked ) also_skipped=$( attr "$WORK/o" also_skipped )"; fi +FO1="$WORK/fo1"; mkfx "$FO1"; printf 'def dupname():\n return 1\n' >"$FO1/py/a.py" +g "$FO1" add -A; g "$FO1" commit -qm one; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO1/py/a.py"; g "$FO1" add -A; g "$FO1" commit -qm todo; mkacks "$FO1" +qdin "$FO1" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/O1A.xml" +qdin "$FO1" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$( attr "$WORK/O1A.xml" acked )" = 1 ] && [ -z "$( rows "$WORK/O1A.xml" placeholder )" ] && [ -n "$( rows "$WORK/o" placeholder )" ] \ + && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac && [ "$RC" = "$( degrc "$WORK/o" )" ]; then + ok "(O) a legacy ack on a name one file defines: the full run re-keys it; the degraded run names ack-healing as skipped and shows the row (rc=$RC)" +else no "(O) one definition: full acked=$( attr "$WORK/O1A.xml" acked ) rows full=$( rows "$WORK/O1A.xml" placeholder | grep -c . ) degraded=$( rows "$WORK/o" placeholder | grep -c . ) also_skipped=$( attr "$WORK/o" also_skipped ) rc=$RC"; fi +# no ledger: nothing to heal, so ack-healing is never named +if [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ]; then ok "(O) with no ledger, ack-healing is not named (nothing to heal)" +else no "(O) no ledger: also_skipped=$( attr "$WORK/B.xml" also_skipped )"; fi + +# ── (P) R4: a changed-files ingest never reads or writes an ingest cache ─────────────────────────────────────── +# The target is HEAD, whose full ingest uses the HEAD ingest-cache family. Run with the cache ON. +PT="$WORK/ptmp"; mkdir -p "$PT" +blobsum(){ find "$PT" -type f -name '*.bin' -exec cksum {} + 2>/dev/null | sort; } +( cd "$FX" && TMPDIR="$PT" "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P1.xml" 2>/dev/null ); p1=$? +s1="$( blobsum )" +( cd "$FX" && TMPDIR="$PT" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P2.xml" 2>/dev/null ); p2=$? +s2="$( blobsum )" +( cd "$FX" && TMPDIR="$PT" "$BIN" . --quality-delta=HEAD~1..HEAD >"$WORK/P3.xml" 2>/dev/null ); p3=$? +if [ -n "$s1" ] && [ "$p1" = "$ARC" ] && cmp -s "$WORK/P1.xml" "$WORK/A.xml"; then + if [ -n "$( attr "$WORK/P2.xml" kinds_skipped )" ] && [ "$p2" = "$( degrc "$WORK/P2.xml" )" ] && [ "$s1" = "$s2" ] && [ "$p3" = "$ARC" ] && cmp -s "$WORK/P3.xml" "$WORK/A.xml"; then + ok "(P) P1 with the cache on: degraded (rc=$p2), every cache blob unchanged, and the next cached run is byte-identical to (A)" + else no "(P) p2=$p2 degraded=$( attr "$WORK/P2.xml" ingest_scope ) blobs_unchanged=$( [ "$s1" = "$s2" ] && echo yes || echo no ) p3=$p3 identical=$( cmp -s "$WORK/P3.xml" "$WORK/A.xml" && echo yes || echo no )"; fi +else + no "(P) premise: the cached unpressured run wrote no blob or differs from (A) (rc=$p1, blobs: $( echo "$s1" | grep -c . ))" +fi + +# ── (Q) R5: the target phase stops after the projection said it fits ─────────────────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=ingest:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/Q.xml" +if [ "$( attr "$WORK/Q.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/Q.xml" memory_stop )" = crawl ] && [ "$( attr "$WORK/Q.xml" memory_need )" = unmeasured ] \ + && [ "$( ingests )" = 4 ] && [ "$RC" = "$( degrc "$WORK/Q.xml" )" ] && [ "$( rows "$WORK/Q.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/A.xml" "$LOCAL_CSV" )" ]; then + ok "(Q) a stop in the target's full ingest falls to the changed files (four ingests, rc=$RC), LOCAL rows equal (A); defect-shape kept (the base's macro set was read)" +else no "(Q) rc=$RC kinds_skipped=$( attr "$WORK/Q.xml" kinds_skipped ) stop=$( attr "$WORK/Q.xml" memory_stop ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (R) R7: a peak over the cap is said; a replay with nothing it can run refuses ────────────────────────────── +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +pk="$( attr "$WORK/o" memory_peak )" +if [ "$RC" = "$ARC" ] && isnum "${pk%M}" && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" )" ] && ! hasattr "$WORK/o" ingest_scope \ + && [ "$( grep -c '^ripwire: memory peak ' "$WORK/e" )" = 1 ] && ! hasattr "$WORK/A.xml" memory_peak; then + ok "(R) a full answer whose peak went over the limit says so (memory_peak=\"$pk\" + one stderr line); rows and rc unchanged" +else no "(R) peak:1 rc=$RC (want $ARC) memory_peak=$pk stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=' && validxml "$WORK/o"; then ok "(R) --legend=full defines memory_peak= when it rides, and parses as XML" +else no "(R) the legend lacks memory_peak= (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$WHOLE_KINDS" --no-cache +if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'whole-tree' "$WORK/e"; then + ok "(R) a whole-tree-only selection under pressure refuses (exit 5, one line naming why) — never an empty pass" +else no "(R) whole-tree-only replay under pressure: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + +# ── (S) R9: stale acks under --quality-kinds are checked for the selected kinds only ─────────────────────────── +FSS="$WORK/fss"; cp -R "$FX" "$FSS" +( cd "$FSS" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --ack-only=dead-code --no-cache >/dev/null 2>&1 ) +if grep -q '^ack dead-code' "$FSS/.ripwire_quality_acks" 2>/dev/null; then + qdin "$FSS" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --show-stale --no-cache + if { [ "$RC" = 0 ] || [ "$RC" = 2 ]; } && [ -s "$WORK/o" ] && [ -n "$( rows "$WORK/o" complexity )" ] && ! grep -q ']*kind="dead-code' "$WORK/o"; then + ok "(S) --quality-kinds=complexity: a dead-code ack is not reported finding-gone (stale acks checked for the selected kinds only; rc=$RC)" + else no "(S) rc=$RC sa rows: $( grep -o ']*>' "$WORK/o" | head -c 300 )"; fi +else + no "(S) premise: --quality-ack --ack-only=dead-code wrote no dead-code ack" +fi + +# ── (T) S2: a limit near (above) the measured peak answers in full ───────────────────────────────────────────── +peak_mb="" +if /usr/bin/time -l true >/dev/null 2>&1; then + ( cd "$FX" && TMPDIR="$WORK/tmp" /usr/bin/time -l "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >/dev/null 2>"$WORK/t.err" ) + peak_mb="$( awk '/peak memory footprint/ { printf "%d", $1 / 1048576 + 1 }' "$WORK/t.err" )" + [ -n "$peak_mb" ] || peak_mb="$( awk '/maximum resident set size/ { printf "%d", $1 / 1048576 + 1 }' "$WORK/t.err" )" +elif /usr/bin/time -v true >/dev/null 2>&1; then + ( cd "$FX" && TMPDIR="$WORK/tmp" /usr/bin/time -v "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >/dev/null 2>"$WORK/t.err" ) + peak_mb="$( awk -F: '/Maximum resident set size/ { printf "%d", $2 / 1024 + 1 }' "$WORK/t.err" )" +fi +if isnum "$peak_mb"; then + lim=$(( peak_mb * 2 )); [ "$lim" -lt 64 ] && lim=64 + qd "$BIN" . --quality-delta=HEAD~1..HEAD "--max-memory=${lim}M" --no-cache + if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(T) --max-memory=${lim}M (2x the measured ${peak_mb}M peak): answered in full, byte-identical to (A)" + else no "(T) --max-memory=${lim}M rc=$RC scope=$( attr "$WORK/o" ingest_scope ) — the projection degraded a run that fits"; fi +else + printf ' SKIP (T) no /usr/bin/time that reports a peak (-l or -v) — the near-the-line arm needs a measured peak\n' +fi + +# ── (V) R8: a degraded answer with rows and no gating row exits 6 ────────────────────────────────────────────── +FV="$WORK/fv"; mkfx "$FV"; printf 'def other():\n # TODO: later\n return 1\n' >"$FV/py/mod.py"; g "$FV" add -A; g "$FV" commit -qm todo +qdin "$FV" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +if [ "$RC" = 6 ] && [ "$( attr "$WORK/o" gating )" = 0 ] && [ -n "$( rows "$WORK/o" placeholder )" ] && [ -n "$( attr "$WORK/o" kinds_skipped )" ]; then + ok "(V) a degraded answer with a placeholder row and gating=0 exits 6 (partial, nothing gating), stdout non-empty" +else no "(V) rc=$RC gating=$( attr "$WORK/o" gating ) rows=$( rows "$WORK/o" | grep -c . )"; fi + [ "$fail" = 0 ] && echo "qddegradecheck: ALL PASS" || echo "qddegradecheck: FAIL" exit "$fail" From e3004201ac1d235230373cb89d46962551bbf3fe Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 18:53:38 -0400 Subject: [PATCH 051/162] regen: LIMITS.md, TUNING.md and the README cap count for kDefTableCap docs/limits_build.py and bench/capsweep/capsweep.py emit after src/defattrib.h's new definition-table cap (254 compile-time caps). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/LIMITS.md | 18 +++++++++++++----- docs/TUNING.md | 18 +++++++++--------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index bf380fff8..7bfedf631 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 253 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 254 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index b83ac804a..241531b58 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 253 | 98 | 149 | **104** | +| 254 | 99 | 149 | **105** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 253 + 7 is the 260 constants this generator parses out of `src/`. +are not counted as caps, and 254 + 7 is the 261 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 253 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 114 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 254 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 115 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 98 files that declare a cap — the 253 caps counted above, and no parameter. +One table for each of the 99 files that declare a cap — the 254 caps counted above, and no parameter. ### `src/abicheck.h` @@ -204,6 +204,14 @@ Discloses: `capped`, `reads_capped` | `kMaxEnvNameLen` | `128` | BOUNDARY | longest plausible environment-variable name | | `kMaxSitesShown` | `8` | OUTPUT | sites per gate; a DEFAULT, raisable by --limit=N (effectiveRowCap), lifted by --detail | +### `src/defattrib.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kDefTableCap` | `16` | — | — | + ### `src/didyoumean.h` Discloses: **none** diff --git a/docs/TUNING.md b/docs/TUNING.md index 8836074e1..85bec1644 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 161 | 160 | 112 | 12 | **37** | 75 | +| 162 | 161 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **161 cap -declarations** under **160 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **162 cap +declarations** under **161 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 160 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **37 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 161 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **38 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, -`kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+31 more). 111 + 12 + 37 = 160, and `emit` refuses to render a partition that does not add up. The +`kChurnMergeBombMaxFiles`, `kDefTableCap`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, +32 +more). 111 + 12 + 38 = 161, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 161" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 162" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 161-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 162-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. From 22b7be4268aa6fa4c5435b0d5ce833f26f953e8c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 19:02:13 -0400 Subject: [PATCH 052/162] test(fornotesbudgetcheck): notes under a soft target (rule 4) The est_tokens <= N / over_ceiling arms asserted the hard budget. They now assert: the default answer serves the whole head with every note and discloses past_target (DH); at 1200/1640/3000 and across the 700..2000 sweep the rows with their notes fit T or the rank 1..4 floor and are maximal (ST), carry their notes, never over_ceiling (NOLBL), are a rank prefix monotone in N (RP) and agree across dialects (CF); a notes tree serves fewer rows than the same tree without notes. Green on HEAD; the new-behaviour arms red on BASE, the twins green on both. Co-Authored-By: Claude Opus 5.5 --- test/fornotesbudgetcheck.sh | 334 +++++++++++++++++++++++++++++------- 1 file changed, 269 insertions(+), 65 deletions(-) diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index f4f5c8e31..b91096f21 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -10,6 +10,33 @@ # Exits non-zero on any failure; prints PASS/FAIL per check and ALL PASS on success. # DO NOT edit regression.sh — this is a standalone gate invoked from there. # +# ── BY-DESIGN BREAK — forbudget-soft (rule 4); owner decisions 2026-10-14. READ THIS FIRST. ────────────────────────────── +# An explicit --token-budget=N on --for is now a SOFT TARGET, not a byte ceiling over the whole document: T = N x 2.36 x 0.90 +# bytes (serialize.h kMinBytesPerToken x kBudgetHeadroom) is charged against the bytes INSIDE only — the rows, each +# with its children (the JSON twin: the emitted "sigs" array, jsonSigEntryCost's exact model, notes included). Rows +# are added in rank order while they fit T, the rank 1..4 floor always ships, the cut carries next=; the header and every +# disclosure are never charged, so est_tokens MAY exceed N and nothing is labelled over_ceiling= for it. The DEFAULT --for +# (no budget) stops when complete: the ranked head ships whole, rows past the 7.5 KB target disclosed as past_target=. +# The SUBJECT of this gate survives the move intact — W3-N2: note bytes are CHARGED to the budget like the rows they ride, +# never trimmed — only the thing they are charged against moved from the whole document to the rows. So the arms +# that asserted the deleted contract ("est_tokens <= N in both dialects" at 1200/1640/3000; the 700..2000 sweep's +# "est_tokens <= N OR over_ceiling=1"; the no-notes tree's "est_tokens <= 900" / "over 900 and labelled") are REPLACED by +# the invariants that pin W3-N2 under the soft target, on the same fixture and the same rungs: +# ST the rows' bytes inside , notes included, fit T — or only the rank 1..4 floor ships — and a cut is MAXIMAL +# (the next row of the same dialect's no-budget answer would cross T; 160 B slack, as forsoftbudgetcheck's (ST)). +# A note that were NOT charged would let a cut run past T by its note bytes: that is the W3-N2 leak, measured here. +# NOTES every served row carries its note — charged, never trimmed — and a notes tree serves NO MORE rows than the same +# tree without notes at the same N (and strictly fewer somewhere: the charge is real, not vacuous). +# NOLBL no over_ceiling= on a budget_tokens answer (a soft target is never "over"). +# RP rows(N) are a rank prefix of the no-budget answer's rows, and monotone in N (rows(N-10) a prefix of rows(N)). +# NX every cut names its continuation ( / "sigs_next"). +# CF the compact and full legends serve the same rows. +# DH the no-budget answer on the note-heavy tree serves the whole ranked head, every row with its note, unlabelled. +# Unchanged and kept: notes are never trimmed (arm 3), the dialects agree on what a budget buys (arm 2), the stated budget +# is echoed as budget_tokens on every rung (sweep), est_tokens is always served, L3 inertness, determinism, G4. This gate +# never had a --max-tokens arm (that knob shapes the map, not --for). The CEILING MARGIN history below describes the RETIRED +# hard-ceiling regime; it is kept as the record of why the rungs sit where they do. +# # What the audit found: `used +=` in packSignatures added the doc and the signature and nothing else, # while appendNoteChildren streamed children straight into the writer for free. On a note-heavy # tree the XML lens therefore ran far past the ceiling its own header reports — measured est_tokens 1279 @@ -222,34 +249,165 @@ done TASK="widget dispatcher pipeline stage" -xmlEst(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" 2>/dev/null | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9'; } -xmlRows(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" 2>/dev/null | grep -o '/dev/null \ - | python3 -c 'import sys,json; print(json.load(sys.stdin)["est_tokens"])' 2>/dev/null; } -jsonRows(){ "$BIN" "$CORPUS" --for="$TASK" --token-budget="$1" --json 2>/dev/null \ - | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d["sigs"]))' 2>/dev/null; } # P7: flat sigs array +# ── forbudget-soft (rule 4): the soft-target reader every replaced arm below uses ───────────────────────────────────────── +# soft FILE N ROWSOUT [REF [PREV]] prints ONE line, "OK|BAD rows= total= charged= T= est= over= echo= next= notes= fits= maximal= rp=": +# rows / total the served rows / the ranked head ( | "sigs_total") +# charged what the soft target is charged against. XML: the bytes inside — every WITH its children +# — plus the zero cx=/ccx=/in= the lean spelling elides (serialize.h entryCost charges them). JSON: the +# emitted "sigs" array, notes included (jsonSigEntryCost is that array's exact byte model). +# T N x 2.36 x 0.90 (kMinBytesPerToken x kBudgetHeadroom) +# est / over / echo est_tokens ("-" when absent) / over_ceiling= on the ROOT / budget_tokens == N on the root +# next a cut (rows < total) names its continuation ( | "sigs_next"); 1 when nothing was cut +# notes served rows that carry a note +# fits charged <= T, or only the rank 1..4 floor is served — and the floor is never short (rows >= min(4,total)) +# maximal with REF (the SAME dialect's no-budget answer): a cut stops only where REF's next row would cross T +# (160 B slack, as forsoftbudgetcheck's (ST)) — a binary that stops early at a hard ceiling is not ST +# rp RP: the rows are a non-empty rank prefix of REF's rows, and the rows file PREV (when named: the next +# narrower rung's ROWSOUT) is a prefix of these rows — monotone in N. A named PREV that is missing is a 0. +# (One python3 per answer, the fields read with bash's own regex: this machine class pays ~0.1 s per process spawn, and the +# sweep below reads 393 answers — the reader does in-process what a pipe of head/wc/sed would spawn for.) +# The verdict is OK when est is numeric and fits, maximal and next all hold. ROWSOUT gets the rows, p:l:n in rank (r=) order. +# exit 3 = the answer has no block / "sigs" array: a missing premise, FAILed by the caller, never skipped. +cat > "$TMP/soft.py" <<'PY' +import json, re, sys +def read( path ): + s = open( path, encoding='utf-8', errors='replace' ).read() + rows = [] + if s.lstrip().startswith( '{' ): + try: d = json.loads( s ) + except Exception: return None + i = s.find( '"sigs":[' ) + if i < 0 or not isinstance( d.get( 'sigs' ), list ): return None + _, end = json.JSONDecoder().raw_decode( s, i + 7 ) + charged = len( s[ i + 7:end ].encode() ) + for e in d[ 'sigs' ]: + r = e.get( 'r' ) + cost = len( json.dumps( e, separators=( ',', ':' ), ensure_ascii=False ).encode() ) + 1 + rows.append( ( r if isinstance( r, int ) else 10**9, "%s:%s:%s" % ( e.get( 'p' ), e.get( 'l' ), e.get( 'n' ) ), cost, + 1 if e.get( 'notes' ) else 0 ) ) + total = d.get( 'sigs_total', len( rows ) ) + nxt = bool( d.get( 'sigs_next' ) ) + over = d.get( 'over_ceiling' ) is True + est = d.get( 'est_tokens' ); est = str( est ) if isinstance( est, int ) else '-' + bt = d.get( 'budget_tokens' ) + else: + root = re.search( r']*>', s ) + m = re.search( r']*)>(.*?)', s, re.S ) + if not root or not m: return None + inner = m.group( 2 ); charged = len( inner.encode() ) + for x in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = x.group( 1 ) or x.group( 2 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + elided = sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) + charged += elided + r = g( 'r' ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( 'p' ) + ":" + g( 'l' ) + ":" + g( 'n' ), len( x.group( 0 ).encode() ) + elided, + 1 if '= min( 4, a[ 'total' ] ) +cut = k < a[ 'total' ] +nxt = a[ 'next' ] or not cut +maximal = True +if len( sys.argv ) > 4: + ref = read( sys.argv[ 4 ] ) + if ref is None: sys.exit( 3 ) + if cut and k < len( ref[ 'rows' ] ): + maximal = a[ 'charged' ] + ref[ 'rows' ][ k ][ 2 ] > X - 160 +mine = [ x[ 1 ] for x in a[ 'rows' ] ] +rp = len( sys.argv ) > 4 and k > 0 and mine == [ x[ 1 ] for x in ref[ 'rows' ] ][ :k ] +if rp and len( sys.argv ) > 5 and sys.argv[ 5 ]: + try: prev = [ x for x in open( sys.argv[ 5 ] ).read().split( "\n" ) if x ] + except OSError: prev = None + rp = prev is not None and len( prev ) > 0 and prev == mine[ :len( prev ) ] +ok = a[ 'est' ] != '-' and fits and maximal and nxt +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d echo=%d next=%d notes=%d fits=%d maximal=%d rp=%d" + % ( "OK" if ok else "BAD", k, a[ 'total' ], a[ 'charged' ], X, a[ 'est' ], int( a[ 'over' ] ), int( a[ 'bt' ] == N ), + int( nxt ), sum( x[ 3 ] for x in a[ 'rows' ] ), int( fits ), int( maximal ), int( rp ) ) ) +PY +soft(){ python3 "$TMP/soft.py" "$@"; } +field(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # field LINE KEY: one key= of a soft line +isnum(){ case "${1:-}" in ''|*[!0-9]*) return 1;; esac; return 0; } + +# The no-budget answers, one per dialect the arms below read: the REF each cut's maximality and rank prefix are measured +# against, and the DH arm's subject. +"$BIN" "$CORPUS" --for="$TASK" > "$TMP/def.xml" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --legend=compact > "$TMP/def.compact" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --json > "$TMP/def.json" 2>/dev/null +for dialect in xml compact json; do + dl="$( soft "$TMP/def.$dialect" 0 "$TMP/def_$dialect.rows" )" || { no "no-budget answer ($dialect) has no block — every soft arm below lacks its reference"; continue; } + dr="$( field "$dl" rows )" + isnum "$dr" && [ "$dr" -ge 5 ] || no "no-budget answer ($dialect) serves '$dr' rows — the reference cannot measure a cut past the floor" +done + +# ── arm DH (NEW, forbudget-soft (rule 4)): the default answer on a note-heavy tree serves the WHOLE head, every note intact ── +# The default --for stops when complete: kForPayloadBudgetBytes (7,500 B) is a TARGET whose row drop only runs past the +# 32,000 B runaway guard. This tree's 40-row head with a ~130 B note on every row is ~13 KB of : notes are charged +# against the target (the rows past it are DISCLOSED as past_target=) and never cost a row, never get trimmed, never +# label the answer over_ceiling=. HEAD d099ec21: 40 of 40 rows, 40 notes, past_target=20, both dialects. RED on the base +# binary (d4325a8c): 20 of 40 rows, no past_target= (the 7.5 KB ceiling's row drop). +for dialect in xml json; do + dl="$( soft "$TMP/def.$dialect" 0 /dev/null )" || { no "(DH) default ($dialect): no block"; continue; } + dr="$( field "$dl" rows )"; dt="$( field "$dl" total )"; dn="$( field "$dl" notes )"; dov="$( field "$dl" over )" + case "$dialect" in json) pt="$( python3 -c 'import sys,json; print(json.load(open(sys.argv[1])).get("sigs_past_target",""))' "$TMP/def.json" 2>/dev/null )" ;; + *) pt="$( grep -o ']*>' "$TMP/def.xml" | head -1 | sed -n 's/.*past_target="\([0-9]*\)".*/\1/p' )" ;; esac + if isnum "$dr" && isnum "$dt" && isnum "$dn" && [ "$dr" -eq "$dt" ] && [ "$dn" -eq "$dr" ] && [ "$dov" = 0 ] && isnum "$pt" && [ "$pt" -ge 1 ]; then + ok "(DH) default ($dialect): the whole head ships ($dr of $dt rows, $dn notes), $pt row(s) disclosed past_target, no over_ceiling" + else + no "(DH) default ($dialect): rows=$dr total=$dt notes=$dn over=$dov past_target='$pt' — the default must ship the whole head with every note, past the target disclosed, unlabelled" + fi +done -# ── arm 1: est_tokens must fit the ceiling the user asked for, in BOTH dialects ──────────────────── +# ── arm 1: notes are CHARGED against the budget, in BOTH dialects ───────────────────────────────── +# REPLACED — forbudget-soft (rule 4). Was: "est_tokens <= --token-budget" in XML and JSON at 1200/1640/3000 (the whole +# document fit the ceiling). Under the soft target est_tokens may exceed N by the never-charged header (HEAD d099ec21: XML +# 1564 / 1940 / 3085, JSON 1232 at 1200), so that bar asserts the deleted contract. What W3-N2 means now, asserted here at +# the same three rungs in both dialects: ST (the rows — each WITH its note — fit T = N x 2.36 x 0.90 or are the floor, and +# the cut is maximal), NOTES (every served row carries its note), NOLBL (no over_ceiling=), est_tokens still served. +# MEASURED (HEAD d099ec21, this fixture in a scratch dir), rows / charged bytes of T: 1200 -> 7 / 2338 of 2548 (JSON 7 / +# 2371), 1640 -> 10 / 3322 of 3483 (JSON 10 / 3388), 3000 -> 19 / 6280 of 6372 (JSON 18 / 6106); a note on every row. +# RED on the base binary (d4325a8c): 1200 -> XML 4 rows (1355 B), JSON 6 (2033 B); 1640 -> XML 7; 3000 -> 16 / 17 — every +# one a cut a whole row short of T (not maximal: the hard ceiling reserved the header the soft target no longer charges). # (tight budget 1100, re-anchored 2026-09-11 — see the CEILING MARGIN block above for the arithmetic) # RE-ANCHORED 2026-10-10 (train 26c): the tight rung 1100 -> 1200. lego-charge charges / at their served size # and #362's rides the bundle, so at 1100 this corpus's XML answer is the 4-row floor with its next= unpaid # (est_tokens 1213, over_ceiling="1" + the unpaid clause: disclosed, not silent — the knob-honesty contract, not the leak # this arm pins). Swept on the merged binary: 1150 / 1200 serve 4 rows at est_tokens=1121 (JSON 1098), 1250..1350 five rows # at 1246; 1200 sits mid-plateau with 79 tokens of headroom, one step clear of each edge. 1640 and 3000 unchanged. -for tb in 1200 1640 3000; do - xe="$( xmlEst "$tb" )"; je="$( jsonEst "$tb" )" - if [ -z "$xe" ] || [ -z "$je" ]; then no "budget=$tb: could not read est_tokens from one of the dialects (xml='$xe' json='$je')"; continue; fi - if [ "$xe" -le "$tb" ]; then ok "budget=$tb: XML est_tokens=$xe fits the ceiling" - else no "budget=$tb: XML est_tokens=$xe blows the ceiling by $(( 100 * ( xe - tb ) / tb ))% — note bytes are not charged"; fi - if [ "$je" -le "$tb" ]; then ok "budget=$tb: JSON est_tokens=$je fits the ceiling" - else no "budget=$tb: JSON est_tokens=$je blows the ceiling — the reference side regressed"; fi +for tb in 1200 1640 3000 900; do + "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/n_$tb.xml" 2>/dev/null + "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/n_$tb.json" 2>/dev/null + [ "$tb" = 900 ] && continue # 900 is read by arm 4's notes-vs-no-notes comparison (and by its CF twin), not asserted here + for dialect in xml json; do + sl="$( soft "$TMP/n_$tb.$dialect" "$tb" "$TMP/n_${tb}_$dialect.rows" "$TMP/def.$dialect" )" \ + || { no "budget=$tb ($dialect): no block in the answer or its reference — the charge cannot be measured"; continue; } + sr="$( field "$sl" rows )"; sn="$( field "$sl" notes )" + case "$sl" in OK*) ok "(ST) budget=$tb ($dialect): the rows WITH their notes fit the target — $sl" ;; + *) no "(ST) budget=$tb ($dialect): $sl — note bytes ride the rows and must be charged with them: fit T (or the floor), and stop only where the next row would cross it" ;; esac + if isnum "$sr" && isnum "$sn" && [ "$sr" -ge 1 ] && [ "$sn" -eq "$sr" ]; then ok "(NOTES) budget=$tb ($dialect): every served row carries its note ($sn of $sr) — charged, not dropped to make room" + else no "(NOTES) budget=$tb ($dialect): $sn note(s) on $sr row(s) — a note was dropped instead of charged"; fi + case "$sl" in *" over=0 "*) ok "(NOLBL) budget=$tb ($dialect): no over_ceiling= on a soft target" ;; + *) no "(NOLBL) budget=$tb ($dialect): over_ceiling= on a budget_tokens answer — a soft target is never 'over' ($sl)" ;; esac + done done # ── arm 2: the two dialects select COMPARABLE row counts (they need not be equal) ────────────────── # Before the fix the XML lens bought 2-2.4x the rows with the same budget, because notes were free. +# (forbudget-soft: the rows are read off arm 1's answers — the same argv, so one run each instead of two.) for tb in 1200 1640 3000; do - xr="$( xmlRows "$tb" )"; jr="$( jsonRows "$tb" )" - if [ -z "$jr" ] || [ "$jr" -eq 0 ]; then no "budget=$tb: JSON selected no rows — the comparison has no denominator"; continue; fi + xr="$( grep -c . "$TMP/n_${tb}_xml.rows" 2>/dev/null )"; jr="$( grep -c . "$TMP/n_${tb}_json.rows" 2>/dev/null )" + if ! isnum "$xr" || ! isnum "$jr" || [ "$jr" -eq 0 ]; then no "budget=$tb: JSON selected no rows (xml='$xr' json='$jr') — the comparison has no denominator"; continue; fi if [ "$xr" -le $(( jr * 13 / 10 + 1 )) ] && [ "$xr" -ge $(( jr * 7 / 10 )) ]; then ok "budget=$tb: XML $xr rows vs JSON $jr rows — the two dialects agree on what fits" else @@ -268,7 +426,18 @@ fullText="$( printf '%s' "$OUT800" | grep -c "stage table downstream" )" if [ "$fullText" -ge 1 ]; then ok "a surviving note carries its FULL text (no note-level truncation)" else no "a surviving note lost its tail — notes must be charged, never trimmed"; fi -# ── arm 6: an explicit ceiling is NEVER silently overshot — every dialect, every rung in the band ── +# ── arm 6: every rung of the band, every dialect: notes charged against a SOFT target — swept, not sampled ── +# REPLACED — forbudget-soft (rule 4). Was, per rung and dialect: "est_tokens <= --token-budget OR over_ceiling=1 on the +# root" (HEAD d099ec21: 322 of 393 runs over and unlabelled — by design: est_tokens may exceed N, and a soft target is never +# labelled over). The per-rung invariant now, over the SAME band (700..2000), the SAME step (10) and the SAME three dialects +# (393 runs): ST (the rows, notes included, fit T or are the floor; a cut is maximal against that dialect's no-budget answer; +# est_tokens served; the cut names its next=) + NOLBL (no over_ceiling=) + RP (rows a rank prefix of the no-budget rows, and +# rows(N-10) a prefix of rows(N): monotone) + CF (the default and --legend=compact XML dialects serve the same rows). The +# budget_tokens ECHO half of the arm is unchanged. The history below records the retired disjunction and why the band +# starts at 700; the band, the step and the dialects are kept as they were (the density is the arm's value, CHECKLIST 14). +# MEASURED: HEAD d099ec21 green on all 393 runs (every rung a cut; the 40-row head is never complete below 2000). RED on base +# (d4325a8c): 304 runs not maximal (the hard ceiling's header reserve), 87 labelled over_ceiling= (first: 700, xml); RP, CF +# and the echo are green on both. # PLACED HERE, before arm 4, deliberately: this arm needs the NOTE-BEARING corpus (notes are the exempt- # adjacent bytes that make the ladder's rungs tight enough to overshoot at all), and arm 4 deletes # .ripwire_notes to test inertness. Moving it below arm 4 would make it green for the wrong reason. @@ -320,48 +489,55 @@ else no "a surviving note lost its tail — notes must be charged, never trimmed # --legend=compact. This arm is the regression lock on that rule, swept rather than sampled, because the # rung a single sample happens to sit on is exactly what made the defect survive four earlier re-anchors of # the middle rung above: 1550 and 1600 are both inside a FLAT stretch, and the edge is at ~1610. -sweepFail=0; sweepOver=0; sweepFirstRed=""; sweepRungs=0; sweepEchoFail=0; sweepEchoFirst="" +sweepRungs=0; sweepEchoFail=0; sweepEchoFirst="" +stFail=0; stFirst=""; lblFail=0; lblFirst=""; rpFail=0; rpFirst=""; cfFail=0; cfFirst=""; sweepCut=0 tb=700 while [ "$tb" -le 2000 ]; do sweepRungs=$(( sweepRungs + 1 )) for dialect in xml compact json; do case "$dialect" in - xml) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" 2>/dev/null )" ;; - compact) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --legend=compact 2>/dev/null )" ;; - json) out="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json 2>/dev/null )" ;; - esac - case "$dialect" in - json) est="$( printf '%s' "$out" | grep -o '"est_tokens":[0-9]*' | head -1 | tr -dc '0-9' )" - case "$out" in *'"over_ceiling":true'*) lab=1 ;; *) lab=0 ;; esac - case "$out" in *"\"budget_tokens\":$tb"*) echoed=1 ;; *) echoed=0 ;; esac ;; - *) est="$( printf '%s' "$out" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" - case "$out" in *'over_ceiling="1"'*) lab=1 ;; *) lab=0 ;; esac - case "$out" in *"budget_tokens=\"$tb\""*) echoed=1 ;; *) echoed=0 ;; esac ;; + xml) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/sw.$dialect" 2>/dev/null ;; + compact) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --legend=compact > "$TMP/sw.$dialect" 2>/dev/null ;; + json) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/sw.$dialect" 2>/dev/null ;; esac - if [ "$echoed" -eq 0 ]; then - sweepEchoFail=$(( sweepEchoFail + 1 )) - [ -z "$sweepEchoFirst" ] && sweepEchoFirst="budget=$tb ($dialect)" - fi - if [ -z "$est" ]; then - no "sweep budget=$tb ($dialect): no est_tokens in the answer — the price a budget shapes against must always be served" - sweepFail=$(( sweepFail + 1 )) - continue - fi - if [ "$est" -gt "$tb" ]; then - sweepOver=$(( sweepOver + 1 )) - if [ "$lab" -eq 0 ]; then - sweepFail=$(( sweepFail + 1 )) - [ -z "$sweepFirstRed" ] && sweepFirstRed="budget=$tb ($dialect) est_tokens=$est, $(( est - tb )) over, NO over_ceiling" - fi + cur="$TMP/sw_$(( sweepRungs % 2 ))_$dialect.rows"; prv="$TMP/sw_$(( ( sweepRungs + 1 ) % 2 ))_$dialect.rows" + [ "$tb" -eq 700 ] && prv="" + if ! sl="$( soft "$TMP/sw.$dialect" "$tb" "$cur" "$TMP/def.$dialect" "$prv" )"; then + no "sweep budget=$tb ($dialect): no block in the answer or its reference — the price and the rows a budget shapes must always be served" + stFail=$(( stFail + 1 )); continue fi + case "$sl" in *" echo=1 "*) ;; *) + sweepEchoFail=$(( sweepEchoFail + 1 )) + [ -z "$sweepEchoFirst" ] && sweepEchoFirst="budget=$tb ($dialect)" ;; esac + case "$sl" in OK*) ;; *) + stFail=$(( stFail + 1 )) + [ -z "$stFirst" ] && stFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" over=0 "*) ;; *) + lblFail=$(( lblFail + 1 )) + [ -z "$lblFirst" ] && lblFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" rp=1"*) ;; *) + rpFail=$(( rpFail + 1 )) + [ -z "$rpFirst" ] && rpFirst="budget=$tb ($dialect): the rows are not a rank prefix of the no-budget rows, or rows(N=$(( tb - 10 ))) are not a prefix of them — $sl" ;; esac + [ "$( field "$sl" rows )" != "$( field "$sl" total )" ] && sweepCut=$(( sweepCut + 1 )) done + if ! cmp -s "$TMP/sw_$(( sweepRungs % 2 ))_xml.rows" "$TMP/sw_$(( sweepRungs % 2 ))_compact.rows"; then + cfFail=$(( cfFail + 1 )) + [ -z "$cfFirst" ] && cfFirst="budget=$tb: the default and --legend=compact rows differ" + fi tb=$(( tb + 10 )) done -if [ "$sweepFail" -eq 0 ]; then - ok "sweep 700..2000 step 10 x 3 dialects ($(( sweepRungs * 3 )) runs): every overshoot is LABELLED ($sweepOver of $(( sweepRungs * 3 )) rungs ran over the stated ceiling, every one carrying over_ceiling)" +runs=$(( sweepRungs * 3 )) +if [ "$stFail" -eq 0 ]; then + ok "(ST) sweep 700..2000 step 10 x 3 dialects ($runs runs): the rows WITH their notes fit T = N x 2.36 x 0.90 (or are the floor), every cut maximal and naming next= ($sweepCut cut)" else - no "sweep 700..2000 step 10 x 3 dialects: $sweepFail SILENT overshoot(s) — first: $sweepFirstRed" + no "(ST) sweep 700..2000 step 10 x 3 dialects: $stFail run(s) broke stop-at-target — first: $stFirst" fi +if [ "$lblFail" -eq 0 ]; then ok "(NOLBL) sweep 700..2000 step 10 x 3 dialects ($runs runs): no over_ceiling= on any soft target" +else no "(NOLBL) sweep 700..2000 step 10 x 3 dialects: $lblFail run(s) labelled a soft target over_ceiling= — first: $lblFirst"; fi +if [ "$rpFail" -eq 0 ]; then ok "(RP) sweep 700..2000 step 10 x 3 dialects ($runs runs): rows are a rank prefix of the no-budget rows, monotone in N" +else no "(RP) sweep 700..2000 step 10 x 3 dialects: $rpFail run(s) broke the rank prefix — first: $rpFirst"; fi +if [ "$cfFail" -eq 0 ]; then ok "(CF) sweep 700..2000 step 10: the default and --legend=compact dialects serve the same rows on all $sweepRungs rungs" +else no "(CF) sweep 700..2000 step 10: $cfFail rung(s) where the legend changed the rows — first: $cfFirst"; fi if [ "$sweepEchoFail" -eq 0 ]; then ok "sweep 700..2000 step 10 x 3 dialects: every rung ECHOES the ceiling it was shaped against (budget_tokens on the root)" else @@ -375,24 +551,52 @@ rm -f "$CORPUS/.ripwire_notes" # postures fit — 808 / 817 — and 900 holds 865 in the full legend with margin). The notes corpus's 800 runs above are unchanged. bare="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 2>/dev/null )" case "$bare" in *" element";; *) ok "a tree with no notes emits none (L3 inertness)";; esac -# L1 (2026-09-19): the 800-token fit was calibrated in the full legend, the default when it was written, and is asked -# for by name. The DEFAULT (compact) answer on this tree carries more rows and lands over 800 at this one budget; that is -# the --for sig ledger's exemption design (disclosure clauses exempt from the sig charge, recovered by rung zero, which -# the compact dialect's shorter clauses rarely pay for). RE-MEASURED in the L1 fix round with a script, not a count typed -# here: 66 budgets (300..3550 step 50) on the P4-shape fixtures, the default is over on 15 (72 functions) and 15 (12 -# functions), --legend=full on 11 and 12; the earlier "8 of 66 vs 2, 55 before" did not reproduce. What the default MUST do -# there is say so — asserted below. -bareFull="$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 --legend=full 2>/dev/null )" -bareEst="$( printf '%s' "$bareFull" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -n "$bareEst" ] && [ "$bareEst" -le 900 ]; then ok "no-notes tree also fits the ceiling in the full legend (est_tokens=$bareEst)" -else no "no-notes tree reports est_tokens='$bareEst' against a budget of 900 (full legend)"; fi -defEst="$( printf '%s' "$bare" | grep -o 'est_tokens="[0-9]*"' | head -1 | tr -dc '0-9' )" -if [ -z "$defEst" ]; then no "no-notes tree at the default posture carries no est_tokens=" -elif [ "$defEst" -le 900 ]; then ok "no-notes tree at the default posture fits (est_tokens=$defEst)" -else - case "$bare" in *'over_ceiling="1"'*) ok "no-notes tree at the default posture is over 900 (est_tokens=$defEst) and SAYS so (over_ceiling=\"1\")" ;; - *) no "no-notes tree at the default posture is over 900 (est_tokens=$defEst) in SILENCE" ;; esac -fi +# L1 (2026-09-19) — the retired hard-ceiling reading of this arm: the full legend fit 900, the default (compact) answer +# carried more rows and was over 900, which it had to SAY (over_ceiling="1"). +# REPLACED — forbudget-soft (rule 4). Was: "--legend=full est_tokens <= 900" and "default est_tokens <= 900, or over and +# labelled over_ceiling=1" (HEAD d099ec21: 1736 / 1304, unlabelled — by design). Now, on the no-notes tree at 900, both +# postures: ST (the rows fit T, maximal against the no-budget answer, next= on the cut) + NOLBL; and CF — the two legends +# serve the SAME rows (the retired ladder let the compact dialect's shorter clauses buy it more rows than the full legend). +# Then the W3-N2 twin this tree exists for: at every rung arm 1 measured (and 900), in both dialects, the notes tree served +# NO MORE rows than this tree without notes — and strictly fewer on at least one, so the charge is real, not vacuous. +# MEASURED (HEAD d099ec21): no-notes 900 -> 13 rows in both legends (1786 B of T 1911); notes vs no-notes rows: 900 5/13, +# 1200 7/18 (JSON 7/17), 1640 10/25 (10/24), 3000 19/40 (18/40). RED on base (d4325a8c): 7 rows default vs 6 full (CF), each a +# cut short of T (ST). The NOTES twins are green on both binaries — W3-N2 held before the soft target and holds after it. +"$BIN" "$CORPUS" --for="$TASK" --token-budget=900 --legend=full > "$TMP/b_900.full" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" > "$TMP/b_def.xml" 2>/dev/null +"$BIN" "$CORPUS" --for="$TASK" --json > "$TMP/b_def.json" 2>/dev/null +printf '%s' "$bare" > "$TMP/b_900.xml" +for posture in xml full; do + if sl="$( soft "$TMP/b_900.$posture" 900 "$TMP/b_900_$posture.rows" "$TMP/b_def.xml" )"; then + case "$sl" in OK*) ok "(ST) no-notes tree, budget=900 ($posture legend): the rows fit the target — $sl" ;; + *) no "(ST) no-notes tree, budget=900 ($posture legend): $sl" ;; esac + case "$sl" in *" over=0 "*) ok "(NOLBL) no-notes tree, budget=900 ($posture legend): no over_ceiling= on a soft target" ;; + *) no "(NOLBL) no-notes tree, budget=900 ($posture legend): over_ceiling= on a soft target ($sl)" ;; esac + else + no "no-notes tree, budget=900 ($posture legend): no block in the answer or the no-budget reference" + fi +done +if [ -s "$TMP/b_900_xml.rows" ] && cmp -s "$TMP/b_900_xml.rows" "$TMP/b_900_full.rows"; then ok "(CF) no-notes tree, budget=900: the default and full legends serve the same $( grep -c . "$TMP/b_900_xml.rows" ) rows" +else no "(CF) no-notes tree, budget=900: default rows ($( grep -c . "$TMP/b_900_xml.rows" 2>/dev/null )) != full-legend rows ($( grep -c . "$TMP/b_900_full.rows" 2>/dev/null )) — the legend bought rows"; fi +fewer=0 +for tb in 900 1200 1640 3000; do + for dialect in xml json; do + if [ ! -s "$TMP/b_$tb.$dialect" ]; then + case "$dialect" in json) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" --json > "$TMP/b_$tb.$dialect" 2>/dev/null ;; + *) "$BIN" "$CORPUS" --for="$TASK" --token-budget="$tb" > "$TMP/b_$tb.$dialect" 2>/dev/null ;; esac + fi + nl="$( soft "$TMP/n_$tb.$dialect" "$tb" /dev/null )"; bl="$( soft "$TMP/b_$tb.$dialect" "$tb" /dev/null )" + nr="$( field "$nl" rows )"; br="$( field "$bl" rows )"; nn="$( field "$nl" notes )" + if ! isnum "$nr" || ! isnum "$br" || ! isnum "$nn" || [ "$nn" -lt 1 ]; then + no "(NOTES) budget=$tb ($dialect): could not read both row counts with notes on the notes tree (notes tree '$nr' rows / '$nn' notes, no-notes tree '$br')"; continue + fi + if [ "$nr" -le "$br" ]; then ok "(NOTES) budget=$tb ($dialect): the notes tree serves $nr rows, the same tree without notes $br — notes are charged like the rows they ride" + else no "(NOTES) budget=$tb ($dialect): the notes tree serves $nr rows, MORE than the same tree without notes ($br) — note bytes are not charged"; fi + [ "$nr" -lt "$br" ] && fewer=$(( fewer + 1 )) + done +done +if [ "$fewer" -ge 1 ]; then ok "(NOTES) the notes tree served strictly fewer rows on $fewer of 8 rung/dialect pairs — the charge is real" +else no "(NOTES) the notes tree never served fewer rows than the no-notes tree on any of 8 rung/dialect pairs — notes cost nothing (W3-N2)"; fi # ── arm 5: still deterministic and well-formed after the accounting change ───────────────────────── if [ "$( "$BIN" "$CORPUS" --for="$TASK" --token-budget=900 2>/dev/null )" = "$bare" ]; then ok "output is byte-identical run-to-run" From 13297e3a573d1b53b078be8f005178993e470881 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 19:36:27 -0400 Subject: [PATCH 053/162] test(compactlegendcheck): a ";" inside the P4 floor twin's message read as a call to at() manifestcheck's defined-before-use scan split the PASS message on "; at 900" and saw a call to the (P5) section's at() helper ahead of its definition. Reworded; the assertion is unchanged. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index dd0aaf794..98efa4a6b 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -1363,7 +1363,7 @@ for tb in 500 900; do p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: est_tokens unreadable (compact '$pc', full '$pf')" fi done -[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) soft target on the twelve-routine fixture: at 500 both dialects serve the same maximal rank prefix within T, unlabelled, its next= serving the whole head; at 900 the complete head ships uncut; est_tokens honest; compact never further over than full ($p4facts)" +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) soft target on the twelve-routine fixture: at 500 both dialects serve the same maximal rank prefix within T, unlabelled, its next= serving the whole head, and with 900 the complete head ships uncut, est_tokens honest; compact never further over than full ($p4facts)" # (P5) the BUDGET LEDGER survives compaction (orchestrator rule, METHODOLOGY §9.3/§9.4: never cut silently). --pack-task's # full legend ends with "budget=N bytes (T-token target, ceiling C) | ranking: … | bodies: … | callers: … | notes: … | tests: From 73a09f0454c987cb289f1345c1384c281e14c1d1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 19:46:48 -0400 Subject: [PATCH 054/162] =?UTF-8?q?test:=20qddegradecheck=20round=202=20?= =?UTF-8?q?=E2=80=94=20five=20arm=20corrections=20found=20running=20the=20?= =?UTF-8?q?gate=20against=20the=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each is a defect in an arm this lane wrote in 66a9e574, not a relaxed contract: - attr() matched a root attribute by suffix, so `gating` read the LAST `…gating="N"` — scoped-out-gating= on a scoped answer — and (J)'s exact exit code was computed from the wrong count. The name is space-led now. - (B) asked for memory_need >= 64M and for kinds_next= to replay at memory_need itself. R7c's rule is the REPLAY's value: strictly above the projection as spelled and at least the parser's 64M floor; the need is whatever was projected (35M on the fixture). - (G) rename-only compared the header new-symbol= with the full answer's, but that counts the rows of the kinds that ran (the full answer's extra one is a dead-code row). It now equals the full answer's LOCAL rows carrying origin="new-symbol" (S5's intent). - (O) compared stale-ack rows too; stale acks are a whole-answer fact the degraded answer names as skipped. It compares rows. - (Q) expected four cache-stats lines; an ingest the seam cuts at its first crawl entry parses nothing and prints none. It asserts three and that the stderr line names the target commit's stop. Co-Authored-By: Claude Opus 5.5 --- test/qddegradecheck.sh | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index 84c150629..a5f44e31a 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -53,7 +53,7 @@ # on a name one file defines, the degraded answer names ack-healing as skipped and shows the row. # (P) R4 cache isolation: P1 with the cache on leaves the HEAD ingest-cache blob untouched; a cached run after it is # byte-identical to (A). -# (Q) R5 the target-phase stop (ingest:2 cuts the target's full ingest): a degraded answer, four ingests. +# (Q) R5 the target-phase stop (ingest:2 cuts the target's full ingest): a degraded answer naming the target's stop. # (R) R7b a peak over the limit (peak:1) is disclosed on a full answer, rc and rows unchanged; R7c a replay whose # selection holds only whole-tree kinds refuses under pressure (exit 5), never an empty pass. # (S) R9 --quality-kinds=complexity reports no false finding-gone for a dead-code ack. @@ -157,7 +157,8 @@ for m in re.finditer(r'<(r|sa) [^>]*/>', doc): print("\n".join(sorted(out))) PY } -attr(){ sed -n 's/.*]*'"$2"'="\([^"]*\)".*/\1/p' "$1" | head -n 1; } +# the root attribute NAMED $2 — space-led, so gating= never reads scoped-out-gating= (the greedy match takes the last) +attr(){ sed -n 's/.*]* '"$2"'="\([^"]*\)".*/\1/p' "$1" | head -n 1; } hasattr(){ grep -q "]* $2=\"" "$1"; } ingests(){ grep -c '^ripwire: cache-stats ' "$WORK/e"; } memlines(){ grep -c '^ripwire: memory limit ' "$WORK/e"; } @@ -191,14 +192,17 @@ bnext="$( attr "$WORK/B.xml" kinds_next )" if [ -s "$WORK/B.xml" ] && validxml "$WORK/B.xml" \ && [ "$( attr "$WORK/B.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/B.xml" skipped_why )" = memory ] \ && [ "$( attr "$WORK/B.xml" ingest_scope )" = changed-files ] && hasattr "$WORK/B.xml" memory_limit \ - && isnum "$needn" && [ "$needn" -ge 64 ] && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ + && isnum "$needn" && [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ] && ! hasattr "$WORK/B.xml" stale \ && ! hasattr "$WORK/B.xml" memory_stop && ! hasattr "$WORK/B.xml" register-macro-excluded && isnum "$bgate" && [ "$BRC" = "$wantrc" ]; then - ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need} (>= the 64M floor), also_skipped, no stale=/dead-code counts" + ok "(B) P1: a degraded answer (rc=$BRC, gating=$bgate) names kinds_skipped=$WHOLE_KINDS, the limit, need=${need}, also_skipped, no stale=/dead-code counts" else no "(B) P1: rc=$BRC (want $wantrc) gating=$bgate kinds_skipped=$( attr "$WORK/B.xml" kinds_skipped ) why=$( attr "$WORK/B.xml" skipped_why ) scope=$( attr "$WORK/B.xml" ingest_scope ) need=$need stdout=$( wc -c <"$WORK/B.xml" | tr -d ' ' )B" fi -if [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=$need" ]; then ok "(B) kinds_next= replays exactly the skipped kinds at the projected need" -else no "(B) kinds_next='$bnext'"; fi +# R7c: the replay's --max-memory is strictly above the projected need (in whole MiB) and clears the parser's 64M floor +replaymb=""; isnum "$needn" && { replaymb=$(( needn + 1 )); [ "$replaymb" -lt 64 ] && replaymb=64; } +if [ -n "$replaymb" ] && [ "$bnext" = "--quality-delta=$BASE..$TGT --quality-kinds=$WHOLE_KINDS --max-memory=${replaymb}M" ]; then + ok "(B) kinds_next= replays exactly the skipped kinds at --max-memory=${replaymb}M (above need=${need}, at least the 64M floor)" +else no "(B) kinds_next='$bnext' (want --max-memory=${replaymb:-?}M)"; fi if [ "$( memlines )" = 1 ] && grep -q "^ripwire: memory limit .*dead-code, duplication, new-clone-of-reused-helper" "$WORK/B.err" && [ "$BING" = 3 ]; then ok "(B) one stderr line names the skipped kinds; three ingests (the base in full, then both commits' changed files)" else no "(B) memory lines=$( memlines ) ingests=$BING stderr: $( grep '^ripwire: [^c]' "$WORK/B.err" | head -c 300 )"; fi @@ -279,16 +283,19 @@ fi # ── (G) near misses under project:1 ──────────────────────────────────────────────────────────────────────────── nm(){ # $1 label, $2 repo; compares LOCAL rows with the unpressured run and requires the degraded header and the exact rc + # S5: new-symbol= counts the rows of the kinds that RAN, so the degraded count equals the unpressured run's LOCAL rows + # carrying origin="new-symbol" (a rename makes every moved symbol new in both modes); api-new-surface= equal outright. local L="$1" R="$2" ( cd "$R" && TMPDIR="$WORK/tmp" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmA.xml" 2>/dev/null ) ( cd "$R" && TMPDIR="$WORK/tmp" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache >"$WORK/nmB.xml" 2>"$WORK/nmB.err" ); local rc=$? + local localnew; localnew="$( rows "$WORK/nmA.xml" "$LOCAL_CSV" | grep -c 'origin="new-symbol"' )" if [ "$( attr "$WORK/nmB.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( rows "$WORK/nmB.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/nmA.xml" "$LOCAL_CSV" )" ] \ && ! grep -q 'could not materialize' "$WORK/nmB.err" && [ "$rc" = "$( degrc "$WORK/nmB.xml" )" ] \ - && [ "$( attr "$WORK/nmB.xml" new-symbol )" = "$( attr "$WORK/nmA.xml" new-symbol )" ] \ + && [ "$( attr "$WORK/nmB.xml" new-symbol )" = "$localnew" ] \ && [ "$( attr "$WORK/nmB.xml" api-new-surface )" = "$( attr "$WORK/nmA.xml" api-new-surface )" ]; then - ok "(G) $L: degraded answer (rc=$rc), LOCAL rows, new-symbol= and api-new-surface= equal the unpressured run" + ok "(G) $L: degraded answer (rc=$rc), LOCAL rows, new-symbol=$localnew (the LOCAL new-symbol rows) and api-new-surface= equal the unpressured run" else - no "(G) $L: rc=$rc (want $( degrc "$WORK/nmB.xml" )) kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) new-symbol $( attr "$WORK/nmA.xml" new-symbol )/$( attr "$WORK/nmB.xml" new-symbol ) stderr: $( head -c 300 "$WORK/nmB.err" )" + no "(G) $L: rc=$rc (want $( degrc "$WORK/nmB.xml" )) kinds_skipped=$( attr "$WORK/nmB.xml" kinds_skipped ) new-symbol want $localnew got $( attr "$WORK/nmB.xml" new-symbol ) stderr: $( head -c 300 "$WORK/nmB.err" )" fi } FE="$WORK/fe"; mkfx "$FE"; g "$FE" commit -q --allow-empty -m empty; nm "empty diff" "$FE" @@ -440,22 +447,24 @@ else ok "(N) CMakeCache: neither answer rows the pruned bld/x.py"; fi # Legacy-scheme ack key of a scope-less name = fnv1a64( bare name ) (quality.h addSchemeAliases: canonicalId degrades to the # bare name). The full run re-keys it only when ONE file defines the name; two definitions poison it. mkacks(){ printf '# ripwire quality acks v1\nack placeholder %s 5 legacy-scheme row\n' "$( fnv dupname )" >"$1/.ripwire_quality_acks"; } +# the finding rows () of a kind — stale-ack rows are a whole-answer fact the degraded answer names as skipped +frows(){ rows "$1" "$2" | grep '^"$FO/py/a.py"; printf 'def dupname():\n return 2\n' >"$FO/py/b.py" g "$FO" add -A; g "$FO" commit -qm two; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO/py/a.py"; g "$FO" add -A; g "$FO" commit -qm todo; mkacks "$FO" qdin "$FO" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/OA.xml" qdin "$FO" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache -if [ -n "$( rows "$WORK/OA.xml" placeholder )" ] && [ "$( rows "$WORK/o" placeholder )" = "$( rows "$WORK/OA.xml" placeholder )" ] \ +if [ -n "$( frows "$WORK/OA.xml" placeholder )" ] && [ "$( frows "$WORK/o" placeholder )" = "$( frows "$WORK/OA.xml" placeholder )" ] \ && [ "$( attr "$WORK/o" acked )" = "$( attr "$WORK/OA.xml" acked )" ] && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac; then ok "(O) a legacy ack on a name two files define (one changed): the same placeholder rows and acked= as the full run; ack-healing named as skipped" -else no "(O) two definitions: rows full=$( rows "$WORK/OA.xml" placeholder | grep -c . ) degraded=$( rows "$WORK/o" placeholder | grep -c . ) acked $( attr "$WORK/OA.xml" acked )/$( attr "$WORK/o" acked ) also_skipped=$( attr "$WORK/o" also_skipped )"; fi +else no "(O) two definitions: rows full=$( frows "$WORK/OA.xml" placeholder | grep -c . ) degraded=$( frows "$WORK/o" placeholder | grep -c . ) acked $( attr "$WORK/OA.xml" acked )/$( attr "$WORK/o" acked ) also_skipped=$( attr "$WORK/o" also_skipped )"; fi FO1="$WORK/fo1"; mkfx "$FO1"; printf 'def dupname():\n return 1\n' >"$FO1/py/a.py" g "$FO1" add -A; g "$FO1" commit -qm one; printf 'def dupname():\n # TODO: one\n return 1\n' >"$FO1/py/a.py"; g "$FO1" add -A; g "$FO1" commit -qm todo; mkacks "$FO1" qdin "$FO1" "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/O1A.xml" qdin "$FO1" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache -if [ "$( attr "$WORK/O1A.xml" acked )" = 1 ] && [ -z "$( rows "$WORK/O1A.xml" placeholder )" ] && [ -n "$( rows "$WORK/o" placeholder )" ] \ +if [ "$( attr "$WORK/O1A.xml" acked )" = 1 ] && [ -z "$( frows "$WORK/O1A.xml" placeholder )" ] && [ -n "$( frows "$WORK/o" placeholder )" ] \ && case ",$( attr "$WORK/o" also_skipped )," in *,ack-healing,*) true;; *) false;; esac && [ "$RC" = "$( degrc "$WORK/o" )" ]; then ok "(O) a legacy ack on a name one file defines: the full run re-keys it; the degraded run names ack-healing as skipped and shows the row (rc=$RC)" -else no "(O) one definition: full acked=$( attr "$WORK/O1A.xml" acked ) rows full=$( rows "$WORK/O1A.xml" placeholder | grep -c . ) degraded=$( rows "$WORK/o" placeholder | grep -c . ) also_skipped=$( attr "$WORK/o" also_skipped ) rc=$RC"; fi +else no "(O) one definition: full acked=$( attr "$WORK/O1A.xml" acked ) rows full=$( frows "$WORK/O1A.xml" placeholder | grep -c . ) degraded=$( frows "$WORK/o" placeholder | grep -c . ) also_skipped=$( attr "$WORK/o" also_skipped ) rc=$RC"; fi # no ledger: nothing to heal, so ack-healing is never named if [ "$( attr "$WORK/B.xml" also_skipped )" = "config-warnings,stale-acks" ]; then ok "(O) with no ledger, ack-healing is not named (nothing to heal)" else no "(O) no ledger: also_skipped=$( attr "$WORK/B.xml" also_skipped )"; fi @@ -478,10 +487,13 @@ else fi # ── (Q) R5: the target phase stops after the projection said it fits ─────────────────────────────────────────── +# ingest:2 cuts the process's 2nd ingest (the target's full tree) at its first crawl entry: that ingest parses nothing and +# so prints no cache-stats line — 3 lines (base in full, both changed-file sets), and the stderr line names the target. qd RIPWIRE_TEST_MEMGUARD=ingest:2 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; cp "$WORK/o" "$WORK/Q.xml" if [ "$( attr "$WORK/Q.xml" kinds_skipped )" = "$WHOLE_KINDS" ] && [ "$( attr "$WORK/Q.xml" memory_stop )" = crawl ] && [ "$( attr "$WORK/Q.xml" memory_need )" = unmeasured ] \ - && [ "$( ingests )" = 4 ] && [ "$RC" = "$( degrc "$WORK/Q.xml" )" ] && [ "$( rows "$WORK/Q.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/A.xml" "$LOCAL_CSV" )" ]; then - ok "(Q) a stop in the target's full ingest falls to the changed files (four ingests, rc=$RC), LOCAL rows equal (A); defect-shape kept (the base's macro set was read)" + && [ "$( ingests )" = 3 ] && grep -q '^ripwire: memory limit .*stopped the crawl of the target commit' "$WORK/e" \ + && [ "$RC" = "$( degrc "$WORK/Q.xml" )" ] && [ "$( rows "$WORK/Q.xml" "$LOCAL_CSV" )" = "$( rows "$WORK/A.xml" "$LOCAL_CSV" )" ]; then + ok "(Q) a stop in the target's full ingest falls to the changed files (rc=$RC), LOCAL rows equal (A); defect-shape kept (the base's macro set was read)" else no "(Q) rc=$RC kinds_skipped=$( attr "$WORK/Q.xml" kinds_skipped ) stop=$( attr "$WORK/Q.xml" memory_stop ) ingests=$( ingests ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi # ── (R) R7: a peak over the cap is said; a replay with nothing it can run refuses ────────────────────────────── From a466e664362c12b90e522b8feb49fc690a837d5b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 19:47:05 -0400 Subject: [PATCH 055/162] feat(quality): --quality-delta=A..B answers the kinds it can under memory pressure (exit 6 when nothing gates) A tripped memory guard in the range form gave exit 5 and nothing, so on a large repository a four-file change could not get even the findings those four files decide (#387 follow-up). Now a stop in either commit's full tree (crawl/parse soft line or the hard line), or a projection after the base phase that says the target phase will not fit, releases what was loaded and ingests only both commits' CHANGED files (never through an ingest cache). The kinds those files decide run with the same code and bars as a full answer: api-surface, complexity, error-masking, nesting, params, placeholder, verbosity, and defect-shape when the changed files define the same C-family macro names on both sides and either the base's whole macro set was read or no changed file is C++. dead-code, duplication and new-clone-of-reused-helper need every file and are skipped. Every cut is named: kinds_skipped=, skipped_why="memory", ingest_scope="changed-files", memory_limit=, memory_need= (projected, or "unmeasured" with memory_stop= after a stop), also_skipped= and kinds_next=, the replay of exactly the skipped kinds. - Exit: 2 when a row of a kind that ran gates; 6 (new) for a partial answer with no gating row; 5 only when even the changed files do not fit or cannot stand in for the trees (a symlink, export-subst, a changed .gitattributes with an export attribute). --quality-ack refuses there. - The changed set adds the one crawl verdict another file decides: a CMakeCache.txt in an ancestor directory. - Ack healing over the changed files runs the git rename route only; the two tree-wide rescues are skipped and named (also_skipped ack-healing). - --quality-kinds=K[,K] (range form) runs only the named kinds; stale acks are judged for those kinds only; passes no named kind needs do not run. - os::mem_footprint_peak (macOS rev3+ ledger peak, Linux VmHWM, Windows PeakPagefileUsage) feeds the projection need = max(after, before + 1.75 (base peak - before)); k calibrated on 5 corpora (max 1.48). - memory_peak= (present-only) + one stderr line when a range answer's peak went over the limit after the guard's last reading; exit code unchanged. - Seams for gates: project:N, ingest:N, peak:N. RIPWIRE_STATS prints the projection's readings. When memory suffices the answer is byte-identical to before. memguardcheck B23's refusal is replaced by design (its twin keeps the exit-5 refusal when the changed files are cut too); B22 is unchanged. Gate: qddegradecheck. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 32 ++ docs/COMMANDS.md | 26 +- src/cli.h | 34 +- src/compactlegend.h | 11 + src/infra/os.h | 64 +++- src/infra/os_win32.cpp | 9 +- src/memguard.h | 161 +++++++-- src/quality.h | 629 ++++++++++++++++++++++++++++++------ src/verbs_quality.h | 622 ++++++++++++++++++++++++++++++----- test/printf_parity.manifest | 4 +- 10 files changed, 1372 insertions(+), 220 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a9b5e3f..79871c958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,38 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. +### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) + +A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change +could not get even the findings those four files decide. Now, when the guard stops either commit's full tree (a crawl +or parse stop, or the hard limit), or when a projection after the base commit says the target phase will not fit the +limit, the range form releases what it loaded and ingests only both commits' **changed files** (never through a cache). +There it runs the kinds those files decide — api-surface, complexity, error-masking, nesting, params, placeholder, +verbosity, and defect-shape when the changed files define the same C-family macro names on both sides (and either the +base's whole macro set was read or no changed file is C++) — with the same code and bars as a full answer, so their +rows are the full answer's rows of those kinds. dead-code, duplication and new-clone-of-reused-helper need every file +and are skipped. Every cut is named in the header: `kinds_skipped=`, `skipped_why="memory"`, +`ingest_scope="changed-files"`, `memory_limit=`, `memory_need=` (the projected need, or `"unmeasured"` after a stop, +with `memory_stop=` naming it), `also_skipped=` (config-warnings, stale-acks, and ack-healing when the ledger is not +empty: there acks match by their recorded key only), and `kinds_next=`, the call that runs exactly the skipped kinds on +the same two commits. One stderr line says the same. + +- **Exit codes.** A partial answer exits **2** when a row of a kind that ran gates (the skipped kinds can only add + rows, so that verdict is final) and **6** (new) otherwise — a partial answer is never a clean exit 0. Exit **5** now + means, for this form, that even the changed files did not fit, or that they cannot stand in for the trees (a symlink + anywhere, an export-subst attribute, or a changed `.gitattributes` with an export attribute), on one line naming why. + `--quality-ack` under pressure refuses (exit 5, ledger untouched): accepting needs every kind's rows. +- **`--quality-kinds=K[,K…]`** (new, range form only) runs only the named kinds — the replay `kinds_next=` names. A + named kind's rows are its rows in a full answer; stale acks are judged for the named kinds only; passes no named kind + needs (the clone passes, the dead-code evidence) do not run. Refused beside `--quality-ack`/`--quality-baseline` + and on the working-tree form; the MCP `quality_delta` tool (working-tree form) has no selector yet. +- **`memory_peak=`** (new, present-only) on any range answer whose peak footprint went over the limit after the guard's + last reading, with one stderr line; the exit code is unchanged. +- **Behaviour change, by design:** `test/memguardcheck.sh` B23 pinned "a cut base ingest → exit 5 and nothing". That is + the refusal this change replaces: the cut tree is still never scored (one cut ingest, then two changed-file ingests, + and no row names a file outside the diff), and a twin arm keeps the exit-5 refusal when even the changed files are cut. + When memory suffices the answer is byte-identical to before. Gate: `test/qddegradecheck.sh`. + ### Fixed — `--quality-delta=A..B` indexed the working tree it never reads (#387) The range form materializes and indexes both commits itself. `main()` still ingested and graphed the working tree diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a5ab7628a..3c0683537 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -44,7 +44,7 @@ Two limits apply to nearly everything here and are not repeated in every section **zoom the detail ladder** — [`--detail`](#--detailn) · [`--pack-signatures`](#--pack-signatures) · [`--outline`](#--outlineab) · [`--expand`](#--expandab) · [`--compress`](#--compress) · [`--pack-top-n`](#--pack-top-nn) · [`--no-redact`](#--no-redact) -**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--lint-max-per-rule`](#--lint-max-per-rulen) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--show-stale`](#--show-stale) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-full-text`](#--whereis-full-text) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--lint-max-per-rule`](#--lint-max-per-rulen) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--show-stale`](#--show-stale) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--quality-kinds`](#--quality-kindskk) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-full-text`](#--whereis-full-text) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) **self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) @@ -2860,7 +2860,7 @@ $ ./build/ripwire . --quality-baseline (empty) ``` -**Shaped by:** `--allow-dirty`, `--quality-delta`, `--legend` +**Shaped by:** `--allow-dirty`, `--quality-delta`, `--quality-kinds`, `--legend` **Caveats (stated by the binary):** @@ -2930,7 +2930,7 @@ $ ./build/ripwire . --quality-delta ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--quality-kinds`, `--dmm`, `--quality-ack`, `--scope` **Caveats (stated by the binary):** @@ -2942,7 +2942,7 @@ $ ./build/ripwire . --quality-delta **Answers:** the same 12-kind report between two committed trees — a whole branch at once the same 12-kind report between two COMMITTED TREES instead of the working tree vs a baseline — the WAVE-level measurement (=A..B = tree B against tree A; -=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. +=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. UNDER MEMORY PRESSURE IT ANSWERS PART OF THE QUESTION rather than nothing: when the guard stops either commit's full tree, or projects that the target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest (kinds_skipped=, ingest_scope="changed-files", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees (a symlink, an export attribute). --quality-ack is refused there (exit 5, ledger untouched): accepting needs every kind's rows. **Try it** @@ -2955,7 +2955,7 @@ $ ./build/ripwire . --quality-delta ``` -**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--dmm`, `--quality-ack`, `--scope`, `--json` +**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--show-stale`, `--quality-kinds`, `--dmm`, `--quality-ack`, `--scope` **Caveats (stated by the binary):** @@ -2963,6 +2963,18 @@ $ ./build/ripwire . --quality-delta - Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. - ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. +### `--quality-kinds=K[,K...]` + +**Answers:** (with --quality-delta=A..B) run only the named kinds — the replay a partial answer hands back (with --quality-delta=A..B) run ONLY the named kinds, comma-separated, of the twelve: api-surface, complexity, dead-code, defect-shape, duplication, error-masking, nesting, new-clone-of-reused-helper, params, placeholder, short-horizon-churn, verbosity. + +A named kind's rows are exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). + +**Caveats (stated by the binary):** + +- the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. +- An unknown name is refused (exit 1) naming the twelve; +- refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). + ### `--dmm[=REV|A..B]` **Answers:** score a change as ONE number in [0,1], so quality trends across commits the DELTA MAINTAINABILITY MODEL scalar: ONE comparable number in [0,1] for a change, so quality becomes TRENDABLE across commits instead of a per-kind list (di Biase, Rastogi, Bruntink and van Deursen, TechDebt 2019; @@ -3005,7 +3017,7 @@ $ ./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope (empty) ``` -**Shaped by:** `--ack-only`, `--scope`, `--legend` +**Shaped by:** `--quality-delta`, `--quality-kinds`, `--ack-only`, `--scope`, `--legend` **Caveats (stated by the binary):** @@ -4849,7 +4861,7 @@ $ ./build/ripwire . --max-file-size=8K --top-k=3 **Answers:** the memory guard's limit (default 65% of this machine's memory, env RIPWIRE_MAX_MEMORY) the memory guard is on for every run and silent on normal ones: it measures this process's footprint (on Linux, in a cgroup v2 leaf with a memory.max, the leaf's unreclaimable charge when that is larger) at most once per 5 s, from 5 s into an ingest. -Past its lines the crawl (growth of limit/8) or the parse (half the limit) stops, and the default map answers from what was built, disclosed in its header (memory_stop=, memory_parsed=, memory_limit=; a parse stop keeps the first K slots it claimed of its work order — uncached files first, largest first within a tier, or path order when every grammar-bearing file was an ingest-cache hit — so a partial map repeats for a given memory_parsed=K); critical OS pressure stops them too. Every other verb (--html, --mermaid, --expand, --outline and, after a crawl stop, --in included) refuses a partial index, and so does any verb whose own internal ingest was cut; at the limit itself ripwire exits 5 with one line naming it. Nothing derived from a partial ingest is cached. The default is 65% of physical RAM (or of the cgroup's memory.max when lower); this flag, or RIPWIRE_MAX_MEMORY when the flag is absent, replaces it; below 64M is refused. +Past its lines the crawl (growth of limit/8) or the parse (half the limit) stops, and the default map answers from what was built, disclosed in its header (memory_stop=, memory_parsed=, memory_limit=; a parse stop keeps the first K slots it claimed of its work order — uncached files first, largest first within a tier, or path order when every grammar-bearing file was an ingest-cache hit — so a partial map repeats for a given memory_parsed=K); critical OS pressure stops them too. Every other verb (--html, --mermaid, --expand, --outline and, after a crawl stop, --in included) refuses a partial index, and so does any verb whose own internal ingest was cut; at the limit itself ripwire exits 5 with one line naming it. --quality-delta=A..B alone answers from both commits' changed files instead (exit 2 or 6, see its row). Nothing derived from a partial ingest is cached. The default is 65% of physical RAM (or of the cgroup's memory.max when lower); this flag, or RIPWIRE_MAX_MEMORY when the flag is absent, replaces it; below 64M is refused. **Shaped by:** `--mcp` diff --git a/src/cli.h b/src/cli.h index 8dd92e8ee..d8b99151c 100644 --- a/src/cli.h +++ b/src/cli.h @@ -324,6 +324,7 @@ struct Config bool qualityAck = false; // --quality-ack[=REASON]: accept the current findings into .ripwire_quality_acks (per-finding ratchet); shares qualityDelta's baseline resolution std::string_view qualityAckReason; // the reason recorded next to each acked finding std::string_view qualityAckOnly; // --ack-only=SUBSTR[,SUBSTR]: ack only findings whose kind, canonical id or facet (e.g. format-arity) contains one of these (default: all) + std::string_view qualityKinds; // qd-memory-degrade --quality-kinds=K[,K]: run only these kinds of the range form (the names are validated in the verb) std::string_view qualityScope; // P1 --scope=GLOB[,GLOB...]: the OWNERSHIP partition for a shared working tree — // findings outside it are printed but never gate, and --quality-ack refuses to write them. // Grammar + floors: rw::quality's SCOPE block. Only with --quality-delta/--quality-ack (refused elsewhere) @@ -1797,6 +1798,17 @@ inline constexpr char kHelpHead[] = " No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison.\n" " ONE KIND CANNOT BE MEASURED HERE and says so as churn=\"unavailable\": short-horizon-churn needs git history at the tree being judged, and both trees are\n" " materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them.\n" + " UNDER MEMORY PRESSURE IT ANSWERS PART OF THE QUESTION rather than nothing: when the guard stops either commit's full tree, or projects that the\n" + " target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest\n" + " (kinds_skipped=, ingest_scope=\"changed-files\", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the\n" + " skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees\n" + " (a symlink, an export attribute). --quality-ack is refused there (exit 5, ledger untouched): accepting needs every kind's rows.\n" + " --quality-kinds=K[,K...] (with --quality-delta=A..B) run only the named kinds — the replay a partial answer hands back\n" + " (with --quality-delta=A..B) run ONLY the named kinds, comma-separated, of the twelve: api-surface, complexity, dead-code, defect-shape,\n" + " duplication, error-masking, nesting, new-clone-of-reused-helper, params, placeholder, short-horizon-churn, verbosity. A named kind's rows are\n" + " exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why=\"selector\"), stale acks are\n" + " judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused\n" + " beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet).\n" " --dmm[=REV|A..B] score a change as ONE number in [0,1], so quality trends across commits\n" " the DELTA MAINTAINABILITY MODEL scalar: ONE comparable number in [0,1] for a change, so quality becomes TRENDABLE across commits instead of a per-kind list (di Biase, Rastogi, Bruntink and van Deursen, TechDebt 2019; thresholds and arithmetic from PyDriller's deltamaintainability reference implementation). Bare = the WORKING TREE vs git HEAD (what --quality-delta compares); =REV = that commit vs its FIRST PARENT (the per-commit scalar); =A..B = tree B vs tree A.\n" " A UNIT is a function/method definition with a body; its VOLUME is its line span. Per property a unit is LOW risk iff size: loc<=15, complexity: cyclomatic<=5, interfacing: params<=2. good = low-risk volume ADDED plus high-risk volume REMOVED; bad = low-risk REMOVED plus high-risk ADDED; dmm = good/(good+bad). So DELETING a god function scores 1.000 and GROWING one scores 0.000.\n" @@ -2776,7 +2788,8 @@ inline constexpr char kHelpTail[] = " critical OS pressure stops them too. Every other verb (--html, --mermaid, --expand,\n" " --outline and, after a crawl stop, --in included) refuses a partial index, and so does any\n" " verb whose own internal ingest was cut; at the limit itself ripwire exits 5 with one\n" - " line naming it. Nothing derived from a partial ingest is cached. The\n" + " line naming it. --quality-delta=A..B alone answers from both commits' changed files instead\n" + " (exit 2 or 6, see its row). Nothing derived from a partial ingest is cached. The\n" " default is 65% of physical RAM (or of the cgroup's memory.max when lower); this flag,\n" " or RIPWIRE_MAX_MEMORY when the flag is absent, replaces it; below 64M is refused.\n" " --refetch when the root is a git URL, clone it fresh instead of reusing the cached copy\n" @@ -3380,6 +3393,8 @@ inline constexpr ViewFlag kViewFlags[] = // that reads as "everything is mine", which is the exact belief this flag exists to stop an agent // holding in a shared tree. `--scope=` (an unset shell variable) must never expand to a blanket ack. { "--scope=", &Config::qualityScope , EmptyValue::Refuse, "one or more path globs, comma-separated", "--scope=src/quality.h" }, + // qd-memory-degrade: `--quality-kinds=` (an unset variable) must not read as "every kind" — the caller asked for a subset + { "--quality-kinds=", &Config::qualityKinds , EmptyValue::Refuse, "one or more quality kinds, comma-separated", "--quality-kinds=dead-code,duplication" }, // --regex= sets the pattern AND the regex-mode bool; it already called refuseEmptyValue directly and the // table prints the SAME sentence, so this row is byte-identical to the arm it replaces. { "--regex=", &Config::grep , EmptyValue::Refuse, "a regular expression", "--regex='parse[A-Z]'", @@ -3596,7 +3611,7 @@ inline constexpr IntFlag kIntFlags[] = // nowhere to keep) // • a bare no-op / bare pair --route, --quality-ack (the =REASON form is a kViewFlags row) inline constexpr std::size_t kHandWrittenFlagArms = 26; // +2 lane/lean-answers-068 (2026-10-07): --whereis-listing= and --mcp-legend= (closed-value arms, same shape as --grep-in=); +1 #350 (2026-09-28): --max-memory= (byte-size arm, same shape as --max-file-size=); +1: --color-by= (enum-value arm); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (repeatable-value arms, same shape as --exclude=); +1 R-H: --grep-in= (closed-value arm, same shape as --grep-scope=); +1 lane/flag-biggest-first (2026-09-23): --readability (deprecation-warning alias, same shape as --stable/--most-important-last/--no-auto-order) — its kBoolFlags row is renamed to --biggest-first (same Config::readability member), so the old spelling moves OUT of the table and becomes a hand-written arm, per the house rule that deprecation-warning aliases stay hand-written -inline constexpr std::size_t kTotalFlagArms = 221; // +1 lane/whereis-lean (2026-10-10, review C1): --whereis-full-text (kBoolFlags row) — the same --whereis page with every t= line whole, the replay; +1 lane/lean-trio (2026-10-10): --show-stale (kBoolFlags row) — prints the stale-ack rows a regression-free --quality-delta collapses; +1 lane/map-inventory-d7 (2026-10-08): --no-inventory (kBoolFlags row) — the default map without its tier; +1 knob-honesty-068 (2026-10-07): --lint-max-per-rule= (kIntFlags row) — the per-rule lint budget a floored answer's findings_next= names; +2 lane/lean-answers-068 (2026-10-07): --whereis-listing= and --mcp-legend= (hand-written closed-value arms, see kHandWrittenFlagArms); +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it +inline constexpr std::size_t kTotalFlagArms = 222; // +1 lane/qd-memory-degrade (2026-10-14): --quality-kinds= (kViewFlags row) — run only the named quality kinds of the range form (the replay a degraded answer names); +1 lane/whereis-lean (2026-10-10, review C1): --whereis-full-text (kBoolFlags row) — the same --whereis page with every t= line whole, the replay; +1 lane/lean-trio (2026-10-10): --show-stale (kBoolFlags row) — prints the stale-ack rows a regression-free --quality-delta collapses; +1 lane/map-inventory-d7 (2026-10-08): --no-inventory (kBoolFlags row) — the default map without its tier; +1 knob-honesty-068 (2026-10-07): --lint-max-per-rule= (kIntFlags row) — the per-rule lint budget a floored answer's findings_next= names; +2 lane/lean-answers-068 (2026-10-07): --whereis-listing= and --mcp-legend= (hand-written closed-value arms, see kHandWrittenFlagArms); +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it static_assert( std::size( kBoolFlags ) + std::size( kViewFlags ) + std::size( kIntFlags ) + kHandWrittenFlagArms == kTotalFlagArms, "a --flag arm was added or removed without updating the ledger above — count the arms in parseArgs and fix the counter" ); @@ -5283,6 +5298,21 @@ inline void validateConfig( Config& c ) noexcept c.ok = false; } + // qd-memory-degrade — --quality-kinds selects kinds of the RANGE form only: the working-tree form (and its MCP twin) has + // no selector yet, so there it would be a silently ignored flag. Beside --quality-ack it would accept a partial report as + // if it were the whole one; beside --quality-baseline it would pin a floor missing every unnamed kind. Refused by name. + if( !c.qualityKinds.empty() && ( c.qualityAck || c.qualityBaseline ) ) + { + rw::emitTo( stderr, "ripwire: --quality-kinds runs a subset of the kinds, and {} needs every kind's rows — drop one of the two\n", + c.qualityAck ? "--quality-ack" : "--quality-baseline" ); + c.ok = false; + } + else if( !c.qualityKinds.empty() && c.qualityDeltaRange.empty() ) + { + rw::emitRaw( stderr, "ripwire: --quality-kinds selects kinds of the range form --quality-delta=A..B (two commits); the working-tree --quality-delta has no kind selector\n" ); + c.ok = false; + } + // --format=candidates is a flat top-K export of a RANKED set — only --for/--query produce one; alone it // would silently no-op on the plain map. Refuse loudly (mirrors --adaptive/--detail). if( c.candidates && c.forTask.empty() && c.query.empty() ) diff --git a/src/compactlegend.h b/src/compactlegend.h index bfcb2316b..ba3752710 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -766,6 +766,17 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "renames", "renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, // also defines rename_window_commits= { "acked_by_rename", "acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, // also defines acked_by_content= { "renames_window_truncated", "renames_window_truncated=1: history is deeper than the rename window, older renames unread", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + // qd-memory-degrade: present-only on an answer that did not run every kind (verbs_quality.h kindsDisclosure) + { "kinds_skipped", "kinds_skipped=: kinds NOT run, so none of their rows is here and every count covers the rest; a count only they feed is absent", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "skipped_why", "skipped_why=selector (the caller chose; stale acks judged for those kinds only) | memory (see ingest_scope=)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "ingest_scope", "ingest_scope=changed-files: the full trees did not fit, so both commits' changed files answered the kinds they decide, rows as a full answer's; exit 2 if one gates, else 6", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_need", "memory_need=NM: the full answer's projected need; unmeasured after a stop (memory_stop=)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "also_skipped", "also_skipped=: whole-answer facts not computed (config-warnings, stale-acks, ack-healing: acks match by recorded key only)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "kinds_next", "kinds_next=: the call running exactly the skipped kinds on the same commits (after a stop its max-memory is a guess)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_stop", "memory_stop=crawl|parse|hard: the guard's stop that sent it to the changed files; a soft stop does not show the need exceeds the limit", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_limit", "memory_limit=NM: the guard's limit, spelled as the max-memory value that raises it", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_pressure", "memory_pressure=1: OS memory pressure stopped it, not the limit", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_peak", "memory_peak=NM: the peak went OVER the limit after the guard's last reading; the answer is complete, the exit code unchanged", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, // readability: src/readability.h (the root emit) { "functions", "functions=N: functions and methods measured (bodyless declarations are not)", false, "readability", MapHeaderRead::No, {}, "readability" }, // scan-skills: src/skillscan.h printSkillScanArtifact diff --git a/src/infra/os.h b/src/infra/os.h index b5077e458..dd4ad8705 100644 --- a/src/infra/os.h +++ b/src/infra/os.h @@ -503,6 +503,12 @@ struct dirwatch_event // seconds, plus once at each hard-line check between phases. // mem_footprint: bytes this process holds. macOS: TASK_VM_INFO phys_footprint (what Activity Monitor and the // jetsam killer count — compressed and swapped pages included). Linux: resident pages from /proc/self/statm. +// mem_footprint_peak: the most this process has held so far, in mem_footprint's own measure (qd-memory-degrade: the +// range form's projection reads the base phase's PEAK, which the readings between phases miss). macOS: TASK_VM_INFO +// ledger_phys_footprint_peak, read only when the kernel returned a structure long enough to hold it (rev3+); Linux: +// VmHWM from /proc/self/status (the resident high-water mark, the peak of what statm reports). 0 = unknown, and a +// caller then must not project from it. Never cgroup memory.peak: it is lifetime and leaf-wide (an earlier CI step's +// high-water mark), and it counts the page cache measuredFootprint deliberately leaves out. // mem_physical: bytes of physical RAM. macOS hw.memsize; Linux _SC_PHYS_PAGES x _SC_PAGESIZE. // mem_cgroup_max: the cgroup v2 memory.max of this process's OWN (leaf) cgroup (Linux; 0 = none, "max", or unreadable). // Known floor: a limit set on an ANCESTOR cgroup (a systemd slice's MemoryMax) and cgroup v1's memory.limit_in_bytes @@ -517,16 +523,30 @@ struct dirwatch_event // reaches 20%, warning when "some avg10" does. Integer percent only — no float parse, no locale. #if defined( __APPLE__ ) -inline std::uint64_t mem_footprint() +// the Mach reader the two footprint bodies below share. A static member of a struct, like the Linux branch's procfs: a +// helper of those bodies, not a name of this layer (the Windows branch has no Mach to declare it for). +struct machvm { - task_vm_info_data_t info {}; - mach_msg_type_number_t count = TASK_VM_INFO_COUNT; - if( ::task_info( ::mach_task_self(), TASK_VM_INFO, reinterpret_cast( &info ), &count ) != KERN_SUCCESS ) + // TASK_VM_INFO's footprint: phys_footprint, or with `peak` ledger_phys_footprint_peak — read only when the kernel filled + // a structure long enough to hold it (rev3+): an older kernel's stops before the field, and a zero-filled read is not + // a peak. 0 when unknown. + static std::uint64_t footprint( bool peak ) { - return 0; + task_vm_info_data_t info {}; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + if( ::task_info( ::mach_task_self(), TASK_VM_INFO, reinterpret_cast( &info ), &count ) != KERN_SUCCESS ) + { + return 0; + } + if( !peak ) + { + return std::uint64_t( info.phys_footprint ); + } + return count >= TASK_VM_INFO_REV3_COUNT && info.ledger_phys_footprint_peak > 0 ? std::uint64_t( info.ledger_phys_footprint_peak ) : 0; } - return std::uint64_t( info.phys_footprint ); -} +}; +inline std::uint64_t mem_footprint() { return machvm::footprint( false ); } +inline std::uint64_t mem_footprint_peak() { return machvm::footprint( true ); } inline std::uint64_t mem_physical() { std::uint64_t bytes = 0; @@ -634,6 +654,27 @@ inline std::uint64_t mem_footprint() const long pageSize = ::sysconf( _SC_PAGESIZE ); return pages > 0 && pageSize > 0 ? std::uint64_t( pages ) * std::uint64_t( pageSize ) : 0; } +inline std::uint64_t mem_footprint_peak() +{ + char buf[ 4096 ]; // /proc/self/status is ~1.4 KiB; VmHWM sits in its first third + if( procfs::read_small( "/proc/self/status", buf, sizeof( buf ) ) == 0 ) + { + return 0; + } + const std::string_view text( buf ); + const std::size_t at = text.find( "\nVmHWM:" ); + if( at == std::string_view::npos ) + { + return 0; + } + std::string_view rest = text.substr( at + 7 ); + while( !rest.empty() && ( rest.front() == ' ' || rest.front() == '\t' ) ) + { + rest.remove_prefix( 1 ); + } + const long long kib = procfs::leading_int( rest ); // "VmHWM:\t 123456 kB" + return kib > 0 ? std::uint64_t( kib ) * 1024u : 0; +} inline std::uint64_t mem_physical() { const long pages = ::sysconf( _SC_PHYS_PAGES ); @@ -687,6 +728,7 @@ inline int mem_pressure() } #else inline std::uint64_t mem_footprint() { return 0; } +inline std::uint64_t mem_footprint_peak() { return 0; } inline std::uint64_t mem_physical() { return 0; } inline std::uint64_t mem_cgroup_max() { return 0; } inline std::uint64_t mem_cgroup_unreclaimable() { return 0; } @@ -1082,10 +1124,12 @@ int dirwatch_open(); int dirwatch_add( int watchFd, int dirFd, dirwatch_event* change ); int dirwatch_poll( int watchFd, dirwatch_event* events, int eventCount, const ::timespec* timeout ); -// ── memory (see the POSIX branch): PrivateUsage from GetProcessMemoryInfo, ullTotalPhys from GlobalMemoryStatusEx, -// no cgroup (0, both cgroup helpers), and QueryMemoryResourceNotification on a low-memory notification object created -// once (3 when the system signals low memory, else 1). +// ── memory (see the POSIX branch): PrivateUsage from GetProcessMemoryInfo (its peak: PeakPagefileUsage, the same commit +// charge's high-water mark), ullTotalPhys from GlobalMemoryStatusEx, no cgroup (0, both cgroup helpers), and +// QueryMemoryResourceNotification on a low-memory notification object created once (3 when the system signals low +// memory, else 1). std::uint64_t mem_footprint(); +std::uint64_t mem_footprint_peak(); std::uint64_t mem_physical(); [[gnu::always_inline]] inline std::uint64_t mem_cgroup_max() { return 0; } [[gnu::always_inline]] inline std::uint64_t mem_cgroup_unreclaimable() { return 0; } diff --git a/src/infra/os_win32.cpp b/src/infra/os_win32.cpp index 6e1e61045..d73efb008 100644 --- a/src/infra/os_win32.cpp +++ b/src/infra/os_win32.cpp @@ -2804,8 +2804,9 @@ int setsockopt_nosigpipe( int, const void*, socklen_t ) { return 0; } // ── memory (#350's guard): each a single call, 0 when it fails ───────────────────────────────────────────── // PrivateUsage is the commit charge this process owns, the Windows analogue of phys_footprint: it counts what the -// process allocated whether or not it is resident, which is the quantity that runs a machine out of memory. -std::uint64_t mem_footprint() +// process allocated whether or not it is resident, which is the quantity that runs a machine out of memory. commitCharge +// reads it, or with `peak` PeakPagefileUsage (the peak of PagefileUsage, which equals PrivateUsage); 0 when the call fails. +static std::uint64_t commitCharge( bool peak ) { PROCESS_MEMORY_COUNTERS_EX counters {}; counters.cb = sizeof( counters ); @@ -2813,8 +2814,10 @@ std::uint64_t mem_footprint() { return 0; } - return std::uint64_t( counters.PrivateUsage ); + return std::uint64_t( peak ? counters.PeakPagefileUsage : counters.PrivateUsage ); } +std::uint64_t mem_footprint() { return commitCharge( false ); } +std::uint64_t mem_footprint_peak() { return commitCharge( true ); } std::uint64_t mem_physical() { diff --git a/src/memguard.h b/src/memguard.h index bae76491e..4adf5a29d 100644 --- a/src/memguard.h +++ b/src/memguard.h @@ -23,6 +23,10 @@ // became a hard stop after the ingest instead of a partial answer. // hard at the limit itself, checked between phases (main.cpp; --quality-delta=A..B, which dispatches before main()'s // ingest, between its own: verbs_quality.h refPairOverHardLimit) and before each MCP tool call: no partial answer. +// The range form alone answers PART of the question instead (qd-memory-degrade): a stop in either commit's full +// tree — or a projection that says the target phase will not fit (projectTargetPhase) — releases the full trees +// and re-asks from both commits' CHANGED FILES the kinds those files decide, naming every kind it skipped +// (exit 2 when a row gates, else 6); exit 5 is then left for "even the changed files did not fit". // pressure: the OS signal (os::mem_pressure) at "critical" stops the crawl or the parse too — but only once this // process holds at least max( 256 MiB, limit/8 ), so a small run is never cut because something ELSE is // using the machine. It never causes a hard stop. @@ -32,21 +36,28 @@ // completion, and only the thread that wins the time slot pays the syscall. A CLI run shorter than five seconds reads // the footprint three times — at ingest start, after the ingest and after the graph build (the hard line) — and an MCP // tool call once more. The `--quality-delta=A..B` range form reads it up to 2 + 6 times: two outside its commit -// ingests and the six hard-line readings around them. None of it changes a byte of output unless a line is crossed -// (test/memguardcheck.sh (C)). No thread, no timer, no signal handler. +// ingests and the six hard-line readings around them, plus one projection after the base commit (two footprint readings +// and one PEAK reading, os::mem_footprint_peak) and one peak reading after the answer. None of it changes a byte of +// output unless a line is crossed (test/memguardcheck.sh (C), test/qddegradecheck.sh (L)). No thread, no timer, no +// signal handler. // // THE STOP. A soft stop sets an atomic flag the crawl loop and the parse workers read before the next unit of work, // records WHY through DISCLOSE on IngestResult::memoryStop, and lets ingest() finish with what it has: the crawl keeps // the entries it saw (then sorts them, as always); the parse keeps every file it claimed — a prefix of its parse order // (a worker checks the flag BEFORE claiming a file, so every claimed file completes). Nothing derived from a partial ingest is ever persisted — not the ingest // cache, not a quality snapshot, not a baseline. Whether the partial ingest may answer is the caller's decision — main.cpp answers the -// default map with memory_stop= in its header and refuses every other verb (exit 5); the MCP server answers with -// `_memory_stop` in the envelope. +// default map with memory_stop= in its header and refuses every other verb (exit 5) except --quality-delta=A..B, which +// releases the cut tree unscored and answers from both commits' changed files (see "hard" above); the MCP server +// answers with `_memory_stop` in the envelope. // // THE TEST SEAM. RIPWIRE_TEST_MEMGUARD=crawl:N | pressure:N | parse:N | request:N | hard:N ADDS a trip — at the Nth crawl entry // (over the line, or under critical pressure), after exactly N files of the parse order are parsed, or at the Nth and // later MCP tool calls, or at the Nth and later hard-line readings (hard:N); eager:1 instead drops the five-second time gate, so every guarded unit takes a REAL reading — so test/memguardcheck.sh can drive every stop path deterministically. It never replaces a real -// reading: every real line, the time gate and the hard line still apply, so it can only make a run stricter. +// reading: every real line, the time gate and the hard line still apply, so it can only make a run stricter. crawl:N and +// parse:N count per INGEST (each ingest's own Watch). Three more for the range form (test/qddegradecheck.sh): project:N — +// the Nth target-phase projection reads "does not fit"; ingest:N — the Nth ingest of the process stops its crawl at its +// first entry (a stop in ONE chosen ingest, e.g. the target's after the base fit); peak:N — the Nth post-answer peak +// reading reads as over the limit. // // REPEATABILITY. A crawl stop keeps what the walk had seen, then sorts it. A parse stop keeps the first K slots of the // parse ORDER (cache misses, then cache hits, then grammarless files, each largest first, then fileId — or plain fileId @@ -98,8 +109,9 @@ inline Limits& limitsSlot() noexcept } inline const Limits& limits() noexcept { return limitsSlot(); } -// 65% of min( physical RAM, cgroup memory.max ), or 0 when the machine cannot say how much RAM it has. -inline std::uint64_t defaultLimitBytes() +// the machine's own ceiling: physical RAM, or the cgroup's memory.max when that is lower; 0 when unknown. Also what a +// degraded --quality-delta=A..B compares its replay's need with (a need above it is recoverable only on a larger machine). +inline std::uint64_t machineMemoryBytes() { std::uint64_t machine = os::mem_physical(); const std::uint64_t cgroup = os::mem_cgroup_max(); @@ -107,7 +119,12 @@ inline std::uint64_t defaultLimitBytes() { machine = cgroup; } - return machine / 100 * 65; + return machine; +} +// 65% of min( physical RAM, cgroup memory.max ), or 0 when the machine cannot say how much RAM it has. +inline std::uint64_t defaultLimitBytes() +{ + return machineMemoryBytes() / 100 * 65; } // the footprint every line is measured on: this process's own (os::mem_footprint), or, when it is larger, the @@ -143,6 +160,9 @@ enum class TripAt : std::uint8_t Request, // request:N — the Nth MCP tool call reads as over the hard limit Hard, // hard:N — the Nth hard-line reading (overHardLimit: between CLI phases, per workspace root, between --quality-delta=A..B's phases) reads as over Eager, // eager:1 — no five-second time gate: every guarded unit takes a REAL footprint reading (only stricter) + Project, // project:N — the Nth target-phase projection (projectTargetPhase) reads as "does not fit" + Ingest, // ingest:N — the Nth ingest of the process stops its crawl at its first entry (a crawl stop) + Peak, // peak:N — the Nth post-answer peak reading (peakOverLimit) reads as over the limit }; struct TestTrip { @@ -159,6 +179,7 @@ inline TripAt tripAtFor( std::string_view phase ) noexcept } kPhases[] = { { "crawl", TripAt::Crawl }, { "pressure", TripAt::Pressure }, { "parse", TripAt::Parse }, { "request", TripAt::Request }, { "hard", TripAt::Hard }, { "eager", TripAt::Eager }, + { "project", TripAt::Project }, { "ingest", TripAt::Ingest }, { "peak", TripAt::Peak }, }; for( const auto& p : kPhases ) { @@ -207,6 +228,32 @@ inline const TestTrip& testTrip() noexcept using GateClock = std::chrono::steady_clock; inline constexpr GateClock::rep kCheckIntervalTicks = std::chrono::duration_cast( std::chrono::nanoseconds( kCheckIntervalNs ) ).count(); +// The seams' process-wide call counters, one struct (stopCounts' shape): every ingest() (the ingest:N ordinal, counted +// at its Watch), the MCP tool calls (request:N), the range form's projections (project:N) and its post-answer peak +// readings (peak:N). Relaxed: each is read by the call that bumps it. +struct SeamCounters +{ + std::atomic ingests{ 0 }; + std::atomic requests{ 0 }; + std::atomic projections{ 0 }; + std::atomic peaks{ 0 }; +}; +inline SeamCounters& seamCounters() noexcept +{ + static SeamCounters counters; + return counters; +} +// one call of a call-counted seam: true for the Nth call (`laterToo` false) or the Nth and every later one +inline bool seamCallTrips( TripAt at, std::atomic& seen, bool laterToo ) noexcept +{ + if( testTrip().at != at ) + { + return false; + } + const std::uint64_t n = seen.fetch_add( 1, std::memory_order_relaxed ) + 1; + return laterToo ? n >= testTrip().index : n == testTrip().index; +} + // install: main() calls this once, before any thread, with the resolved limit. A zero `bytes` means "the default". inline void install( std::uint64_t bytes, Source source ) { @@ -225,6 +272,7 @@ class Watch Watch() noexcept : trip_( testTrip() ) , hardBytes_( limits().hardBytes ) + , ingestOrdinal_( seamCounters().ingests.fetch_add( 1, std::memory_order_relaxed ) + 1 ) { baseFootprint_ = measuredFootprint(); nextCheckTick_.store( GateClock::now().time_since_epoch().count() + kCheckIntervalTicks, std::memory_order_relaxed ); @@ -243,6 +291,10 @@ class Watch { return tripSoft( trip_.at == TripAt::Pressure ); } + if( trip_.at == TripAt::Ingest && ingestOrdinal_ == trip_.index ) + { + return tripSoft( false ); // ingest:N — this whole ingest is the one the seam cuts, at its first entry + } if( ( entryCount & ( kCrawlStride - 1 ) ) != 0 || !claimTimeSlot() ) { return false; @@ -329,6 +381,7 @@ class Watch const TestTrip& trip_; const std::uint64_t hardBytes_; + const std::uint64_t ingestOrdinal_; std::uint64_t baseFootprint_ = 0; std::atomic nextCheckTick_{ 0 }; std::atomic stopped_{ false }; @@ -362,14 +415,79 @@ inline bool overHardLimit() // pre-check is `requestSeamTrips() || overHardLimit()` — additive, like every seam. inline bool requestSeamTrips() { - if( testTrip().at != TripAt::Request ) + return seamCallTrips( TripAt::Request, seamCounters().requests, /*laterToo=*/true ); +} + +// ── the range form's projection and peak (qd-memory-degrade) ─────────────────────────────────────────────── +// --quality-delta=A..B scores the base commit and releases it before the target is materialized (#387). The target +// phase then holds the base snapshot, the full target tree and computeDelta's passes; on the measured repositories its +// growth over the starting footprint was 0.9-1.4x the base phase's. What the base released is reused, not added: the +// footprint once it is released (`after`) is anywhere from the start plus the snapshot (the allocator returned the +// pages) to the base phase's peak (it kept them), and the target phase grows into that. So after the base phase: +// need = max( after, before + k * ( basePeak - before ) ) against the hard limit, and +// ingestNeed = max( after, ingestPeak ) against the parse line (half the limit), +// where `before` is this process's own footprint before the base phase and `basePeak` / `ingestPeak` its PEAK at the end +// of the base phase / of the base ingest (os::mem_footprint_peak — the readings between phases miss the clone passes' +// transient peak). k is kProjectionScalePct / 100, calibrated in the qd-memory-degrade report as the max over the measured +// corpora of ( full peak - before ) / ( base-phase peak - before ), rounded up. A run whose true need lies within that +// margin below the limit may be answered degraded; the projection never lets a run past the limit by its own choice (an +// under-projection is caught by the guard's stops, which degrade). An unknown peak (0) projects nothing: the full attempt +// runs, and a stop in it still degrades. +inline constexpr std::uint64_t kProjectionScalePct = 175; // max measured 1.48 over 11 cold runs on 5 corpora (qd-memory-degrade report), + ~18% +struct TargetProjection +{ + bool measured = false; // false: no peak reading — nothing was projected + bool fits = true; + std::uint64_t needBytes = 0; // the projected need (0 when not measured) +}; +inline TargetProjection projectTargetPhase( std::uint64_t before, std::uint64_t ingestPeak, std::uint64_t basePeak, std::uint64_t after ) +{ + TargetProjection p; + const std::uint64_t limit = limits().hardBytes; + p.measured = basePeak != 0 && ingestPeak != 0 && after != 0 && basePeak >= before; + if( p.measured ) { - return false; + const std::uint64_t growth = basePeak - before; + const std::uint64_t scaled = growth / 100 * kProjectionScalePct + growth % 100 * kProjectionScalePct / 100; // no overflow below 2^57 B + p.needBytes = std::max( after, before + scaled ); + const std::uint64_t ingestNeed = std::max( after, ingestPeak ); + p.fits = limit == 0 || ( p.needBytes < limit && ingestNeed < limit / 2 ); } - static std::atomic requestsSeen{ 0 }; - return requestsSeen.fetch_add( 1, std::memory_order_relaxed ) + 1 >= testTrip().index; + if( seamCallTrips( TripAt::Project, seamCounters().projections, /*laterToo=*/false ) ) + { + p.fits = false; // the seam: additive — it can only make a run degrade, never let one run full past a real projection + } + return p; +} +// --max-memory for a replay of what a degraded run skipped: strictly above the projected need, in whole MiB, at least the +// parser's floor, and at most what a size_t can hold (item 27: the value the continuation names is one the parser +// accepts). After a stop no need was measured, so the replay names twice the limit: a guess, and said to be one. +inline std::uint64_t replayLimitBytes( std::uint64_t needBytes, bool needMeasured ) +{ + const std::uint64_t limit = limits().hardBytes; + std::uint64_t want = needMeasured ? ( ( needBytes + kMiB - 1 ) / kMiB + 1 ) * kMiB // one MiB above the need as spelled + : ( limit > ( UINT64_MAX / 2 ) ? UINT64_MAX : limit * 2 ); + want = std::max( want, kFloorBytes ); + const std::uint64_t ceiling = std::uint64_t( SIZE_MAX ) / kMiB * kMiB; + return std::min( want, ceiling ); +} +// The peak after the answer was computed, when it went over the limit (or the peak:N seam says so), else 0. computeDelta +// runs after the last hard-line reading, so a full answer can finish over --max-memory (rv-pr391 N1: 1.09-1.11x); this +// is the reading that SAYS so instead of answering silently over it. It never changes the exit code. +inline std::uint64_t peakOverLimit() +{ + const std::uint64_t limit = limits().hardBytes; + if( limit == 0 ) + { + return 0; + } + std::uint64_t peak = os::mem_footprint_peak(); + if( seamCallTrips( TripAt::Peak, seamCounters().peaks, /*laterToo=*/false ) ) + { + peak = std::max( peak, limit + kMiB ); // the seam: the reading says over, whatever the real peak was + } + return peak > limit ? peak : 0; } - // ── the backstop: a stop nobody answered for ──────────────────────────────────────────────────────────── // ingest() counts every stop it discloses; the caller that decides what the partial ingest may answer (main.cpp's // memoryStopExit) marks every stop so far as answered for. A CLI run that ends with a stop nobody answered for — an @@ -405,13 +523,20 @@ inline std::string_view phaseName( MemoryStop::Phase phase ) noexcept // every message ends with the override inline constexpr std::string_view kOverride = "raise it with --max-memory=[K|M|G] or RIPWIRE_MAX_MEMORY, or pass a smaller root"; -// the hard stop's one line: nothing was built that could answer -inline std::string hardStopLine( std::string_view where ) +// where the limit came from, as the clause every message spells it with +inline std::string_view limitSourceClause() noexcept { const Limits& l = limits(); - return "memory limit reached during the " + std::string( where ) + " (limit " + limitSpelling( l.hardBytes ) - + ( l.source == Source::Flag ? ", from --max-memory" : l.source == Source::Env ? ", from RIPWIRE_MAX_MEMORY" : ", 65% of this machine's memory" ) - + "); stopped cleanly without an answer — " + std::string( kOverride ); + return l.source == Source::Flag ? "from --max-memory" : l.source == Source::Env ? "from RIPWIRE_MAX_MEMORY" : "65% of this machine's memory"; +} + +// the hard stop's one line: nothing was built that could answer. `also` is said between the limit and the verdict — the +// range form's "the changed files alone did not fit either" — and is empty everywhere else (the line keeps its bytes). +inline std::string hardStopLine( std::string_view where, std::string_view also = {} ) +{ + std::string line = "memory limit reached during the "; + line.append( where ).append( " (limit " ).append( limitSpelling( limits().hardBytes ) ).append( ", " ).append( limitSourceClause() ).append( "); " ); + return line.append( also ).append( "stopped cleanly without an answer — " ).append( kOverride ); } // which line a soft stop crossed, spelled against the limit that sets it (a soft stop is never the limit itself) diff --git a/src/quality.h b/src/quality.h index 0a0b746df..350f1f97d 100644 --- a/src/quality.h +++ b/src/quality.h @@ -287,6 +287,74 @@ struct Snapshot std::vector defectSites; // the defect-shape kind — sorted (anchor, site) records, one per offending site (a multiset), plus each scanned file's presence record }; +// ─── THE TWELVE KINDS AS A SET (qd-memory-degrade) ───────────────────────────────────────────────────────── +// --quality-kinds=K[,K] runs a subset of the kinds (the replay a degraded --quality-delta=A..B hands back), and the +// degraded range form runs the subset its changed files can decide. Each kind stays a separately reported, separately +// skippable unit: the mask decides which passes run and which rows are kept, never how a row is computed — a kind that +// runs computes its rows with the same code and the same bars as in a full answer. Alphabetical, the order every list +// of them (kinds_skipped=, the refusal, the help) is spelled in. +inline constexpr std::string_view kQualityKindNames[] = { + "api-surface", "complexity", "dead-code", "defect-shape", "duplication", "error-masking", "nesting", + "new-clone-of-reused-helper", "params", "placeholder", "short-horizon-churn", "verbosity", +}; +using KindMask = std::uint16_t; +inline constexpr KindMask kAllKinds = KindMask( ( 1u << std::size( kQualityKindNames ) ) - 1u ); +static_assert( std::size( kQualityKindNames ) == 12 && std::size( kQualityKindNames ) <= 16, "one bit per kind" ); +// the kind's bit, or 0 for a name that is not one of the twelve +inline constexpr KindMask kindBit( std::string_view kind ) noexcept +{ + const auto* const at = std::find( std::begin( kQualityKindNames ), std::end( kQualityKindNames ), kind ); + return at == std::end( kQualityKindNames ) ? KindMask( 0 ) : KindMask( 1u << ( at - std::begin( kQualityKindNames ) ) ); +} +// the kinds that need EVERY file of the target tree (dead-code: the whole graph's evidence; the two clone kinds: every +// body) — what a changed-files answer cannot run +inline constexpr KindMask kWholeTreeKinds = KindMask( kindBit( "dead-code" ) | kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ); +inline constexpr bool kindSelected( KindMask mask, std::string_view kind ) noexcept { return ( mask & kindBit( kind ) ) != 0; } +// the kinds of `mask`, alphabetical, joined by `sep` +inline std::string kindList( KindMask mask, std::string_view sep ) +{ + std::string out; + for( std::size_t k = 0; k < std::size( kQualityKindNames ); ++k ) + { + if( ( mask & ( 1u << k ) ) != 0 ) + { + out.append( out.empty() ? std::string_view{} : sep ).append( kQualityKindNames[ k ] ); + } + } + return out; +} +// What a snapshot or a delta is computed FOR (qd-memory-degrade): the kinds to run, and — when the index holds only some +// of the tree's files (a changed-files answer) — the tree's C-family macro names to scan its C++ files against +// (defectSitesOf). The default is every kind over a whole tree: the computation these functions always did. +struct KindScope +{ + KindMask kinds = kAllKinds; + const std::vector* macroOverride = nullptr; +}; + +// --quality-kinds' value parsed: the mask, or 0 with `bad` naming the first token that is not a kind (an empty token too) +inline KindMask parseKindList( std::string_view spec, std::string& bad ) +{ + KindMask mask = 0; + while( true ) + { + const std::size_t comma = spec.find( ',' ); + const std::string_view token = spec.substr( 0, comma ); + const KindMask bit = kindBit( token ); + if( bit == 0 ) + { + bad.assign( token ); + return 0; + } + mask |= bit; + if( comma == std::string_view::npos ) + { + return mask; + } + spec.remove_prefix( comma + 1 ); + } +} + // Q1 api-surface — the PUBLIC/exported contract surface. DEFINITION (deterministic, documented): // a symbol is PUBLIC iff it is DECLARED IN A HEADER file (`.h/.hpp/.hh/.hxx`) — the C/C++/ObjC export surface // by convention, exactly the convention `isDeadCandidate` already uses ("header-exported by convention"). @@ -4184,7 +4252,8 @@ inline RefSpec resolveRefSpec( const std::string& root, std::string_view spec ) // a failed archive/extract/ingest → returns {snapshot, false}. rootPath is shell-escaped (shSingleQuote) and // quotepath=false — no injection, deterministic path handling. inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, - const std::vector* fileInBaseline = nullptr ); // fwd — defined below; computeHeadSnapshot reuses it + const std::vector* fileInBaseline = nullptr, // fwd — defined below; computeHeadSnapshot reuses it + const KindScope& scope = {} ); // `excludes` MUST mirror the working-tree side's cfg.excludes (A4-F5): the HEAD snapshot is compared key-for-key // against the working tree, and any in-edge-derived kind (dead-code, api-surface) diverges if one side honors // --exclude and the other does not — e.g. a helper called only from tests/ is dead on a --exclude=tests working @@ -4388,6 +4457,293 @@ inline RefTreeLoad loadRefTree( const std::string& repoRoot, const std::string& return RefTreeLoad::Loaded; } +// ─── qd-memory-degrade: a commit pair's CHANGED FILES, and their trees ───────────────────────────────────────── +// When the memory guard stops either commit's full tree, or projects that the target phase will not fit, the range form +// re-asks the question of each commit's CHANGED files only. That answer is exact for a kind only if every file that +// could yield a row of it is in the set, and is materialized exactly as the full archive materializes it. So the set is +// the paths `git diff --no-renames` names (per side, those that exist there as files), plus the control files the crawl +// reads to decide those paths, and the pair is REFUSED by name when an archive or a crawl would treat a path git does not +// list differently: +// * a symlink anywhere in either tree — the crawl follows in-root links, so an UNCHANGED link to a changed file rows +// the link's path too (and a changed link to an unchanged file dangles in a partial tree); +// * an export-subst attribute in either tree — `git archive` rewrites the bytes of files git calls unchanged; +// * a changed .gitattributes that carries an export attribute on either side — an export-ignore flip adds or removes +// files the diff does not name. +// The one crawl verdict decided by another file is the build-tree prune: a directory holding CMakeCache.txt is skipped +// whole (ingest_crawl.h), so a changed path under such a directory brings that CMakeCache.txt along. Nothing else in +// the crawl of a materialized tree reads another file: .gitignore is consulted through `git ls-files`, which a temp +// tree does not answer, and .ripwire_config feeds only dead-code (a skipped kind). What it does not cover, stated: +// attributes from $GIT_DIR/info/attributes or core.attributesFile (outside the tree, so the same on both sides). +struct ChangedFiles +{ + std::vector base; // root-relative paths to materialize at the base commit (sorted, unique) + std::vector target; // ... at the target commit + std::vector targetTree; // every file of the target commit, sorted — filled only when asked (--scope's test) + std::string refusal; // non-empty: why the changed files cannot stand in for the trees (the line names it) + bool ok = false; // git answered every question this needed + enum class DisclosureWhy : std::uint8_t + { + GitFailed, // a diff or ls-tree over the pair did not complete: the set is unknown, and the caller refuses + }; + void disclose( DisclosureWhy ) noexcept { ok = false; } +}; + +// Stream one git command's NUL-separated records into `onRecord`; true when git exited 0. Never holds the whole listing: +// on a large tree `ls-tree -r` is tens of megabytes, and this runs exactly when memory is short. +template +inline bool forEachGitNulRecord( const std::string& root, const std::string& args, OnRecord&& onRecord ) +{ + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " " + args + " 2>/dev/null"; + std::FILE* pipe = os::popen( cmd.c_str(), "r" ); + if( pipe == nullptr ) + { + return false; + } + std::string record; + char buf[ 16384 ]; + std::size_t got = 0; + while( ( got = std::fread( buf, 1, sizeof( buf ), pipe ) ) > 0 ) + { + for( std::size_t at = 0; at < got; ) + { + const char* nul = static_cast( std::memchr( buf + at, '\0', got - at ) ); + const std::size_t end = nul == nullptr ? got : std::size_t( nul - buf ); + record.append( buf + at, end - at ); + if( nul == nullptr ) + { + break; + } + onRecord( std::string_view( record ) ); + record.clear(); + at = end + 1; + } + } + if( !record.empty() ) + { + onRecord( std::string_view( record ) ); + } + return os::pclose( pipe ) == 0; +} + +// The changed files of `baseSha`..`targetSha` (two resolved commit names), with the refusals above decided. +inline ChangedFiles changedFilesBetween( const std::string& root, const std::string& baseSha, const std::string& targetSha, bool wantTargetTree ) +{ + ChangedFiles out; + // 1. what git says changed: "\0\0" pairs; --no-renames reports a move as its delete and its add + std::vector changed; + std::vector statusOf; + { + bool wantPath = false; + char status = 0; + const bool ran = forEachGitNulRecord( root, "diff --no-renames --no-ext-diff --no-textconv --name-status -z " + shSingleQuote( baseSha ) + " " + shSingleQuote( targetSha ), + [ & ]( std::string_view rec ) + { + if( !wantPath ) + { + status = rec.empty() ? 0 : rec.front(); + wantPath = true; + return; + } + changed.emplace_back( rec ); + statusOf.push_back( status ); + wantPath = false; + } ); + if( !ran ) + { + DISCLOSE( out, ChangedFiles::DisclosureWhy::GitFailed, "quality: git diff over the commit pair failed — the changed files are unknown" ); + return out; + } + } + // 2. each side's tree: which changed paths exist there as FILES (a submodule's gitlink is no file), the build-tree + // sentinels, the attribute files, and any symlink at all. One streamed `ls-tree -r` per side. + std::vector order( changed.size() ); + for( std::size_t k = 0; k < order.size(); ++k ) { order[k] = k; } + std::sort( order.begin(), order.end(), [ & ]( std::size_t a, std::size_t b ) { return changed[a] < changed[b]; } ); + const auto changedIndex = [ & ]( std::string_view path ) -> std::size_t + { + const auto it = std::lower_bound( order.begin(), order.end(), path, [ & ]( std::size_t k, std::string_view p ) { return std::string_view( changed[k] ) < p; } ); + return ( it != order.end() && changed[ *it ] == path ) ? *it : SIZE_MAX; + }; + const auto readSide = [ & ]( const std::string& sha, std::vector& existsHere, std::vector& cmakeDirs, + std::vector* tree ) -> bool + { + existsHere.assign( changed.size(), 0 ); + return forEachGitNulRecord( root, "ls-tree -r -z --full-tree " + shSingleQuote( sha ), [ & ]( std::string_view rec ) + { + const std::size_t tab = rec.find( '\t' ); + if( tab == std::string_view::npos || tab < 7 ) + { + return; + } + const std::string_view mode = rec.substr( 0, 6 ); + const std::string_view path = rec.substr( tab + 1 ); + if( mode == "120000" && out.refusal.empty() ) + { + out.refusal = "the commit " + sha.substr( 0, 12 ) + " holds a symlink (" + std::string( path ) + "), which the crawl follows into files git diff does not name"; + } + const bool isFile = mode == "100644" || mode == "100755"; + if( isFile && ( path == "CMakeCache.txt" || ( path.size() > 15 && path.substr( path.size() - 15 ) == "/CMakeCache.txt" ) ) ) + { + cmakeDirs.emplace_back( path.substr( 0, path.size() - 14 ) ); // the directory with its trailing '/', "" for the root + } + if( const std::size_t k = changedIndex( path ); k != SIZE_MAX && isFile ) + { + existsHere[k] = 1; + } + if( tree != nullptr && isFile ) + { + tree->emplace_back( path ); + } + } ); + }; + std::vector atBase, atTarget; + std::vector cmakeBase, cmakeTarget; + if( !readSide( baseSha, atBase, cmakeBase, nullptr ) || !readSide( targetSha, atTarget, cmakeTarget, wantTargetTree ? &out.targetTree : nullptr ) ) + { + DISCLOSE( out, ChangedFiles::DisclosureWhy::GitFailed, "quality: git ls-tree over a commit of the pair failed — the changed files are unknown" ); + return out; + } + // 3. export attributes: ":\0\n" per hit (exit 1 = no hit, a clean answer) + const auto exportAttrs = [ & ]( const std::string& sha, std::vector& files ) + { + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " grep -I -z -o -e export-subst -e export-ignore " + + shSingleQuote( sha ) + " -- " + shSingleQuote( "*.gitattributes" ) + " 2>/dev/null"; + const std::string text = popenTrimmed( cmd ); + for( std::size_t at = 0; at < text.size(); ) + { + const std::size_t nl = std::min( text.find( '\n', at ), text.size() ); + const std::string_view line( text.data() + at, nl - at ); + const std::size_t nul = line.find( '\0' ); + if( nul != std::string_view::npos && line.size() > sha.size() + 1 ) + { + const std::string_view path = line.substr( sha.size() + 1, nul - sha.size() - 1 ); + const std::string_view match = line.substr( nul + 1 ); + if( match == "export-subst" && out.refusal.empty() ) + { + out.refusal = "the commit " + sha.substr( 0, 12 ) + " carries an export-subst attribute (" + std::string( path ) + "), which rewrites files git diff calls unchanged"; + } + files.emplace_back( path ); + } + at = nl + 1; + } + }; + std::vector attrFiles; + exportAttrs( baseSha, attrFiles ); + exportAttrs( targetSha, attrFiles ); + for( const std::string& p : changed ) + { + if( out.refusal.empty() && std::find( attrFiles.begin(), attrFiles.end(), p ) != attrFiles.end() ) + { + out.refusal = "the pair changes " + p + ", which carries an export attribute (an export-ignore flip adds or removes files git diff does not name)"; + } + } + // 4. the per-side lists, each changed path that is a file there, plus the CMakeCache.txt of any ancestor directory + std::sort( cmakeBase.begin(), cmakeBase.end() ); + std::sort( cmakeTarget.begin(), cmakeTarget.end() ); + const auto addWithSentinels = [ & ]( const std::string& path, const std::vector& cmakeDirs, std::vector& list ) + { + list.push_back( path ); + for( std::size_t slash = path.find( '/' ); slash != std::string::npos; slash = path.find( '/', slash + 1 ) ) + { + const std::string dir = path.substr( 0, slash + 1 ); + if( std::binary_search( cmakeDirs.begin(), cmakeDirs.end(), dir ) ) + { + list.push_back( dir + "CMakeCache.txt" ); + } + } + }; + for( std::size_t k = 0; k < changed.size(); ++k ) + { + if( atBase[k] ) + { + addWithSentinels( changed[k], cmakeBase, out.base ); + } + if( atTarget[k] ) + { + addWithSentinels( changed[k], cmakeTarget, out.target ); + } + } + for( std::vector* list : { &out.base, &out.target } ) + { + std::sort( list->begin(), list->end() ); + list->erase( std::unique( list->begin(), list->end() ), list->end() ); + } + std::sort( out.targetTree.begin(), out.targetTree.end() ); + out.ok = true; + return out; +} + +// Materialize `paths` of `sha` (already-resolved, each a file there) into a fresh temp tree: the same archive-then-extract +// discipline as materializeCommitTree, in chunks so no command line grows with the change. Each path is a :(literal) +// pathspec, so a name with glob characters is itself. An empty list is an empty tree (an empty diff is a legal question). +// Returns the temp root, or "" on any failure (the guard the caller passes removes a partial tree). +inline std::string materializeCommitSubset( const std::string& root, const std::string& sha, const std::vector& paths, + const char* tag, TmpTreeGuard& guard ) +{ + namespace fs = std::filesystem; + std::error_code ec; + const std::string tmpRoot = cacheDirLadder() + "/ripwire-" + tag + "-" + std::to_string( os::getpid() ); + fs::remove_all( fs::path( tmpRoot ), ec ); + if( !fs::create_directories( fs::path( tmpRoot ), ec ) && ec ) + { + DISCLOSE( Diagnostics::answerRefused, "the range form refuses with the sha named; nothing is scored", "quality: cannot create the changed-files temp dir" ); + return {}; + } + guard.p = tmpRoot; + const std::string archiveFile = tmpRoot + ".tar"; + constexpr std::size_t kChunkBytes = 64 * 1024; // well under every ARG_MAX this builds on + for( std::size_t at = 0; at < paths.size(); ) + { + std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " archive --format=tar --output=" + shSingleQuote( archiveFile ) + + " " + shSingleQuote( sha ) + " --"; + const std::size_t first = at; + while( at < paths.size() && ( at == first || cmd.size() + paths[at].size() < kChunkBytes ) ) + { + cmd += " " + shSingleQuote( ":(literal)" + paths[at] ); + ++at; + } + const bool archived = os::system( ( cmd + " 2>/dev/null" ).c_str() ) == 0; + const bool extracted = archived && os::system( ( "tar -x -f " + shSingleQuote( archiveFile ) + " -C " + shSingleQuote( tmpRoot ) + " 2>/dev/null" ).c_str() ) == 0; + fs::remove( fs::path( archiveFile ), ec ); + if( !extracted ) + { + DISCLOSE( Diagnostics::answerRefused, "the range form refuses with the sha named; nothing is scored", "quality: archiving a commit's changed files failed" ); + return {}; + } + } + return tmpRoot; +} + +// The changed files of one commit, ingested and graphed — loadRefTree's shape with three differences: never through an +// ingest cache (a subset must not read a full-tree blob or write one under the commit's key — checklist 21), an empty +// subset is a legal answer, and `tag` names it apart from the full trees. +inline RefTreeLoad loadRefSubset( const std::string& repoRoot, const std::string& sha, const std::vector& paths, + const std::vector& excludes, std::size_t maxFileBytes, const char* tag, + TmpTreeGuard& guard, RefTree& out ) +{ + ASSUME( tag != nullptr && *tag != '\0' ); + const std::string tmpRoot = materializeCommitSubset( repoRoot, sha, paths, tag, guard ); + if( tmpRoot.empty() ) + { + return RefTreeLoad::Unavailable; + } + { + std::lock_guard ingestLk( headSnapshotIngestMutex() ); + out.ing = ingest( tmpRoot.c_str(), excludes, std::string_view {}, maxFileBytes ); + } + if( out.ing.memoryStop.isSet() ) + { + return RefTreeLoad::MemoryStopped; + } + if( memguard::overHardLimit() ) + { + return RefTreeLoad::OverHardLimit; + } + out.g = buildGraph( out.ing, nullptr ); + out.root = tmpRoot; + return RefTreeLoad::Loaded; +} + // Signal-to-noise round — the committed-thrash evidence for short-horizon-churn: the per-canonId RAW-body // hashes of the tree at the churn-window reference commit (gitWindowRefSha). A symbol whose baseline (HEAD) // body differs from this ref's body — or that is absent here but present at HEAD — was already written/ @@ -4793,22 +5149,34 @@ inline std::vector baselineCloneGroupHashes( const IngestResult& // leaves its presence record; a file that does not read leaves the unreadable record instead and contributes // no site (a partial read is evidence of nothing, forEachSymbolBody's rule) — the delta then judges no current // site in that file against it. Sorted by key; deterministic (files in id order, scanners pure). -inline std::vector defectSitesOf( const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline ) +// format-arity's F1 skip input: every macro NAME the index holds in a C-family file, tree-wide (a call in one file may +// use a macro another file defines). Sorted, unique: the scanner binary-searches it. Also what a degraded +// --quality-delta=A..B keeps of the base commit's full tree (names only) so the changed files are scanned against the +// tree's set rather than their own (qd-memory-degrade). +inline std::vector cFamilyMacroNames( const IngestResult& ing ) { - std::vector out; - const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte && s.kind != SymKind::ModuleScope; } ); - // format-arity's F1 skip: every macro the index holds in a C-family file, tree-wide (a call in one file may use - // a macro another file defines). Sorted, unique: the scanner binary-searches it. - std::vector treeMacros; + std::vector names; for( const Symbol& s : ing.symbols ) { if( s.kind == SymKind::Macro && ( s.lang == Lang::Cpp || s.lang == Lang::C || s.lang == Lang::ObjC ) ) { - treeMacros.push_back( s.name ); + names.push_back( s.name ); } } - std::sort( treeMacros.begin(), treeMacros.end() ); - treeMacros.erase( std::unique( treeMacros.begin(), treeMacros.end() ), treeMacros.end() ); + std::sort( names.begin(), names.end() ); + names.erase( std::unique( names.begin(), names.end() ), names.end() ); + return names; +} + +// `macroOverride`, when given, is the macro-name set to scan C++ files against instead of this index's own — the whole +// tree's, when this index holds only some of its files (a degraded --quality-delta=A..B's changed files). +inline std::vector defectSitesOf( const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline, + const std::vector* macroOverride = nullptr ) +{ + std::vector out; + const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte && s.kind != SymKind::ModuleScope; } ); + const std::vector ownMacros = macroOverride == nullptr ? cFamilyMacroNames( ing ) : std::vector{}; + const std::vector& treeMacros = macroOverride == nullptr ? ownMacros : *macroOverride; for( std::uint32_t f = 0; f < ing.files.size(); ++f ) { if( !fileIsInBaseline( fileInBaseline, f ) ) @@ -4868,17 +5236,72 @@ inline std::vector defectSitesOf( const IngestResult& ing, std::st return out; } -inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, - const std::vector* fileInBaseline ) +// The dead set of a snapshot: every dead-code candidate the --dead-code verb's rule leaves dead, keyed like every per-symbol +// record. Its own pass (and its four evidence structures) so a snapshot computed for kinds without dead-code skips it. +inline void addDeadSet( Snapshot& snap, const IngestResult& ing, const Graph& g, std::string_view root, const std::vector* fileInBaseline ) { - Snapshot snap; - const std::vector codeLoc = codeLocByNode( ing ); // Q-DIAL-3: the verbosity kind's metric is CODE lines const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); const ValueRefIndex valueRefs( ing ); // a value-held function is not dead for( NodeId i = 0; i < ing.symbols.size(); ++i ) + { + if( i >= g.canonId.size() || g.canonId[i].empty() || !fileIsInBaseline( fileInBaseline, ing.symbols[i].fileId ) ) + { + continue; + } + if( isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch ) && !declinedCallMayReach( g, i ) + && !valueRefs.isValueReferenced( i ) ) + { + snap.dead.push_back( qualityKey( ing, i, root ) ); + } + } +} + +// The snapshot's whole-file passes, each only for the kinds it feeds (see computeSnapshot): clone groups, construct +// counts, defect sites, body hashes. +inline void addKindPasses( Snapshot& snap, const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline, const KindScope& scope ) +{ + if( ( scope.kinds & ( kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ) ) != 0 ) + { + snap.cloneGroups = baselineCloneGroupHashes( ing, root, fileInBaseline ); + } + // §D#4 error-masking + placeholder baseline: per-canonId counts (the SUMs the delta compares), from one pass. + if( ( scope.kinds & ( kindBit( "error-masking" ) | kindBit( "placeholder" ) ) ) != 0 ) + { + ConstructCounts counts = constructCountsBySym( ing, root ); + snap.maskBySym = std::move( counts.mask ); + snap.maskReportBySym = std::move( counts.maskReport ); + snap.placeholderBySym = std::move( counts.placeholder ); + } + // the defect-shape baseline: every offending site (a sorted multiset of anchor + site identity) and each + // scanned file's presence record. Filtered to the baseline files inside defectSitesOf (IDENTITY BASIS). + if( kindSelected( scope.kinds, "defect-shape" ) ) + { + const std::vector sites = defectSitesOf( ing, root, fileInBaseline, scope.macroOverride ); + snap.defectSites.resize( sites.size() ); + std::ranges::transform( sites, snap.defectSites.begin(), &DefectSiteAt::key ); + } + // §D#4 short-horizon-churn baseline: per-canonId RAW-body hash so the delta detects a rewrite that moved no + // metric (a literal-only edit). Compared, never bar-checked — presence-or-difference IS the rewrite signal. + if( kindSelected( scope.kinds, "short-horizon-churn" ) ) + { + snap.bodyHashBySym = bodyHashesBySym( ing, root ); + } +} + +// `scope` (qd-memory-degrade): the kinds this snapshot will be compared for. A pass that feeds only unselected kinds is +// not run and its records stay empty — the dead set without dead-code, the clone groups without either clone kind, the +// construct counts without error-masking or placeholder, the defect sites without defect-shape, the body hashes without +// short-horizon-churn. The per-symbol metric maps always run (locBySym is every kind's existence oracle). The default +// scope is every pass, the snapshot this function has always returned; its macroOverride: see defectSitesOf. +inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, + const std::vector* fileInBaseline, const KindScope& scope ) +{ + Snapshot snap; + const std::vector codeLoc = codeLocByNode( ing ); // Q-DIAL-3: the verbosity kind's metric is CODE lines + for( NodeId i = 0; i < ing.symbols.size(); ++i ) { if( i >= g.canonId.size() || g.canonId[i].empty() ) { @@ -4904,37 +5327,16 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // (editcheck.h). A COUNT is overload-collision-proof for the opposite reason a MAX is: it is the one // number a collision cannot hide. (maskBySym is the other non-MAX kind; it sums for its own reason.) { std::uint32_t& slot = snap.defsBySym[ key ]; slot += 1; } - if( isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch ) && !declinedCallMayReach( g, i ) - && !valueRefs.isValueReferenced( i ) ) - { - snap.dead.push_back( key ); - } if( isPublicApi( ing, i ) ) { snap.publicApi.push_back( key ); } } - snap.cloneGroups = baselineCloneGroupHashes( ing, root, fileInBaseline ); - - // §D#4 error-masking + placeholder baseline: per-canonId counts (the SUMs the delta compares), from one pass. - { - ConstructCounts counts = constructCountsBySym( ing, root ); - snap.maskBySym = std::move( counts.mask ); - snap.maskReportBySym = std::move( counts.maskReport ); - snap.placeholderBySym = std::move( counts.placeholder ); - } - - // the defect-shape baseline: every offending site (a sorted multiset of anchor + site identity) and each - // scanned file's presence record. Filtered to the baseline files inside defectSitesOf (IDENTITY BASIS). + if( kindSelected( scope.kinds, "dead-code" ) ) { - const std::vector sites = defectSitesOf( ing, root, fileInBaseline ); - snap.defectSites.resize( sites.size() ); - std::ranges::transform( sites, snap.defectSites.begin(), &DefectSiteAt::key ); + addDeadSet( snap, ing, g, root, fileInBaseline ); // sorted below with the rest; its order of insertion never reaches a reader } - - // §D#4 short-horizon-churn baseline: per-canonId RAW-body hash so the delta detects a rewrite that moved no - // metric (a literal-only edit). Compared, never bar-checked — presence-or-difference IS the rewrite signal. - snap.bodyHashBySym = bodyHashesBySym( ing, root ); + addKindPasses( snap, ing, root, fileInBaseline, scope ); dropNonBaselineKeys( snap, ing, root, fileInBaseline ); // IDENTITY BASIS membership, on the two key-built maps @@ -7800,9 +8202,14 @@ struct IdentityHealing // where the judged tree is a materialized temp dir with no git history at all: passing one root there would // silently lose every rename in the very wave being measured, which is the failure mode this round exists to // end. Collapsing them into one parameter is therefore a bug waiting for the next ref-pair caller. +// `treeWideRescues` (qd-memory-degrade): false when `ing` holds only the changed files. The scheme re-key and the content-id +// rescue both refuse an identity that is ambiguous ACROSS THE TREE (two files defining one scope-less name, two equal +// bodies), and a partial index cannot see that ambiguity — it would rescue what the full run refuses. So both are skipped +// there (and said: also_skipped= names ack-healing); the git rename route is per-path and runs as always. inline IdentityHealing healIdentity( Snapshot& base, gtl::btree_map& acks, const IngestResult& ing, const Graph& g, const std::string& corpusRoot, - const std::string& gitRoot, bool wantContentIds, const std::string& span = {} ) + const std::string& gitRoot, bool wantContentIds, const std::string& span = {}, + bool treeWideRescues = true ) { IdentityHealing h; if( acks.empty() && !wantContentIds ) @@ -7814,7 +8221,10 @@ inline IdentityHealing healIdentity( Snapshot& base, gtl::btree_map scopeDisclosure( const Scope& scope, // Called from computeDelta with its origin oracle (existedAtBaseline) and its locator (stampLoc). template inline void appendDefectShapeRows( std::vector& regs, const IngestResult& ing, const Graph& g, const Snapshot& base, - std::string_view root, ExistedFn&& existedAtBaseline, StampFn&& stampLoc ) + std::string_view root, ExistedFn&& existedAtBaseline, StampFn&& stampLoc, + const std::vector* macroOverride = nullptr ) { - const std::vector nowSites = defectSitesOf( ing, root, nullptr ); + const std::vector nowSites = defectSitesOf( ing, root, nullptr, macroOverride ); const auto isRealSite = []( std::uint64_t v ) { return v != kDefectFilePresent && v != kDefectFileUnreadable; }; std::vector baseVals, nowVals, baseUnreadable; for( const DefectSiteKey& d : base.defectSites ) @@ -8536,6 +8947,64 @@ inline void appendDefectShapeRows( std::vector& regs, const IngestRe } } +// The dead-code kind's delta pass: a dead candidate the --dead-code verb's rule leaves dead, not dead at the baseline. Its +// own function (qd-memory-degrade) so computeDelta runs it, and builds its four evidence structures, only when the kind is +// asked for. The three exclusion counts are the disclosed floors computeDelta's caller prints (any may be null). +struct DeadExclusionCounts +{ + std::size_t* registerMacro = nullptr; // P2.2: would be dead-code but for the register-macro exemption + std::size_t* declinedCall = nullptr; // ... but for a declined call that may reach it + std::size_t* valueRef = nullptr; // ... but for a table/field/argument holding it as a value +}; +template +inline void appendDeadCodeRows( std::vector& regs, const IngestResult& ing, const Graph& g, const Snapshot& base, std::string_view root, + const std::vector& keyByNode, ExistedFn&& existedAtBaseline, StampFn&& stampLoc, DeadExclusionCounts excluded ) +{ + const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once + const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config + const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); + const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); + const ValueRefIndex valueRefs( ing ); // reference-as-value round: the --dead-code verb's rule + for( NodeId i = 0; i < ing.symbols.size(); ++i ) + { + if( i >= g.canonId.size() || g.canonId[i].empty() ) + { + continue; + } + bool macroExempt = false; + if( !isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch, ¯oExempt ) ) + { + if( macroExempt && excluded.registerMacro ) + { + ++( *excluded.registerMacro ); // P2.2: would be dead-code but for the macro exemption — disclosed count + } + continue; + } + if( declinedCallMayReach( g, i ) ) + { + if( excluded.declinedCall ) + { + ++( *excluded.declinedCall ); // would be dead-code but for a declined call that may reach it — disclosed count + } + continue; + } + if( valueRefs.isValueReferenced( i ) ) // checked LAST: one entity, one reason (the --dead-code verb's order) + { + if( excluded.valueRef ) + { + ++( *excluded.valueRef ); // a table, field or argument holds it as a value — disclosed count + } + continue; + } + if( !std::binary_search( base.dead.begin(), base.dead.end(), keyByNode[i] ) ) + { + regs.push_back( { "dead-code", g.canonId[i], 0, 0, keyByNode[i], false, {}, + !existedAtBaseline( keyByNode[i] ) } ); // origin: born uncalled (new) vs lost its last caller (preexisting) + stampLoc( i ); + } + } +} + // current state vs baseline → only what got worse. // complexity/verbosity/nesting/params: a symbol the change pushed/kept OVER the bar (now > baseline AND // now > BAR; a NEW symbol counts with baseline 0). Each is a per-symbol metric whose overloads share a @@ -8560,7 +9029,8 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap std::size_t* apiNewSurfaceOut = nullptr, // Q-DIAL-4: the api-surface new-symbol COUNT that replaced N never-gating rows std::vector* cloneIdiomsOut = nullptr, // every CURRENT-tree clone group's (hash, idiom), for the legacy-ack backfill std::size_t* declinedCallExcludedOut = nullptr, // symbols kept out of dead-code ONLY by a declined call (declinedCallMayReach) - std::size_t* valueRefExcludedOut = nullptr ) // ... ONLY because a table/field/argument holds them as a VALUE + std::size_t* valueRefExcludedOut = nullptr, // ... ONLY because a table/field/argument holds them as a VALUE + const KindScope& scope = {} ) // qd-memory-degrade: the kinds to report (see computeSnapshot) { if( declinedCallExcludedOut ) { @@ -8832,8 +9302,11 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // ran TWICE per call (~46 % of the whole verb was redundant recompute). Compute each pass ONCE here and let // both consumers read the same vectors. Byte-identical BY CONSTRUCTION: both functions are deterministic // pure functions, so their single result is field-for-field the value both call sites received before. - const std::vector exactClones = findClones( ing, int( kMinCloneTokens ) ); - const std::vector type3Clones = findClonesType3( ing, int( kMinCloneTokens ) ); + // qd-memory-degrade: the clone passes feed only the two clone kinds (and the --quality-ack backfill export, which is + // never asked with a kind mask) — not run when neither is selected, the largest saving a kind selection buys. + const bool wantClones = ( scope.kinds & ( kindBit( "duplication" ) | kindBit( "new-clone-of-reused-helper" ) ) ) != 0 || cloneIdiomsOut != nullptr; + const std::vector exactClones = wantClones ? findClones( ing, int( kMinCloneTokens ) ) : std::vector{}; + const std::vector type3Clones = wantClones ? findClonesType3( ing, int( kMinCloneTokens ) ) : std::vector{}; // duplication: a clone group (exact Type-1/2 OR gapped Type-3) whose member-set is not in the // baseline set. Both passes hash by the same sorted-member-canonId identity, so a change that @@ -8994,48 +9467,10 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap reportNewClones( exactClones, exactIdioms ); reportNewClones( type3Clones, type3Idioms ); - const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once - const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config - const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); - const std::vector pythonDispatch = pythonDispatchedMethodIds( ing, g ); - const ValueRefIndex valueRefs( ing ); // reference-as-value round: the --dead-code verb's rule - for( NodeId i = 0; i < ing.symbols.size(); ++i ) + if( kindSelected( scope.kinds, "dead-code" ) ) // qd-memory-degrade: the pass and its four evidence structures, only for its kind { - if( i >= g.canonId.size() || g.canonId[i].empty() ) - { - continue; - } - bool macroExempt = false; - if( !isDeadCandidate( ing, g, i, topLevelCallees, macroIds, pythonDispatch, ¯oExempt ) ) - { - if( macroExempt && registerMacroExcludedOut ) - { - ++( *registerMacroExcludedOut ); // P2.2: would be dead-code but for the macro exemption — disclosed count - } - continue; - } - if( declinedCallMayReach( g, i ) ) - { - if( declinedCallExcludedOut ) - { - ++( *declinedCallExcludedOut ); // would be dead-code but for a declined call that may reach it — disclosed count - } - continue; - } - if( valueRefs.isValueReferenced( i ) ) // checked LAST: one entity, one reason (the --dead-code verb's order) - { - if( valueRefExcludedOut ) - { - ++( *valueRefExcludedOut ); // a table, field or argument holds it as a value — disclosed count - } - continue; - } - if( !std::binary_search( base.dead.begin(), base.dead.end(), keyByNode[i] ) ) - { - regs.push_back( { "dead-code", g.canonId[i], 0, 0, keyByNode[i], false, {}, - !existedAtBaseline( keyByNode[i] ) } ); // origin: born uncalled (new) vs lost its last caller (preexisting) - stampLoc( i ); - } + appendDeadCodeRows( regs, ing, g, base, root, keyByNode, existedAtBaseline, stampLoc, + DeadExclusionCounts{ registerMacroExcludedOut, declinedCallExcludedOut, valueRefExcludedOut } ); } // api-surface (contract drift) — B10.2e TIERED into two shapes on the PUBLIC/exported surface: @@ -9173,7 +9608,9 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // code that did not exist at the baseline — so it is marked origin="new-symbol" on every row and never // gates, whatever symbol it lands in (the same way short-horizon-churn is preexisting by construction). { - const ConstructCounts nowCounts = constructCountsBySym( ing, root ); + // qd-memory-degrade: the construct pass only for its two kinds — without them every count reads 0 and no row is made + const ConstructCounts nowCounts = ( scope.kinds & ( kindBit( "error-masking" ) | kindBit( "placeholder" ) ) ) != 0 + ? constructCountsBySym( ing, root ) : ConstructCounts{}; const auto countIn = []( const gtl::btree_map& m, std::uint64_t key ) { const auto it = m.find( key ); @@ -9211,7 +9648,10 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap } } - appendDefectShapeRows( regs, ing, g, base, root, existedAtBaseline, stampLoc ); // the twelfth kind (see its doc) + if( kindSelected( scope.kinds, "defect-shape" ) ) + { + appendDefectShapeRows( regs, ing, g, base, root, existedAtBaseline, stampLoc, scope.macroOverride ); // the twelfth kind (see its doc) + } // ── §D#4-2 short-horizon churn (GitClear +15% "new code rewritten within two weeks") ─────────────────── // A symbol flags iff THREE independent gates all hold (signal-to-noise round, 2026-07-13): @@ -9231,7 +9671,9 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // Git access uses computeDelta's own `root` (= cfg.rootPath on the CLI, the real repo root over MCP); // no git / no HEAD / no obtainable window ref → this kind simply reports nothing (degrade, precision-first). { - const std::vector commitCounts = gitFileCommitCountsInDayWindow( std::string( root ), ing, kShortHorizonDays ); + // qd-memory-degrade: no window mining when the kind is not asked for (no count reaches the threshold, so no row) + const std::vector commitCounts = kindSelected( scope.kinds, "short-horizon-churn" ) + ? gitFileCommitCountsInDayWindow( std::string( root ), ing, kShortHorizonDays ) : std::vector{}; const bool anyChurn = std::any_of( commitCounts.begin(), commitCounts.end(), []( std::uint32_t c ){ return c >= kShortHorizonMinCommits; } ); if( anyChurn ) @@ -9446,6 +9888,13 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap reportReusedClones( type3Clones, type3Idioms ); } + // qd-memory-degrade: only the selected kinds' rows. A pass shared by two kinds (error-masking and placeholder, the + // two clone kinds) or read by another kind (params, which api-surface's one-fact-one-row rule reads) ran for the + // selected one; its other rows are dropped here, so a selected kind's rows are exactly its rows in a full answer. + if( scope.kinds != kAllKinds ) + { + std::erase_if( regs, [ &scope ]( const Regression& r ) { return !kindSelected( scope.kinds, r.kind ); } ); + } std::sort( regs.begin(), regs.end(), []( const Regression& a, const Regression& b ) { return a.kind != b.kind ? a.kind < b.kind : a.sym < b.sym; } ); return regs; diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 7a86e8bbf..af3575134 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -70,6 +70,16 @@ std::string noBaselineFatalMessage( const std::string& baselineFile, const rw::q // `sameRef` is the A==B case. It is a legal question with an empty answer, and it is answered by materializing // ONE tree and comparing it with itself rather than by a special-cased early return: the empty delta then // falls out of the ordinary machinery instead of being asserted by a branch nobody re-tests. +// qd-memory-degrade: a memory-guard stop in one phase of the range form. Recorded, never printed where it happens: the +// form first tries the changed files, and only if those do not fit either does it say anything (refPairStopRefusal). +struct PhaseStop +{ + bool stopped = false; + bool hard = false; // the hard line (the limit itself); else a soft stop (soft below) + std::string where; // "ingest of the base commit", "graph of the target commit", … + rw::MemoryStop soft; // a soft stop's cause, copied off the cut ingest +}; + struct RefPairDelta { rw::quality::TmpTreeGuard targetGuard; // declared BEFORE the tree so teardown outlives its use @@ -82,72 +92,118 @@ struct RefPairDelta std::string rangeSpan; // R1: ".." — the range the rename // record is read over, against the REAL repo (both trees // here are temp dirs with no history of their own) + // qd-memory-degrade — which kinds this answer runs, and, when the full trees did not fit, what it answers from instead + rw::quality::KindMask selected = rw::quality::kAllKinds; // --quality-kinds (every kind when absent) + rw::quality::KindMask kinds = rw::quality::kAllKinds; // the kinds actually run: selected, less what the changed files cannot decide + bool degraded = false; // answered from both commits' CHANGED files (ingest_scope="changed-files") + std::string memoryStop; // after a stop: "crawl" | "parse" | "hard"; empty when the projection decided + bool memoryPressure = false; // the stop was the OS pressure signal + std::string stopWhere; // the stop's phase, as its sentence names it + std::uint64_t needBytes = 0; // the projected need (the projection decided and measured it) + std::vector baseMacros; // the base commit's whole C-family macro-name set, when its full tree was read + bool haveBaseMacros = false; + std::vector targetFiles; // the target commit's files, for --scope's test (degraded + scoped only) + std::string baseSha, targetSha; + PhaseStop stop; // the guard's stop in a full tree, when there was one + std::uint64_t ingestPeak = 0; // this process's peak at the end of the base commit's ingest (the projection's input) const rw::quality::RefTree& target() const noexcept { return targetTree; } + const std::vector* macroOverride() const noexcept { return degraded && haveBaseMacros ? &baseMacros : nullptr; } + const std::vector* scopeFiles() const noexcept { return degraded ? &targetFiles : nullptr; } }; -// #387: what loadRefPairDelta prints and returns when one commit's tree could not be scored. A memory-guard stop is named -// as one — exit 5 and the guard's own line — because "could not materialize or parse" would send the reader looking for -// a broken commit; anything else is the environment, exit 1, with the sha named. -int refPairLoadRefusal( rw::quality::RefTreeLoad load, const rw::quality::RefTree& tree, const std::string& sha, std::string_view side ) +// #387: what loadRefPairDelta prints and returns when one commit's tree could not be materialized at all — the environment, +// exit 1, with the sha named. A memory-guard stop is not this: it is a PhaseStop, and the range form falls to the changed +// files (qd-memory-degrade). +int refPairUnavailable( const std::string& sha ) { - using namespace rw; - if( load == quality::RefTreeLoad::OverHardLimit ) + rw::emitTo( stderr, "ripwire: --quality-delta: could not materialize or parse the tree at {}\n", sha.c_str() ); + return 1; +} + +// The load outcome of one tree → a PhaseStop, or an exit code for the environment. Loaded = nothing to record. +std::optional recordLoad( rw::quality::RefTreeLoad load, const rw::quality::RefTree& tree, const std::string& sha, std::string_view side, PhaseStop& stop ) +{ + using rw::quality::RefTreeLoad; + if( load == RefTreeLoad::Loaded ) { - memguard::answerStops(); - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: over the memory guard's hard limit after a commit's ingest, before its graph — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( "ingest of the " + std::string( side ) + " commit" ) ); - return 5; + return std::nullopt; } - if( load == quality::RefTreeLoad::MemoryStopped ) + if( load == RefTreeLoad::Unavailable ) { - memguard::answerStops(); // this refusal answers for the stop the commit's ingest recorded - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: the memory guard cut a commit's ingest, so nothing is scored — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::verbRefusalLine( tree.ing.memoryStop, "--quality-delta=A..B" ) ); - return 5; + return refPairUnavailable( sha ); } - rw::emitTo( stderr, "ripwire: --quality-delta: could not materialize or parse the tree at {}\n", sha.c_str() ); - return 1; + stop.stopped = true; + stop.hard = load == RefTreeLoad::OverHardLimit; + stop.where = "ingest of the " + std::string( side ); + stop.soft = tree.ing.memoryStop; + return std::nullopt; } // #387: the hard line between the range form's phases. main() checks it after the working-tree ingest and graph build, -// which this form now dispatches before, so these are its only readings, in order: after each commit's ingest -// (loadRefTree, before the graph is built) and after its graph; for the base, after its snapshot and again once its index -// is released. Over the line nothing answers: exit 5 and one line naming the phase. -bool refPairOverHardLimit( std::string_view phase ) +// which this form dispatches before, so these are its only readings, in order: after each commit's ingest (loadRefTree, +// before the graph is built) and after its graph; for the base, after its snapshot and again once its index is +// released. Over the line the phase is RECORDED as a stop and nothing is printed (qd-memory-degrade: the changed files +// answer next, and refPairStopRefusal speaks only if they cannot) — where #387's refPairOverHardLimit refused on the spot. +bool hardLineStops( std::string_view phase, PhaseStop& stop ) { - using namespace rw; - if( !memguard::overHardLimit() ) + if( !rw::memguard::overHardLimit() ) { return false; } - memguard::answerStops(); - DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: over the memory guard's hard limit between its phases — exit 5, one stderr line" ); - rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( phase ) ); + stop.stopped = true; + stop.hard = true; + stop.where = std::string( phase ); return true; } +// The one line and exit 5 when the range form cannot answer at all: the stop (or the projection) that sent it to the +// changed files, and why those did not answer either. Every stop recorded so far is answered for by this refusal. +int refPairStopRefusal( const PhaseStop& stop, const RefPairDelta& refs, std::string_view why ) +{ + using namespace rw; + memguard::answerStops(); + DISCLOSE( Diagnostics::answerRefused, "--quality-delta=A..B: the full trees did not fit and the changed files did not answer either — exit 5, one stderr line" ); + if( stop.stopped && stop.hard ) + { + rw::emitTo( stderr, "ripwire: {}\n", memguard::hardStopLine( stop.where, std::string( why ) + "; " ) ); + } + else if( stop.stopped ) + { + rw::emitTo( stderr, "ripwire: the memory guard stopped the {} {} in the {}; --quality-delta=A..B cannot answer from a partial index, and {} — {}\n", + memguard::phaseName( stop.soft.phase ), memguard::stopCause( stop.soft ), stop.where, why, memguard::kOverride ); + } + else + { + rw::emitTo( stderr, "ripwire: memory limit {} ({}): the target phase of --quality-delta=A..B was projected to need ~{}; {}; stopped cleanly without an answer — {}\n", + memguard::limitSpelling( memguard::limits().hardBytes ), memguard::limitSourceClause(), memguard::limitSpelling( refs.needBytes ), why, memguard::kOverride ); + } + return 5; +} + // #387: the base commit lives only in this function. Its snapshot is everything the comparison needs from it, so its -// index, graph and temp dir are released on return, before the target tree is materialized. Exit code on a refusal. -std::optional scoreBaseCommit( const std::string& root, const std::string& sha, const rw::Config& cfg, rw::quality::Snapshot& out ) +// index, graph and temp dir are released on return, before the target tree is materialized. qd-memory-degrade: it also +// keeps the base tree's C-family macro NAMES (defect-shape's one tree-wide input, names only) and reads this process's +// peak at the end of the ingest, for the projection. Exit code on an environment failure; a memory stop goes to `stop`. +std::optional scoreBaseCommit( const std::string& root, const std::string& sha, const rw::Config& cfg, RefPairDelta& refs ) { using namespace rw; quality::TmpTreeGuard baseGuard; quality::RefTree baseTree; const quality::RefTreeLoad load = quality::loadRefTree( root, sha, cfg.excludes, cfg.maxFileBytes, "qdpair-base", baseGuard, baseTree ); - if( load != quality::RefTreeLoad::Loaded ) - { - return refPairLoadRefusal( load, baseTree, sha, "base" ); - } - if( refPairOverHardLimit( "graph of the base commit" ) ) + refs.ingestPeak = os::mem_footprint_peak(); + if( const std::optional failed = recordLoad( load, baseTree, sha, "base commit", refs.stop ) ) { - return 5; + return failed; } - out = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root ); - if( refPairOverHardLimit( "snapshot of the base commit" ) ) + if( refs.stop.stopped || hardLineStops( "graph of the base commit", refs.stop ) ) { - return 5; + return std::nullopt; } + refs.baseSnapshot = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root, nullptr, { refs.selected } ); + refs.baseMacros = quality::cFamilyMacroNames( baseTree.ing ); + refs.haveBaseMacros = true; + (void)hardLineStops( "snapshot of the base commit", refs.stop ); return std::nullopt; } @@ -156,29 +212,181 @@ std::optional loadTargetCommit( const std::string& root, const std::string& { using namespace rw; const quality::RefTreeLoad load = quality::loadRefTree( root, sha, cfg.excludes, cfg.maxFileBytes, "qdpair-target", out.targetGuard, out.targetTree ); - if( load != quality::RefTreeLoad::Loaded ) + if( const std::optional failed = recordLoad( load, out.targetTree, sha, "target commit", out.stop ) ) { - return refPairLoadRefusal( load, out.targetTree, sha, "target" ); + return failed; } - if( refPairOverHardLimit( "graph of the target commit" ) ) + if( out.stop.stopped || hardLineStops( "graph of the target commit", out.stop ) ) { - return 5; + return std::nullopt; } if( out.sameRef ) { - out.baseSnapshot = quality::computeSnapshot( out.targetTree.ing, out.targetTree.g, out.targetTree.root ); - if( refPairOverHardLimit( "snapshot of the base commit" ) ) + out.baseSnapshot = quality::computeSnapshot( out.targetTree.ing, out.targetTree.g, out.targetTree.root, nullptr, { out.selected } ); + (void)hardLineStops( "snapshot of the base commit", out.stop ); + } + return std::nullopt; +} + +// qd-memory-degrade — the stderr line of a degraded answer: the limit, why the full trees were not read, the kinds it +// skipped, the replay. One line, beside the gating line, so a caller reading only $? and stderr knows the answer is partial. +std::string degradedStderrLine( const RefPairDelta& refs, rw::quality::KindMask memorySkipped, std::string_view kindsNext ) +{ + using namespace rw; + const std::uint64_t limit = memguard::limits().hardBytes; + std::string why = refs.memoryStop.empty() + ? "the target phase was projected to need ~" + memguard::limitSpelling( refs.needBytes ) + : refs.memoryStop == "hard" ? "the memory limit was reached during the " + refs.stopWhere + "; the full need was not measured" + : "the memory guard stopped the " + refs.stopWhere + ( refs.memoryPressure ? " under critical system memory pressure" : "" ) + "; the full need was not measured"; + std::string line = "memory limit " + ( limit == 0 ? std::string( "unknown" ) : memguard::limitSpelling( limit ) ) + " (" + std::string( memguard::limitSourceClause() ) + + "): --quality-delta=A..B answered from the changed files only (" + why + "); skipped " + quality::kindList( memorySkipped, ", " ); + if( !kindsNext.empty() ) + { + line += " — run them with " + std::string( kindsNext ); + if( refs.memoryStop.empty() ) { - return 5; + const std::uint64_t machine = memguard::machineMemoryBytes(); + if( machine != 0 && refs.needBytes >= machine ) + { + line += " (more than this machine's " + memguard::limitSpelling( machine ) + ": recoverable only on a larger machine)"; + } } + else + { + line += " (its --max-memory is a guess, twice the limit: no need was measured)"; + } + } + return line; +} + +// qd-memory-degrade — THE CHANGED-FILES ANSWER. The full trees are gone (released, or never read); both commits' changed +// files are ingested alone (never through a cache) and the base snapshot is taken from the base's. A kind runs here only +// if its rows are decided by those files: the LOCAL kinds always, defect-shape when the changed files define the same +// C-family macro names on both sides (then the tree's set is unchanged) and either the base's whole set was read or no +// changed file is C++ (the only scanner that reads it). Exit code on a refusal; nullopt when `out` is ready. +std::optional loadChangedFiles( const std::string& root, const rw::Config& cfg, RefPairDelta& out, const PhaseStop& stop ) +{ + using namespace rw; + if( cfg.qualityAck ) + { + return refPairStopRefusal( stop, out, "--quality-ack needs every kind's rows, so nothing was acked (the ledger is untouched)" ); + } + out.kinds = out.selected & static_cast( ~quality::kWholeTreeKinds ); + if( out.kinds == 0 ) + { + return refPairStopRefusal( stop, out, "the selected kinds are whole-tree kinds, which the changed files cannot decide" ); + } + if( memguard::overHardLimit() ) + { + return refPairStopRefusal( stop, out, "the changed files alone did not fit either" ); + } + quality::ChangedFiles changed = quality::changedFilesBetween( root, out.baseSha, out.targetSha, !cfg.qualityScope.empty() ); + if( !changed.ok ) + { + memguard::answerStops(); + rw::emitTo( stderr, "ripwire: --quality-delta={}..{}: the full trees did not fit the memory limit, and git could not list the changed files to answer from\n", + out.baseSha.c_str(), out.targetSha.c_str() ); + return 1; + } + if( !changed.refusal.empty() ) + { + return refPairStopRefusal( stop, out, "the changed files cannot stand in for the trees: " + changed.refusal ); + } + PhaseStop subsetStop; + quality::TmpTreeGuard baseGuard; + quality::RefTree baseTree; + quality::RefTreeLoad load = quality::loadRefSubset( root, out.baseSha, changed.base, cfg.excludes, cfg.maxFileBytes, "qdpair-base-changed", baseGuard, baseTree ); + if( const std::optional failed = recordLoad( load, baseTree, out.baseSha, "changed files of the base commit", subsetStop ) ) + { + return failed; + } + load = subsetStop.stopped ? quality::RefTreeLoad::Loaded + : quality::loadRefSubset( root, out.targetSha, changed.target, cfg.excludes, cfg.maxFileBytes, "qdpair-target-changed", out.targetGuard, out.targetTree ); + if( const std::optional failed = recordLoad( load, out.targetTree, out.targetSha, "changed files of the target commit", subsetStop ) ) + { + return failed; + } + if( subsetStop.stopped ) + { + return refPairStopRefusal( subsetStop, out, "the changed files alone were cut too" ); + } + // defect-shape's one tree-wide input (see the header comment of this function) + const std::vector baseChanged = quality::cFamilyMacroNames( baseTree.ing ); + const std::vector targetChanged = quality::cFamilyMacroNames( out.targetTree.ing ); + const auto anyCpp = []( const IngestResult& ing ) { return std::any_of( ing.files.begin(), ing.files.end(), []( const std::string& f ) { return langOfPath( f ) == Lang::Cpp; } ); }; + if( baseChanged != targetChanged || ( !out.haveBaseMacros && ( anyCpp( baseTree.ing ) || anyCpp( out.targetTree.ing ) ) ) ) + { + out.kinds &= static_cast( ~quality::kindBit( "defect-shape" ) ); + } + out.degraded = true; + out.baseSnapshot = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root, nullptr, { out.kinds, out.macroOverride() } ); + if( !cfg.qualityScope.empty() ) + { + out.targetFiles = std::move( changed.targetTree ); } + out.memoryStop = !stop.stopped ? std::string() : stop.hard ? std::string( "hard" ) : std::string( memguard::phaseName( stop.soft.phase ) ); + out.memoryPressure = stop.stopped && !stop.hard && stop.soft.byPressure; + out.stopWhere = stop.stopped ? ( stop.hard ? stop.where : std::string( memguard::phaseName( stop.soft.phase ) ) + " of the " + stop.where.substr( stop.where.find( "the " ) + 4 ) ) : std::string(); + memguard::answerStops(); // the degraded answer answers for the stop that sent it here return std::nullopt; } +// qd-memory-degrade: the two full trees, as #387 reads them — the base scored and released, then the target — with the +// projection between them. A stop in either is recorded in out.stop; `projectedNoFit` says the projection chose the +// changed files before the target was read. Exit code on an environment failure only. +std::optional loadFullTrees( const std::string& root, const rw::Config& cfg, RefPairDelta& out, bool& projectedNoFit ) +{ + using namespace rw; + if( !out.sameRef ) + { + const std::uint64_t before = os::mem_footprint(); + if( const std::optional failed = scoreBaseCommit( root, out.baseSha, cfg, out ) ) + { + return failed; + } + // #387: the base tree's index, graph and temp dir were released when scoreBaseCommit returned. + if( out.stop.stopped || hardLineStops( "base commit's snapshot, with its index released", out.stop ) ) + { + return std::nullopt; + } + const std::uint64_t basePeak = os::mem_footprint_peak(); + const std::uint64_t after = memguard::measuredFootprint(); + const memguard::TargetProjection proj = memguard::projectTargetPhase( before, out.ingestPeak, basePeak, after ); + out.needBytes = proj.needBytes; + projectedNoFit = !proj.fits; + if( std::getenv( "RIPWIRE_STATS" ) ) // the projection's own readings, the calibration's instrument (kProjectionScalePct) + { + rw::emitTo( stderr, "ripwire: memory-projection before={} ingest_peak={} base_peak={} after={} need={} measured={} fits={}\n", + memguard::limitSpelling( before ), memguard::limitSpelling( out.ingestPeak ), memguard::limitSpelling( basePeak ), + memguard::limitSpelling( after ), memguard::limitSpelling( proj.needBytes ), proj.measured ? 1 : 0, proj.fits ? 1 : 0 ); + } + if( projectedNoFit ) + { + return std::nullopt; + } + } + return loadTargetCommit( root, out.targetSha, cfg, out ); +} + +// A cut target is never scored: its index, graph and temp dir go before the changed files are read, and so does the full +// base snapshot (the changed files' own base snapshot replaces it). +void releaseFullTrees( RefPairDelta& out ) +{ + out.targetTree = {}; + out.baseSnapshot = {}; + rw::quality::TmpTreeGuard dropped; + std::swap( dropped.p, out.targetGuard.p ); // its destructor removes the cut tree's temp dir now +} + // Resolve the spec and materialize both sides. Returns an EXIT CODE on any refusal or environment failure // (already reported on stderr), or nullopt when `out` is ready to compare. Splitting user error from // environment failure is the same line --dmm's handler draws: a revision that does not resolve is a typo the // caller can fix, everything else is the machine. +// +// qd-memory-degrade — THE LADDER. Full trees first, exactly as before, so a run that fits is the run it always was. A +// memory-guard stop in either commit's full tree (a soft crawl/parse stop or the hard line), or a projection after the +// base phase that says the target phase will not fit, releases what was loaded and answers from both commits' changed +// files instead (loadChangedFiles); exit 5 only when those do not fit or cannot stand in for the trees. std::optional loadRefPairDelta( const std::string& root, std::string_view spec, const rw::Config& cfg, RefPairDelta& out ) { using namespace rw; @@ -219,25 +427,25 @@ std::optional loadRefPairDelta( const std::string& root, std::string_view s rw::emitRaw( stderr, "ripwire: --quality-delta needs TWO commits (A..B); use the bare --quality-delta for the working tree\n" ); return 1; } + out.baseSha = ref.baseSha; + out.targetSha = ref.targetSha; + out.kinds = out.selected; // DISTINCT tags — see quality::loadRefTree's header. The base tree is gone before the target is extracted, but // a shared tag would still let one side's extraction delete the other's directory if that order ever changed. out.sameRef = ( ref.baseSha == ref.targetSha ); - if( !out.sameRef ) + bool projectedNoFit = false; + if( const std::optional failed = loadFullTrees( root, cfg, out, projectedNoFit ) ) { - if( const std::optional refused = scoreBaseCommit( root, ref.baseSha, cfg, out.baseSnapshot ) ) + return failed; + } + if( out.stop.stopped || projectedNoFit ) + { + releaseFullTrees( out ); + if( const std::optional refused = loadChangedFiles( root, cfg, out, out.stop ) ) { return refused; } - // #387: the base tree's index, graph and temp dir were released when scoreBaseCommit returned. - if( refPairOverHardLimit( "base commit's snapshot, with its index released" ) ) - { - return 5; - } - } - if( const std::optional refused = loadTargetCommit( root, ref.targetSha, cfg, out ) ) - { - return refused; } // Both values are bare 40-char object names straight out of `rev-parse --verify` (resolveRefSpec accepts @@ -280,6 +488,7 @@ struct DeltaBasis std::size_t valueRefExcluded = 0; // dead-code exemption: held as a VALUE (valuerefindex.h) std::size_t apiNewSurface = 0; // Q-DIAL-4: new PUBLIC symbols this change added — the count that replaced one never-gating row each std::size_t acksBadLines = 0; // 2026-09-06: .ripwire_quality_acks lines skipped as unparseable (disclosed on the root) + bool ackHealingSkipped = false; // qd-memory-degrade: a changed-files answer with a non-empty ledger (also_skipped ack-healing) // #228: WHICH basis produced baseSel.snapshot when the marker is one of the git-HEAD family, and WHY when // the fast one was refused for a reason the caller can act on. nullptr = the attribute is absent, which is // the ordinary archived comparison every git-HEAD marker has always meant. See quality::HeadBasis. @@ -291,6 +500,33 @@ struct DeltaBasis std::vector cloneIdioms; }; +// The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the floor, tree B the judged +// tree, the ledger healed against it, the delta taken. +void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +{ + using namespace rw; + out.baseSel.snapshot = std::move( refs.baseSnapshot ); + out.baseSel.marker = "ref-pair"; + out.deltaRoot = refs.target().root; + // R1 IDENTITY. The SPAN is what makes this honest here: both trees are materialized OUT of the repo + // into temp dirs, so asking them about renames answers "not a git repo" — the renames are recorded in + // the REAL repo, over exactly the range this comparison is about. `root` (the repo) supplies the + // record; `refs.rangeSpan` scopes it to the pair. Everything else is identical to the working-tree + // form, which is the point of having one healIdentity. + // qd-memory-degrade: over the changed files the two tree-wide rescues are skipped (healIdentity's header) — said + // as also_skipped ack-healing when the ledger holds any row they could have re-filed. The clone-idiom export is + // asked for only where it is read (the --quality-ack backfill), so a kind selection without the clone kinds skips + // the clone passes. + out.acks = quality::readAckRecords( quality::acksPath( root ) ); + out.ackHealingSkipped = refs.degraded && !out.acks.empty(); + out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, refs.target().ing, refs.target().g, + out.deltaRoot, root, cfg.qualityAck, refs.rangeSpan, /*treeWideRescues=*/!refs.degraded ); + out.regs = quality::computeDelta( refs.target().ing, refs.target().g, out.baseSel.snapshot, + out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface, + cfg.qualityAck ? &out.cloneIdioms : nullptr, &out.declinedCallExcluded, &out.valueRefExcluded, + quality::KindScope{ refs.kinds, refs.macroOverride() } ); +} + // Returns an EXIT CODE when there is nothing to compare against (already reported), nullopt when `out` holds // a usable comparison. `refs` is the CALLER's because it owns the materialized trees' teardown, and those // trees must outlive every read of `out.regs`. @@ -312,20 +548,7 @@ std::optional resolveDeltaBasis( const MainDispatch& d, const std::string& // sidecar's whole contract is "pinned at the current HEAD", which says nothing about a pair of // arbitrary commits — and the bare form's self-heal deleting a user's pinned floor as a side effect // of a wave-level measurement would be a side effect nobody asked for. - out.baseSel.snapshot = std::move( refs.baseSnapshot ); - out.baseSel.marker = "ref-pair"; - out.deltaRoot = refs.target().root; - // R1 IDENTITY. The SPAN is what makes this honest here: both trees are materialized OUT of the repo - // into temp dirs, so asking them about renames answers "not a git repo" — the renames are recorded in - // the REAL repo, over exactly the range this comparison is about. `root` (the repo) supplies the - // record; `refs.rangeSpan` scopes it to the pair. Everything else is identical to the working-tree - // form, which is the point of having one healIdentity. - out.acks = quality::readAckRecords( quality::acksPath( root ) ); - out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, refs.target().ing, refs.target().g, - out.deltaRoot, root, cfg.qualityAck, refs.rangeSpan ); - out.regs = quality::computeDelta( refs.target().ing, refs.target().g, out.baseSel.snapshot, - out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface, - &out.cloneIdioms, &out.declinedCallExcluded, &out.valueRefExcluded ); + refPairBasis( cfg, root, refs, out ); return std::nullopt; } @@ -472,8 +695,11 @@ std::optional expandScopeDiff( rw::quality::Scope& scope, bool refPair, con // that includes only ingest.h. Returns an exit code (already reported on stderr) or nullopt when the scope // is usable. `judged` is the tree the findings were taken from — the working tree, or tree B in the // ref-pair form — because "names nothing indexed" has to be asked of the corpus that was actually measured. +// `commitFiles` (qd-memory-degrade): a changed-files answer indexed only the changed files, so a scope naming an UNCHANGED +// file is not a typo there — the test reads the target commit's whole file list instead (its root-relative paths). std::optional refuseUnusableScope( const rw::Config& cfg, const rw::quality::Scope& scope, - const rw::IngestResult& judged, std::string_view deltaRoot ) + const rw::IngestResult& judged, std::string_view deltaRoot, + const std::vector* commitFiles = nullptr ) { if( !scope.active() ) { @@ -499,6 +725,10 @@ std::optional refuseUnusableScope( const rw::Config& cfg, const rw::quality return std::nullopt; } } + if( commitFiles != nullptr && std::any_of( commitFiles->begin(), commitFiles->end(), [ & ]( const std::string& p ) { return scope.matchesPath( p ); } ) ) + { + return std::nullopt; + } rw::emitTo( stderr, "ripwire: --scope={} matches no indexed path — an exit 0 under a scope that owns nothing is a failure, not a clean tree\n" " (patterns are matched ROOT-RELATIVE, the same spelling p= prints: --scope=src or --scope=src/quality.h, not an absolute path)\n", std::string_view( cfg.qualityScope.data(), cfg.qualityScope.size() ) ); return 1; @@ -920,8 +1150,39 @@ struct QualityDeltaLegendParts bool anyDeclinedCallExcluded = false; // declined-call-excluded= is on the root bool anyValueRefExcluded = false; // value-ref-excluded= is on the root bool staleCollapsed = false; // idea #11: stale_by_kind=/stale_next= are on the root + bool kindsSkipped = false; // qd-memory-degrade: kinds_skipped=/skipped_why= are on the root + bool degraded = false; // ... ingest_scope= memory_limit= memory_need= also_skipped= kinds_next= + bool memoryStopped = false; // ... memory_stop= (and memory_pressure= when it was pressure) + bool memoryPeak = false; // ... memory_peak= (any range answer whose peak went over the limit) }; +// qd-memory-degrade — present-only: each rides only with the attributes it defines. No double dash anywhere in them +// (they sit inside the legend comment): a flag is named by its words, never by its spelling. +inline constexpr const char* kQdKindsSkippedLegend = + "kinds_skipped= names the kinds this answer did NOT run (alphabetical): no row of them is in this document, which is not a " + "statement that none exists, and regressions=, gating= and every count cover only the kinds that ran; a count only a " + "skipped kind feeds (register-macro-excluded=) is absent, never 0. skipped_why=\"selector\": the caller chose the kinds " + "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=. "; +inline constexpr const char* kQdDegradedLegend = + "ingest_scope=\"changed-files\": the two full trees did not fit the memory guard's limit (memory_limit=, the max-memory value " + "that sets it), so both commits' CHANGED files were ingested alone and the kinds those files decide ran with the same code and " + "bars as a full answer: their rows are the full answer's rows of those kinds, and the skipped kinds can only ADD rows. " + "memory_need= is the full answer's projected need, from the base commit's measured peak (a run within that margin of the " + "limit may be answered this way though it would have fit), or \"unmeasured\" after a stop. also_skipped= names the " + "whole-answer facts not computed because they read the whole target tree: config-warnings (the .ripwire_config check), " + "stale-acks (no stale= and no stale sa rows), ack-healing (acks matched by their recorded key only: a row that only a " + "legacy-scheme re-key or a content-id match would hide in the full answer is shown here, and can gate). kinds_next= is the " + "call that runs exactly the skipped kinds over the same two commits, at a max-memory just above memory_need= (after a stop: " + "twice the limit, a guess, since no need was measured). EXIT 2 when a row of a kind that ran gates (final: the skipped kinds " + "only add rows); otherwise EXIT 6, a partial answer with no gating row, never 0. "; +inline constexpr const char* kQdMemoryStopLegend = + "memory_stop= names the guard's stop that sent this answer to the changed files: crawl (the crawl line, an eighth of the " + "limit grown) or parse (half the limit) is a soft line, memory_pressure=1 when the OS pressure signal stopped it instead, " + "hard is the limit itself; a soft stop does not show the full need exceeds the limit. "; +inline constexpr const char* kQdMemoryPeakLegend = + "memory_peak= is this process's peak footprint when it went OVER the memory limit: the answer is complete and its exit " + "code unchanged, but the last stretch of the comparison ran past the limit the guard holds between phases. "; + // A DEFINITION IS EMITTED WHEN THE THING IT DEFINES IS IN THE DOCUMENT. Nothing is dropped and no limit is // softened: a reader can never meet an undefined name, because a name and its sentence appear together or // not at all. See the constants above for what each section covers and why it is conditional. @@ -1051,6 +1312,22 @@ inline void emitQualityDeltaLegend( const QualityDeltaLegendParts& p, std::FILE* { std::fputs( kForeignAcksLegend, out ); } + if( p.kindsSkipped ) + { + std::fputs( kQdKindsSkippedLegend, out ); + } + if( p.degraded ) + { + std::fputs( kQdDegradedLegend, out ); + } + if( p.memoryStopped ) + { + std::fputs( kQdMemoryStopLegend, out ); + } + if( p.memoryPeak ) + { + std::fputs( kQdMemoryPeakLegend, out ); + } if( p.staleCollapsed ) { // idea #11: the collapse, named where its attributes are. No flag spelling (a double dash is illegal in a comment). @@ -1241,6 +1518,155 @@ int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFil // branches, one of which was 298 lines. That one body is now runQualityDelta below; the residual // runQualityViews keeps only --dead-code. ONE extraction, verbatim: the 298-line body is unsplit, because // its interior is a sequential pipeline over shared locals, not independent arms. +// qd-memory-degrade: --quality-kinds=K[,K] (range form only; cli.h refuses it elsewhere) — the kinds to run. The vocabulary +// is quality.h's, so an unknown name is refused here, before any tree is read. nullopt when absent or valid. +std::optional applyKindSelector( const rw::Config& cfg, RefPairDelta& refs ) +{ + if( cfg.qualityKinds.empty() ) + { + return std::nullopt; + } + std::string bad; + refs.selected = rw::quality::parseKindList( cfg.qualityKinds, bad ); + if( refs.selected != 0 ) + { + return std::nullopt; + } + rw::emitTo( stderr, "ripwire: --quality-kinds: '{}' is not a kind — name one or more of: {}\n", bad, rw::quality::kindList( rw::quality::kAllKinds, ", " ) ); + return 1; +} + +// L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the ledger against, so it +// judges none (also_skipped= says so). Under --quality-kinds only the selected kinds' acks are judged, against a snapshot of +// those kinds: a dead-code ack read against a snapshot that never computed the dead set would read finding-gone, which is +// false (R9). Every kind selected: the ledger and snapshot every range answer has always used. +std::vector refPairStaleAcks( const gtl::btree_map& acks, const RefPairDelta& refs ) +{ + using namespace rw; + if( refs.degraded ) + { + return {}; + } + if( refs.selected == quality::kAllKinds ) + { + return quality::computeStaleAcks( acks, quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root ) ); + } + gtl::btree_map selectedAcks; + for( const auto& [ mapKey, rec ] : acks ) + { + if( quality::kindSelected( refs.selected, std::string_view( rec.kind ).substr( 0, rec.kind.find( ':' ) ) ) ) // the kind, without an origin facet + { + selectedAcks.emplace( mapKey, rec ); + } + } + return selectedAcks.empty() ? std::vector{} + : quality::computeStaleAcks( selectedAcks, quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root, nullptr, { refs.selected } ) ); +} + +// qd-memory-degrade — what an answer that did not run every kind says about it, built ONCE for both emitters (the +// identityDisclosure rule: the two surfaces cannot disclose different things). Empty strings on a full answer, which then +// keeps every byte it had. kindsNext is the replay of the kinds memory skipped (empty when none). +struct KindsDisclosure +{ + std::string xml, json, kindsNext; + rw::quality::KindMask memorySkipped = 0; + std::uint64_t peakOver = 0; // memory_peak=: the peak, when it went over the limit (0 = absent) +}; +KindsDisclosure kindsDisclosure( const rw::Config& cfg, const RefPairDelta& refs, const DeltaBasis& basis, bool refPair ) +{ + using namespace rw; + KindsDisclosure out; + if( !refPair ) + { + return out; + } + const auto add = [ & ]( std::string_view name, const std::string& value, bool list ) + { + out.xml += " " + std::string( name ) + "=\"" + value + "\""; + if( !list ) + { + out.json += ",\"" + std::string( name ) + "\":\"" + rw::jsonesc::escapeMcp( value ) + "\""; + return; + } + out.json += ",\"" + std::string( name ) + "\":["; + for( std::size_t at = 0; at <= value.size(); ) + { + const std::size_t comma = std::min( value.find( ',', at ), value.size() ); + out.json += ( at == 0 ? "\"" : ",\"" ) + value.substr( at, comma - at ) + "\""; + at = comma + 1; + } + out.json += "]"; + }; + const quality::KindMask skipped = static_cast( quality::kAllKinds & ~refs.kinds ); + if( skipped != 0 ) + { + add( "kinds_skipped", quality::kindList( skipped, "," ), true ); + add( "skipped_why", refs.degraded ? "memory" : "selector", false ); + } + if( refs.degraded ) + { + out.memorySkipped = static_cast( refs.selected & ~refs.kinds ); + const std::uint64_t limit = memguard::limits().hardBytes; + add( "ingest_scope", "changed-files", false ); + if( limit != 0 ) + { + add( "memory_limit", memguard::limitSpelling( limit ), false ); + } + if( !refs.memoryStop.empty() ) + { + add( "memory_stop", refs.memoryStop, false ); + if( refs.memoryPressure ) + { + add( "memory_pressure", "1", false ); + } + } + add( "memory_need", refs.memoryStop.empty() ? memguard::limitSpelling( refs.needBytes ) : std::string( "unmeasured" ), false ); + add( "also_skipped", basis.ackHealingSkipped ? "ack-healing,config-warnings,stale-acks" : "config-warnings,stale-acks", true ); + if( out.memorySkipped != 0 ) + { + out.kindsNext = "--quality-delta=" + refs.baseSha + ".." + refs.targetSha + " --quality-kinds=" + quality::kindList( out.memorySkipped, "," ); + if( limit != 0 ) + { + out.kindsNext += " --max-memory=" + memguard::limitSpelling( memguard::replayLimitBytes( refs.needBytes, refs.memoryStop.empty() ) ); + } + if( !cfg.qualityScope.empty() ) + { + out.kindsNext += " " + rw::nextFlag( "--scope=", cfg.qualityScope ); // the same partition, so the union keeps its gating + } + add( "kinds_next", out.kindsNext, false ); + } + } + out.peakOver = memguard::peakOverLimit(); + if( out.peakOver != 0 ) + { + add( "memory_peak", memguard::limitSpelling( out.peakOver ), false ); + } + return out; +} + +// qd-memory-degrade: the stderr lines of kindsDisclosure's facts — the degraded answer's one line, and the peak's. +void emitMemoryLines( const KindsDisclosure& kd, const RefPairDelta& refs ) +{ + using namespace rw; + if( refs.degraded ) + { + rw::emitTo( stderr, "ripwire: {}\n", degradedStderrLine( refs, kd.memorySkipped, kd.kindsNext ) ); + } + if( kd.peakOver != 0 ) + { + rw::emitTo( stderr, "ripwire: memory peak {} went over the {} limit ({}): the answer is complete, but its last stretch ran past the limit the guard holds " + "between phases — {}\n", + memguard::limitSpelling( kd.peakOver ), memguard::limitSpelling( memguard::limits().hardBytes ), memguard::limitSourceClause(), memguard::kOverride ); + } +} + +// The exit code every --quality-delta emitter returns: 2 when a row gates; a changed-files answer with no gating row is 6 +// (a partial answer, never a clean pass — the kinds it skipped could still gate); otherwise 0. +int qualityDeltaExit( std::size_t gatingCount, const RefPairDelta& refs ) noexcept +{ + return gatingCount > 0 ? 2 : refs.degraded ? 6 : 0; +} + std::optional runQualityDelta( const MainDispatch& d ) { using namespace rw; @@ -1317,6 +1743,10 @@ std::optional runQualityDelta( const MainDispatch& d ) const bool refPair = !cfg.qualityDeltaRange.empty(); RefPairDelta refs; DeltaBasis basis; + if( const std::optional refused = applyKindSelector( cfg, refs ) ) + { + return refused; + } if( const std::optional refused = resolveDeltaBasis( d, baselineFile, refs, basis ) ) { return refused; @@ -1334,7 +1764,7 @@ std::optional runQualityDelta( const MainDispatch& d ) { return refused; } - if( const std::optional refused = refuseUnusableScope( cfg, scope, judged, deltaRoot ) ) + if( const std::optional refused = refuseUnusableScope( cfg, scope, judged, deltaRoot, refs.scopeFiles() ) ) { return refused; } @@ -1547,10 +1977,11 @@ std::optional runQualityDelta( const MainDispatch& d ) // R-I: "the CURRENT tree" is the JUDGED tree, which in the ref-pair form is tree B — checking the // ledger against the working tree there would report acks as stale because of edits that have nothing // to do with the comparison being made. + // qd-memory-degrade: refPairStaleAcks owns the two range-form departures (a changed-files answer judges none; a + // kind selection judges its own kinds' acks only). const std::vector staleAcks = acks.empty() ? std::vector{} - : quality::computeStaleAcks( acks, refPair - ? quality::computeSnapshot( refs.target().ing, refs.target().g, refs.target().root ) - : quality::computeSnapshot( ing, g, cfg.rootPath ) ); + : refPair ? refPairStaleAcks( acks, refs ) + : quality::computeStaleAcks( acks, quality::computeSnapshot( ing, g, cfg.rootPath ) ); // P1.4 — the family now carries THREE whys, and the third is counted SEPARATELY: stale= means // "this ack no longer applies", and a foreign ack applies fine — it is the SESSION that was wrong, @@ -1610,8 +2041,16 @@ std::optional runQualityDelta( const MainDispatch& d ) // registerMacroConfigWarningAttr just above runQualityViews for why the stderr wording lives there). // #387: the inert-name test reads the JUDGED tree's index, which in the ref-pair form is tree B — the working tree // is not indexed there at all (runQualityDeltaRangeBeforeIngest). - const rw::quality::RegisterMacroConfigDiagnostics configDiag = rw::quality::diagnoseRegisterMacroConfig( judged, root ); + // qd-memory-degrade: not over a changed-files index, which cannot tell an inert name from one the tree defines (also_skipped=) + const rw::quality::RegisterMacroConfigDiagnostics configDiag = refs.degraded ? rw::quality::RegisterMacroConfigDiagnostics{} + : rw::quality::diagnoseRegisterMacroConfig( judged, root ); const std::string configWarnAttr = registerMacroConfigWarningAttr( configDiag ); + // qd-memory-degrade: the kinds this answer did not run, why, and the replay — plus the peak, read once now that the + // comparison is done (kindsDisclosure). Their stderr lines come first: an answer that is partial, or that ran past + // the limit, says so on the channel a human reads before anything else. + const KindsDisclosure kd = kindsDisclosure( cfg, refs, basis, refPair ); + emitMemoryLines( kd, refs ); + const bool deadRan = !refPair || quality::kindSelected( refs.kinds, "dead-code" ); // dead-code's side counts exist only when it ran // P2.5 — one stderr line NAMING the gating finding, in --token-budget's style ("ripwire: --token-budget // exceeded: est_tokens=… > budget=…"). stdout is the machine artifact and a caller that only checks @@ -1679,11 +2118,14 @@ std::optional runQualityDelta( const MainDispatch& d ) + ( basis.declinedCallExcluded == 0 ? std::string() // the XML twin's declined-call-excluded=, absent at zero : ",\"declined-call-excluded\":" + std::to_string( basis.declinedCallExcluded ) ) + rw::countFieldOrEmpty( "value-ref-excluded", basis.valueRefExcluded, /*json=*/true ); - rw::emitTo( stdout, "{{\"baseline\":\"{}\",\"regressions\":{},\"minor\":{},\"acked\":{},\"stale\":{}," - "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{},\"register-macro-excluded\":{},\"api-new-surface\":{},\"at\":{}{}{}{}{}{}{},\"r\":[", - jsonStr( baseMarkerJ ).c_str(), regs.size(), minorCount, ackedCount, staleAcks.size(), - preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, atJsonJ.c_str(), refs.jsonAttrs.c_str(), - identityJson.c_str(), scopeJson.c_str(), configWarnJson.c_str(), absorbedJson.c_str(), headBasisJson.c_str() ); + // qd-memory-degrade: stale and register-macro-excluded are absent (never 0) when their pass did not run + const std::string staleJ = refs.degraded ? std::string() : ",\"stale\":" + std::to_string( staleAcks.size() ); + const std::string rmeJ = deadRan ? ",\"register-macro-excluded\":" + std::to_string( basis.registerMacroExcluded ) : std::string(); + rw::emitTo( stdout, "{{\"baseline\":\"{}\",\"regressions\":{},\"minor\":{},\"acked\":{}{}," + "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{}{},\"api-new-surface\":{},\"at\":{}{}{}{}{}{}{}{},\"r\":[", + jsonStr( baseMarkerJ ).c_str(), regs.size(), minorCount, ackedCount, staleJ, + preexistingCount, newSymbolCount, gatingCount, rmeJ, basis.apiNewSurface, atJsonJ.c_str(), refs.jsonAttrs.c_str(), + identityJson.c_str(), scopeJson.c_str(), configWarnJson.c_str(), absorbedJson.c_str(), headBasisJson.c_str(), kd.json ); // P1: one row emitter, called for both halves of the scope partition — the disclosed rows carry // the identical key set, so nothing about a row changes by being someone else's. `gatingAllowed` // is the ONE difference: an out-of-scope row is not what the exit code fires on, so claiming @@ -1761,7 +2203,7 @@ std::optional runQualityDelta( const MainDispatch& d ) } std::fputs( quality::staleAcksJsonArray( saRows ).c_str(), stdout ); // L2 — "sa":[...], same taxonomy as the XML sa= rows below rw::emitRaw( stdout, "}" ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } std::vector esc; @@ -1900,7 +2342,8 @@ std::optional runQualityDelta( const MainDispatch& d ) emitQualityDeltaLegend( { baseSel.marker, refPair, identityAttrs, !served.empty(), ackedCount > 0, basis.registerMacroExcluded > 0, configDiag.total() > 0, regs, outOfScope, scope.active() || !foreignAcks.empty(), !foreignAcks.empty(), baselineAbsorbed, - basis.headBasis, basis.declinedCallExcluded > 0, basis.valueRefExcluded > 0, collapse }, out ); + basis.headBasis, basis.declinedCallExcluded > 0, basis.valueRefExcluded > 0, collapse, + refs.kinds != quality::kAllKinds, refs.degraded, !refs.memoryStop.empty(), kd.peakOver != 0 }, out ); const char* baseMarker = baseSel.marker; // R3: ditto — one seam decides staleness AND names it // 2026-09-06: what the sidecar readers skipped, on the root (absent means none) — see kQdBaseHeadUnreadable std::string sidecarHealthAttrs; @@ -1912,14 +2355,17 @@ std::optional runQualityDelta( const MainDispatch& d ) if( basis.declinedCallExcluded > 0 ) { sidecarHealthAttrs += " declined-call-excluded=\"" + std::to_string( basis.declinedCallExcluded ) + "\""; } sidecarHealthAttrs += rw::countAttrXmlOrEmpty( "value-ref-excluded", basis.valueRefExcluded ); // absent at zero, beside its sibling // at= anchors this regression list to the commit (+dirty state) it was computed against. - rw::emitTo( out, "", - baseMarker, regs.size(), minorCount, ackedCount, staleAcks.size(), preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, + // qd-memory-degrade: stale= and register-macro-excluded= are absent (never 0) when their pass did not run + const std::string staleX = refs.degraded ? std::string() : " stale=\"" + std::to_string( staleAcks.size() ) + "\""; + const std::string rmeX = deadRan ? " register-macro-excluded=\"" + std::to_string( basis.registerMacroExcluded ) + "\"" : std::string(); + rw::emitTo( out, "", + baseMarker, regs.size(), minorCount, ackedCount, staleX, preexistingCount, newSymbolCount, gatingCount, rmeX, basis.apiNewSurface, // R-I: at= is OMITTED for the ref-pair form rather than stamped with the working tree's // sha, which would anchor the list to a commit it was not computed from. base_ref= and // target_ref= are the anchor there, and they carry FULL shas because a wave measurement // gets quoted into handoffs where a 9-char prefix is one collision from unverifiable. atAttrs.c_str(), refs.attrs.c_str(), identityAttrs.c_str(), - scopeAttrs.c_str(), configWarnAttr.c_str(), baselineAbsorbedAttr.c_str(), ( sidecarHealthAttrs + ( collapse ? staleCollapseAttrs : std::string() ) ).c_str() ); + scopeAttrs.c_str(), configWarnAttr.c_str(), baselineAbsorbedAttr.c_str(), ( sidecarHealthAttrs + ( collapse ? staleCollapseAttrs : std::string() ) + kd.xml ).c_str() ); for( const quality::Regression& r : regs ) { emitRow( out, r, /*gatingAllowed=*/true ); @@ -1961,7 +2407,7 @@ std::optional runQualityDelta( const MainDispatch& d ) if( !collapseStale ) { emitDocument( stdout, /*collapse=*/false ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } const Rendered full = renderToString( [ & ]( std::FILE* f ) { emitDocument( f, /*collapse=*/false ); } ); const Rendered folded = renderToString( [ & ]( std::FILE* f ) { if( verdictOnly ) { emitVerdict( f ); } else { emitDocument( f, /*collapse=*/true ); } } ); @@ -1969,11 +2415,11 @@ std::optional runQualityDelta( const MainDispatch& d ) { // renderToString DISCLOSEd the failed buffer; the full document is the superset, written straight to stdout emitDocument( stdout, /*collapse=*/false ); - return gatingCount > 0 ? 2 : 0; + return qualityDeltaExit( gatingCount, refs ); } const std::string& chosen = foldPays( folded.text, full.text ) ? folded.text : full.text; // compactlegend.h: the margin and why std::fwrite( chosen.data(), 1, chosen.size(), stdout ); - return gatingCount > 0 ? 2 : 0; // r26: only a PREEXISTING-worse AND major regression gates (== gating=) + return qualityDeltaExit( gatingCount, refs ); // r26: only a PREEXISTING-worse AND major regression gates (== gating=) } return std::nullopt; } diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 573236c98..c88d24ac8 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 1dd0a41c39f89e4b35fb40750a4b2fd56376daf2161d24b03d32423d788b352d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 1b039d143be854f26a34412e4a2949e98e3b71caf41f76bd4fc4935c89047295 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 039bd6985a6bf5d6b93ed02b19d9405f1a3f8421e4c1e3aecbfba1250604ae9d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 0cca7164a48d6543694bf07b4252209558d189771d50e913e46783bb98ae4bed e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 827b569b350640938954e34497f0430f757b4baac8f0daa2194306113cb46a36 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 e3535cdf695ad1d3e9ffbec44d529478059317b226fb60b2dfffaebc90b44218 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 3a8337704c1a55c31816e1bd43c5389186062503f35f1ecac548ca52ff37e84c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 de8c0030151f952e432e193a0979f3fb49c3703fadc09e0764647a29bff68ffb e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 aae4511de7c635204af71006620c1efc8a64020935fb79cce102c3e807321b25 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 157493dafe9f909a72b00ddb5800693138ee55d9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 20:07:09 -0400 Subject: [PATCH 056/162] fix(quality): the kinds_next replay is not degraded again, and never answers empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the lane's H3 replay on this repository (HEAD~20..HEAD at --max-memory=1000M): the degraded answer's kinds_next= ran at just above the projected need, the base phase's peak came out ~14% higher on the second run, the projection degraded the replay too, and — its selection being whole-tree kinds plus defect-shape, which the changed files could not decide — it answered with NO kind run and exit 6. - A selection the changed files cannot answer for certain (whole-tree kinds, with or without defect-shape) is never pre-empted by the projection: it runs in full, and only a real memory stop refuses it. That is the replay kinds_next= names, so the replay no longer depends on the projection's run-to-run spread. - When the changed-files answer is left with no kind to run (defect-shape was the only local kind selected and the changed files cannot decide it), it refuses (exit 5, one line naming why) instead of passing empty. Gate: qddegradecheck (R) — a whole-tree-only selection under a STOP refuses; a defect-shape-only selection under a stop with a changed C++ file refuses; the replay's own selection under project:1 answers in full with exactly the unpressured rows of those kinds. Co-Authored-By: Claude Opus 5.5 --- docs/COMMANDS.md | 2 +- src/cli.h | 2 ++ src/verbs_quality.h | 14 +++++++++++--- test/printf_parity.manifest | 2 +- test/qddegradecheck.sh | 25 ++++++++++++++++++++----- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 3c0683537..155f75164 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -2967,7 +2967,7 @@ $ ./build/ripwire . --quality-delta **Answers:** (with --quality-delta=A..B) run only the named kinds — the replay a partial answer hands back (with --quality-delta=A..B) run ONLY the named kinds, comma-separated, of the twelve: api-surface, complexity, dead-code, defect-shape, duplication, error-masking, nesting, new-clone-of-reused-helper, params, placeholder, short-horizon-churn, verbosity. -A named kind's rows are exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). +A named kind's rows are exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). A selection the changed files cannot answer (whole-tree kinds, with or without defect-shape) is never pre-empted by the memory projection: it runs in full, and only a real memory stop refuses it (exit 5) — so the kinds_next= replay is not degraded again. **Caveats (stated by the binary):** diff --git a/src/cli.h b/src/cli.h index d8b99151c..7c19a129d 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1809,6 +1809,8 @@ inline constexpr char kHelpHead[] = " exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why=\"selector\"), stale acks are\n" " judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused\n" " beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet).\n" + " A selection the changed files cannot answer (whole-tree kinds, with or without defect-shape) is never pre-empted by the memory\n" + " projection: it runs in full, and only a real memory stop refuses it (exit 5) — so the kinds_next= replay is not degraded again.\n" " --dmm[=REV|A..B] score a change as ONE number in [0,1], so quality trends across commits\n" " the DELTA MAINTAINABILITY MODEL scalar: ONE comparable number in [0,1] for a change, so quality becomes TRENDABLE across commits instead of a per-kind list (di Biase, Rastogi, Bruntink and van Deursen, TechDebt 2019; thresholds and arithmetic from PyDriller's deltamaintainability reference implementation). Bare = the WORKING TREE vs git HEAD (what --quality-delta compares); =REV = that commit vs its FIRST PARENT (the per-commit scalar); =A..B = tree B vs tree A.\n" " A UNIT is a function/method definition with a body; its VOLUME is its line span. Per property a unit is LOW risk iff size: loc<=15, complexity: cyclomatic<=5, interfacing: params<=2. good = low-risk volume ADDED plus high-risk volume REMOVED; bad = low-risk REMOVED plus high-risk ADDED; dmm = good/(good+bad). So DELETING a god function scores 1.000 and GROWING one scores 0.000.\n" diff --git a/src/verbs_quality.h b/src/verbs_quality.h index af3575134..5a35796fb 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -318,6 +318,10 @@ std::optional loadChangedFiles( const std::string& root, const rw::Config& { out.kinds &= static_cast( ~quality::kindBit( "defect-shape" ) ); } + if( out.kinds == 0 ) // the selection was defect-shape (with whole-tree kinds) and the changed files cannot decide it: never an empty pass + { + return refPairStopRefusal( stop, out, "the selected kinds need the whole tree (defect-shape too: the changed files' C-family macro names differ, or a C++ file changed without the base's whole set)" ); + } out.degraded = true; out.baseSnapshot = quality::computeSnapshot( baseTree.ing, baseTree.g, baseTree.root, nullptr, { out.kinds, out.macroOverride() } ); if( !cfg.qualityScope.empty() ) @@ -333,10 +337,14 @@ std::optional loadChangedFiles( const std::string& root, const rw::Config& // qd-memory-degrade: the two full trees, as #387 reads them — the base scored and released, then the target — with the // projection between them. A stop in either is recorded in out.stop; `projectedNoFit` says the projection chose the -// changed files before the target was read. Exit code on an environment failure only. +// changed files before the target was read. The projection is asked only when the changed files could answer a selected +// kind for certain: a selection of whole-tree kinds (with or without defect-shape, which the changed files may not +// decide) gains nothing from pre-empting the full attempt — that is the replay kinds_next= names, and it runs in full, +// refused only by a real stop. Exit code on an environment failure only. std::optional loadFullTrees( const std::string& root, const rw::Config& cfg, RefPairDelta& out, bool& projectedNoFit ) { using namespace rw; + const quality::KindMask certainLocal = static_cast( out.selected & ~( quality::kWholeTreeKinds | quality::kindBit( "defect-shape" ) ) ); if( !out.sameRef ) { const std::uint64_t before = os::mem_footprint(); @@ -345,9 +353,9 @@ std::optional loadFullTrees( const std::string& root, const rw::Config& cfg return failed; } // #387: the base tree's index, graph and temp dir were released when scoreBaseCommit returned. - if( out.stop.stopped || hardLineStops( "base commit's snapshot, with its index released", out.stop ) ) + if( out.stop.stopped || hardLineStops( "base commit's snapshot, with its index released", out.stop ) || certainLocal == 0 ) { - return std::nullopt; + return out.stop.stopped ? std::nullopt : loadTargetCommit( root, out.targetSha, cfg, out ); } const std::uint64_t basePeak = os::mem_footprint_peak(); const std::uint64_t after = memguard::measuredFootprint(); diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index c88d24ac8..87103e9a2 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -38,5 +38,5 @@ safe_delete 0 e3535cdf695ad1d3e9ffbec44d529478059317b226fb60b2dfffaebc90b44218 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 3a8337704c1a55c31816e1bd43c5389186062503f35f1ecac548ca52ff37e84c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 aae4511de7c635204af71006620c1efc8a64020935fb79cce102c3e807321b25 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 8d48218a0cf5e7e2e7a0d907a539508c50c742f41efad21047bee7cf1fac3f92 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index a5f44e31a..9d2cfcdbc 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -54,8 +54,9 @@ # (P) R4 cache isolation: P1 with the cache on leaves the HEAD ingest-cache blob untouched; a cached run after it is # byte-identical to (A). # (Q) R5 the target-phase stop (ingest:2 cuts the target's full ingest): a degraded answer naming the target's stop. -# (R) R7b a peak over the limit (peak:1) is disclosed on a full answer, rc and rows unchanged; R7c a replay whose -# selection holds only whole-tree kinds refuses under pressure (exit 5), never an empty pass. +# (R) R7b a peak over the limit (peak:1) is disclosed on a full answer, rc and rows unchanged; R7c a selection the +# changed files cannot answer refuses under a stop (exit 5), never an empty pass, and is never pre-empted by the +# projection (the replay runs in full). # (S) R9 --quality-kinds=complexity reports no false finding-gone for a dead-code ack. # (T) S2 fits near the line: --max-memory at 2x the measured full peak answers in full, byte-identical (guard). # (V) R8 a degraded answer with rows and no gating row exits 6, not 2 and not 0. @@ -506,10 +507,24 @@ else no "(R) peak:1 rc=$RC (want $ARC) memory_peak=$pk stderr: $( grep '^ripwire qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --legend=full --no-cache if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=' && validxml "$WORK/o"; then ok "(R) --legend=full defines memory_peak= when it rides, and parses as XML" else no "(R) the legend lacks memory_peak= (or invalid XML)"; fi -qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$WHOLE_KINDS" --no-cache +# A selection the changed files could not answer (whole-tree kinds, with or without defect-shape) is never pre-empted by +# the projection — it is the replay kinds_next= names, and degrading it could only refuse: it runs in full. A real stop +# still refuses it, by name, and so does a defect-shape-only selection whose changed files cannot decide defect-shape +# (found on this repository's own HEAD~20..HEAD: the replay degraded again and answered EMPTY with exit 6). +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$WHOLE_KINDS" --no-cache if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'whole-tree' "$WORK/e"; then - ok "(R) a whole-tree-only selection under pressure refuses (exit 5, one line naming why) — never an empty pass" -else no "(R) whole-tree-only replay under pressure: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi + ok "(R) a whole-tree-only selection under a stop refuses (exit 5, one line naming why) — never an empty pass" +else no "(R) whole-tree-only selection under a stop: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=defect-shape --no-cache +if [ "$RC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'defect-shape' "$WORK/e"; then + ok "(R) a defect-shape-only selection the changed files cannot decide (a changed C++ file, no whole macro set) refuses (exit 5) — never an empty pass" +else no "(R) defect-shape-only under a stop: rc=$RC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +REPLAY_KINDS="dead-code,defect-shape,duplication,new-clone-of-reused-helper" +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$REPLAY_KINDS" --no-cache +if [ -s "$WORK/o" ] && ! hasattr "$WORK/o" ingest_scope && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" "$REPLAY_KINDS" )" ] && [ -n "$( rows "$WORK/o" )" ] \ + && { [ "$RC" = 0 ] || [ "$RC" = 2 ]; }; then + ok "(R) the replay's selection is not pre-empted by the projection (project:1): answered in full, exactly (A)'s rows of those kinds (rc=$RC)" +else no "(R) replay selection under project:1: rc=$RC scope=$( attr "$WORK/o" ingest_scope ) rows=$( rows "$WORK/o" | grep -c . ) want $( rows "$WORK/A.xml" "$REPLAY_KINDS" | grep -c . )"; fi # ── (S) R9: stale acks under --quality-kinds are checked for the selected kinds only ─────────────────────────── FSS="$WORK/fss"; cp -R "$FX" "$FSS" From 2b7ae3e4b93c377cc12f5b50b3b6a6ffd8d51e5e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 20:21:38 -0400 Subject: [PATCH 057/162] fix(for): Q1's prov="split" clause is exempt from the ledger, as e= is CI (run 38650732997): the clause charged the sig side whenever the header could carry it, so the auto bundle's capped continuation asked for --token-budget=1200 where --signatures-only asks 1100 (fordisclosurecheck #2, forautobodycheck, estchargecheck #11, compactlegendcheck P1-S). A disclosure on the same contract is exempt on both the compact and the full-dialect ledger; the four gates are green locally. Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index 8b328ae55..63742e0c1 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -3652,8 +3652,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // on the same contract: exempt, so is byte-identical with and without the owner rows (the rows ride on top of // the hop budget; their reading never costs a signature row). Gate: ownerhopcheck (S). const std::size_t ownerLegendEmitted = ownerLegendExemptBytes( headerParts, compactLegendOn ); + // attrib-multidef Q1: the prov="split" clause is a disclosure on the same contract — exempt, as e= is, so (rows and + // its continuation's budget) is byte-identical with and without it (fordisclosurecheck #2: auto vs --signatures-only) + const std::size_t splitLegendEmitted = !headerParts.splitPresent ? 0u + : ( compactLegendOn ? rw::kForCompactSplitClause.size() : rw::kForSplitClause.size() ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted - + endLineLegendEmitted + ownerLegendEmitted; + + endLineLegendEmitted + ownerLegendEmitted + splitLegendEmitted; if( exemptBytes > headerStr.size() ) { DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -3678,7 +3682,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ) - + ownerLegendExemptBytes( headerParts, /*compact=*/false ); + + ownerLegendExemptBytes( headerParts, /*compact=*/false ) + + ( headerParts.splitPresent ? rw::kForSplitClause.size() : 0u ); // attrib-multidef Q1 std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; fullCharged -= std::min( fullCharged, rw::forZeroNoteBytes( fullHeader ) ); // lean-answers: exempt, as above chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); From e6690694a07c2e47cf6222f3ecfd159ca69c9205 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 20:21:39 -0400 Subject: [PATCH 058/162] fix(attrib): path legend clause opens with no attribute; two gates read of_def= on their rows CI (run 38650732997): compactlegendcheck (S) reads the first name= a conditional graphlegend.h clause opens with and wants a completeness row for it; kPathUnboundLegend opened with from_p=, whose reading lives in the per-verb table. Reworded to open with prose. rubyschemacheck and chainguardcheck pinned exact callers/callees rows; multi-definition rows now carry of_def= (defattrib.h), which their regexes accept. Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 4 ++-- test/chainguardcheck.sh | 3 ++- test/rubyschemacheck.sh | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 31a872acb..93272b7cb 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -938,8 +938,8 @@ inline constexpr const char* kSplitArmRowLegend = "arm of a call split between same-named definitions, so the row may be another arm's; NOT a claim the edge is false. "; inline const char* splitArmLegend( bool on ) noexcept { return on ? kSplitArmRowLegend : ""; } inline constexpr const char* kPathUnboundLegend = - "from_p=/to_p= are left out when that endpoint has several definitions and nothing bound one: unreached, or (to_p) reached " - "only through a split or by-name hop. "; + "An endpoint with several definitions that nothing bound prints no from_p or to_p: unreached, or (to) reached only through a " + "split or by-name hop. "; inline const char* pathUnboundLegend( bool on ) noexcept { return on ? kPathUnboundLegend : ""; } // ── issue #60: the module-scope owner's clause, emitted exactly when a t="modscope" ROW is in the answer ── diff --git a/test/chainguardcheck.sh b/test/chainguardcheck.sh index c6c157bf8..5d6aa3eb0 100755 --- a/test/chainguardcheck.sh +++ b/test/chainguardcheck.sh @@ -214,7 +214,8 @@ GO2="$( "$BIN" "$FIX2" --callees=go --no-cache 2>/dev/null | grep -o '/dev/null >"$TMP/callers_name" +# attrib-multidef: the 19 definitions form several groups, so each row also names its definitions (of_def=, defattrib.h) [ "$( callershead name )" = 'of="name" defs="19" count="2"' ] \ - && grep -q '' "$TMP/callers_name" \ - && grep -q '' "$TMP/callers_name" \ + && grep -qE '' "$TMP/callers_name" \ + && grep -qE '' "$TMP/callers_name" \ && ok 'defs-only: the 2 name callers are METHODS (render_label by name alone, lookup pinned by its own class) — the 9 columns contribute zero edges to the 19-def set; SingleColumn.new.name binds nothing (no in-tree def on that model)' \ || no "defs-only: name callers: $( callershead name )" From ad5d4a449bcc209d758600822e7f50f2f726ce8f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 20:26:14 -0400 Subject: [PATCH 059/162] fix(quality): exit-2 wording names the ack-healing exception; memguardcheck B23 counts the ingests that print - The degraded legend and CHANGELOG said a gating row of a kind that ran is final because the skipped kinds only add rows. With ack-healing skipped (a non-empty ledger) a row only an identity-healed ack hides in the full answer is shown and can gate, so the sentence now names that exception (checklist 3: wording = mechanism). - memguardcheck B23 (this lane's rewrite) expected three cache-stats lines; the QR fixture's change is an add, so the base's changed-file set is empty and that ingest parses nothing and prints no line: two lines, plus the stderr line must name the base commit's stop. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++- src/verbs_quality.h | 3 ++- test/memguardcheck.sh | 9 ++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79871c958..46b9cf82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,8 @@ empty: there acks match by their recorded key only), and `kinds_next=`, the call the same two commits. One stderr line says the same. - **Exit codes.** A partial answer exits **2** when a row of a kind that ran gates (the skipped kinds can only add - rows, so that verdict is final) and **6** (new) otherwise — a partial answer is never a clean exit 0. Exit **5** now + rows, so that verdict is final — except, with ack-healing skipped, for a row only an identity-healed ack hides in the + full answer) and **6** (new) otherwise — a partial answer is never a clean exit 0. Exit **5** now means, for this form, that even the changed files did not fit, or that they cannot stand in for the trees (a symlink anywhere, an export-subst attribute, or a changed `.gitattributes` with an export attribute), on one line naming why. `--quality-ack` under pressure refuses (exit 5, ledger untouched): accepting needs every kind's rows. diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 5a35796fb..7d2109e93 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -1182,7 +1182,8 @@ inline constexpr const char* kQdDegradedLegend = "legacy-scheme re-key or a content-id match would hide in the full answer is shown here, and can gate). kinds_next= is the " "call that runs exactly the skipped kinds over the same two commits, at a max-memory just above memory_need= (after a stop: " "twice the limit, a guess, since no need was measured). EXIT 2 when a row of a kind that ran gates (final: the skipped kinds " - "only add rows); otherwise EXIT 6, a partial answer with no gating row, never 0. "; + "only add rows, though with ack-healing skipped that row may be one an identity-healed ack hides in the full answer); " + "otherwise EXIT 6, a partial answer with no gating row, never 0. "; inline constexpr const char* kQdMemoryStopLegend = "memory_stop= names the guard's stop that sent this answer to the changed files: crawl (the crawl line, an eighth of the " "limit grown) or parse (half the limit) is a soft line, memory_pressure=1 when the OS pressure signal stopped it instead, " diff --git a/test/memguardcheck.sh b/test/memguardcheck.sh index dd21b647f..4db353370 100755 --- a/test/memguardcheck.sh +++ b/test/memguardcheck.sh @@ -682,7 +682,9 @@ fi # cut tree is released unscored and BOTH commits' changed files answer the kinds that need only them — one cut # ingest + two changed-file ingests, a degraded header (ingest_scope="changed-files", memory_stop= naming the stop), # exit 2 if a row gates else 6, and no row names a file outside the diff (a/extra.c). test/qddegradecheck.sh pins -# the degraded answer's contents; this arm pins that nothing of the cut full tree reaches it. +# the degraded answer's contents; this arm pins that nothing of the cut full tree reaches it. Cache-stats lines: 2 — +# the cut base ingest and the target's changed files; the base's changed-file set is EMPTY (a/extra.c is an add), +# and an ingest that parses nothing prints no cache-stats line. for trip in parse:3 crawl:20; do qr_run "$trip"; rc=$? gt="$( sed -n 's/.*]* gating="\([0-9]*\)".*/\1/p' "$TMP/b22.out" | head -n 1 )" @@ -690,8 +692,9 @@ for trip in parse:3 crawl:20; do outside="$( grep -o ' p="[^"]*"' "$TMP/b22.out" | grep -vc ' p="a/extra\.c:' )" if [ -n "$gt" ] && [ "$rc" = "$want" ] && grep -q ']* ingest_scope="changed-files"' "$TMP/b22.out" \ && grep -q "]* memory_stop=\"${trip%%:*}\"" "$TMP/b22.out" && [ "$outside" = 0 ] \ - && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 3 ] && [ "$( grep -c '^ripwire: memory limit ' "$TMP/b22.err" )" = 1 ]; then - ok "(B23) $trip cuts the base commit's ingest: the cut tree is never scored — 1 cut + 2 changed-file ingests answer (exit $rc, degraded header)" + && [ "$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" )" = 2 ] && [ "$( grep -c '^ripwire: memory limit ' "$TMP/b22.err" )" = 1 ] \ + && grep -q "^ripwire: memory limit .*stopped the ${trip%%:*} of the base commit" "$TMP/b22.err"; then + ok "(B23) $trip cuts the base commit's ingest: the cut tree is never scored — the changed files answer (exit $rc, degraded header naming the base's stop)" else no "(B23) $trip rc=$rc (want $want) gating=$gt rows-outside-the-diff=$outside ingests=$( grep -c '^ripwire: cache-stats ' "$TMP/b22.err" ) stderr: $( grep '^ripwire: [^c]' "$TMP/b22.err" | head -c 250 )" fi From ab79c71b7cf2028577c1a51ca33237e9b07c50c2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 20:50:09 -0400 Subject: [PATCH 060/162] fix(grep): count carrier reads only for the top hit, under the memory guard The rich re-ingest behind reads= now fires only when the TOP hit's carrier is readable (the row that decides next=); a lower carrier keeps floor_next= as its clue. The second ingest runs under the memory guard: a soft stop or the hard line after it keeps the lean answer and the root discloses reads_skipped= + reads_next=. An explicit --cache=F is honoured (uncached second ingest, no TMPDIR rich blob). Workspace runs re-ingest each root rich, so CLI and MCP print the same reads; MCP applies the same top-hit rule. Order wording: tier, path, line (not the uses verb's printed order). Ruby comment: a bare name is a call there, already an edge. Gate: test/countfloorcheck.sh P/top, P/mem, P/cache, P/ws. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 11 +++- src/compactlegend.h | 4 +- src/grepcarrier.h | 45 ++++++++------ src/ingest_valuerefs.h | 5 +- src/mcpverbs.h | 7 ++- src/verbs_grep.h | 127 ++++++++++++++++++++++++++++++++-------- test/countfloorcheck.sh | 67 ++++++++++++++++++++- 7 files changed, 213 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b6d0d903..eb412217e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,13 +29,18 @@ value-preserving operators — JS/TS `??`, `||`, `&&` and the logical assignment When a hit sits inside a variable, constant or initialiser (its enclosing definition's kind is a variable), the `` row now says where that value goes instead of only a call count nobody can satisfy: `reads=N reads_at=` are the -uses verb's read sites for the name (first three, the uses verb's order; only where that verb indexes reads — +uses verb's read sites for the name (first three, in tier, path, line order — not the order the uses verb prints; only where that verb indexes reads — Python, C++ and ObjC outside headers — and absent, never zero, elsewhere), and `vals=` names up to three function values the carrier stores, as `slot=fn@line` of the one definition each resolves to (`vals_total=` beyond three). When the top hit's carrier has reads, the root `next=` is `--uses=`. The MCP `grep` verb's `enclosing` rows carry the same `reads`/`reads_at`/`vals` keys (`src/grepcarrier.h`, shared). Reads are rich-family facts, so a -lean `--grep` run whose page holds such a carrier ingests the same root once more through the rich warm cache; a page -without one costs nothing extra. Gate: `test/countfloorcheck.sh` arm P. +lean `--grep` run counts them only when the TOP hit sits in such a carrier (the row that decides `next=`): it then +ingests the same root(s) once more through the rich warm cache — per root on a workspace run, so the CLI and the MCP +workspace print the same reads — or uncached when `--cache=F` names the cache (F keeps the lean blob; no other cache file +is written). Any other page costs nothing extra; a lower carrier keeps `floor_next="--uses="` as its clue. The +second ingest runs under the memory guard (`--max-memory`, then the hard line): a stop keeps the lean answer and the +root says so with `reads_skipped=memory_stop|hard_limit` and `reads_next=--uses=`. Gate: +`test/countfloorcheck.sh` arm P (P/top, P/mem, P/cache, P/ws). ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) diff --git a/src/compactlegend.h b/src/compactlegend.h index 0156c6193..c83acef1d 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -894,9 +894,11 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "floor_next", "floor_next=: the call that lists what that count could not see (the uses verb; the literal scan where reads are not indexed)", true, "enc", MapHeaderRead::No, {}, "grep" }, // L4 (src/grepcarrier.h): a carrier row (every def a variable/constant/initialiser) — its read sites and stored function values. { "reads", "reads=N: read sites the uses verb lists for this carrier's name (by name; present only when nonzero where reads are indexed; absent = none counted)", true, "enc", MapHeaderRead::No, {}, "grep" }, - { "reads_at", "reads_at=: the first three of those sites, file:line, uses order; the root next= is then the uses verb", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "reads_at", "reads_at=: the first three of those sites, file:line, tier/path/line order; the root next= is then the uses verb", true, "enc", MapHeaderRead::No, {}, "grep" }, { "vals", "vals=: up to three function values the carrier stores, slot=fn@line of its one resolved def (path:line elsewhere; no @ if several)", true, "enc", MapHeaderRead::No, {}, "grep" }, { "vals_total", "vals_total=N: every stored function value, when more than vals= shows", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "reads_skipped", "reads_skipped=memory_stop|hard_limit: the memory guard stopped the index that counts the top carrier's reads; no reads= on any row", false, "grep", MapHeaderRead::No, {}, "grep" }, + { "reads_next", "reads_next=: the uses verb on the top hit's carrier, the call that lists the reads reads_skipped= left out", false, "grep", MapHeaderRead::No, {}, "grep" }, // count-floor + CALLSITE-LINE on safe-delete (src/verbs_navigate.h runSafeDelete): root markers beside the counts they qualify. { "callers_floor", "callers_floor=1: callers= and impact_reaches= may be short (a declined, unbound or value use, or a kind read not called); not proof of more", false, {}, MapHeaderRead::No, {}, "safe-delete" }, { "uses_floor", "uses_floor=1: this kind's reads and type mentions are not indexed in this run; uses= does not count them", false, {}, MapHeaderRead::No, {}, "safe-delete" }, diff --git a/src/grepcarrier.h b/src/grepcarrier.h index 72470f029..8cec7ab43 100644 --- a/src/grepcarrier.h +++ b/src/grepcarrier.h @@ -13,7 +13,8 @@ // * reads= / reads_at= are the uses verb's role="read" sites for the carrier's NAME (matched by name, the uses verb's own // unit), counted only where that verb can see reads at all: an ingest that captured value uses and a language whose // reads reach the use-site table (countfloor.h valueUsesArmedFor), never a C-family header, whose readers are `.c` -// files the uses verb never reads (the countFloorNext rule). Anywhere else the attributes are ABSENT: unknown, never 0; +// files the uses verb never reads (the countFloorNext rule), and only on a page whose TOP hit sits in such a carrier +// (grepCarriersWantReads — the CLI pays a second ingest for them). Anywhere else the attributes are ABSENT: unknown, never 0; // * vals= are the function values the carrier itself stores: value references (valuerefindex.h) made inside the // carrier's definition whose name resolves to an indexed function, in source order, as slot=fn@line: the slot key as // the value index normalised it, and the line of the ONE definition the name resolves to (@path:line when that @@ -63,7 +64,7 @@ struct GrepCarrier bool isCarrier = false; // every definition on the row is a Var bool readsKnown = false; // reads= is a count the uses verb could make (see the rule above) std::uint32_t reads = 0; // role="read" sites spelled like the carrier's name - std::vector at; // the first kGrepCarrierShown of them, in the uses verb's base order + std::vector at; // the first kGrepCarrierShown of them, in tier, path, line order (not the order the uses verb prints) std::vector vals; // the first kGrepCarrierShown stored function values std::uint32_t valsTotal = 0; // every stored function value on the row }; @@ -80,14 +81,27 @@ inline bool isGrepCarrierRow( const IngestResult& ing, const GrepEncRow& row ) n return !row.ids.empty() && std::ranges::all_of( row.ids, [ & ]( NodeId id ) { return id < ing.symbols.size() && ing.symbols[id].kind == SymKind::Var; } ); } -// Do any rows want reads= — a carrier whose language/path the uses verb reads? The CLI asks this before paying for a -// rich ingest, so an answer with no such row costs nothing extra. -inline bool grepCarriersWantReads( const IngestResult& ing, std::span rows ) noexcept +// The row holding the TOP hit's enclosing definition — the row the root next= is decided on — or rows.size() when the +// top hit has no enclosing definition or no row holds it. +inline std::size_t grepTopHitRow( std::span hits, std::span rows ) noexcept { - return std::ranges::any_of( rows, [ & ]( const GrepEncRow& row ) + if( hits.empty() || hits[0].enclosingId == kNoNode ) { - return isGrepCarrierRow( ing, row ) && grepCarrierReadsVisible( ing, ing.symbols[ row.ids.front() ] ); - } ); + return rows.size(); + } + const auto row = std::ranges::find_if( rows, [ & ]( const GrepEncRow& r ) { return std::ranges::find( r.ids, hits[0].enclosingId ) != r.ids.end(); } ); + return std::size_t( row - rows.begin() ); +} + +// Does this page count reads at all? Only when the TOP hit's row is a carrier the uses verb reads for (a language whose +// reads reach the use-site table, not a header): that row decides the root next=, so it is the one whose reads are worth +// a rich ingest (the CLI's second ingest is paid here and only here). Once counted, every readable carrier row on the +// page gets reads=; on a page that does not count, a lower carrier keeps its floor_next= (the uses verb on its name) as +// the clue. The CLI and the MCP twin both ask this, so the two surfaces count reads on the same pages. +inline bool grepCarriersWantReads( const IngestResult& ing, std::span hits, std::span rows ) noexcept +{ + const std::size_t top = grepTopHitRow( hits, rows ); + return top < rows.size() && isGrepCarrierRow( ing, rows[top] ) && grepCarrierReadsVisible( ing, ing.symbols[ rows[top].ids.front() ] ); } namespace grepcarrier @@ -181,7 +195,8 @@ struct ReadHit }; // The uses verb's role="read" rows (collectUseSites' filters) for every name in `names` (sorted, unique), in ONE pass -// over the reads ingest, sorted by name, then the uses verb's base order (tier, path, line). +// over the reads ingest, sorted by name, then tier, path, line (source before tests) — the order +// the uses verb starts from before it ranks; its printed order is rankUseSites', which reads_at= does not follow. inline std::vector readSitesOf( const IngestResult& rd, std::span names ) { const std::vector valueSites = valueSitesOf( rd, names ); @@ -286,18 +301,12 @@ inline std::vector grepCarriers( const IngestResult& ing, const Val inline std::string grepCarrierNext( const IngestResult& ing, std::span hits, std::span rows, std::span carriers ) { - if( hits.empty() || hits[0].enclosingId == kNoNode ) + const std::size_t top = grepTopHitRow( hits, rows ); + if( top >= rows.size() || top >= carriers.size() || !carriers[top].readsKnown || carriers[top].reads == 0 ) { return {}; } - for( std::size_t r = 0; r < rows.size() && r < carriers.size(); ++r ) - { - if( std::ranges::find( rows[r].ids, hits[0].enclosingId ) != rows[r].ids.end() ) - { - return carriers[r].readsKnown && carriers[r].reads > 0 ? nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ) : std::string(); - } - } - return {}; + return nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ); } // The two list attributes as plain text items (the emitters escape them for their own dialect): reads_at= as diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index eefbc71ee..21d262a1a 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -75,8 +75,9 @@ inline constexpr std::size_t kVrTextCap = 96; // a written slot / callee // Families with no row here, and why: C/C++ and Go `||` / `&&` yield a truth value (int or bool), never an operand, so // a function there is a test, not a stored value. The conditional `c ? f : g` (C, C++, JS/TS; Python `f if c else g`) // and parentheses are handled by transparent() for every family already. Value references are captured for the C, JS, -// Python and Go families only (model.h kValueRefFamilyOfLang); Ruby `a || m`, Java, C#, Swift, Rust, Bash have no -// value-reference capture at all, so they have no operator to walk either. +// Python and Go families only (model.h kValueRefFamilyOfLang); Ruby, Java, C#, Swift, Rust, Bash have no +// value-reference capture at all, so they have no operator to walk either. (Ruby `a || m` needs none: a bare `m` is a +// call there and is already an edge; Ruby's function value is `method(:m)`, a different shape.) struct VrPassOp { VrFam fam; diff --git a/src/mcpverbs.h b/src/mcpverbs.h index c3f510f52..93ac36ec4 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1194,9 +1194,12 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat const std::vector hits = grepEnrich( ing, std::span( collected.raw ).subspan( grepPage.begin, rowCount ), 0, 0 ); // R1 rows and L4's carrier verdict, once, before the root: next= reads them (grepCarrierNext) and so does the // enclosing block. The MCP index is the RICH family (mcpindex.h ingests with value uses), so its own references - // carry the reads — no second ingest here, unlike the lean CLI run. + // carry the reads — no second ingest here, unlike the lean CLI run. Reads are counted on the SAME pages the CLI counts + // them (grepCarriersWantReads: the top hit's carrier), so the two surfaces print the same reads on every page, single + // root or workspace, although this index could count them on every page. const std::vector encRowsJ = grepEnclosingRows( ing, ix.g, std::span( hits ) ); - const std::vector carriersJ = grepCarriers( ing, valueRefIndexOf( ix ), encRowsJ, &ing ); + const std::vector carriersJ = grepCarriers( ing, valueRefIndexOf( ix ), encRowsJ, + grepCarriersWantReads( ing, hits, encRowsJ ) ? &ing : nullptr ); const std::string carrierNextJ = grepCarrierNext( ing, hits, encRowsJ, carriersJ ); // §R-J: the CLI's unindexed_files_scanned=/unindexed block twin (search.h grepCollectAux) — literal-only, diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 37ad94120..19fdec0b3 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -189,45 +189,109 @@ void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span // L4 (grepcarrier.h): the carrier verdict for a page's rows. vals= reads this run's own value index (both ingest // families capture value references). reads= needs role="read" references, which only the RICH family captures, and -// --grep runs LEAN (cli.h needsValueUses): so when a carrier on THIS page is one the uses verb can read for (an armed -// language, not a header) and this run is lean, the same root is ingested once more, rich, through that family's own -// warm cache (the uses verb's blob, so a tree that answered the uses verb already pays a cache load, not a parse), and -// dropped before the answer prints. A page with no such carrier — every hit inside a function, a C initialiser, a -// JS constant — pays nothing. Multi-root lean runs leave reads= unknown (absent): the rich re-ingest is single-root. +// --grep runs LEAN (cli.h needsValueUses): so when the TOP hit sits in a carrier the uses verb can read for +// (grepCarriersWantReads — the row the root next= is decided on) and this run is lean, the same root(s) are ingested once +// more, rich, and dropped before the answer prints. Any other page pays nothing: a lower carrier keeps its floor_next= +// (the uses verb on its name) as the clue instead of reads=. +// * cache: the rich family's own warm blob per root (the uses verb's, so a tree that answered the uses verb already +// pays a cache load, not a parse) — unless the user NAMED a cache (--cache=F): F holds the lean family, the rich +// family would evict it (the two never cross-hit), and nothing else may be written behind the user's back, so the +// second ingest then runs uncached. --no-cache: uncached too. +// * workspace (multi-root): one rich ingest per root, merged exactly as main() merges the lean ones, so reads_at= +// spells the same label/path the MCP workspace index (rich) prints. +// * memory: the guard covers it like any ingest — the soft line inside it (--max-memory), then the hard line after +// each root. A stop drops the rich ingest; the answer keeps the lean rows and its root says so in band +// (reads_skipped= and reads_next=, the uses verb on the top hit's carrier), never a silent drop or stderr alone. struct GrepCarrierReadsSource { const rw::IngestResult* reads = nullptr; - enum class DisclosureWhy : std::uint8_t { MemoryStop }; - void disclose( DisclosureWhy ) noexcept { reads = nullptr; } // a stopped ingest's reads would be short: leave reads= unknown + const char* skipped = nullptr; // the root's reads_skipped= value, or nullptr when nothing was skipped + enum class DisclosureWhy : std::uint8_t { MemoryStop, HardLimit }; + void disclose( DisclosureWhy why ) noexcept // a stopped ingest's reads would be short: leave reads= unknown, say so on the root + { + reads = nullptr; + skipped = why == DisclosureWhy::MemoryStop ? "memory_stop" : "hard_limit"; + } +}; + +struct GrepPageCarriers +{ + std::vector carriers; + std::string skippedAttrs; // " reads_skipped=… reads_next=…" on a memory-stopped second ingest, else "" }; -std::vector grepPageCarriers( const rw::Config& cfg, const rw::IngestResult& ing, const std::string& root, - std::span rows, bool valueUses ) +// The second, rich ingest of this run's root(s), or nullopt when the memory guard stopped it (`source` then says why). +std::optional grepReadsIngest( const MainDispatch& d, GrepCarrierReadsSource& source ) { + using namespace rw; + const Config& cfg = d.cfg; + const auto one = [ & ]( const std::string& dir, std::string_view label, std::string_view cachePath ) + { + return ingest( dir.c_str(), cfg.excludes, cachePath, cfg.maxFileBytes, /*captureValueUses=*/true, label, /*respectGitignore=*/!cfg.noIgnore ); + }; + std::vector parts; + if( d.multiRoot ) + { + for( const WorkspaceRoot& r : d.ws ) + { + parts.push_back( one( r.arg, r.label, cfg.noCache ? std::string() : defaultCachePath( r.arg, true ) ) ); + if( parts.back().memoryStop.isSet() || memguard::overHardLimit() ) + { + break; // the next root would only add to an ingest already over the line + } + } + } + else + { + parts.push_back( one( d.root, {}, cfg.noCache || !cfg.cacheFile.empty() ? std::string() : defaultCachePath( d.root, true ) ) ); + } + const bool stopped = std::ranges::any_of( parts, []( const IngestResult& p ) { return p.memoryStop.isSet(); } ); + if( stopped || ( d.multiRoot && parts.size() < d.ws.size() ) || memguard::overHardLimit() ) + { + memguard::answerStops(); // answered here: reads= stays absent (unknown), the rest of the answer is the lean run's + if( stopped ) + { + DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::MemoryStop, "grep: the rich re-ingest for reads= stopped at the memory guard's soft line; the root carries reads_skipped=" ); + } + else + { + DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::HardLimit, "grep: over the memory guard's hard line after the rich re-ingest for reads=; the root carries reads_skipped=" ); + } + return std::nullopt; + } + IngestResult rich = d.multiRoot ? mergeWorkspaceIngests( d.ws, parts ) : std::move( parts.front() ); + if( cfg.ignoreTests ) + { + applyIgnoreTests( rich ); // the same population the lean ingest answers from + } + return rich; +} + +GrepPageCarriers grepPageCarriers( const MainDispatch& d, std::span hits, std::span rows ) +{ + const rw::IngestResult& ing = d.ing; if( std::ranges::none_of( rows, [ & ]( const rw::GrepEncRow& row ) { return rw::isGrepCarrierRow( ing, row ); } ) ) { return {}; } const rw::ValueRefIndex vri( ing ); - const bool singleRoot = ing.realPaths.empty() && cfg.roots.size() == 1; - if( valueUses || !singleRoot || !rw::grepCarriersWantReads( ing, rows ) ) + if( !rw::grepCarriersWantReads( ing, hits, rows ) ) { - return rw::grepCarriers( ing, vri, rows, valueUses ? &ing : nullptr ); + return { rw::grepCarriers( ing, vri, rows, nullptr ), {} }; } - const std::string cachePath = cfg.noCache ? std::string() : defaultCachePath( root, true ); - rw::IngestResult rich = rw::ingest( root.c_str(), cfg.excludes, cachePath, cfg.maxFileBytes, /*captureValueUses=*/true, - /*excludeLabel=*/{}, /*respectGitignore=*/!cfg.noIgnore ); - if( cfg.ignoreTests ) + if( d.valueUses ) { - rw::applyIgnoreTests( rich ); // the same population the lean ingest answers from + return { rw::grepCarriers( ing, vri, rows, &ing ), {} }; } - GrepCarrierReadsSource source{ &rich }; - if( rich.memoryStop.isSet() ) + GrepCarrierReadsSource source; + const std::optional rich = grepReadsIngest( d, source ); + if( !rich ) { - rw::memguard::answerStops(); // answered here: reads= stays absent (unknown), the rest of the answer is the lean run's - DISCLOSE( source, GrepCarrierReadsSource::DisclosureWhy::MemoryStop, "grep: the rich re-ingest for reads= stopped at the memory guard" ); + const std::string next = rw::nextFlag( "--uses=", ing.symbols[ hits[0].enclosingId ].name ); + return { rw::grepCarriers( ing, vri, rows, nullptr ), + std::string( " reads_skipped=\"" ) + source.skipped + "\"" + rw::nextAttrXml( next, "reads_next" ) }; } - return rw::grepCarriers( ing, vri, rows, source.reads ); + return { rw::grepCarriers( ing, vri, rows, &*rich ), {} }; } // count-floor: callers= beside evidence of a miss for THAT row is a floor (countfloor.h). Computed once per page, before @@ -253,12 +317,20 @@ inline constexpr const char* kGrepCarrierClause = "On a row whose definitions are all variables, constants or initialisers (a carrier), reads= counts the read sites the " "uses verb lists for that NAME (matched by name, every same-named definition; present only when nonzero and only where " "that verb indexes reads, so its absence means none counted, never none exist) and reads_at= names the first three as " - "file:line in that verb's order (source before tests, then path and line); vals= names up to three function values the " + "file:line in tier, path, line order (source before tests, then path and line, not the order the uses verb prints); vals= names up to three function values the " "carrier itself stores (resolved by name to an indexed function), as slot=function@line: the slot key (absent for a " "plain assignment) and the line of the one definition that name resolves to (path:line when it is in another file, no " "line when the name resolves to several); vals_total= counts all of them when more than three. When the top hit sits in a carrier with " "reads=, the root next= is the uses verb on that carrier instead of the at verb. "; +// L4: the root's disclosure when the memory guard stopped the rich ingest that counts reads (grepReadsIngest) — spliced +// only on such an answer. Same rules as the clause above. +inline constexpr const char* kGrepCarrierSkipClause = + "When the root carries reads_skipped, the top hit sits in a carrier whose reads were NOT counted: the second, rich index " + "that counts them stopped at the memory guard (memory_stop: the soft line of the max memory flag or system pressure " + "during it; hard_limit: the footprint was over the hard line after it), so no row has reads= or reads_at= and the root " + "next= is unchanged; reads_next is the uses verb on that carrier, the call that lists those reads on its own. "; + inline constexpr const char* kGrepEncFloorClause = "callers_floor= (value 1, present only on such a row) marks a count the index holds evidence of a miss for: a declined or " "unbound call spelled like the name, a use as a value, or a kind used by reading or naming it (a variable, a class, an " @@ -865,7 +937,8 @@ int emitGrepReport( const MainDispatch& d ) const std::vector encRows = grepEnclosingRows( ing, g, std::span( hits ) ); // R1b, before the legend const std::vector encFloors = grepEncFloors( ing, g, encRows ); const bool anyEncFloor = std::ranges::any_of( encFloors, []( const CallerFloor& f ) { return f.isFloor; } ); - const std::vector encCarriers = grepPageCarriers( cfg, ing, root, encRows, valueUses ); // L4, before the legend + const GrepPageCarriers pageCarriers = grepPageCarriers( d, hits, encRows ); // L4, before the legend + const std::vector& encCarriers = pageCarriers.carriers; const bool anyCarrierAttr = std::ranges::any_of( encCarriers, []( const GrepCarrier& c ) { return ( c.readsKnown && c.reads > 0 ) || !c.vals.empty(); } ); const std::vector hitRefs = grepHitRefs( cfg, ing, tierReport, encRows, hits ); // idea #5: ref= (empty off a code+string answer) std::vector esc; @@ -1015,7 +1088,7 @@ int emitGrepReport( const MainDispatch& d ) // parse this verb's header counters by grep, and a quoted numeric example here would be matched first — the quality-delta legend's rule. "After the hit rows, rows list each DISTINCT enclosing symbol NAME of THIS page (first-appearance order, bounded by the page) with callers= its 1-hop DISTINCT-caller count, " "unioned across same-named defs like the callers verb (a FLOOR — dynamic dispatch contributes no edge), defs= how many defs the name grouped (only when more than one), cx= complexity; " - "{}{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=); L4: kGrepCarrierClause, the same + "{}{}{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=); L4: kGrepCarrierClause and kGrepCarrierSkipClause, the same "amp=/tested= join only when a metrics co-run already computed that lens. On a zero-hit answer a element may follow: SUGGESTIONS, never matches — near= the nearest indexed " "symbol name (did-you-mean), next= a ready-to-paste conceptual fallback; absent for regex/non-word-like patterns or when nothing plausible exists. " // T1: the completeness claim, defined where it appears (no attribute=value literal in these @@ -1068,7 +1141,8 @@ int emitGrepReport( const MainDispatch& d ) // P3 (L7): next= on the root, defined where the reader meets it "next= is the one pasteable follow-up: the at verb on the top hit; the next page (compact legend) when cut; " "the conceptual lens on a zero-hit answer. " - "{} -->", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", rw::kPageRaiseCapClause ); + "{} -->", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", + pageCarriers.skippedAttrs.empty() ? "" : kGrepCarrierSkipClause, rw::kPageRaiseCapClause ); } emitGrepHandleLegend( cfg.grepHandles ); // G3: terms=/scope=/suppressed= — only when AND/NOT was actually given, so a plain --grep answer @@ -1099,6 +1173,7 @@ int emitGrepReport( const MainDispatch& d ) { scanShortAttr += rw::kGraphCountFloorAttrXml; } + scanShortAttr += pageCarriers.skippedAttrs; // L4: the reads the memory guard stopped (reads_skipped=, reads_next=), or "" const char* schemaAttr = cfg.legend == "compact" ? " schema=\"ripwire.grep/v1\"" : ""; // P3 (L7, nextverb.h): the one follow-up. A CUT answer → the next page, under the compact legend (the page // is what the agent wants, not the prose it has already read); a hit → the enclosing-definition chain at the diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index fdda894a9..3eaeadeb3 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -432,7 +432,7 @@ printf 'function homeHandler() { return 1; }\nexport const ROUTES = { home: home OUT_P="$( run pc --grep=PC_NAMED_MARK )"; ROW_P="$( enc_row "$OUT_P" NAMED )" [ -n "$ROW_P" ] || no "(P) premise: no row" if [ "$( attr "$ROW_P" reads )" = 3 ]; then ok "(P) reads=\"3\" — the uses verb's three read sites of NAMED"; else no "(P) reads on NAMED: $ROW_P"; fi -if [ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ]; then ok "(P) reads_at= lists them in the uses verb's order (source before tests, then path, line)"; else no "(P) reads_at on NAMED: $ROW_P"; fi +if [ "$( attr "$ROW_P" reads_at )" = "lib/reader.py:4 lib/reader.py:7 test_reader.py:4" ]; then ok "(P) reads_at= lists them in tier, path, line order (source before tests, then path, line)"; else no "(P) reads_at on NAMED: $ROW_P"; fi USES_P="$( run pc --uses=NAMED | grep -oE ' "$W/pm/lib/aa.py" +printf 'from lib import aa\n\ndef reader():\n return aa.LATE\n' > "$W/pm/lib/rd.py" +printf 'TOPV = "PW_WS_MARK"\n' > "$W/pw/a/lib/top.py" +printf 'from lib import top\n\ndef r2():\n return top.TOPV\n' > "$W/pw/a/lib/use.py" +printf 'OTHER = "PW_WS_MARK"\n\ndef r3():\n return OTHER\n' > "$W/pw/b/bb.py" +OUT_PM="$( cd "$W/pm" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PM_MIX_MARK 2>/dev/null )" +ROW_PM="$( enc_row "$OUT_PM" LATE )"; R_PM="$( root_tag "$OUT_PM" grep )" +[ -n "$ROW_PM" ] || no "(P/top) premise: no row" +[ -n "$( enc_row "$OUT_PM" early )" ] || no "(P/top) premise: no row (the top hit's function)" +case "$ROW_PM" in *reads=*|*reads_at=*) no "(P/top) a lower carrier counted reads although the top hit is in a function: $ROW_PM" ;; *) ok "(P/top) top hit in a function: the lower carrier LATE has no reads=" ;; esac +if [ "$( attr "$ROW_PM" floor_next )" = "--uses=LATE" ]; then ok "(P/top) the lower carrier keeps floor_next=\"--uses=LATE\" as the clue"; else no "(P/top) clue on LATE: $ROW_PM"; fi +case "$R_PM" in *reads_skipped=*) no "(P/top) a second ingest ran (the hard-line reading after it fired): $R_PM" ;; *) ok "(P/top) no second ingest: the hard-line reading it would add never happened" ;; esac +if [ "$( attr "$R_PM" next )" = "--at=lib/aa.py:2" ]; then ok "(P/top) next= stays the at verb on the top hit"; else no "(P/top) next: $R_PM"; fi +MCP_PM="$( mcp_call grep "{\"path\":\"$W/pm\",\"pattern\":\"PM_MIX_MARK\"}" )" +if printf '%s' "$MCP_PM" | python3 -c ' +import json, sys +r = { x["n"]: x for x in json.loads( sys.stdin.read() ).get( "enclosing", [] ) }.get( "LATE" ) +sys.exit( 0 if r is not None and "reads" not in r and r.get( "floor_next" ) == "--uses=LATE" else 1 )'; then ok "(P/top) MCP: the same page, the same verdict (no reads key on LATE, floor_next clue)"; else no "(P/top) MCP LATE: $MCP_PM"; fi + +# P/mem (fix N4): the second ingest runs under the memory guard. Over the hard line after it (seam: the third hard-line +# reading of a single-root grep, after main's ingest and graph readings), the answer keeps the lean rows and its root +# discloses the skipped reads in band, with the call that recovers them; exit 0, nothing refused. +OUT_PX="$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK 2>/dev/null; echo "rc=$?" )" +R_PX="$( root_tag "$OUT_PX" grep )"; ROW_PXM="$( enc_row "$OUT_PX" NAMED )" +[ -n "$ROW_PXM" ] || no "(P/mem) premise: no row on the stopped answer" +if [ "$( attr "$R_PX" reads_skipped )" = hard_limit ] && [ "$( attr "$R_PX" reads_next )" = "--uses=NAMED" ]; then ok "(P/mem) stopped: root reads_skipped=\"hard_limit\" reads_next=\"--uses=NAMED\""; else no "(P/mem) root on a stopped reads ingest: $R_PX"; fi +case "$ROW_PXM" in *reads=*) no "(P/mem) a stopped ingest still printed reads=: $ROW_PXM" ;; *) ok "(P/mem) stopped: no reads= on the row (unknown, never a short count)" ;; esac +case "$OUT_PX" in *rc=0) ok "(P/mem) stopped: exit 0, the lean answer stands" ;; *) no "(P/mem) stopped exit: ${OUT_PX##*rc=}" ;; esac +comments "$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK --legend=compact 2>/dev/null )" | grep -qF "reads_skipped=" \ + && ok "(P/mem) compact legend defines reads_skipped= on that answer" || no "(P/mem) compact legend lacks reads_skipped=" +comments "$( cd "$W/pc" && RIPWIRE_TEST_MEMGUARD=hard:3 "$BIN" . --no-cache --grep=PC_NAMED_MARK --legend=full 2>/dev/null )" | grep -qF "reads_next" \ + && ok "(P/mem) full legend defines reads_next on that answer" || no "(P/mem) full legend lacks reads_next" +case "$OUT_P" in *reads_skipped*) no "(P/mem) near miss: an unstopped answer carries reads_skipped" ;; *) ok "(P/mem) near miss: an unstopped answer carries no reads_skipped (nor its legend)" ;; esac + +# P/cache (fix N5): an explicit --cache=F is honoured — the second ingest writes no rich auto-cache under TMPDIR. +mkdir -p "$W/tmpd" +OUT_PK="$( cd "$W/pc" && TMPDIR="$W/tmpd" XDG_CACHE_HOME="$W/tmpd" "$BIN" . --cache="$W/named.bin" --grep=PC_NAMED_MARK 2>/dev/null )" +[ "$( attr "$( enc_row "$OUT_PK" NAMED )" reads )" = 3 ] || no "(P/cache) premise: reads=\"3\" under --cache= (the second ingest ran)" +if [ -z "$( find "$W/tmpd" -type f 2>/dev/null )" ] && [ -f "$W/named.bin" ]; then ok "(P/cache) --cache=F: F written, no hidden rich auto-cache under TMPDIR"; else no "(P/cache) files under TMPDIR: $( find "$W/tmpd" -type f | tr '\n' ' ' )"; fi +OUT_PK2="$( cd "$W/pc" && TMPDIR="$W/tmpd" XDG_CACHE_HOME="$W/tmpd" "$BIN" . --cache="$W/named.bin" --grep=PC_NAMED_MARK 2>/dev/null )" +if [ "$OUT_PK" = "$OUT_PK2" ]; then ok "(P/cache) warm == cold under --cache=F"; else no "(P/cache) warm and cold answers differ under --cache=F"; fi + +# P/ws (fix N6): reads= on a workspace (multi-root) run — the CLI re-ingests each root rich and merges, so it prints the +# same reads/reads_at as the MCP workspace index (rich) on the same page. +OUT_PW="$( "$BIN" "$W/pw/a" "$W/pw/b" --no-cache --grep=PW_WS_MARK 2>/dev/null )" +ROW_PWA="$( enc_row "$OUT_PW" TOPV )"; ROW_PWB="$( enc_row "$OUT_PW" OTHER )" +if [ "$( attr "$ROW_PWA" reads_at )" = "a/lib/use.py:4" ] && [ "$( attr "$ROW_PWB" reads_at )" = "b/bb.py:4" ]; then ok "(P/ws) CLI workspace: reads_at= per root, label/path"; else no "(P/ws) CLI workspace rows: $ROW_PWA $ROW_PWB"; fi +if [ "$( attr "$( root_tag "$OUT_PW" grep )" next )" = "--uses=TOPV" ]; then ok "(P/ws) CLI workspace next=\"--uses=TOPV\""; else no "(P/ws) CLI workspace next: $( root_tag "$OUT_PW" grep )"; fi +MCP_PW="$( mcp_call grep "{\"paths\":[\"$W/pw/a\",\"$W/pw/b\"],\"pattern\":\"PW_WS_MARK\"}" )" +CLI_PW="$( for n in TOPV OTHER; do r="$( enc_row "$OUT_PW" "$n" )"; printf '%s %s %s\n' "$n" "$( attr "$r" reads )" "$( attr "$r" reads_at )"; done )" +MCPS_PW="$( printf '%s' "$MCP_PW" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +for x in d.get( "enclosing", [] ): + print( x["n"], x.get( "reads", "" ), " ".join( x.get( "reads_at", [] ) ) ) +print( "next", d.get( "next" ) )' )" +if [ "$MCPS_PW" = "$CLI_PW +next --uses=TOPV" ]; then ok "(P/ws) MCP workspace: reads/reads_at/next identical to the CLI"; else no "(P/ws) MCP vs CLI: [$MCPS_PW] vs [$CLI_PW]"; fi +OUT_PWS="$( RIPWIRE_TEST_MEMGUARD=hard:4 "$BIN" "$W/pw/a" "$W/pw/b" --no-cache --grep=PW_WS_MARK 2>/dev/null )" +if [ "$( attr "$( root_tag "$OUT_PWS" grep )" reads_skipped )" = hard_limit ] && [ -z "$( attr "$( enc_row "$OUT_PWS" OTHER )" reads )" ]; then ok "(P/ws) a workspace stopped after its first rich root: reads_skipped=, no partial reads= on any row"; else no "(P/ws) stopped workspace: $( root_tag "$OUT_PWS" grep )"; fi + # ── N: legend ──────────────────────────────────────────────────────────────────────────────────────────── echo "=== N: every new attribute is defined where it rides ===" for dialect in compact full; do From c50c5b38bd60c3ef85584fa55fa4df15927e925d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 14 Oct 2026 21:29:19 -0400 Subject: [PATCH 061/162] feat(graph): an element access calls its receiver class's subscript operator where the receiver is proven; Ruby's main object in --lego Ruby `recv[ k ]` / `recv[ k ] = v` and Python `obj[ k ]` / `obj[ k ] = v` now record a RefRole::Subscript reference named by the language's operator-method table (kSubscriptOperatorMethods: `[]`/`[]=`, `__getitem__`/`__setitem__`). graph.h's resolve loop takes one up as a call ONLY where a receiver rule proves the class (subscriptOperatorProven): Ruby's lookup on self in a def through include/extend ancestry, a receiver the code builds, a class constant's own `def self.[]`; Python's receiver evidence (self in a class or its bases, a typed parameter, a constructed local, an assigned field). Every other element access is dropped before any tally: no edge, no via="name" hedge, no count, and no other reference reader sees it (--uses, the builtin-method gate's evidence, the external surface, context ratio). rake: --callees=invoke_task names TaskManager#[] (task_manager.rb:54). A Ruby file's top-level `include M` / `extend M` / `self.extend M` records LocalBindKind::RubyMainMixin; --lego=M lists ``, counted in implementors=, with a present-only legend clause (rake: --lego=DSL names dsl_definition.rb:196). Parser version 174 (the train renumbers). Gate: test/subscriptopcheck.sh. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 +++ README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- queries/python/tags.scm | 8 + queries/ruby/tags.scm | 7 + src/contextratio.h | 4 +- src/eval.h | 5 +- src/externalnames.h | 3 +- src/graph.h | 136 +++++++++++- src/ingest_binds.h | 123 ++++++++++- src/ingest_cache.h | 8 +- src/ingest_relations.h | 10 +- src/ingest_ruby.h | 36 ++++ src/ingest_sidecap.h | 11 + src/mcpverbs.h | 6 +- src/model.h | 32 ++- src/quality.h | 2 +- src/resolve.h | 5 +- src/serialize.h | 30 ++- src/tsprobe.cpp | 2 +- src/valuerefindex.h | 2 +- src/verbs_for.h | 2 +- src/verbs_navigate.h | 4 +- test/regression.sh | 2 +- test/subscriptopcheck.sh | 382 +++++++++++++++++++++++++++++++++ 26 files changed, 817 insertions(+), 44 deletions(-) create mode 100755 test/subscriptopcheck.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a9b5e3f..19b40a739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,31 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — an element access is a call of the receiver class's subscript operator, where the receiver is proven (Ruby, Python) + +Ruby `recv[ k ]` runs the receiver class's `[]` (`[]=` as an assignment's target), and Python `obj[ k ]` its +`__getitem__` (`__setitem__`). Neither was a call before, so rake's `--callees=invoke_task` missed `t = self[ name ]`, +which runs TaskManager#[] through Application's `include TaskManager`. The site is now a call **only where a receiver +rule proves the class**: Ruby's lookup on `self` in a def (through include/extend ancestry), a receiver the code builds +(`r = Registry.new`, `Registry.new[ k ]`), a class constant's own `def self.[]` (`Rake::Task[ name ]`); Python's receiver +evidence (`self` in a class or its bases, a typed parameter, a constructed local, a field the class assigns). Every +other element access — a Hash/Array/dict/list local, a literal, an untyped parameter, a Python class object — makes no +edge, no `via="name"` hedge and no count: a `[]` or `__getitem__` definition elsewhere in the tree never decides it by +name. It does NOT mean every subscript of an in-tree class is found: a Ruby instance variable receiver is not typed, a +compound assignment (`h[ k ] ||= v`) is recorded as the read form, and `del o[ k ]`, a multiple-assignment target and an +annotation record nothing. On rake 93 callers of `[]` appear; on textual 42 of `__getitem__`. C++ `operator[]` could +share the path and is not armed. `--uses` lists no subscript site. Parser version 174 (the train renumbers). Gate: +`test/subscriptopcheck.sh`. + +### Added — `--lego=M` lists Ruby's main object when a file's top level mixes M in + +A top-level `include M`, `extend M` or `self.extend M` makes Ruby's main object answer M's methods (rake's +dsl_definition.rb ends with `self.extend Rake::DSL`). `--lego=M` now lists it as ``, counted in `implementors=`, with a present-only legend clause. Ruby-only: no other indexed language has +an object that a file's top level mixes a module into. Not read: a directive inside a top-level `if`/`begin` or a block, +`prepend`, a mixin onto another object, and a module the tree never opens. The ranked `--for` lego section is unchanged. +Gate: `test/subscriptopcheck.sh` (L arms). + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. diff --git a/README.md b/README.md index 2432bb90f..bf380fff8 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-684 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **684 gate scripts** and is the authoritative list; +`test/regression.sh` names **685 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 234344482..11acf723b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 684 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **684 gate scripts**, all of which exist on disk. +naming **685 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 684. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e545797c6..db11d6c46 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["684 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "684", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["684 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/queries/python/tags.scm b/queries/python/tags.scm index b2a7b9122..3d0ca57c9 100644 --- a/queries/python/tags.scm +++ b/queries/python/tags.scm @@ -65,6 +65,14 @@ attribute: (identifier) @name) ]) @reference.call +; An element access `obj[ k ]` is a call of the receiver class's subscript operator method — `__getitem__`, or +; `__setitem__` as an assignment's target — WHEN a receiver rule proves that class (`self[ k ]` in a class, a typed +; parameter or local, a constructed receiver). @name is the RECEIVER here, not a callee name: ingest_binds.h +; subscriptOperatorRef names the operator from the language's table (kSubscriptOperatorMethods) and records the site +; as RefRole::Subscript, which graph.h's resolve loop admits only on that proof (test/subscriptopcheck.sh). +(subscript + value: (_) @name) @reference.subscript + ; Shared import vocabulary (#358). The dependency target is still ONE per statement: the first ; name of a plain import (including its alias spelling), or the module_name of a from-import. ; The child anchor captures only the first plain-import name. The C++ side (ingest_importcap.h) diff --git a/queries/ruby/tags.scm b/queries/ruby/tags.scm index d44ff08bf..5ffc826f4 100644 --- a/queries/ruby/tags.scm +++ b/queries/ruby/tags.scm @@ -79,6 +79,13 @@ ; raises in a class and in a module) and defines nothing. (call method: (identifier) @name) @reference.call +; An element access `recv[ k ]` is a call of the receiver class's `[]` — `[]=` as an assignment's target — WHEN a +; receiver rule proves that class (`self[ k ]` in a def, through the class's include/extend ancestry; a receiver the +; code builds; a class constant's own `def self.[]`). @name is the RECEIVER here, not a callee name: ingest_binds.h +; subscriptOperatorRef names the operator from the language's table (kSubscriptOperatorMethods) and records the site +; as RefRole::Subscript, which graph.h's resolve loop admits only on that proof (test/subscriptopcheck.sh). +(element_reference + object: (_) @name) @reference.subscript ; The Rails schema capture (test/rubyschemacheck.sh): a file whose tree holds a ; `create_table "x", … do |t| … end` call is a rendered db/schema.rb (content-gated — path plays no ; part), and each `t. "name"` / `t. :name` in the table block mints ONE data-kind def diff --git a/src/contextratio.h b/src/contextratio.h index 73a17d22c..b6711819e 100644 --- a/src/contextratio.h +++ b/src/contextratio.h @@ -303,9 +303,9 @@ inline Facts collectFacts( const IngestResult& ing, const NameDefs& byName, for( const Reference& r : ing.references ) { - if( r.isDocLink || r.lang == Lang::Markdown ) + if( r.isDocLink || r.lang == Lang::Markdown || r.role == RefRole::Subscript ) { - continue; // a doc→code mention is a reader's cross-reference, not a thing the code resolves + continue; // a doc→code mention is a reader's cross-reference, not a thing the code resolves; a subscript is a call only once graph.h proves it } // member-variable round (card A3): a MEMBER ACCESS (`o.count`, `this->name`, model.h isMemberAccessSite) // names a field, and a field is never in ing.symbols; resolving it here by NAME would bind `s.name` to diff --git a/src/eval.h b/src/eval.h index 7260f7bbe..0d401c2b0 100644 --- a/src/eval.h +++ b/src/eval.h @@ -188,7 +188,10 @@ inline int runEval( const std::string& root, const IngestResult& ing, const Grap } for( const Reference& rf : ing.references ) { - add( rf.fileId, rf.calleeName ); + if( rf.role != RefRole::Subscript ) // an element access names an operator, not a name the source spells + { + add( rf.fileId, rf.calleeName ); + } } double avgW = 0, avgS = 0; diff --git a/src/externalnames.h b/src/externalnames.h index 6415835b5..56afd88e2 100644 --- a/src/externalnames.h +++ b/src/externalnames.h @@ -270,7 +270,8 @@ inline constexpr std::string_view kGoBuiltinNames[] = { // if(typeof Object.getOwnPropertyDescriptor(t.prototype,n).value==='function')s.add(n)}console.log([...s].sort())" // 102 names. TypeScript reads the same table: it runs on the same builtin objects. // kRubyBuiltinMethodNames — Ruby 4.0.7, Hash Array String public_instance_methods(false), identifier-shaped names only -// (operators such as `<<` and `[]` are never a call reference's name): +// (an operator such as `<<` is never a call reference's name; `[]`/`[]=` is one only as an element access whose +// receiver class graph.h subscriptOperatorProven has proven, which no name table decides): // ruby -e 'puts [Hash,Array,String].flat_map{|t| t.public_instance_methods(false)}.map(&:to_s) // .select{|n| n =~ /\A[a-z_][A-Za-z0-9_]*[?!=]?\z/}.uniq.sort' // 233 names. diff --git a/src/graph.h b/src/graph.h index 658a09606..d7184735a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -72,6 +72,10 @@ struct Graph // lane lego-transitive fix 1: parallel to implementors — how each base→derived edge was BOUND (BaseBound). The direct rows // and every other reader keep reading implementors alone; only the --lego closure (legoImplementorClosure) walks this. std::vector> implementorBound; + // lane ruby-pair: the Ruby main object as an implementor — each top-level `include M` / `extend M` whose constant the + // tree opens (rubyMainObjectMixins), sorted by (base, fileId, line). Not in `implementors`: main is no symbol, so the + // graph's class readers (closure, CHA, ranks) never see it; only --lego's targeted answer lists it. + std::vector rubyMainMixins; std::vector> mentions; // code symbol id → markdown file-nodes that name it in a `backtick` (doc↔code) std::vector ambOut; // per-symbol: # outgoing calls that STILL resolved to MULTIPLE in-repo // defs AFTER canonical/narrow/locality resolution (the resolver could @@ -519,6 +523,7 @@ inline bool namespaceCompatible( RefRole role, SymKind kind ) noexcept } case RefRole::Call: case RefRole::Read: case RefRole::Write: case RefRole::Import: case RefRole::Value: case RefRole::Through: // never in the call loop; graph.h valueRefIndex narrows by kind itself + case RefRole::Subscript: // the call loop's own receiver proof decides it (subscriptOperatorProven) { return true; // un-narrowed — see the doctrine above; a NEW role is a -Werror=switch decision here } @@ -4995,7 +5000,7 @@ inline void collectFileClassEvidence( const IngestResult& ing, const std::vector // a backtick mention in prose is not code evidence; a read/write USE-site is recorded only by the rich ingest (--uses, // MCP), so reading it here made the same call bind on MCP and decline on the CLI map — evidence must not depend on // which ingest ran (FE-B found it through mcpclidiffcheck's stanza on a Python `set().update()` beside a method) - if( !r.isDocLink && r.role != RefRole::Read && r.role != RefRole::Write ) + if( !r.isDocLink && r.role != RefRole::Read && r.role != RefRole::Write && r.role != RefRole::Subscript ) // a subscript names an operator, and is a call only once proven { note( r.fileId, r.calleeName ); note( r.fileId, r.recvVar ); @@ -5004,7 +5009,7 @@ inline void collectFileClassEvidence( const IngestResult& ing, const std::vector } for( const Binding& b : ing.bindings ) { - if( !isReceiverEvidenceKind( b.kind ) ) // FE-B's receiver evidence is read by ReceiverEvidence alone + if( !isReceiverEvidenceKind( b.kind ) && b.kind != LocalBindKind::RubyMainMixin ) // FE-B's receiver evidence is read by ReceiverEvidence alone; a main mixin by --lego alone { noteBindingClasses( gate, fileGated, b ); } @@ -5034,6 +5039,37 @@ inline void collectFileClassEvidence( const IngestResult& ing, const std::vector } } +// The main object's mixins (LocalBindKind::RubyMainMixin, ingest_ruby.h captureRubyMainMixins) as implementor rows: the +// constant a Ruby file's top level writes is read as Ruby reads it there — at the top level, so the path as written is the +// fully-qualified constant (a leading `::` changes nothing) — and each class or module whose own open is that constant +// (RubyBaseScope::classesByFqn) gets one row per directive. A constant the tree never opens (`include FileUtils`) lists +// nothing: the module is outside the tree. Ruby-only by construction (no other language records the kind). +inline std::vector rubyMainObjectMixins( const IngestResult& ing, const RubyBaseScope& bases ) +{ + std::vector rows; + for( const Binding& b : ing.bindings ) + { + if( b.kind != LocalBindKind::RubyMainMixin ) + { + continue; + } + const std::string_view written = std::string_view( b.typeName ).starts_with( "::" ) ? std::string_view( b.typeName ).substr( 2 ) : std::string_view( b.typeName ); + const auto it = bases.classesByFqn.find( std::string( written ) ); + if( it == bases.classesByFqn.end() ) + { + continue; + } + for( const NodeId base : it->second ) + { + rows.push_back( RubyMainMixinRow { base, b.fileId, b.spanStart } ); + } + } + std::sort( rows.begin(), rows.end() ); + rows.erase( std::unique( rows.begin(), rows.end(), []( const RubyMainMixinRow& a, const RubyMainMixinRow& b ) { return !( a < b ) && !( b < a ); } ), rows.end() ); + ENSURES( std::is_sorted( rows.begin(), rows.end() ), "legoMainRowsOf binary-searches the rows by base" ); + return rows; +} + inline BuiltinMethodGate buildBuiltinMethodGate( const IngestResult& ing, const ExternalVeto& veto, const ExternalVetoTables& vetoTables, const std::vector>& directIncludes, const JsImportTables& jsImports ) { @@ -7171,10 +7207,99 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.pinCensus.addOracleRow( nd.from, nd.calleeName, nd.kind ); } } + // SUBSCRIPT = OPERATOR-METHOD CALL (RefRole::Subscript; ingest_binds.h subscriptOperatorRef; test/subscriptopcheck.sh). + // An element access `recv[ k ]` runs the receiver class's subscript operator method (Ruby `[]`/`[]=`, Python + // `__getitem__`/`__setitem__`) — but only a class the tree defines can make it a call into the tree, and the receiver + // of nearly every element access is a builtin Hash/Array/dict/list whose own operator runs. So the site is taken up + // as a call ONLY where a receiver rule PROVES the class: Ruby's lookup on self in a def (RubyClassObjects::selfCall, + // then RubySelfReach through the class's include/extend/superclass ancestry), a receiver the code builds + // (RubyTypedReceivers), a class constant's closed lookup (`Registry[ :x ]` reaching its `def self.[]`); Python's + // receiver evidence (`self` in a class and its bases, a typed parameter or local, a constructed receiver); and Rule 1 + // with its base walk for `self`. Anything else — an untyped local, a literal, a parameter, an instance variable — + // is dropped HERE, before any tally: no edge, no hedge, no unresolved=/external=/declined= count, so an answer that + // holds no proven subscript is byte-identical to one built before this rule. The decision is never by name: no `[]` + // or `__getitem__` definition anywhere in the tree makes a site a call unless its receiver's class reaches it. + // A proven site then runs the ladder below as the call `recv.[]( k )` would (role Call on a copy): the same rules + // decide its candidates, so it lands where that call lands; the census leaves it out (pincensus.h's population). + Reference subscriptCall; // the site as the call it runs (role Call), reused per proven subscript + std::vector subscriptCand; // scratch: what the proof found (the ladder recomputes it) + CallDispositionCounts subscriptDispositions {}; // a proven subscript's tally: outside the census's call population + // Python: `Registry[ k ]` / `cls[ k ]` subscripts the CLASS object, which runs its metaclass's `__getitem__` (or the + // class's `__class_getitem__`), never the instance method receiver evidence would read a class-name receiver's call as. + const auto subscriptOnClassObject = [ & ]( const Reference& r ) -> bool + { + const ReceiverEvidence::Chain ch = ReceiverEvidence::chainOf( r ); + bool isStatic = false, superOnly = false; + return ch.valid && ch.path.empty() && ch.ctor.empty() && ( ch.root == "cls" || ( !recvEv.rootClass( r, ch, isStatic, superOnly ).empty() && isStatic ) ); + }; + const auto subscriptOperatorProven = [ & ]( const Reference& r ) -> bool + { + EXPECTS( r.role == RefRole::Call, "the proof reads the site as the call it runs" ); + if( r.fromSymbol == kNoNode || !byName.contains( r.calleeName ) ) + { + return false; // no caller node, or no definition of the operator method anywhere: nothing to prove + } + subscriptCand.clear(); + const std::string& scope = callerSelfScope( ing, r ); + bool rubySelfDecided = false; + if( r.lang == Lang::Ruby ) + { + const RubySideAnswer self = rubyClasses.selfCall( r ); + rubySelfDecided = self.side.has_value(); + if( rubySelfDecided && narrowTo( self.hit, r, subscriptCand ) ) + { + return true; + } + if( const std::string* typed = rubyTypes.classOf( r ); typed != nullptr ) + { + const RubySideAnswer built = rubyClasses.typedLookup( r, *typed ); + return ( built.side && narrowTo( built.hit, r, subscriptCand ) ) + || narrowTo( built.side ? rubyClasses.onSide( rubyTypes.lookup( r, *typed ), *built.side ) : rubyTypes.lookup( r, *typed ), r, subscriptCand ); + } + if( const RubyClosedLookup closed = rubyClosedLookup( r, rubyTypes, rubyClasses ); closed.closed ) + { + return narrowTo( closed.hit, r, subscriptCand ); + } + if( r.recv == RecvKind::ThisObj && RubySelfReach::judges( r, scope ) ) + { + const std::string& key = rubyReach.selfOf( ing, r ); + for( NodeId c : byName.find( r.calleeName )->second ) + { + if( langCompatible( ing.symbols[ c ].lang, r.lang ) && sameRoot( c, r.fileId ) && rubyTop.proves( key, c ) ) + { + return true; // Ruby's own lookup on self reaches it (an included module's `def []`) + } + } + } + } + else if( !subscriptOnClassObject( r ) && recvEv.narrow( r ) ) + { + return true; // a stated class: `self` in a class or its bases, a typed parameter or local, a constructed receiver + } + if( r.recv != RecvKind::ThisObj || rubySelfDecided ) + { + return false; + } + return narrowTo( narrower.rule1ClassMember( r, scope ), r, subscriptCand ) + || narrowTo( narrower.rule1BaseWalk( r, scope, r.lang == Lang::Ruby ? rubyUp : chaUp, chaUpDeclared ), r, subscriptCand ); + }; { PROFILE_SCOPE_DESCRIBE( "buildGraph/3: resolve loop (per reference)" ); - for( const Reference& r : ing.references ) + for( const Reference& site : ing.references ) { + // An element access is a call only where its receiver's class is proven (subscriptOperatorProven above); the + // rest of the loop reads it as that call. + const bool subscript = site.role == RefRole::Subscript; + if( subscript ) + { + subscriptCall = site; + subscriptCall.role = RefRole::Call; + if( !subscriptOperatorProven( subscriptCall ) ) + { + continue; + } + } + const Reference& r = subscript ? subscriptCall : site; // inheritance / doc-mention / HAS-A → not a call. ABS-3: read/write/import use-sites are ALSO excluded // here — they live only in the use-site index, NEVER in the call graph CSR, so PageRank and the default // ranked map are unchanged by them (G5). Role Macro (the macro-edges round) IS admitted beside Call: an @@ -7188,7 +7313,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // From here on every exit names its disposition and the tally counts it when the iteration ends, // whichever `continue` ends it. An exit that names nothing is counted Unaccounted (DispositionTally). CallDisposition disposition = CallDisposition::Unaccounted; - const DispositionTally tally{ disposition, g.callDispositions }; + const DispositionTally tally{ disposition, subscript ? subscriptDispositions : g.callDispositions }; if( r.fromSymbol == kNoNode ) { disposition = CallDisposition::FileScope; // no caller node for an edge; the use-site index still lists the site @@ -8528,7 +8653,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // Recorded HERE and nowhere else: this is the exact point where the resolver has finished deciding // and is about to commit the edge, so the row states what was committed and what committed it. // pincensus.h::classifyPin owns the precedence rule; this site only reports the stage outcomes. - if( census ) + if( census && !subscript ) { // `narrowed` is passed WITHOUT the import pin: it is a latch this loop sets so the receiver // rules below cannot append more candidates, and on the ES-import path it says nothing about @@ -8834,6 +8959,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null v.erase( std::unique( v.begin(), v.end() ), v.end() ); } fillImplementorBound( g, boundEdges ); + g.rubyMainMixins = rubyMainObjectMixins( ing, rubyBases ); // doc↔code edges: isDocLink refs (markdown `backtick` → code name) → mentions[codeDef] += docFileNode. // Resolve the name to real DEFINITIONS (body present), any file; stored OUT of the call graph so a doc diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 3901d1c1c..8612c8012 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -1033,6 +1033,55 @@ inline void rubyForEachDeclaredSymbol( TSNode call, RubyDeclared kind, std::stri } ); } +// ── Subscript = operator-method call (RefRole::Subscript; queries/{ruby,python}/tags.scm @reference.subscript) ─────────── +// An element access `recv[ args ]` runs the receiver class's subscript OPERATOR METHOD. Which method is the language's +// own spelling, kept HERE as data rather than per-language code: the read form, and the form an assignment's target runs. +// C++ `operator[]` could ride the same row and the same graph.h proof (its Rules 1/2/2b type `this`, a local and a field); +// it is not armed: its element accesses are mostly builtin arrays, pointers and std containers, whose answers this lane +// keeps byte-identical. Every other indexed language has no user-definable subscript method (C, Go, Java, Bash, Lua) +// or spells it otherwise (C# indexers `this[ ]`, Swift `subscript`, Rust `Index::index`, Kotlin `operator fun get`, +// JS/TS proxies) and records nothing for it. +struct SubscriptOperatorNames +{ + Lang lang; + std::string_view read; // `recv[ k ]` + std::string_view write; // `recv[ k ] = v` +}; +inline constexpr SubscriptOperatorNames kSubscriptOperatorMethods[] = { + { Lang::Python, "__getitem__", "__setitem__" }, + { Lang::Ruby, "[]", "[]=" }, +}; + +inline const SubscriptOperatorNames* subscriptOperatorNamesOf( Lang lang ) noexcept +{ + const auto it = std::find_if( std::begin( kSubscriptOperatorMethods ), std::end( kSubscriptOperatorMethods ), + [ lang ]( const SubscriptOperatorNames& n ) { return n.lang == lang; } ); + return it == std::end( kSubscriptOperatorMethods ) ? nullptr : &*it; +} + +// The operator method element access `access` runs: the write form when it is an assignment's whole target (`h[ k ] = v`), +// "" where no one name is right — a target inside a multiple assignment (`a[ 0 ], b = …`), a `del x[ k ]` (Python's +// `__delitem__`, not read), an annotation (`x: Dict[ str, int ]`, a type, not a value). A compound assignment +// (`h[ k ] += 1`, `h[ k ] ||= v`) runs both forms and is recorded as the read one, the setter capture's floor. +inline std::string_view subscriptOperatorName( TSNode access, const SubscriptOperatorNames& names ) noexcept +{ + const TSNode parent = ts_node_parent( access ); + if( ts_node_is_null( parent ) ) + { + return names.read; + } + const char* pt = ts_node_type( parent ); + if( kindIs( pt, "assignment" ) ) + { + return ts_node_eq( fieldChild( parent, NodeField::Left ), access ) ? names.write : names.read; + } + const bool multiTarget = kindIs( pt, "pattern_list" ) || kindIs( pt, "tuple_pattern" ) || kindIs( pt, "list_pattern" ) + || kindIs( pt, "left_assignment_list" ) || kindIs( pt, "destructured_left_assignment" ); + const bool notValue = kindIs( pt, "delete_statement" ) || kindIs( pt, "type" ) || kindIs( pt, "generic_type" ) + || ( kindIs( pt, "expression_list" ) && kindIs( ts_node_type( ts_node_parent( parent ) ), "delete_statement" ) ); + return multiTarget || notValue ? std::string_view {} : names.read; +} + // One call whose receiver is typed (RubyBareCallWalk): the call node's start (its reference's startByte) and method, // whether the receiver is an (identifier) — the reference's NamedVar — or a value, and what it builds; for a local, the // scope and name whose binding types are read once the walk has seen every binding (`local` non-empty, `type` filled by @@ -1091,6 +1140,8 @@ class RubyBareCallWalk void noteLetType( TSNode letCall, std::string_view name ); void noteImplicitSubject(); void noteTypedCall( TSNode call ); + void noteTypedSubscript( TSNode access ); + void noteTypedSend( std::uint32_t at, TSNode recv, std::string_view method ); void noteDeclaredCall( TSNode call ); void noteNamedReceiver( std::uint32_t at, std::string_view method, std::string_view name ); const RubyTypedSite* siteOf( const RawRef& r ) const; @@ -1161,6 +1212,10 @@ inline void RubyBareCallWalk::visit( const Item& item, ChildCursor& cursor ) noteTypedCall( n ); noteDeclaredCall( n ); } + else if( kindIs( t, "element_reference" ) ) + { + noteTypedSubscript( n ); + } binding_.clear(); if( const std::optional role = rubyLocalRole( t ) ) { @@ -1321,13 +1376,25 @@ inline void RubyBareCallWalk::noteImplicitSubject() // a call whose receiver is typed: a value written in place, a let RSpec runs there, or a local of the scope (read at the end) inline void RubyBareCallWalk::noteTypedCall( TSNode call ) { - const TSNode recv = fieldChild( call, NodeField::Receiver ); - const std::string_view method = fieldChildTextOfKind( call, NodeField::Method, "identifier", src_ ); + noteTypedSend( ts_node_start_byte( call ), fieldChild( call, NodeField::Receiver ), fieldChildTextOfKind( call, NodeField::Method, "identifier", src_ ) ); +} + +// an element access `recv[ k ]` sends its receiver the operator method its reference names (subscriptOperatorRef), typed +// the way a call's receiver is +inline void RubyBareCallWalk::noteTypedSubscript( TSNode access ) +{ + const SubscriptOperatorNames* names = subscriptOperatorNamesOf( Lang::Ruby ); + ASSUME( names != nullptr, "kSubscriptOperatorMethods carries a Ruby row" ); + noteTypedSend( ts_node_start_byte( access ), fieldChild( access, NodeField::Object ), subscriptOperatorName( access, *names ) ); +} + +// the typed-receiver record of one send at `at`: `method` on `recv` +inline void RubyBareCallWalk::noteTypedSend( std::uint32_t at, TSNode recv, std::string_view method ) +{ if( ts_node_is_null( recv ) || method.empty() ) { return; } - const std::uint32_t at = ts_node_start_byte( call ); const bool ident = kindIs( ts_node_type( recv ), "identifier" ); if( !ident || !scopes_.visible( pattern::nodeText( recv, src_ ) ) ) { @@ -1456,7 +1523,7 @@ inline void RubyBareCallWalk::sendTargets( std::size_t firstRef, std::size_t end inline const RubyTypedSite* RubyBareCallWalk::siteOf( const RawRef& r ) const { const bool ident = r.recv == RecvKind::NamedVar; - if( r.lang != Lang::Ruby || r.role != RefRole::Call || ( !ident && r.recv != RecvKind::FieldOfVar ) ) + if( r.lang != Lang::Ruby || ( r.role != RefRole::Call && r.role != RefRole::Subscript ) || ( !ident && r.recv != RecvKind::FieldOfVar ) ) { return nullptr; } @@ -1938,11 +2005,18 @@ inline RecvShape receiverOf( TSNode nameNode, Lang lang, std::string_view src ) // RubyClassObjects). A qualified constant's whole path; the path through the described class for `described_class` and // for `described_class::Worker`; empty for a final segment alone, and for a path with any other head that is no constant // (`self.class::Foo`). +inline std::string rubyWrittenPathOfReceiver( TSNode recv, std::string_view final, std::string_view src ); inline std::string rubyReceiverWrittenPath( TSNode nameNode, std::string_view final, std::string_view src ) { const TSNode call = calleeAccessParent( nameNode ); - const TSNode recv = ts_node_is_null( call ) || !kindIs( ts_node_type( call ), "call" ) ? TSNode {} : fieldChild( call, NodeField::Receiver ); - std::string written; + return rubyWrittenPathOfReceiver( ts_node_is_null( call ) || !kindIs( ts_node_type( call ), "call" ) ? TSNode {} : fieldChild( call, NodeField::Receiver ), + final, src ); +} + +// rubyReceiverWrittenPath's answer for the receiver node itself (an element access's `object:` reads it too) +inline std::string rubyWrittenPathOfReceiver( TSNode recv, std::string_view final, std::string_view src ) +{ + std::string written; if( ts_node_is_null( recv ) ) { return written; @@ -1961,6 +2035,43 @@ inline std::string rubyReceiverWrittenPath( TSNode nameNode, std::string_view fi return written == final ? std::string {} : written; } +// The RefRole::Subscript reference of element access `access` (whose receiver is `recv`), or nullopt where it can name no +// class: a language with no operator row, a form subscriptOperatorName names nothing for, a receiver whose class no rule +// can type — a literal (`{ a: 1 }[ :a ]`, `[ 1, 2 ][ 0 ]`), a call result in Python, another element access. The +// receiver is read as a member call's is (classifyReceiver), so graph.h's receiver rules read the same fields; a Ruby +// receiver too rich for them stays FieldOfVar, which the typing walk may still type (`Report.new[ :x ]`, +// RubyBareCallWalk::noteTypedSend). +inline std::optional subscriptOperatorRef( TSNode access, TSNode recv, std::uint32_t fileId, Lang lang, std::string_view src ) +{ + const SubscriptOperatorNames* names = subscriptOperatorNamesOf( lang ); + const std::string_view op = names == nullptr ? std::string_view {} : subscriptOperatorName( access, *names ); + if( op.empty() || ts_node_is_null( recv ) ) + { + return std::nullopt; + } + RecvShape rs = classifyReceiver( recv, lang, src, /*allowChain=*/ true ); + if( rs.kind == RecvKind::None && lang != Lang::Ruby ) + { + return std::nullopt; + } + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( access ); + r.line = ts_node_start_point( access ).row + 1; + r.lang = lang; + r.role = RefRole::Subscript; + r.name = op; + r.recv = rs.kind == RecvKind::None ? RecvKind::FieldOfVar : rs.kind; // receiverOf's "too rich" member shape + r.recvVar = std::move( rs.var ); + r.fieldName = std::move( rs.field ); + if( lang == Lang::Ruby && r.recv == RecvKind::NamedVar ) + { + r.fieldName = rubyWrittenPathOfReceiver( recv, r.recvVar, src ); // a constant's path as written (parser version 137) + } + ENSURES( r.recv != RecvKind::None, "a subscript always has a receiver: graph.h's receiver rules read it as a member call" ); + return r; +} + // Java method references are not ordinary member-access nodes. The pinned grammar's first named // child is the receiver and deliberately uses the SAME `identifier` node for a simple type and a // variable. Preserve that uncertainty for graph.h: simple and dotted type candidates are retained diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 2b7366370..0f9282d4f 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -320,7 +320,13 @@ constexpr std::uint32_t kCacheVersion = 31; // 31: train 27 — lane l // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 171; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 174; // bump on any grammar/.scm/extraction change + // 174 = lane ruby-pair (train 28, 2026-10-14; the train renumbers): Ruby and Python element + // accesses record RefRole::Subscript references (tags.scm @reference.subscript, ingest_binds.h + // subscriptOperatorRef; a NEW role byte, which readRef validates against kRefRoleCount), the + // Ruby typing walk types their receivers, and a Ruby file's top-level `include M` / `extend M` + // records LocalBindKind::RubyMainMixin (an appended kind). 172/173 skipped: rich file tag 172 + // is 171's. Rich file tag 175. kCacheVersion stays 31. // 171 = 2026-10-13 (PR #349 lands, @tag-und-nacht): Clojure grammar, tags.scm and extraction // (ingest_clojure.h) and the qualified-call namespace in Reference::qualifier. The branch built // 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168). 171, not 168: a lean 168 diff --git a/src/ingest_relations.h b/src/ingest_relations.h index cbba4cf32..11ca162b5 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -2011,6 +2011,7 @@ inline std::string_view rubyNamedDirective( TSNode n, std::string_view src, std: // records, in SOURCE order (the same shape as elixirAliasGroup). A non-constant argument (`include // Object.const_get(:X)`, `include mod`) contributes nothing; the constant ones beside it still do. The constant // NODES: captureIncludes reads each one's text for its Include record, and captureRubyMixinBases its byte. +inline std::vector rubyConstantArguments( TSNode n ); inline std::vector rubyMixinTargets( TSNode n, std::string_view src ) { std::vector out; @@ -2019,7 +2020,14 @@ inline std::vector rubyMixinTargets( TSNode n, std::string_view src ) { return out; } - const TSNode args = fieldChild( n, NodeField::Arguments ); + return rubyConstantArguments( n ); +} + +// The constant (or constant-path) arguments of call `n`, in source order — a mixin directive's targets +inline std::vector rubyConstantArguments( TSNode n ) +{ + std::vector out; + const TSNode args = fieldChild( n, NodeField::Arguments ); if( ts_node_is_null( args ) ) { return out; diff --git a/src/ingest_ruby.h b/src/ingest_ruby.h index 9dc6c2f02..70da2e118 100644 --- a/src/ingest_ruby.h +++ b/src/ingest_ruby.h @@ -676,6 +676,42 @@ inline void captureRubyClassMixins( TSNode root, std::uint32_t fileId, std::stri } ); } +// Parser version 174 (lane ruby-pair; test/subscriptopcheck.sh): a TOP-LEVEL `include M` / `extend M` / `self.extend M`. +// Outside every class and module, self is Ruby's main object, and these make it answer M's methods — rake extends main +// with Rake::DSL so a Rakefile's bare `task` / `file` reach it. graph.h lists main as M's implementor (``). Ruby-only: main is Ruby's top-level self; no other indexed language has an object that a file's top level +// mixes a module into (Python/JS module scope is a namespace, not an instance). Read: a statement of the program itself, +// receiver-less or on `self`, and each constant argument. Not read (stated floors, test/subscriptopcheck.sh): a +// directive inside a top-level `if` / `begin` or a block, `prepend` (main has none), a non-constant argument. +inline void captureRubyMainMixins( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& binds ) +{ + if( ts_node_is_null( root ) || ( src.find( "extend" ) == std::string_view::npos && src.find( "include" ) == std::string_view::npos ) ) + { + return; + } + ChildCursor cursor( root ); + forEachNamedChild( root, cursor.cur, [ & ]( TSNode n ) + { + if( !kindIs( ts_node_type( n ), "call" ) ) + { + return true; + } + const TSNode recv = fieldChild( n, NodeField::Receiver ); + const std::string_view verb = fieldIdentifierText( n, NodeField::Method, src ); + if( ( verb != "include" && verb != "extend" ) || !( ts_node_is_null( recv ) || kindIs( ts_node_type( recv ), "self" ) ) ) + { + return true; + } + for( const TSNode c : rubyConstantArguments( n ) ) + { + binds.push_back( RawBind { .fileId = fileId, .startByte = ts_node_start_byte( c ), .lang = Lang::Ruby, .kind = LocalBindKind::RubyMainMixin, + .spanStart = ts_node_start_point( c ).row + 1, .importedName = std::string( verb ), + .typeName = rubyConstantText( c, src ) } ); + } + return true; + } ); +} + // ─── FactoryBot's factory definitions (parser version 132, test/rubytypedrecvcheck.sh) ───────────────────────────────── // `factory :user do … end` inside a `FactoryBot.define` (or `FactoryGirl.define`) block defines what `create( :user )` // builds, and its class is FactoryBot's own rule: the `class:` option as written — a constant, or a string naming one (its diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 7983f738c..1f8385786 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -2407,6 +2407,16 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t } else if( isRef && full ) { + // An element access: @name is its RECEIVER, and the site names the receiver class's subscript operator + // method (ingest_binds.h subscriptOperatorRef) — never a call by itself (RefRole::Subscript). + if( refCapSv == "reference.subscript" ) + { + if( std::optional sub = subscriptOperatorRef( roleNode, nameNode, fileId, le.lang, src ) ) + { + refs.push_back( std::move( *sub ) ); + } + continue; + } // H4: a C++ cast keyword is not a call — see isCppCastKeyword. Valid input, skipped, no alert. if( le.lang == Lang::Cpp && isCppCastKeyword( nameTxt ) ) { @@ -2574,6 +2584,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t captureRubyHelperMethods( root, fileId, src, binds ); captureRubySingletonAccessors( root, fileId, src, binds ); captureRubyClassMixins( root, fileId, src, binds ); + captureRubyMainMixins( root, fileId, src, binds ); if( le.ext == kJbuilderExt ) { typeRubyJbuilderLocal( fileId, refs ); diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f3e720b51..ad62f9ddc 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2684,7 +2684,7 @@ inline std::string legoText( const std::string& root, const std::string& type, R const std::string legoTargetedAttr = unprovenDefsAttrXml( unprovenDefs ) + graphCountFloorAttrXml( ix.g ); // outlives the call packLego( mem, ing, ix.g.implementors, flat, 1, redact, &impure, focus, /*withPaths=*/true, ing.realPaths.empty() ? std::string_view( root ) : std::string_view(), // R-R: root-relative - LegoTargetedArgs( legoTargetedAttr, page.limit, page.offset, &ix.g.implementorBound ) ); // H1 + M15 + the deeper-row page, as on the CLI + LegoTargetedArgs( legoTargetedAttr, page.limit, page.offset, &ix.g.implementorBound, ix.g.rubyMainMixins ) ); // H1 + M15 + the deeper-row page, as on the CLI rw::emitRaw( mem, "" ); } ); } @@ -3210,9 +3210,9 @@ inline std::string usesSelectorRefusal( const IngestResult& ing, const std::stri { continue; } - if( r.isCompose || r.isDocLink ) + if( r.isCompose || r.isDocLink || r.role == RefRole::Subscript ) { - continue; // type edge / doc mention — not a use-site + continue; // type edge / doc mention / an element access (no use-site: valuerefindex.h UsesValueFilter::skip) — not a use-site } if( r.lang == Lang::Markdown ) { diff --git a/src/model.h b/src/model.h index 970e1897c..3e1b04b97 100644 --- a/src/model.h +++ b/src/model.h @@ -403,16 +403,22 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a // may-call clue); never a use site, never in the CSR. -enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through }; +// Subscript — an element access `recv[ args ]` (Ruby, Python; ingest_binds.h subscriptOperatorRef), named as the +// receiver class's subscript OPERATOR METHOD (kSubscriptOperatorMethods: Ruby `[]` / `[]=`, Python +// `__getitem__` / `__setitem__`); receiver fields as a call's. NOT a call by itself: graph.h's resolve loop +// takes it up as one ONLY where a receiver rule proves the class (subscriptOperatorProven) — `self[ k ]` in a +// class, a receiver the source types — and drops it otherwise, with no count anywhere (a `hash[ k ]` is the +// builtin's own operator, not an unresolved call). Never a use site. +enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Subscript }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). -inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Through ) + 1; +inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Subscript ) + 1; static_assert( enumCountIsExact(), "kRefRoleCount must name the LAST RefRole enumerator — move it with the append" ); static_assert( sizeof( RefRole ) == 1, "RefRole must be a single byte (SoA-friendly, smallest int that fits)" ); // the terse `role=` attribute string for the use-site index — a declarative table indexed by the enum, in enum // order. The static_assert is the guard a switch's -Werror=switch used to be: a NEW role without a spelling is a // build error, never a silent fallback. -inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through" }; +inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through", "subscript" }; static_assert( std::size( kRefRoleTagTable ) == kRefRoleCount, "kRefRoleTagTable: one spelling per RefRole, in enum order" ); inline const char* refRoleTag( RefRole r ) noexcept { @@ -1103,9 +1109,15 @@ enum class LocalBindKind : std::uint8_t StaticMember, // a JS/TS class member declared `static`: var = the member's name, typeName "static", recorded INSIDE its // body so it attributes to the member. A call on the CLASS reaches only these, a call on an INSTANCE // only the others (the two sides of the lookup; #373's Ruby class-object rule, here for JS/TS). + RubyMainMixin, // parser version 174 (lane ruby-pair): a Ruby file's TOP-LEVEL `include M` / `extend M` / `self.extend M` — + // self there is the main object, which then answers M's methods (rake's `self.extend Rake::DSL`). One + // record per constant, at the constant's own start: typeName = the constant as written, importedName = + // the directive ("include" / "extend"), spanStart = the constant's 1-based line; var stays EMPTY and the + // record is file-scope, so no local-name table reads it. Read by graph.h rubyMainObjectMixins ONLY + // (--lego's `` row). APPENDED (cache u8). }; // The number of LocalBindKind enumerators — the bound readBind validates a cached kind byte against (see kSymKindCount). -inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::StaticMember ) + 1; +inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::RubyMainMixin ) + 1; // FE-B: the five kinds only graph.h ReceiverEvidence reads (see RecvType above). inline bool isReceiverEvidenceKind( LocalBindKind k ) noexcept { @@ -1122,6 +1134,18 @@ static_assert( enumCountIsExact(), "kLocalBi inline constexpr const char* kFnBindLambdaTarget = "(lambda)"; // parens are illegal in identifiers, so inline constexpr const char* kFnBindClobberTarget = "(unknown)"; // neither sentinel can match a real def +// One main-object implementor row (Graph::rubyMainMixins): the module the top level mixes in, and the directive's site. +struct RubyMainMixinRow +{ + NodeId base = kNoNode; + std::uint32_t fileId = 0; + std::uint32_t line = 0; // the constant's 1-based line + friend bool operator<( const RubyMainMixinRow& a, const RubyMainMixinRow& b ) noexcept + { + return std::tie( a.base, a.fileId, a.line ) < std::tie( b.base, b.fileId, b.line ); + } +}; + struct Binding { NodeId fromSymbol = kNoNode; // enclosing function/method (the binding's scope); kNoNode if file-scope diff --git a/src/quality.h b/src/quality.h index 0a0b746df..2da0f79bf 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2268,7 +2268,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 31; // MUST equal ingest.cpp's kCacheVersion (gated); 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 171; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 174; // MUST equal ingest.cpp's kParserVer (gated) // 171 = 2026-10-13 (PR #349 Clojure lands above main's 167 / rich tag 168 and the // branch's own 169 / rich tag 170; see the kParserVer note) // 167 = 2026-10-11 (train 27 release: lane fix27-cpp-subscript's 166 renumbered above the lane build; diff --git a/src/resolve.h b/src/resolve.h index 5221412a5..a9142a0f2 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -1805,7 +1805,7 @@ inline std::pair resolveRubyConstant( const RubyCo // byName rule — the pre-scoping answer, never a guess in either direction — and is COUNTED by `disclosure` below // (RubyBaseScopeDisclosure), which buildGraph copies to Graph::rubyBasesUnscoped: the gauge attribute // ruby_bases_unscoped=N, absent at zero (graphlegend.h graphGaugeAttrXml/Json, test/rubyinheritcheck.sh). -// Built only when the corpus holds a Ruby inherit reference; empty otherwise. +// Built only when the corpus holds a Ruby inherit reference or a main-object mixin (LocalBindKind::RubyMainMixin); empty otherwise. // // The sink models Diagnostics::DisclosureSink: one reason, one count, read by the emitter as the gauge attribute. struct RubyBaseScopeDisclosure @@ -2078,7 +2078,8 @@ inline RubyBaseScope buildRubyBaseScope( const IngestResult& ing ) { RubyBaseScope sc; const auto isRubyBase = []( const Reference& r ) noexcept { return r.isInherit && r.lang == Lang::Ruby; }; - if( std::none_of( ing.references.begin(), ing.references.end(), isRubyBase ) ) + const auto isMainMixin = []( const Binding& b ) noexcept { return b.kind == LocalBindKind::RubyMainMixin; }; // graph.h rubyMainObjectMixins reads classesByFqn too + if( std::none_of( ing.references.begin(), ing.references.end(), isRubyBase ) && std::none_of( ing.bindings.begin(), ing.bindings.end(), isMainMixin ) ) { return sc; } diff --git a/src/serialize.h b/src/serialize.h index 830ed426e..f9f88c631 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -8921,10 +8921,24 @@ struct LegoTargetedArgs int pageLimit = 0; // --limit / MCP limit over the deeper implementor rows (0 = the default page) int pageOffset = 0; // --offset / MCP offset const std::vector>* implementorBound = nullptr; // fix 1: Graph::implementorBound (null: nothing proven) + std::span mainMixins; // lane ruby-pair: Graph::rubyMainMixins (sorted by base) — the main-object rows LegoTargetedArgs() = default; - LegoTargetedArgs( std::string_view floorAttr, int limit = 0, int offset = 0, const std::vector>* bound = nullptr ) - : graphCountFloorAttr( floorAttr ), pageLimit( limit ), pageOffset( offset ), implementorBound( bound ) {} + LegoTargetedArgs( std::string_view floorAttr, int limit = 0, int offset = 0, const std::vector>* bound = nullptr, + std::span mains = {} ) + : graphCountFloorAttr( floorAttr ), pageLimit( limit ), pageOffset( offset ), implementorBound( bound ), mainMixins( mains ) {} }; + +// lane ruby-pair: the main-object implementor rows of base `id` (LegoTargetedArgs::mainMixins), in (file, line) order. +inline std::span legoMainRowsOf( std::span rows, NodeId id ) noexcept +{ + const auto lo = std::lower_bound( rows.begin(), rows.end(), id, []( const RubyMainMixinRow& r, NodeId v ) { return r.base < v; } ); + const auto hi = std::upper_bound( lo, rows.end(), id, []( NodeId v, const RubyMainMixinRow& r ) { return v < r.base; } ); + return { lo, hi }; +} + +// The present-only legend clause of a main-object row (no `--` inside: it rides in an XML comment). +inline constexpr std::string_view kLegoMainRowClause = + ""; inline const std::vector> kNoImplementorBound{}; // the empty bound map: every edge reads ByName // One row. `line` (S2): the TARGETED answer puts the definition's line on p= (p="file:LINE", the --uses and @@ -9082,10 +9096,11 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector : ( impls.size() < 16 ? impls.size() : 16 ); const std::size_t implementorTotal = id < countMap.size() ? std::max( countMap[id].size(), impls.size() ) : impls.size(); std::string hdr; + const std::span mainRows = focusId != kNoNode ? legoMainRowsOf( targeted.mainMixins, id ) : std::span {}; const LegoClosurePage deep = focusId != kNoNode ? legoClosurePage( implementors, targeted.implementorBound != nullptr ? *targeted.implementorBound : kNoImplementorBound, id, targeted.pageLimit, targeted.pageOffset ) : LegoClosurePage{}; if( focusId != kNoNode ) { - hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() ) + "\"" + hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() + mainRows.size() ) + "\"" + legoClosureAttrs( deep, std::string( pathRel( isym.fileId ) ) + ":" + isym.name, deep.unwalked.empty() ? std::string() : std::string( pathRel( ing.symbols[ deep.unwalked.front() ].fileId ) ) + ":" + ing.symbols[ deep.unwalked.front() ].name, targeted.pageLimit ); @@ -9179,6 +9194,15 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector { w.write( "" ); } + for( const RubyMainMixinRow& m : mainRows ) // targeted only (empty otherwise): after the class rows, before the deeper ones + { + w.write( "" ); + } + if( !mainRows.empty() ) + { + w.write( kLegoMainRowClause ); + } for( std::size_t j = deep.window.begin; j < deep.window.end; ++j ) // empty unless TARGETED with a deeper row { writeLegoImplRow( w, ing, deep.deeper[j].id, pathRel( ing.symbols[ deep.deeper[j].id ].fileId ), withPaths, true, &deep.deeper[j], false, esc ); diff --git a/src/tsprobe.cpp b/src/tsprobe.cpp index 395a62418..688338a3d 100644 --- a/src/tsprobe.cpp +++ b/src/tsprobe.cpp @@ -150,7 +150,7 @@ int main( int argc, char** argv ) { // A value use (a function stored or passed) and a call THROUGH a value are not call references: this dump is // the pre-resolution CALL list test/callformcheck.sh reads, so they stay out of it (src/valuerefs.h serves them). - if( r.fromSymbol != rw::kNoNode && r.role != rw::RefRole::Value && r.role != rw::RefRole::Through ) + if( r.fromSymbol != rw::kNoNode && r.role != rw::RefRole::Value && r.role != rw::RefRole::Through && r.role != rw::RefRole::Subscript ) { bySym[ r.fromSymbol ].push_back( &r ); } diff --git a/src/valuerefindex.h b/src/valuerefindex.h index cdf1c79e4..0d9887bcc 100644 --- a/src/valuerefindex.h +++ b/src/valuerefindex.h @@ -841,7 +841,7 @@ class UsesValueFilter // True when reference `refIndex` is NOT a use-site row of this answer. bool skip( std::uint32_t refIndex, const Reference& r ) const { - if( r.role == RefRole::Through ) + if( r.role == RefRole::Through || r.role == RefRole::Subscript ) // a subscript is a use only as the call it may resolve to { return true; } diff --git a/src/verbs_for.h b/src/verbs_for.h index a14ab77a9..b04a161b4 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -4568,7 +4568,7 @@ std::optional runTargetedViews( const MainDispatch& d ) rw::legoClosureLegendComment( g.implementors, g.implementorBound, focus ).c_str() ); // lane lego-transitive: present-only, the deeper rows const std::string legoTargetedAttr = rw::unprovenDefsAttrXml( legoUnprovenDefs ) + rw::graphCountFloorAttrXml( g ); // outlives the call packLego( stdout, ing, g.implementors, flat, 1, d.redactPtr, &legoImpure, focus, /*withPaths=*/true, tvRootArg, - rw::LegoTargetedArgs( legoTargetedAttr, cfg.pageLimit, cfg.pageOffset, &g.implementorBound ) ); // H1 + M15: residue, gauge, marker; the deeper-row page + rw::LegoTargetedArgs( legoTargetedAttr, cfg.pageLimit, cfg.pageOffset, &g.implementorBound, g.rubyMainMixins ) ); // H1 + M15: residue, gauge, marker; the deeper-row page rw::emitRaw( stdout, "" ); reportRedactions( stderr, d.redactCounts ); // W3-N1: a contract sig is a redacting seam — disclose the tally return 0; diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index cbcc9acdb..55d9a3204 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2098,9 +2098,9 @@ accumulateExternalSurface( const rw::IngestResult& ing, const rw::HashMap> ext; for( const Reference& r : ing.references ) { - if( r.isCompose || r.isDocLink ) + if( r.isCompose || r.isDocLink || r.role == RefRole::Subscript ) { - continue; // type edge / doc mention — not a code reference + continue; // type edge / doc mention / an element access (its operator is a builtin's unless graph.h proves the class) — not a code reference } if( r.lang == Lang::Markdown ) { diff --git a/test/regression.sh b/test/regression.sh index d5e21f5d4..12ee44bfe 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/subscriptopcheck.sh b/test/subscriptopcheck.sh new file mode 100755 index 000000000..30a84f756 --- /dev/null +++ b/test/subscriptopcheck.sh @@ -0,0 +1,382 @@ +#!/usr/bin/env bash +# subscriptopcheck.sh — gate for SUBSCRIPT = OPERATOR-METHOD CALL (Ruby `recv[ k ]` -> `[]` / `[]=`, Python `obj[ k ]` -> +# `__getitem__` / `__setitem__`) and for Ruby's MAIN OBJECT as a --lego implementor (``). +# +# Before this round an element access was no call at all: rake's Application#invoke_task does `t = self[ name ]`, which runs +# TaskManager#[] (Application includes TaskManager), and --callees=invoke_task listed only parse_task_string and invoke. +# And rake's dsl_definition.rb ends with `self.extend Rake::DSL` — the top level's self is the main object, which then +# answers DSL's methods — but --lego=DSL listed only classes. +# +# THE RULE (queries/{ruby,python}/tags.scm @reference.subscript; ingest_binds.h subscriptOperatorRef; graph.h +# subscriptOperatorProven; parser version 174): +# * an element access names its receiver class's subscript operator method, by the language's table +# (kSubscriptOperatorMethods): the write form as an assignment's whole target, the read form otherwise; +# * it is a CALL only where a receiver rule PROVES the class: Ruby's lookup on self in a def (through include/extend +# ancestry), a receiver the code builds (`r = Registry.new`, `Registry.new[ k ]`), a class constant's own +# `def self.[]`; Python's receiver evidence (`self` in a class or its bases, a typed parameter, a constructed local, +# a field the class assigns). Everything else — a Hash/Array/dict/list local, a literal, an untyped parameter — makes +# NO edge and NO hedge: no definition named `[]` anywhere decides it by name; +# * a Ruby file's top-level `include M` / `extend M` / `self.extend M` lists main as M's implementor in --lego=M. +# Stated floors, each pinned below: a compound assignment (`self[ k ] ||= v`, `o[ k ] += 1`) is recorded as the read form; +# a Ruby instance variable receiver is not typed (`@reg[ k ]`); a multiple-assignment target, `del o[ k ]` and an +# annotation record nothing; a Python class-object subscript (`Registry[ k ]`, `cls[ k ]`) runs the metaclass and binds +# nothing; a top-level directive inside `if`, `prepend`, and a mixin onto another object list no main row. +# +# Usage: test/subscriptopcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire test/subscriptopcheck.sh +# Exits non-zero on any failure. Self-contained via mktemp. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "subscriptopcheck: BIN=$BIN" +fail=0 +ok(){ echo " PASS $1" || { fail=1; echo " FAIL could not write the PASS line for: $1"; }; return 0; } +no(){ echo " FAIL $1"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } + +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +RB="$DIR/rb"; PY="$DIR/py"; mkdir -p "$RB" "$PY" # the corpora — outputs live in $DIR, never inside them + +cat > "$RB/registry.rb" <<'RUBY' +module Lookup + def [](name) + fetch_entry(name) + end + + def []=(name, value) + store_entry(name, value) + end +end + +class Registry + include Lookup + + def fetch_entry(name) + name + end + + def store_entry(name, value) + value + end + + def find_self(name) + self[name] + end + + def put_self(name, value) + self[name] = value + end + + def or_assign(name) + self[name] ||= 1 + end + + def self.[](name) + new.find_self(name) + end +end + +class Plain + def [](k) + k + end +end + +class Catalog + def initialize + @reg = Registry.new + end + + def get_built(name) + r = Registry.new + r[name] + end + + def get_value(name) + Registry.new[name] + end + + def by_class(name) + Registry[name] + end + + def by_plain_class(name) + Plain[name] + end + + def via_ivar(name) + @reg[name] + end + + def hash_only(name) + h = {} + h[name] = 1 + h[name] + end + + def array_only(i) + a = [1, 2] + a[i] + end + + def untyped(thing, name) + thing[name] + end + + def literal + { a: 1 }[:a] + end +end + +module Tasks + def task(name) + name + end +end + +module Inner + def inner_m + 1 + end +end + +module Prepended + def pre_m + 1 + end +end + +module Other + def other_m + 1 + end +end + +self.extend Tasks +include Lookup +extend ::Tasks +if true + include Inner +end +prepend Prepended +Object.new.extend Other +include Comparable +RUBY + +cat > "$PY/registry.py" <<'PY' +class Registry: + def __init__(self): + self.items = {} + + def __getitem__(self, key): + return self.items[key] + + def __setitem__(self, key, value): + self.items[key] = value + + def lookup_self(self, key): + return self[key] + + def store_self(self, key, value): + self[key] = value + + @classmethod + def by_cls(cls, key): + return cls[key] + + +class Sub(Registry): + def first(self): + return self["first"] + + +class Holder: + def __init__(self): + self.reg = Registry() + + def via_field(self, k): + return self.reg[k] + + +def use_typed(reg: Registry): + return reg["x"] + + +def use_built(): + r = Registry() + return r["y"] + + +def use_dict(): + d = {} + d["k"] = 1 + return d["k"] + + +def use_untyped(thing): + return thing["z"] + + +def use_literal(): + return {"a": 1}["a"] + + +def use_list(xs): + return xs[0] + + +def remove(reg: Registry): + del reg["x"] + + +def class_side(): + return Registry["x"] + + +def multi(other: Registry): + other["a"], other["b"] = 1, 2 + + +def aug(other: Registry): + other["n"] += 1 + + +def annotated(x: "dict[str, int]") -> None: + y: dict[str, int] = {} + return y +PY + +line_of(){ grep -n -- "$2" "$1" | head -1 | cut -d: -f1; } +RGET=$( line_of "$RB/registry.rb" 'def \[\](name)' ) +RSET=$( line_of "$RB/registry.rb" 'def \[\]=(name, value)' ) +RCLS=$( line_of "$RB/registry.rb" 'def self.\[\](name)' ) +PGET=$( line_of "$PY/registry.py" 'def __getitem__' ) +PSET=$( line_of "$PY/registry.py" 'def __setitem__' ) +for v in RGET RSET RCLS PGET PSET; do + case "${!v}" in ''|*[!0-9]*) echo " FAIL fixture line for $v is not numeric: '${!v}'"; exit 1 ;; esac +done + +# one callees answer's rows; the answer must have exited 0 and carried its root +callees(){ # dir name tag + local out="$DIR/$3.xml" + "$BIN" "$1" --callees="$2" --no-cache > "$out" 2> "$DIR/$3.err" + local rc=$? + if [ $rc -ne 0 ] || ! grep -q ']*>' "$out" > "$DIR/$3.rows" + return 0 +} +has_row(){ grep -qF -- "n=\"$2\" p=\"$3\"" "$DIR/$1.rows"; } +no_op_row(){ ! grep -qE -- 'n="(\[\]|\[\]=|__getitem__|__setitem__)"' "$DIR/$1.rows"; } + +echo "== Ruby: an element access is a call of the receiver class's []/[]= where a rule proves the class" +pos_rb(){ # name tag op line what + if callees "$RB" "$1" "$2"; then + if has_row "$2" "$3" "registry.rb:$4" && ! grep -q 'via=' "$DIR/$2.rows"; then ok "R $5: $1 -> $3 registry.rb:$4 (proven, no via=)" + else no "R $5: $1 should call $3 at registry.rb:$4 with no via=: $( tr '\n' ' ' < "$DIR/$2.rows" )"; fi + fi +} +pos_rb find_self r1 '[]' "$RGET" "self[ k ] through an included module" +pos_rb put_self r2 '[]=' "$RSET" "self[ k ] = v names the writer" +pos_rb get_built r3 '[]' "$RGET" "a local the code builds (r = Registry.new)" +pos_rb get_value r4 '[]' "$RGET" "a value receiver (Registry.new[ k ])" +pos_rb by_class r5 '[]' "$RCLS" "a class constant reaches its def self.[]" +pos_rb or_assign r6 '[]' "$RGET" "floor: self[ k ] ||= v is recorded as the read form" +if [ -s "$DIR/r5.rows" ] && ! has_row r5 '[]' "registry.rb:$RGET"; then ok "R class side: Registry[ k ] does not reach the instances' []" +else no "R class side: Registry[ k ] must not reach Lookup#[]: $( tr '\n' ' ' < "$DIR/r5.rows" 2>/dev/null )"; fi + +neg_rb(){ # name tag what + if callees "$RB" "$1" "$2"; then + if no_op_row "$2"; then ok "R negative $3: $1 has no []/[]= row" + else no "R negative $3: $1 must make no edge: $( tr '\n' ' ' < "$DIR/$2.rows" )"; fi + fi +} +neg_rb hash_only n1 "a Hash local (h = {})" +neg_rb array_only n2 "an Array local" +neg_rb untyped n3 "an untyped parameter, with in-tree [] namesakes (no hedge either)" +neg_rb literal n4 "a literal receiver" +neg_rb via_ivar n5 "floor: an instance variable receiver is not typed" +neg_rb by_plain_class n6 "a class constant whose class defines only an instance []" + +echo "== Ruby: a top-level include/extend lists the main object in --lego" +lego(){ # type tag + "$BIN" "$RB" --lego="$1" --no-cache > "$DIR/$2.xml" 2> "$DIR/$2.err" + local rc=$? + if [ $rc -ne 0 ] || ! grep -q '' "$DIR/l1.xml" | tr '\n' ' ' ) + if [ "$m" = " " ]; then + ok "L1 self.extend Tasks and extend ::Tasks: two main rows at their constants" + else no "L1 --lego=Tasks main rows: got '$m'"; fi + if grep -q ']* implementors="2"' "$DIR/l1.xml"; then ok "L1 implementors= counts the main rows" + else no "L1 implementors= should be 2: $( grep -o ']*>' "$DIR/l1.xml" )"; fi + if grep -q '/p' "$DIR/h2_full.out" | grep -q 'bare ~n'; say $? "[P] h2_full: the full legend reads the cut table's bare ~n" \ + "[P] h2_full: no legend reading of the bare ~n token" + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$DIR/h2_full.out" 2>/dev/null; say $? "[C] h2_full: --legend=full well-formed" "[C] h2_full: --legend=full malformed" + fi + ( cd "$DIR/capfix2" && "$BIN" . --no-cache --callers=go ) 2>/dev/null | sed -n '1,/-->/p' | grep -q 'bare ~n'; say $? "[P] h2_cmp: the compact legend reads the bare ~n" \ + "[P] h2_cmp: the compact legend does not read the bare ~n" +else no "h2_full: --callers=go --legend=full failed"; fi +FIX="$FIXSAVE" + +# ── (J) R1 (final review): an untied declaration is dropped only when that leaves ONE group; a printed table lists it ────── +echo "(J) untied declarations on a printed table" +if ans j_u2 u2 callers s xml -- --callers=scan_tok; then + [ "$( groups j_u2 )" = 3 ]; say $? "[P] j_u2: three groups listed (definition, untied prototype, static namesake)" \ + "[P] j_u2: rows=$( groups j_u2 ), want 3 (an untied prototype vanished from a printed table)" + defbody P j_u2 prog2/main.c:2 1 + defbody C j_u2 prog1/util.c:2 0 + defbody C j_u2 prog3/other.c:2 0 + expect C j_u2 use_tok1 prog1/util.c:2 + expect C j_u2 use_tok3 prog3/other.c:2 +fi +if ans j_u2i u2 impact s xml -- --impact=scan_tok; then + [ "$( groups j_u2i )" = 3 ]; say $? "[P] j_u2i: impact lists the three groups" "[P] j_u2i: rows=$( groups j_u2i ), want 3" +fi +# the one-group sibling (u1 = prog1 + prog2): dropping the untied prototype leaves one group, so the answer stays plain +mkdir -p "$DIR/u1/prog1" "$DIR/u1/prog2"; cp "$FIX/u2/prog1/util.c" "$DIR/u1/prog1/"; cp "$FIX/u2/prog2/main.c" "$DIR/u1/prog2/" +FIXSAVE="$FIX"; FIX="$DIR" +if ans j_u1 u1 callers s xml -- --callers=scan_tok; then plain C j_u1; fi FIX="$FIXSAVE" # ── (I) Q1 on --for: a body's row (and a hop's) whose edge is a resolver split arm carries prov="split", defined in the diff --git a/test/attribmultideffix/u2/prog1/util.c b/test/attribmultideffix/u2/prog1/util.c new file mode 100644 index 000000000..0e3d88e72 --- /dev/null +++ b/test/attribmultideffix/u2/prog1/util.c @@ -0,0 +1,3 @@ +/* R1 (final review): a definition, an untied prototype in another program, a static namesake in a third */ +int scan_tok(const char *s) { return s[0]; } +int use_tok1(void) { return scan_tok("a"); } diff --git a/test/attribmultideffix/u2/prog2/main.c b/test/attribmultideffix/u2/prog2/main.c new file mode 100644 index 000000000..0e0625d6a --- /dev/null +++ b/test/attribmultideffix/u2/prog2/main.c @@ -0,0 +1,3 @@ +/* a prototype with no #include that ties it to prog1/util.c: the identity rule can only say Unproven */ +int scan_tok(const char *s); +int use_tok2(void) { return scan_tok("b"); } diff --git a/test/attribmultideffix/u2/prog3/other.c b/test/attribmultideffix/u2/prog3/other.c new file mode 100644 index 000000000..5f03c505d --- /dev/null +++ b/test/attribmultideffix/u2/prog3/other.c @@ -0,0 +1,3 @@ +/* a static (internal-linkage) namesake: another function */ +static int scan_tok(const char *s) { return 7; } +int use_tok3(void) { return scan_tok("c"); } From a87c57d6d251b85f2b6ee4e160bcdbe6c9f3b74c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 01:25:02 -0400 Subject: [PATCH 069/162] test(quality): qddegradecheck arms for the final review's C1-C3 (red on 5414abca) (W) a --quality-kinds selection under pressure: the caller's exclusions are not memory cuts, memory_skipped= is exactly what kinds_next= replays, no empty 'skipped' list. (I) --quality-ack is never pre-empted by the projection (acks in full under project:1); a real stop (crawl:12) still refuses it with the ledger untouched (rule 4 twin). (F) the hard-line pre-check refusal says the changed files were not tried. Co-Authored-By: Claude Opus 5.5 --- test/qddegradecheck.sh | 60 +++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index 9d2cfcdbc..ae4a0cd69 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -37,11 +37,13 @@ # (D) the kinds_next= replay yields exactly the skipped kinds' rows, not degraded again; union with (B) = (A). # (E) P0 (crawl seam cuts the base's FULL ingest): answers from the changed files; defect-shape skipped (a changed # C++ file and no whole macro set); memory_stop=, memory_need="unmeasured"; (E2) a Python-only change keeps it. -# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5. +# (F) even the changed files do not fit (hard line after the base ingest, and every later reading): exit 5; the +# pre-check refusal says the changed files were NOT tried, never that they "did not fit" (review C3). # (G) near misses under project:1: an empty diff, a rename-only diff, a non-code-only diff, and A..A. # (H) --quality-kinds: unknown kind refused naming the 12; refused beside --quality-ack and on the bare form; each # single kind = (A)'s rows of that kind; all 12 named = byte-identical to no flag. -# (I) --quality-ack under pressure refuses (exit 5) and leaves the ledger untouched. +# (I) --quality-ack is never pre-empted by the projection (review C2: project:1 acks in full, the same ledger as an +# unpressured ack); a real stop (crawl:12) refuses it (exit 5) and leaves the ledger untouched. # (J) --scope naming an unchanged file under pressure is not refused as "names nothing indexed". # (K) legend + JSON: --legend=full defines every new attribute and parses as XML; --json carries the twins. # (L) guard: a projection seam index past the run's one projection reading changes nothing (byte-identical). @@ -60,6 +62,10 @@ # (S) R9 --quality-kinds=complexity reports no false finding-gone for a dead-code ack. # (T) S2 fits near the line: --max-memory at 2x the measured full peak answers in full, byte-identical (guard). # (V) R8 a degraded answer with rows and no gating row exits 6, not 2 and not 0. +# --- fix round 1 (final review C1) --- +# (W) --quality-kinds with a LOCAL kind under pressure: the kinds the caller left out are skipped_why="selector", +# never memory cuts; memory's share (memory_skipped=, skipped_why="selector,memory") is exactly what kinds_next= +# replays; no stderr line ends in an empty "skipped" list. # # Usage: bash test/qddegradecheck.sh [BIN] set -u @@ -276,8 +282,9 @@ fi # ── (F) even the changed files do not fit ────────────────────────────────────────────────────────────────────── qd RIPWIRE_TEST_MEMGUARD=hard:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; FRC=$RC if [ "$FRC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] \ - && grep -q '^ripwire: memory limit reached during the ingest of the base commit (limit .*changed files' "$WORK/e"; then - ok "(F) the changed files do not fit either: exit 5, nothing on stdout, one line naming the phase and the changed-files attempt" + && grep -q '^ripwire: memory limit reached during the ingest of the base commit (limit .*changed files' "$WORK/e" \ + && grep -q 'after releasing the full tree, so the changed files were not tried' "$WORK/e" && ! grep -q 'did not fit' "$WORK/e"; then + ok "(F) still over the limit after releasing the full tree: exit 5, nothing on stdout, one line naming the phase and that the changed files were not tried" else no "(F) rc=$FRC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )" fi @@ -329,11 +336,19 @@ done qd "$BIN" . --quality-delta=HEAD~1..HEAD "--quality-kinds=$ALL12" --no-cache if [ "$RC" = "$ARC" ] && cmp -s "$WORK/o" "$WORK/A.xml"; then ok "(H) all 12 named: byte-identical to no selector"; else no "(H) all 12 named: rc=$RC or output differs from (A)"; fi -# ── (I) --quality-ack under pressure ────────────────────────────────────────────────────────────────────────── -qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache -if [ "$RC" = 5 ] && [ ! -e "$FX/.ripwire_quality_acks" ] && [ "$( memlines )" = 1 ] && [ ! -s "$WORK/o" ]; then ok "(I) --quality-ack under pressure refuses (exit 5), the ledger untouched" -else no "(I) rc=$RC ledger=$( [ -e "$FX/.ripwire_quality_acks" ] && echo written || echo absent ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi -rm -f "$FX/.ripwire_quality_acks" +# ── (I) --quality-ack: the projection never pre-empts it (C2); a real stop refuses it ─────────────────────────── +# Each run acks in its own copy of the fixture, so the ledgers compare. ackl: the ledger's ack lines (no header). +ackl(){ grep '^ack ' "$1/.ripwire_quality_acks" 2>/dev/null; } +FIA="$WORK/fia"; FIP="$WORK/fip"; FIS="$WORK/fis"; cp -R "$FX" "$FIA"; cp -R "$FX" "$FIP"; cp -R "$FX" "$FIS" +qdin "$FIA" "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache; iarc=$RC +qdin "$FIP" RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache +if [ -n "$( ackl "$FIA" )" ] && [ "$RC" = "$iarc" ] && [ "$( ackl "$FIP" )" = "$( ackl "$FIA" )" ] && [ "$( memlines )" = 0 ]; then + ok "(I) --quality-ack under project:1 is not pre-empted: it acks in full, the same $( ackl "$FIA" | grep -c . ) ack line(s) as an unpressured ack (rc=$RC)" +else no "(I) project:1 --quality-ack: rc=$RC (unpressured $iarc) acks=$( ackl "$FIP" | grep -c . ) want $( ackl "$FIA" | grep -c . ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qdin "$FIS" RIPWIRE_TEST_MEMGUARD=crawl:12 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-ack --no-cache +if [ "$RC" = 5 ] && [ ! -e "$FIS/.ripwire_quality_acks" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] && grep -q 'nothing was acked' "$WORK/e" && [ ! -s "$WORK/o" ]; then + ok "(I) --quality-ack under a real stop (crawl:12) refuses (exit 5, one line), the ledger untouched" +else no "(I) crawl:12 --quality-ack: rc=$RC ledger=$( [ -e "$FIS/.ripwire_quality_acks" ] && echo written || echo absent ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi # ── (J) --scope naming an unchanged file under pressure ─────────────────────────────────────────────────────── qd "$BIN" . --quality-delta=HEAD~1..HEAD --scope=py/u2.py --no-cache; jA=$RC; cp "$WORK/o" "$WORK/JA.xml" @@ -564,5 +579,32 @@ if [ "$RC" = 6 ] && [ "$( attr "$WORK/o" gating )" = 0 ] && [ -n "$( rows "$WORK ok "(V) a degraded answer with a placeholder row and gating=0 exits 6 (partial, nothing gating), stdout non-empty" else no "(V) rc=$RC gating=$( attr "$WORK/o" gating ) rows=$( rows "$WORK/o" | grep -c . )"; fi +# ── (W) C1: a selector's exclusions are not memory cuts ───────────────────────────────────────────────────────── +others(){ echo "$ALL12" | tr ',' '\n' | grep -vx -e "$1" -e "${2:-}" | paste -sd, -; } # the 12 kinds without $1 (and $2) +emptyskip(){ grep -q 'skipped *$' "$WORK/e"; } +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --no-cache +if [ "$( attr "$WORK/o" ingest_scope )" = changed-files ] && [ "$( attr "$WORK/o" kinds_skipped )" = "$( others complexity )" ] \ + && [ "$( attr "$WORK/o" skipped_why )" = selector ] && ! hasattr "$WORK/o" memory_skipped && ! hasattr "$WORK/o" kinds_next \ + && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" complexity )" ] && [ "$RC" = "$( degrc "$WORK/o" )" ] \ + && ! emptyskip && [ "$( memlines )" = 1 ] && grep -q '^ripwire: memory limit .*every selected kind ran' "$WORK/e"; then + ok "(W) project:1 --quality-kinds=complexity: the 11 left out are skipped_why=selector, no memory share, no kinds_next=, stderr says every selected kind ran (rc=$RC)" +else no "(W) complexity: rc=$RC why=$( attr "$WORK/o" skipped_why ) skipped=$( attr "$WORK/o" kinds_skipped ) next=$( attr "$WORK/o" kinds_next ) stderr: $( grep '^ripwire: [^c]' "$WORK/e" | od -c | tail -n 3 | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --no-cache +wnext="$( attr "$WORK/o" kinds_next )" +if [ "$( attr "$WORK/o" ingest_scope )" = changed-files ] && [ "$( attr "$WORK/o" kinds_skipped )" = "$( others complexity )" ] \ + && [ "$( attr "$WORK/o" skipped_why )" = selector,memory ] && [ "$( attr "$WORK/o" memory_skipped )" = dead-code ] \ + && case "$wnext" in "--quality-delta=$BASE..$TGT --quality-kinds=dead-code --max-memory="*M) true;; *) false;; esac \ + && [ "$( rows "$WORK/o" )" = "$( rows "$WORK/A.xml" complexity )" ] && ! emptyskip \ + && grep -q '^ripwire: memory limit .*; skipped dead-code — run them with ' "$WORK/e"; then + ok "(W) project:1 --quality-kinds=complexity,dead-code: skipped_why=selector,memory, memory_skipped=dead-code = exactly what kinds_next= replays" +else no "(W) complexity,dead-code: why=$( attr "$WORK/o" skipped_why ) memory_skipped=$( attr "$WORK/o" memory_skipped ) next='$wnext' stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --legend=full --no-cache +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) --legend=full defines memory_skipped= when it rides, and parses as XML" +else no "(W) the legend lacks memory_skipped= (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --json --no-cache +if python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); s=json.dumps(d); sys.exit(0 if "\"memory_skipped\": [\"dead-code\"]" in s and "\"skipped_why\": \"selector,memory\"" in s else 1)' "$WORK/o" 2>/dev/null; then + ok "(W) --json carries memory_skipped=[dead-code] and skipped_why=selector,memory" +else no "(W) --json: $( grep -o '"memory_skipped[^]]*]\|"skipped_why":"[^"]*"' "$WORK/o" | head -c 200 )"; fi + [ "$fail" = 0 ] && echo "qddegradecheck: ALL PASS" || echo "qddegradecheck: FAIL" exit "$fail" From d04531421147e9e2801203b57d58e2f545d38f30 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 01:32:37 -0400 Subject: [PATCH 070/162] fix(attrib): list every untied declaration on a printed table (R1); a cut table enumerates no by-name tie (R2) R1 (final review): defGroupsDropSilent drops the untied declarations no row names ONLY when that leaves one group (the tmux/ninja prototype + definition answer stays plain); with two or more groups left the table is printed and claims every group defs= forms, so each untied declaration is listed bodyless="1". R2: above kDefTableCap a row prints its K, K~s and K~t tokens and one bare ~n when it also has by-name ties; a row tied by name alone prints no of_def (via="name" hedges it); rows likewise; the cut table lists only the groups a printed token names and always says defs_listed=. Legend clauses (full + compact) read it. textual --callers=render growth over base +5188 -> +1200 B, --impact=render +6631 -> +1665 B; answers of 16 groups or fewer are unchanged. N1: tokens formatted into the row's own buffer. N2: the definition signatures are cached by match position (no linear search). Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 2 +- src/defattrib.h | 109 +++++++++++++++++++++++++++++--------------- src/graphlegend.h | 5 +- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 1bdcc939f..9b14cbb51 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -587,7 +587,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // ties, the cut table's count, and the columnar split column. Present-only, element-scoped. A row's prov="split" (Q1) reads // through the shared prov= row above, which already fires on any prov= in the payload. { "ix", ": the groups defs= forms (a definition + the declarations tied to it; bodyless=1 tied to none)", true, "def" }, - { "defs_listed", "defs_listed=N: above 16 groups, only the N this page's rows name are listed" }, + { "defs_listed", "defs_listed=N: above 16 groups, only the N this page's rows name are listed; K~n not enumerated: a bare ~n = also by name, a by-name-only row has no of_def" }, { "of_def", "of_def=K..: the def ix=K groups the row belongs to; K resolved edge, K~s edge labelled split, K~n by name alone, K~t into a primary template whose specialisation is listed; NOT a claim the edge is false", true, "s" }, { "of_def", "c of_def=K..: the def ix=K groups that caller belongs to (K~s split label, K~n by name, K~t primary template); NOT a claim the edge is false", true, "c" }, { "of_def", "rf of_def=K..: the def ix=K groups its file's reached symbols belong to, strongest tie per group", true, "rf" }, diff --git a/src/defattrib.h b/src/defattrib.h index 9763e436e..863f3b602 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -49,8 +49,9 @@ #include #include -#include +#include #include +#include #include #include #include @@ -61,6 +62,7 @@ namespace rw // The table lists every group up to this many; above it, only the groups a row on the page names (defs_listed= says how // many it listed). textual's `render` has 61 definitions and `compose` 1047: a full table there would outweigh the page. +// Above it a row also enumerates no by-name tie (defTokensTo): its K, K~s, K~t and one bare ~n (final review R2). inline constexpr std::size_t kDefTableCap = 16; // One row's tie to one group, weakest first, so the strongest of several edges is a max(). @@ -80,7 +82,8 @@ struct DefGroups std::vector primaryOfSpec; // per group: its sc= is the template base of another group's sc= (the ~t mark) std::vector untied; // per group: C-family declarations only, which MAY declare a listed definition (the // identity rule's Unproven: a template-qualified or macro-built declarator, no shared - // #include) — silent unless a row ties to it (defGroupsDropSilent) + // #include) — dropped only when no row ties to it and one group is left + // (defGroupsDropSilent); on a printed table it is listed bodyless="1" std::size_t size() const noexcept { return rep.size(); } bool multi() const noexcept { return rep.size() >= 2; } @@ -319,7 +322,7 @@ inline std::vector defGroupUntied( const IngestResult& ing, std::span> defSigs; // each definition's signature, read once (stable refs) + std::vector> defSigs( matches.size() ); // by match position: each one's signature, read once for( std::size_t i = 0; i < matches.size(); ++i ) { const std::uint32_t grp = groups.ofMatch[i]; @@ -334,13 +337,11 @@ inline std::vector defGroupUntied( const IngestResult& ing, std::spansecond ).tie == EditCheckTie::Unproven ? 1 : 0; + untied[grp] = editCheckTieDeclaration( ing, sources, matches[i], matches[j], *defSigs[j] ).tie == EditCheckTie::Unproven ? 1 : 0; } } return untied; @@ -591,6 +592,13 @@ struct DefTableListing std::vector listed; // ascending group indices bool cut = false; }; +// Above the cap the table is cut, and a row's by-name ties are not enumerated (they print as one bare ~n). +inline bool defTableCapped( const DefGroups& groups ) noexcept { return groups.size() > kDefTableCap; } +// A token the row PRINTS: every one up to the cap; above it, no K~n (the groups a cut table lists are the printed ones). +inline bool defTokenPrinted( const DefGroups& groups, std::uint32_t tok ) noexcept +{ + return !defTableCapped( groups ) || DefMark( tok & 3u ) != DefMark::Name; +} inline void defTableMarkNamed( std::vector& named, const DefGroups& groups, const RowDefTokens& tokens, std::size_t begin, std::size_t end ) { named.resize( groups.size(), 0 ); @@ -598,14 +606,14 @@ inline void defTableMarkNamed( std::vector& named, const DefGroups& groups { for( const std::uint32_t t : tokens.of( r ) ) { - named[ t >> 2 ] = 1; + named[ t >> 2 ] = char( named[ t >> 2 ] | ( defTokenPrinted( groups, t ) ? 1 : 0 ) ); } } } inline DefTableListing defTableListing( const DefGroups& groups, const std::vector& named ) { DefTableListing out; - const bool all = groups.size() <= kDefTableCap; + const bool all = !defTableCapped( groups ); for( std::size_t k = 0; k < groups.size(); ++k ) { if( all || ( k < named.size() && named[k] ) ) @@ -613,7 +621,7 @@ inline DefTableListing defTableListing( const DefGroups& groups, const std::vect out.listed.push_back( std::uint32_t( k ) ); } } - out.cut = out.listed.size() < groups.size(); + out.cut = !all; // above the cap even a listing that names every group says defs_listed= (its rows print no K~n) return out; } @@ -622,7 +630,7 @@ inline void defTokenTo( std::string& out, const DefGroups& groups, std::uint32_t { const std::uint32_t grp = tok >> 2; const DefMark mark = DefMark( tok & 3u ); - out += std::to_string( grp + 1 ); + std::format_to( std::back_inserter( out ), "{}", grp + 1 ); // into the row's own buffer (review N1) if( mark == DefMark::Name ) { out += "~n"; @@ -636,50 +644,67 @@ inline void defTokenTo( std::string& out, const DefGroups& groups, std::uint32_t out += "~t"; } } -// A row's tokens, space-separated (the sites_at= convention). -inline std::string defTokens( const DefGroups& groups, std::span toks ) +// A row's printed tokens appended to `out`, each wrapped in `quote` and joined by `sep` (XML/columnar: " " and "", the sites_at= +// convention; JSON: "," and "\""). Above the cap (R2) the K~n ties are not enumerated: the K/K~s/K~t tokens, then ONE bare +// `~n` when the row also has a by-name tie. +inline void defTokensTo( std::string& out, const DefGroups& groups, std::span toks, std::string_view sep, std::string_view quote ) { - std::string text; + std::size_t printed = 0; + bool byName = false; for( const std::uint32_t tok : toks ) { - defTokenTo( text, groups, tok ); - text += ' '; + if( !defTokenPrinted( groups, tok ) ) + { + byName = true; + continue; + } + out.append( printed++ ? sep : std::string_view() ).append( quote ); + defTokenTo( out, groups, tok ); + out.append( quote ); } - if( !text.empty() ) + if( byName ) { - text.pop_back(); // the separator after the last token + out.append( printed ? sep : std::string_view() ).append( quote ).append( "~n" ).append( quote ); } - return text; } +// Above the cap a row tied by name alone (or not at all) carries no of_def: its via="name" is the hedge (R2). +inline bool defRowUnprinted( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) noexcept +{ + return defTableCapped( groups ) && tokens.strongest( row ) <= DefMark::Name; +} +// A row with no printed token on a cut table carries no of_def at all (its via="name" is the hedge); below the cap every row +// keeps the attribute, as before. inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { - if( !groups.multi() || tokens.empty() ) + if( !groups.multi() || tokens.empty() || defRowUnprinted( groups, tokens, row ) ) { return {}; } - return " of_def=\"" + defTokens( groups, tokens.of( row ) ) + "\""; + std::string out = " of_def=\""; + defTokensTo( out, groups, tokens.of( row ), " ", "" ); + out += '"'; + return out; } inline std::string ofDefKeyJson( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { - if( !groups.multi() || tokens.empty() ) + if( !groups.multi() || tokens.empty() || defRowUnprinted( groups, tokens, row ) ) { return {}; } - std::string out = ",\"of_def\":["; - const std::span toks = tokens.of( row ); - for( std::size_t i = 0; i < toks.size(); ++i ) - { - out += i ? ",\"" : "\""; - defTokenTo( out, groups, toks[i] ); - out += '"'; - } - out += "]"; + std::string out = ",\"of_def\":["; + defTokensTo( out, groups, tokens.of( row ), ",", "\"" ); + out += ']'; return out; } -// The columnar of_def column's value for one row: the tokens space-separated (the sites_at column's convention). +// The columnar of_def column's value for one row: the printed tokens space-separated (the sites_at column's convention). inline std::string ofDefColumnValue( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { - return defTokens( groups, tokens.of( row ) ); + std::string text; + if( !defRowUnprinted( groups, tokens, row ) ) + { + defTokensTo( text, groups, tokens.of( row ), " ", "" ); + } + return text; } // Q1: a row of a ONE-group answer whose every non-by-name edge to the selector is a split arm says so, in the map's per-edge @@ -760,8 +785,11 @@ inline std::string defsListedAttrXml( const DefGroups& groups, const DefTableLis return ( groups.multi() && listing.cut ) ? " defs_listed=\"" + std::to_string( listing.listed.size() ) + "\"" : std::string(); } -// An answer's groups once its ties are known: an untied group (defGroupUntied) no row names is dropped, the rest renumbered in -// order, and the tokens rewritten to the new numbers. Fewer than two groups left: a one-group answer (no table, no of_def). +// An answer's groups once its ties are known: the untied groups (defGroupUntied) no row names are dropped ONLY when that leaves +// one group — the tmux/ninja prototype + definition answer stays one group with no table. When two or more groups would remain +// the table is printed anyway, and it claims to list every group defs= forms, so nothing is dropped: each untied declaration +// is listed (bodyless="1": "another function, or the tie is unproven"), as --edit-check counts it as a contract of its own +// (final review R1). Dropping: the rest renumbered in order, the tokens rewritten to the new numbers. inline void defGroupsDropSilent( DefGroups& groups, RowDefTokens& tokens ) { if( !groups.multi() || std::none_of( groups.untied.begin(), groups.untied.end(), []( char c ) { return c != 0; } ) ) @@ -773,6 +801,15 @@ inline void defGroupsDropSilent( DefGroups& groups, RowDefTokens& tokens ) { named[ t >> 2 ] = 1; } + std::size_t left = 0; + for( std::size_t k = 0; k < groups.size(); ++k ) + { + left += ( groups.untied[k] && !named[k] ) ? 0 : 1; + } + if( left >= 2 ) + { + return; // a table is printed: every group is listed + } std::vector renum( groups.size(), UINT32_MAX ); DefGroups kept; for( std::size_t k = 0; k < groups.size(); ++k ) diff --git a/src/graphlegend.h b/src/graphlegend.h index 3e4a76214..93b502fcd 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -922,7 +922,10 @@ inline constexpr const char* kDefTableLegend = "between same-named definitions); K~n = every call along it bound by name alone; K~t = a resolved edge into a primary template " "while a specialisation of it is another group (which one the call reaches is unproven). On impact the tie is a chain of calls (a " "by-name hop makes it ~n, else a split hop ~s) and an rf row unions its file's symbols. ~n/~s/~t do NOT mean the edge is false. "; -inline constexpr const char* kDefsListedLegend = "defs_listed=N: above 16 groups the table lists only the N that this page's rows name. "; +inline constexpr const char* kDefsListedLegend = + "defs_listed=N: above 16 groups the table lists only the N that this page's rows name, and a row enumerates no by-name tie: " + "it prints its K, K~s and K~t tokens and one bare ~n when it also has ties by name alone; a row tied by name alone prints no " + "of_def (its via=name is the hedge). "; inline std::string defTableLegend( bool table, bool cut ) { std::string clause; From 0459f344f23e5d4baf7d7b003c55b919e30359dd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 02:17:45 -0400 Subject: [PATCH 071/162] =?UTF-8?q?fix(quality):=20the=20final=20review's?= =?UTF-8?q?=20C1-C3=20=E2=80=94=20a=20selector's=20exclusions=20are=20not?= =?UTF-8?q?=20memory=20cuts;=20--quality-ack=20skips=20the=20projection;?= =?UTF-8?q?=20honest=20pre-check=20refusal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1: kindsDisclosure splits the kinds the caller left out from the kinds memory skipped: skipped_why=selector|memory|selector,memory, and memory_skipped= names memory's share when both ride (kinds_next= replays exactly it). The degraded stderr line drops its 'skipped' clause when memory skipped nothing ('every selected kind ran'). Exit stays 6 there: the also_skipped= facts are still missing (legend, CHANGELOG). C2: loadFullTrees never asks the projection for --quality-ack (it needs every kind's rows, like a whole-tree selection): it runs in full, and only a real stop refuses it. C3: the hard-line pre-check in loadChangedFiles says the changed files were not tried (the process still held more than the limit after releasing the full tree). N3: the kinds_next legend says the replay carries no output flag. qddegradecheck: the (F) arm reads only ripwire: lines (the dev build's DISCLOSE line names the refusal class); the --json arm dropped: --json is refused beside --quality-kinds. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++++++-- docs/COMMANDS.md | 2 +- src/cli.h | 2 +- src/compactlegend.h | 3 ++- src/memguard.h | 2 +- src/verbs_quality.h | 36 +++++++++++++++++++++++++++--------- test/qddegradecheck.sh | 8 +++----- 7 files changed, 41 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b9cf82c..5d6d27248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,9 +40,13 @@ the same two commits. One stderr line says the same. full answer) and **6** (new) otherwise — a partial answer is never a clean exit 0. Exit **5** now means, for this form, that even the changed files did not fit, or that they cannot stand in for the trees (a symlink anywhere, an export-subst attribute, or a changed `.gitattributes` with an export attribute), on one line naming why. - `--quality-ack` under pressure refuses (exit 5, ledger untouched): accepting needs every kind's rows. + Exit 6 also when every selected kind ran: the `also_skipped=` facts are still missing. `--quality-ack` needs every + kind's rows, so the projection never pre-empts it: it runs in full, and only a real memory stop refuses it (exit 5, + ledger untouched). A refusal whose process still held more than the limit after releasing the full tree says the + changed files were not tried. - **`--quality-kinds=K[,K…]`** (new, range form only) runs only the named kinds — the replay `kinds_next=` names. A - named kind's rows are its rows in a full answer; stale acks are judged for the named kinds only; passes no named kind + named kind's rows are its rows in a full answer; under pressure the kinds it left out stay the caller's + (`skipped_why="selector,memory"`, with `memory_skipped=` naming memory's share, which `kinds_next=` replays); stale acks are judged for the named kinds only; passes no named kind needs (the clone passes, the dead-code evidence) do not run. Refused beside `--quality-ack`/`--quality-baseline` and on the working-tree form; the MCP `quality_delta` tool (working-tree form) has no selector yet. - **`memory_peak=`** (new, present-only) on any range answer whose peak footprint went over the limit after the guard's diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 155f75164..ebda631a4 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -2942,7 +2942,7 @@ $ ./build/ripwire . --quality-delta **Answers:** the same 12-kind report between two committed trees — a whole branch at once the same 12-kind report between two COMMITTED TREES instead of the working tree vs a baseline — the WAVE-level measurement (=A..B = tree B against tree A; -=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. UNDER MEMORY PRESSURE IT ANSWERS PART OF THE QUESTION rather than nothing: when the guard stops either commit's full tree, or projects that the target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest (kinds_skipped=, ingest_scope="changed-files", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees (a symlink, an export attribute). --quality-ack is refused there (exit 5, ledger untouched): accepting needs every kind's rows. +=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 10 kinds are computed exactly as the bare form computes them. UNDER MEMORY PRESSURE IT ANSWERS PART OF THE QUESTION rather than nothing: when the guard stops either commit's full tree, or projects that the target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest (kinds_skipped=, ingest_scope="changed-files", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees (a symlink, an export attribute). --quality-ack (it needs every kind's rows) skips the projection: only a real memory stop refuses it (exit 5, ledger untouched). **Try it** diff --git a/src/cli.h b/src/cli.h index 7c19a129d..8963031ba 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1802,7 +1802,7 @@ inline constexpr char kHelpHead[] = " target phase will not fit the limit, it ingests only both commits' CHANGED files and runs the kinds those decide; the header names the rest\n" " (kinds_skipped=, ingest_scope=\"changed-files\", memory_need=, also_skipped=) and kinds_next= replays exactly them. Exit 2 when a row gates (the\n" " skipped kinds only add rows), else 6 — partial, never 0; exit 5 only when even the changed files do not fit or cannot stand in for the trees\n" - " (a symlink, an export attribute). --quality-ack is refused there (exit 5, ledger untouched): accepting needs every kind's rows.\n" + " (a symlink, an export attribute). --quality-ack (it needs every kind's rows) skips the projection: only a real memory stop refuses it (exit 5, ledger untouched).\n" " --quality-kinds=K[,K...] (with --quality-delta=A..B) run only the named kinds — the replay a partial answer hands back\n" " (with --quality-delta=A..B) run ONLY the named kinds, comma-separated, of the twelve: api-surface, complexity, dead-code, defect-shape,\n" " duplication, error-masking, nesting, new-clone-of-reused-helper, params, placeholder, short-horizon-churn, verbosity. A named kind's rows are\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index ba3752710..a7f3b090e 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -768,7 +768,8 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "renames_window_truncated", "renames_window_truncated=1: history is deeper than the rename window, older renames unread", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, // qd-memory-degrade: present-only on an answer that did not run every kind (verbs_quality.h kindsDisclosure) { "kinds_skipped", "kinds_skipped=: kinds NOT run, so none of their rows is here and every count covers the rest; a count only they feed is absent", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, - { "skipped_why", "skipped_why=selector (the caller chose; stale acks judged for those kinds only) | memory (see ingest_scope=)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "skipped_why", "skipped_why=selector (the caller chose; stale acks judged for those kinds only) | memory (see ingest_scope=) | selector,memory (both)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "memory_skipped", "memory_skipped=: beside skipped_why=selector,memory, the kinds memory skipped; kinds_next= runs exactly them", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "ingest_scope", "ingest_scope=changed-files: the full trees did not fit, so both commits' changed files answered the kinds they decide, rows as a full answer's; exit 2 if one gates, else 6", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "memory_need", "memory_need=NM: the full answer's projected need; unmeasured after a stop (memory_stop=)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "also_skipped", "also_skipped=: whole-answer facts not computed (config-warnings, stale-acks, ack-healing: acks match by recorded key only)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/memguard.h b/src/memguard.h index 4adf5a29d..6645b3ac6 100644 --- a/src/memguard.h +++ b/src/memguard.h @@ -531,7 +531,7 @@ inline std::string_view limitSourceClause() noexcept } // the hard stop's one line: nothing was built that could answer. `also` is said between the limit and the verdict — the -// range form's "the changed files alone did not fit either" — and is empty everywhere else (the line keeps its bytes). +// range form's "the changed files were not tried" / "were cut too" — and is empty everywhere else (the line keeps its bytes). inline std::string hardStopLine( std::string_view where, std::string_view also = {} ) { std::string line = "memory limit reached during the "; diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 7d2109e93..2ed892d3e 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -239,7 +239,8 @@ std::string degradedStderrLine( const RefPairDelta& refs, rw::quality::KindMask : refs.memoryStop == "hard" ? "the memory limit was reached during the " + refs.stopWhere + "; the full need was not measured" : "the memory guard stopped the " + refs.stopWhere + ( refs.memoryPressure ? " under critical system memory pressure" : "" ) + "; the full need was not measured"; std::string line = "memory limit " + ( limit == 0 ? std::string( "unknown" ) : memguard::limitSpelling( limit ) ) + " (" + std::string( memguard::limitSourceClause() ) - + "): --quality-delta=A..B answered from the changed files only (" + why + "); skipped " + quality::kindList( memorySkipped, ", " ); + + "): --quality-delta=A..B answered from the changed files only (" + why + ")" + + ( memorySkipped != 0 ? "; skipped " + quality::kindList( memorySkipped, ", " ) : std::string( "; every selected kind ran" ) ); if( !kindsNext.empty() ) { line += " — run them with " + std::string( kindsNext ); @@ -278,7 +279,9 @@ std::optional loadChangedFiles( const std::string& root, const rw::Config& } if( memguard::overHardLimit() ) { - return refPairStopRefusal( stop, out, "the changed files alone did not fit either" ); + // the footprint still held after the full tree was released (on macOS the allocator keeps its pages): nothing of + // the changed files was ingested, so the line says they were not tried — never that they did not fit + return refPairStopRefusal( stop, out, "this process still held more than the limit after releasing the full tree, so the changed files were not tried" ); } quality::ChangedFiles changed = quality::changedFilesBetween( root, out.baseSha, out.targetSha, !cfg.qualityScope.empty() ); if( !changed.ok ) @@ -344,7 +347,10 @@ std::optional loadChangedFiles( const std::string& root, const rw::Config& std::optional loadFullTrees( const std::string& root, const rw::Config& cfg, RefPairDelta& out, bool& projectedNoFit ) { using namespace rw; - const quality::KindMask certainLocal = static_cast( out.selected & ~( quality::kWholeTreeKinds | quality::kindBit( "defect-shape" ) ) ); + // --quality-ack needs every kind's rows, which the changed files never give: like a whole-tree selection it is never + // pre-empted by the projection, and only a real stop refuses it (review C2: base acked within the cap, head refused) + const quality::KindMask certainLocal = cfg.qualityAck ? quality::KindMask( 0 ) + : static_cast( out.selected & ~( quality::kWholeTreeKinds | quality::kindBit( "defect-shape" ) ) ); if( !out.sameRef ) { const std::uint64_t before = os::mem_footprint(); @@ -1170,7 +1176,8 @@ inline constexpr const char* kQdKindsSkippedLegend = "kinds_skipped= names the kinds this answer did NOT run (alphabetical): no row of them is in this document, which is not a " "statement that none exists, and regressions=, gating= and every count cover only the kinds that ran; a count only a " "skipped kind feeds (register-macro-excluded=) is absent, never 0. skipped_why=\"selector\": the caller chose the kinds " - "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=. "; + "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=; " + "skipped_why=\"selector,memory\": both, and memory_skipped= names memory's share (the rest the caller left out). "; inline constexpr const char* kQdDegradedLegend = "ingest_scope=\"changed-files\": the two full trees did not fit the memory guard's limit (memory_limit=, the max-memory value " "that sets it), so both commits' CHANGED files were ingested alone and the kinds those files decide ran with the same code and " @@ -1181,9 +1188,10 @@ inline constexpr const char* kQdDegradedLegend = "stale-acks (no stale= and no stale sa rows), ack-healing (acks matched by their recorded key only: a row that only a " "legacy-scheme re-key or a content-id match would hide in the full answer is shown here, and can gate). kinds_next= is the " "call that runs exactly the skipped kinds over the same two commits, at a max-memory just above memory_need= (after a stop: " - "twice the limit, a guess, since no need was measured). EXIT 2 when a row of a kind that ran gates (final: the skipped kinds " + "twice the limit, a guess, since no need was measured); it carries no output flag (rows only). EXIT 2 when a row of a kind that ran gates (final: the skipped kinds " "only add rows, though with ack-healing skipped that row may be one an identity-healed ack hides in the full answer); " - "otherwise EXIT 6, a partial answer with no gating row, never 0. "; + "otherwise EXIT 6, a partial answer with no gating row, never 0 — even when every selected kind ran, since the also_skipped= " + "facts are still missing. "; inline constexpr const char* kQdMemoryStopLegend = "memory_stop= names the guard's stop that sent this answer to the changed files: crawl (the crawl line, an eighth of the " "limit grown) or parse (half the limit) is a soft line, memory_pressure=1 when the OS pressure signal stopped it instead, " @@ -1606,15 +1614,25 @@ KindsDisclosure kindsDisclosure( const rw::Config& cfg, const RefPairDelta& refs } out.json += "]"; }; - const quality::KindMask skipped = static_cast( quality::kAllKinds & ~refs.kinds ); + // review C1: the kinds the CALLER left out and the kinds MEMORY skipped are two sets with two reasons — a selector's + // exclusions are never disclosed as memory cuts, and kinds_next= replays exactly the memory set + const quality::KindMask skipped = static_cast( quality::kAllKinds & ~refs.kinds ); + const quality::KindMask selector = static_cast( quality::kAllKinds & ~refs.selected ); + if( refs.degraded ) + { + out.memorySkipped = static_cast( refs.selected & ~refs.kinds ); + } if( skipped != 0 ) { add( "kinds_skipped", quality::kindList( skipped, "," ), true ); - add( "skipped_why", refs.degraded ? "memory" : "selector", false ); + add( "skipped_why", out.memorySkipped == 0 ? "selector" : selector == 0 ? "memory" : "selector,memory", false ); + if( out.memorySkipped != 0 && selector != 0 ) + { + add( "memory_skipped", quality::kindList( out.memorySkipped, "," ), true ); + } } if( refs.degraded ) { - out.memorySkipped = static_cast( refs.selected & ~refs.kinds ); const std::uint64_t limit = memguard::limits().hardBytes; add( "ingest_scope", "changed-files", false ); if( limit != 0 ) diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index ae4a0cd69..7af1a4de7 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -283,7 +283,7 @@ fi qd RIPWIRE_TEST_MEMGUARD=hard:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache; FRC=$RC if [ "$FRC" = 5 ] && [ ! -s "$WORK/o" ] && [ "$( grep -c '^ripwire: [^c]' "$WORK/e" )" = 1 ] \ && grep -q '^ripwire: memory limit reached during the ingest of the base commit (limit .*changed files' "$WORK/e" \ - && grep -q 'after releasing the full tree, so the changed files were not tried' "$WORK/e" && ! grep -q 'did not fit' "$WORK/e"; then + && grep -q 'after releasing the full tree, so the changed files were not tried' "$WORK/e" && ! grep '^ripwire: ' "$WORK/e" | grep -q 'did not fit'; then ok "(F) still over the limit after releasing the full tree: exit 5, nothing on stdout, one line naming the phase and that the changed files were not tried" else no "(F) rc=$FRC stdout=$( wc -c <"$WORK/o" | tr -d ' ' )B stderr: $( grep '^ripwire: [^c]' "$WORK/e" | head -c 300 )" @@ -601,10 +601,8 @@ else no "(W) complexity,dead-code: why=$( attr "$WORK/o" skipped_why ) memory_sk qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --legend=full --no-cache if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) --legend=full defines memory_skipped= when it rides, and parses as XML" else no "(W) the legend lacks memory_skipped= (or invalid XML)"; fi -qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --json --no-cache -if python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); s=json.dumps(d); sys.exit(0 if "\"memory_skipped\": [\"dead-code\"]" in s and "\"skipped_why\": \"selector,memory\"" in s else 1)' "$WORK/o" 2>/dev/null; then - ok "(W) --json carries memory_skipped=[dead-code] and skipped_why=selector,memory" -else no "(W) --json: $( grep -o '"memory_skipped[^]]*]\|"skipped_why":"[^"]*"' "$WORK/o" | head -c 200 )"; fi +# (no --json arm: --json is refused beside --quality-kinds, so memory_skipped= — which rides only beside a selector — has +# no JSON surface yet; the JSON twin is written by the same kindsDisclosure as the XML) [ "$fail" = 0 ] && echo "qddegradecheck: ALL PASS" || echo "qddegradecheck: FAIL" exit "$fail" From d272628e87a60f81fadb07193fe6d9acf6b3c693 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 02:45:03 -0400 Subject: [PATCH 072/162] =?UTF-8?q?refactor(quality):=20memorySkippedClaus?= =?UTF-8?q?e=20=E2=80=94=20degradedStderrLine=20back=20under=20the=20compl?= =?UTF-8?q?exity=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C1 clause moved into its own helper (quality-delta 5414abca..HEAD gated on degradedStderrLine 15 -> 16); kindsDisclosure names the both-reasons case once. No output change. Co-Authored-By: Claude Opus 5.5 --- src/verbs_quality.h | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 2ed892d3e..1f20b72fe 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -228,6 +228,13 @@ std::optional loadTargetCommit( const std::string& root, const std::string& return std::nullopt; } +// review C1: the degraded line's last clause — the kinds memory skipped, or, when it skipped none (a selector of kinds +// the changed files decide), that every selected kind ran: never an empty "skipped" list +std::string memorySkippedClause( rw::quality::KindMask memorySkipped ) +{ + return memorySkipped != 0 ? "; skipped " + rw::quality::kindList( memorySkipped, ", " ) : std::string( "; every selected kind ran" ); +} + // qd-memory-degrade — the stderr line of a degraded answer: the limit, why the full trees were not read, the kinds it // skipped, the replay. One line, beside the gating line, so a caller reading only $? and stderr knows the answer is partial. std::string degradedStderrLine( const RefPairDelta& refs, rw::quality::KindMask memorySkipped, std::string_view kindsNext ) @@ -239,8 +246,7 @@ std::string degradedStderrLine( const RefPairDelta& refs, rw::quality::KindMask : refs.memoryStop == "hard" ? "the memory limit was reached during the " + refs.stopWhere + "; the full need was not measured" : "the memory guard stopped the " + refs.stopWhere + ( refs.memoryPressure ? " under critical system memory pressure" : "" ) + "; the full need was not measured"; std::string line = "memory limit " + ( limit == 0 ? std::string( "unknown" ) : memguard::limitSpelling( limit ) ) + " (" + std::string( memguard::limitSourceClause() ) - + "): --quality-delta=A..B answered from the changed files only (" + why + ")" - + ( memorySkipped != 0 ? "; skipped " + quality::kindList( memorySkipped, ", " ) : std::string( "; every selected kind ran" ) ); + + "): --quality-delta=A..B answered from the changed files only (" + why + ")" + memorySkippedClause( memorySkipped ); if( !kindsNext.empty() ) { line += " — run them with " + std::string( kindsNext ); @@ -1625,8 +1631,9 @@ KindsDisclosure kindsDisclosure( const rw::Config& cfg, const RefPairDelta& refs if( skipped != 0 ) { add( "kinds_skipped", quality::kindList( skipped, "," ), true ); - add( "skipped_why", out.memorySkipped == 0 ? "selector" : selector == 0 ? "memory" : "selector,memory", false ); - if( out.memorySkipped != 0 && selector != 0 ) + const bool both = out.memorySkipped != 0 && selector != 0; // then memory_skipped= names memory's share + add( "skipped_why", both ? "selector,memory" : out.memorySkipped != 0 ? "memory" : "selector", false ); + if( both ) { add( "memory_skipped", quality::kindList( out.memorySkipped, "," ), true ); } From 92c28932cc2999a81642101c32d6afa5e9dad7fc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 02:53:26 -0400 Subject: [PATCH 073/162] =?UTF-8?q?test(forsoftbudgetcheck):=20round=20C?= =?UTF-8?q?=20arms=20=E2=80=94=20the=20tail=20carries=20the=20file=20page?= =?UTF-8?q?=20(C1-C6),=20sections=20reserved=20first=20(RF1,=20ST=20agains?= =?UTF-8?q?t=20T=20less=20the=20sections)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FOLD C: the default answer's lists the rank rows then page 0 of the --for file page with sym=; its total= is every positive-score file and a cut tail's next= is the page from its first unprinted row (C1 rows == the page's, C2 next= runs at that offset, C3 est_tokens prices it, C4 JSON == XML, C5 MCP == CLI, C6 every explicit target's cut tail names a runnable next=). RESERVE SECTIONS FIRST: RF1 pins, on frozen fixtures, that / keep at least the rows the retired hard ceiling (d4325a8c) served them (round B served 0 in every case); ST's maximality reads T less the trailing sections' bytes. E2's clause splitter also splits before the est_tokens clause (spliced after a clause with a bare space). Co-Authored-By: Claude Opus 5.5 --- test/forsoftbudgetcheck.sh | 132 +++++++++++++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 4 deletions(-) diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index a28ece213..6b8d22493 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -27,7 +27,8 @@ # disclosure. Rows are added in rank order while they fit T; the rank 1..4 floor always ships; there is no byte # promise, so nothing is labelled over a ceiling and nothing in the header is dropped to fit: # (ST) stop at target, in the default and --signatures-only modes: the rows' bytes fit T (or are only the -# floor), and when rows were left out the next row of the complete answer would cross T (fill is maximal); +# floor), and when rows were left out the next row of the complete answer would cross T less the trailing +# sections' bytes (round C: sections are reserved first, rows fill the rest; fill is maximal); # (E1) no over_ceiling= at a tight target; the floor rows ship; the cut carries next=; # (E2) no legend clause is dropped: at every N the legend's clauses include the wide answer's (present-only # clauses aside), the root keeps task= and route=, the full dialect keeps its task echo; and a 900-char @@ -145,7 +146,8 @@ def clauses( path ): if not com: sys.exit( 3 ) out = set() for c in com: - for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s)\s*', c ): + # " est_tokens= prices…" is spliced after the last clause with a bare space (pre-existing): a clause boundary too + for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s|\s(?=est_tokens= prices))\s*', c ): seg = re.sub( r'\d+', '#', re.sub( r'\s+', ' ', seg ) ).strip( ' .:;' ) if len( seg ) >= 12: out.add( seg ) return out @@ -437,14 +439,20 @@ def rb( p ): rows.sort() return len( inner.encode() ) + elided, rows a, ref, T = rb( sys.argv[ 1 ] ), rb( sys.argv[ 2 ] ), int( sys.argv[ 3 ] ) +# round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST): the trailing sections (and the rows) are served first +# and the rows past the target cut fill what they leave of T — the maximality bound is T less the bytes of those sections (U, read +# off the answer: everything after but , plus the rows before it) +ans = open( sys.argv[ 1 ] ).read() +U = ( len( ans.split( '', 1 )[ 1 ].encode() ) - len( '' ) if '' in ans else 0 ) +U += sum( len( h.encode() ) for h in re.findall( r']*/>', ans.split( '" ); sys.exit( 0 ) ab, ar = a; rr = ref[ 1 ] k = len( ar ) fits = ab <= T or k <= 4 if k >= len( rr ): print( "OK fits=%d rows=%d/%d charged=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) nxt = rr[ k ][ 1 ] -maximal = ab + nxt > T - 160 -print( "%s fits=%d maximal=%d rows=%d/%d charged=%d next=%d T=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T ) ) +maximal = ab + nxt > T - U - 160 +print( "%s fits=%d maximal=%d rows=%d/%d charged=%d next=%d T=%d U=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T, U ) ) PY )" case "$st" in @@ -547,6 +555,122 @@ if [ "$ea" -ne 0 ]; then no "(P3) MCP explore budget_tokens=300 produced no carries the file page's first page (`--for=TASK --limit=40`): after the rank rows +# (paths only: the positive-score files outside the shown sigs rows), the page's first 40 rows not already listed, each with +# sym=; total= every positive-score file (the page's total=); a cut tail's next= is the page from its first unprinted page row. +# RESERVE SECTIONS FIRST: under an explicit --token-budget the trailing sections (, , , the tail) keep at +# least the share the retired hard ceiling gave them, and the rows past its cut fill what they leave of T (ST above reads T - U). +cat > "$TMP/tail.py" <<'PY' +# tail.py FILE MODE: the of an answer — "rank" (paths of rows without sym=), "page" (psym of rows with sym=), +# "all" (psym), "attrs" (total shown capped next). exit 3: no . +import re, sys, html +s = open( sys.argv[ 1 ] ).read() +m = re.search( r']*)>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +a = dict( re.findall( r'(\w+)="([^"]*)"', m.group( 1 ) ) ) +rows = [ ( html.unescape( p ), html.unescape( y ), bool( h ) ) for p, h, y in re.findall( r'', m.group( 2 ) ) ] +mode = sys.argv[ 2 ] +if mode == "attrs": print( a.get( "total", "" ), a.get( "shown", "" ), a.get( "capped", "" ), html.unescape( a.get( "next", "" ) ) ) +for p, y, h in rows: + if mode == "rank" and not h: print( p ) + if mode == "page" and h: print( p + "\t" + y ) + if mode == "all": print( p + "\t" + y ) +PY +cat > "$TMP/page.py" <<'PY' +# page.py RANKFILE < page: the page's rows (psym) not among RANKFILE's paths, in page order; "total=N" first +import re, sys, html +s = sys.stdin.read(); rank = set( open( sys.argv[ 1 ] ).read().split( "\n" ) ) +m = re.search( r']*\btotal="([0-9]+)"', s ); print( "total=" + ( m.group( 1 ) if m else "" ) ) +for p, y in re.findall( r']*? sym="([^"]*)"', s ): + if html.unescape( p ) not in rank: print( html.unescape( p ) + "\t" + html.unescape( y ) ) +PY +# (C1) the default answer's tail: rank rows outside the shown head, then page 0 of the file page, sym= equal +if python3 "$TMP/tail.py" "$TMP/def.xml" attrs > "$TMP/c1.attrs"; then + read -r C1T C1S C1C C1N < "$TMP/c1.attrs" + python3 "$TMP/tail.py" "$TMP/def.xml" rank > "$TMP/c1.rank"; python3 "$TMP/tail.py" "$TMP/def.xml" page > "$TMP/c1.page" + "$BIN" "$CORPUS" --for="$Q" --limit=40 "$CF" 2>/dev/null | python3 "$TMP/page.py" "$TMP/c1.rank" > "$TMP/c1.want" + C1PT="$( head -1 "$TMP/c1.want" | sed 's/total=//' )"; sed -i.bak '1d' "$TMP/c1.want" + C1DUP="$( python3 - "$TMP/def.xml" "$TMP/c1.rank" <<'PY' +import re, sys +head = set( re.findall( r']*?\bp="([^"]*)"', open( sys.argv[ 1 ] ).read() ) ) +print( sum( 1 for p in open( sys.argv[ 2 ] ).read().split( "\n" ) if p in head ) ) +PY +)" + if [ -s "$TMP/c1.page" ] && cmp -s "$TMP/c1.page" "$TMP/c1.want" && [ "$C1T" = "$C1PT" ] && [ "$C1DUP" = 0 ] && [ "$( lines "$TMP/c1.rank" )" -le 24 ]; then + ok "(C1) default tail: $( lines "$TMP/c1.rank" ) rank rows outside the head, then the page's first rows ($( lines "$TMP/c1.page" ), sym= equal); total=$C1T == the page's" + else + no "(C1) default tail: page rows $( lines "$TMP/c1.page" ) vs the page's $( lines "$TMP/c1.want" ), total=$C1T vs page $C1PT, $C1DUP rank row(s) in the head" + fi + # (C2) a capped tail's next= is the page from its first unprinted page row, and it runs (rc 0, a page at that offset) + if [ "$C1C" = 1 ] && [ "$C1S" -lt "$C1T" ]; then + C2OFF="$( printf '%s' "$C1N" | sed -nE "s/^--for='.*' --limit=40( --offset=([0-9]+))?\$/\2/p" )" + if printf '%s' "$C1N" | grep -qE "^--for='$Q' --limit=40( --offset=[0-9]+)?\$" && run_next "$C1N" "$TMP/c2.xml" \ + && grep -qE "^]* offset=\"${C2OFF:-0}\"" "$TMP/c2.xml" && [ "${C2OFF:-0}" -ge "$( lines "$TMP/c1.page" )" ]; then + ok "(C2) the capped tail's next= ('${C1N:0:70}…') runs and serves the page from offset ${C2OFF:-0} (past every page row it printed)" + else + no "(C2) the capped tail's next= ('$C1N') is not the file page from its first unprinted row, or did not run" + fi + else + no "(C2) premise: the default tail is not capped (total=$C1T shown=$C1S) — re-anchor on a capped tail" + fi + # (C3) the size is disclosed: est_tokens= on the root prices the whole answer, tail included (markup rate 2.5 B/token) + C3E="$( attr ctx est_tokens "$TMP/def.xml" )"; C3B="$( wc -c < "$TMP/def.xml" | tr -d ' ' )" + if isnum "$C3E" && [ "$(( C3E * 25 ))" -ge "$(( C3B * 10 * 97 / 100 ))" ]; then ok "(C3) est_tokens=$C3E prices the $C3B-byte answer, its tail included" + else no "(C3) est_tokens='$C3E' does not price the $C3B-byte answer (the tail rides unpriced)"; fi +else + no "(C1) the default answer carries no " +fi +# (C4) the JSON twin: the same tail rows (files + sym index-aligned), total=, next present exactly when cut +cli_for --json > "$TMP/def.json" +C4="$( python3 - "$TMP/def.json" "$TMP/def.xml" <<'PY' +import json, re, sys, html +t = json.load( open( sys.argv[ 1 ] ) ).get( "tail", {} ); x = open( sys.argv[ 2 ] ).read() +xa = dict( re.findall( r'(\w+)="([^"]*)"', re.search( r']*)>', x ).group( 1 ) ) ) +xr = [ ( html.unescape( p ), html.unescape( y ) ) for p, _, y in re.findall( r'', x ) ] +ok = list( zip( t.get( "files", [] ), t.get( "sym", [] ) ) ) == xr and str( t.get( "total" ) ) == xa.get( "total" ) \ + and ( "next" in t ) == ( xa.get( "capped" ) == "1" ) and ( t.get( "next" ) or "" ) == html.unescape( xa.get( "next", "" ) ) +print( "OK" if ok else "BAD json rows=%d xml rows=%d total %s/%s" % ( len( t.get( "files", [] ) ), len( xr ), t.get( "total" ), xa.get( "total" ) ) ) +PY +)" +[ "$C4" = OK ] && ok "(C4) --json tail == the XML tail (files + sym, total=, next)" || no "(C4) --json tail differs from the XML tail: $C4" +# (C5) the MCP twin serves the same tail rows as the CLI default +mcp_call for "$Q" > "$TMP/mcp_def_tail.xml" +if python3 "$TMP/tail.py" "$TMP/mcp_def_tail.xml" all > "$TMP/c5.mcp" && python3 "$TMP/tail.py" "$TMP/def.xml" all > "$TMP/c5.cli" \ + && [ -s "$TMP/c5.cli" ] && cmp -s "$TMP/c5.mcp" "$TMP/c5.cli"; then ok "(C5) MCP for serves the CLI default's tail rows ($( lines "$TMP/c5.cli" ), sym= included)" +else no "(C5) MCP for's tail rows differ from the CLI default's ($( lines "$TMP/c5.mcp" ) vs $( lines "$TMP/c5.cli" ))"; fi +# (C6) under every explicit target of the sweep the tail is present, and a cut tail names a next= that runs +for n in "${SWEEP[@]}"; do + f="$TMP/t_${n}_full.xml" + if ! python3 "$TMP/tail.py" "$f" attrs > "$TMP/c6.attrs"; then no "(C6) N=$n: no "; continue; fi + read -r T6 S6 C6 N6 < "$TMP/c6.attrs" + if [ "$C6" = 1 ]; then + if [ -n "$N6" ] && run_next "$N6" "$TMP/c6.xml" && grep -q '^ "$TMP/sec.py" <<'PY' +# sec.py SECTION < answer: the section's rows (lego: iface+impl+m; tail: t; hops: h+c) +import re, sys +tags = { "lego": ( "iface", "impl", "m" ), "tail": ( "t", ), "hops": ( "h", "c" ) }[ sys.argv[ 1 ] ] +s = sys.stdin.read(); m0 = re.search( r'<%s\b[^>]*>' % sys.argv[ 1 ], s ) +if not m0: print( 0 ); sys.exit( 0 ) +el = m0.group( 0 ) if m0.group( 0 ).endswith( "/>" ) else s[ m0.start(): s.find( "" % sys.argv[ 1 ], m0.end() ) ] +print( sum( len( re.findall( r'<%s[ >/]' % t, el ) ) for t in tags ) ) +PY +QNODE="where is the node interface implemented"; QHAND="how does the handler process a request and call the service" +for spec in "legotransitivefix|$QNODE|600|lego|9" "legotransitivefix|$QNODE|1000|lego|9" "forcompletefix|$QNODE|1500|tail|11" \ + "forcompletefix|$QNODE|3000|tail|1" "forcompletefix|$QHAND|3000|tail|1"; do + IFS='|' read -r fx q n sec want <<< "$spec" + "$BIN" "test/$fx" --no-cache --for="$q" --token-budget="$n" > "$TMP/rf.xml" 2>/dev/null; rrc=$? + got="$( python3 "$TMP/sec.py" "$sec" < "$TMP/rf.xml" )" + if [ "$rrc" -eq 0 ] && isnum "$got" && [ "$got" -ge "$want" ]; then ok "(RF1) $fx '$q' N=$n: <$sec> keeps $got rows >= the retired ceiling's $want" + else no "(RF1) $fx '$q' N=$n: <$sec> serves '$got' rows < the retired ceiling's $want (rc=$rrc) — a section lost its share to the rows"; fi +done + # ── twins: the CI-gate personality of --token-budget is unchanged on the map, --query and --recall ─────── "$BIN" "$CORPUS" --token-budget=100 "$CM" > "$TMP/map.out" 2>/dev/null; grc=$? GW="$( attr r withheld "$TMP/map.out" 2>/dev/null )" From 84f12d67171e3f2e5467002b0e8732a28071e667 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 02:53:36 -0400 Subject: [PATCH 074/162] feat(for): fold the file page into the tail; an explicit target reserves the sections first Owner decisions 2026-10-14 (forbudget-soft round C). FOLD C: = the rank rows (paths of positive-score files outside the shown sigs rows, as before) then page 0 of --for=TASK --limit=40 not already listed, each row with sym=; total= every positive-score file; a cut tail's next= is the file page from its first unprinted page row. One model (forpage.h computeFileTail) for the CLI XML/JSON and the MCP twin; JSON adds an index-aligned "sym" array. RESERVE SECTIONS FIRST: a soft target's text tier and cut are the retired hard ceiling's sig claim (priced with the full dialect's header, the body-ceiling disclosures aside); / are scoped at that cut and served whole when they fit what it left; hops/bodies/tail are funded as if the rows stopped there; the rows past the cut then fill what those sections leave of T (a refill render), never below the cut. --detail bodies stay outside that accounting (their own ceiling). MCP for refills the same way (its tail rides on top, as before). Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 4 +- src/forpage.h | 194 +++++++++++++++++++++++++++++++++++ src/mcpverbs.h | 234 +++++++++++++++++++++++++----------------- src/serialize.h | 205 ++++++++++-------------------------- src/verbs_for.h | 268 ++++++++++++++++++++++++++++++++++-------------- 5 files changed, 581 insertions(+), 324 deletions(-) diff --git a/src/cli.h b/src/cli.h index b75100f71..4ceebdac2 100644 --- a/src/cli.h +++ b/src/cli.h @@ -958,8 +958,8 @@ inline constexpr char kHelpHead[] = " discloses on every run too: a target the signatures alone exhaust carries bundle=\"compact\" bodies=\"0\"\n" " reason=\"budget\" — three distinct reasons, never collapsed (compact-route = the route\n" " chose edges, no_candidates = nothing scored, budget = the ceiling was spent).\n" - " An explicit --token-budget=N is a soft target over the rows: rows in rank order\n" - " while they fit N, the bodies or hop rows from what they left; --pack-top-n sets the head\n" + " An explicit --token-budget=N is a soft target: the hop rows, bodies and file tail keep\n" + " the share a hard N left them, rows fill the rest by rank; --pack-top-n sets the head\n" " the target fills. --compress composes: the served bodies (auto/anchor and\n" " --detail=N alike) go through the same comment-strip --expand uses, disclosed as\n" " compress=\"1\" on the element (nothing to strip on the compact route).\n" diff --git a/src/forpage.h b/src/forpage.h index 72496fc76..4a0ba157e 100644 --- a/src/forpage.h +++ b/src/forpage.h @@ -631,4 +631,198 @@ inline std::string renderForFilePageXml( const IngestResult& ing, const ForFileP return x; } + +// ── FOLD C (forbudget-soft round C, owner decision 2026-10-14): the --for answer's carries page 0 of this page ── +// One model and one renderer for the CLI --for (XML + JSON) and the MCP `for` twin. The tail lists, in this order: +// 1. RANK rows (deep-tail d2, unchanged): the files of positive-score symbols NOT among the shown sigs rows, in the +// best-symbol projection of the lens's own (score desc, id asc) order — the files of trimmed rows first; at most +// kForFileTailRankCap; paths only (``). H2H-Graft lane 2: a row the ranked head did not show lands here. +// 2. PAGE rows (FOLD C): page 0 of `--for=TASK --limit=40` (computeForFilePage, the same ranking and term evidence), +// minus the files already listed above, each with sym= (its top symbols by lens rank). The measured arm M4 appended +// this page to the answer: 6 of 6 predicted rows reached an R0-missing item; the rank rows stay because two graded rows +// (textual-13/14) reach their item only through them, and M4 kept them too. +// total= every positive-score file (the page's total=); a cut tail's next= is the page from the first page row it did not +// print — a superset of what it left out, so every cut is recoverable in one call. +static_assert( kForFileTailPageRows == std::size_t( kForPageRowsDefault ), "the --for tail carries the file page's first page" ); + +struct FileTail +{ + std::vector paths; // printed order: the rank rows, then the page rows (display form, R-R) + std::vector syms; // a page row's sym= (comma-joined, unescaped); "" on a rank row + std::vector pageNext; // per row: the page offset continuing a tail that ends at it (0 on a rank row + // before any page row: the whole page continues it) + std::size_t total = 0; // every positive-score file: the page's total= + std::size_t pageSeen = 0; // page rows the tail walked (listed here or above): a whole tail continues there + std::string task; // the --for task: a cut tail's next= names the page +}; + +inline FileTail computeFileTail( const IngestResult& ing, const std::vector& rank, const LexTermEvidence& ev, + const std::vector& headIds, std::string_view rootArg, std::string_view task ) +{ + FileTail out; + out.task = std::string( task ); + const std::size_t F = ing.files.size(); + std::vector listed( F, 0 ); + { + // 1. the rank rows: the best-symbol projection of the lens order, outside the shown head's files + std::vector inHead( F, 0 ); + for( NodeId sid : headIds ) + { + if( sid < ing.symbols.size() && ing.symbols[ sid ].fileId < F ) + { + inHead[ ing.symbols[ sid ].fileId ] = 1; + } + } + const std::size_t S = std::min( ing.symbols.size(), rank.size() ); + std::vector order( S ); + for( NodeId i = 0; i < S; ++i ) + { + order[i] = i; + } + sortutil::radixSortByScoreDescId( order, rank ); + for( std::size_t k = 0; k < S && out.paths.size() < kForFileTailRankCap; ++k ) + { + const NodeId id = order[k]; + if( !( rank[id] > 0.0f ) ) + { + break; // (score desc) order: the zero-score tail is contiguous + } + const std::uint32_t f = ing.symbols[id].fileId; + if( f >= F || listed[f] || inHead[f] ) + { + continue; + } + listed[f] = 1; + out.paths.push_back( lensRowPath( ing, f, rootArg ) ); + out.syms.emplace_back(); + out.pageNext.push_back( 0 ); + } + } + // 2. the page rows: page 0 of the file page, minus the files already listed + const ForFilePage page = computeForFilePage( ing, rank, ev ); + out.total = page.rows.size(); + out.pageSeen = std::min( page.rows.size(), kForFileTailPageRows ); + for( std::size_t r = 0; r < page.rows.size() && r < kForFileTailPageRows; ++r ) + { + const ForFileRow& row = page.rows[r]; + if( row.fileId >= F || listed[ row.fileId ] ) + { + continue; + } + listed[ row.fileId ] = 1; + out.paths.push_back( lensRowPath( ing, row.fileId, rootArg ) ); + std::string sym; + for( std::size_t k = 0; k < kForPageSymNameCount && row.top[k] != kNoNode; ++k ) + { + if( k > 0 ) { sym += ','; } + sym += ing.symbols[ row.top[k] ].name; + } + out.syms.push_back( std::move( sym ) ); + out.pageNext.push_back( r + 1 ); + } + ENSURES( out.paths.size() == out.syms.size() && out.paths.size() == out.pageNext.size() && out.paths.size() <= out.total, + "computeFileTail: the rows, their sym= and their continuations disagree, or more rows than positive-score files" ); + return out; +} + +// The shown sigs rows a soft target's trailing sections are funded from (round C, reserve first): the shown ids without the +// `pastTarget` rows past its cut — the lowest-ranked shown rows in the lens's (score desc, id asc) order, which is the order +// the ladder's past-target count is taken in. The tail's rank rows exclude THESE files under a soft target: the rows past +// the cut may be narrowed after the tail is funded, and a narrowed row's file must still land somewhere (H2H lane 2). +inline std::vector rowsAtTargetIds( std::vector shown, const std::vector& rank, std::size_t pastTarget ) +{ + if( pastTarget == 0 || pastTarget > shown.size() ) + { + return shown; + } + const auto score = [ & ]( NodeId id ) { return id < rank.size() ? rank[id] : 0.0f; }; + std::stable_sort( shown.begin(), shown.end(), [ & ]( NodeId a, NodeId b ) + { return score( a ) != score( b ) ? score( a ) > score( b ) : a < b; } ); + shown.resize( shown.size() - pastTarget ); + return shown; +} + +// the call serving the files a tail of `shown` rows left out: the file page from the first page row it did not print +inline std::string fileTailNextInvocation( const FileTail& t, std::size_t shown ) +{ + std::size_t offset = shown >= t.paths.size() ? t.pageSeen : 0; // a whole tail covered every page row it walked + for( std::size_t i = 0; i < shown && i < t.pageNext.size(); ++i ) + { + offset = std::max( offset, t.pageNext[i] ); + } + return forPageInvocation( t.task, kForPageRowsDefault, int( offset ) ); +} + +// The XML shape: `……` — §P8 vocabulary, always +// emitted (a total of 0 means genuinely none remain, never not-computed — the B1.4 rule for elements). next= rides a cut +// tail only (shown < total). `shownCap` lets the explicit-budget caller serve fewer rows than were collected. +inline std::string renderFileTailXml( const FileTail& t, std::size_t shownCap, std::vector& esc ) +{ + const std::size_t shown = std::min( shownCap, t.paths.size() ); + std::string x = ""; + } + x += ""; + return x; +} + +// The JSON twin: `"tail":{"total":T,"shown":K[,"next":"…"],"files":[…],"sym":[…]}` (the caller supplies the leading comma). +// "sym" is index-aligned with "files", "" on a rank row (additive: a reader of the paths-only shape keeps reading "files"). +inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap ) +{ + const std::size_t shown = std::min( shownCap, t.paths.size() ); + std::string j = "\"tail\":{\"total\":" + std::to_string( t.total ) + + ",\"shown\":" + std::to_string( shown ) + + ( shown < t.total ? nextFieldJson( fileTailNextInvocation( t, shown ) ) : std::string() ); + const auto list = [ & ]( const char* key, const std::vector& v ) + { + j += key; + for( std::size_t i = 0; i < shown; ++i ) + { + if( i > 0 ) + { + j += ","; + } + j += '"'; + jsonesc::escapeInto( v[i], j, false, true, false ); // same core appendJsonStrField uses + j += '"'; + } + j += "]"; + }; + list( ",\"files\":[", t.paths ); + list( ",\"sym\":[", t.syms ); + j += "}"; + return j; +} + +// The explicit-budget fill (reserve first, forbudget-soft round C): the FEWEST rows whose rendered size reaches +// `shareBytes` — the tail keeps at least its share, so its last row may cross it — or every collected row. `render` +// is the dialect's renderer at a row count. Deterministic; at most kForFileTailShownCap + 1 renders. +template< class Render > +inline std::size_t fileTailShownToFill( const FileTail& t, std::size_t shareBytes, const Render& render ) +{ + for( std::size_t shown = 0; shown < t.paths.size(); ++shown ) + { + if( render( shown ).size() >= shareBytes ) + { + return shown; + } + } + return t.paths.size(); +} + } // namespace rw diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 00b9c675a..74fd43112 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1911,6 +1911,7 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref rw::SigLensRules lensRules; // train 26b: the --for lens rules (LB-A floor, e=, docs reorder), // decided once in forTaskText (the noRoute/task/budget verdicts live there) const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render + const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by }; inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::string headerStr, std::string legoStr, std::string composeStr, std::vector> legoScoped, std::size_t legoPreCapCount ) @@ -1954,8 +1955,36 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation mcpSigsCut.rowDropBudgetBytes = in.rowDropBudget; // forbudget-soft: IN — the CLI twin's two budgets mcpSigsCut.pastTargetDisclosed = budgetTokens == 0; - std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface - std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) + // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule + // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this + // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver + // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. + std::string mcpHdrXml; + if( !mcpForHdrRows.empty() ) + { + const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); + const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string + { + return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); + }; + std::vector mcpHdrEsc; + for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) + { + // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver + // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. + ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); + mcpHdrXml += ""; + } + } + std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail's rank rows exclude these files + // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); the + // redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it + const std::optional redactBeforeSigs = redact != nullptr ? std::optional( *redact ) : std::nullopt; + const auto renderSigs = [ & ]( std::FILE* m2 ) { packSignatures( m2, ing, lensRank, forTopN, 0 /* no byte budget in MCP (0 = unlimited) */, true, &fanIn, &impure, redact, nullptr, nullptr, nullptr, nullptr, // Q3 lens vectors — the MCP verb has no git/clone pass (as before) @@ -1971,7 +2000,103 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin &mcpSigsCut, // cut-fix lane A: docs_dropped= / shrunk readings; // knob-honesty-068: carries mcpSigsNext IN SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: the CLI twin's row spelling - } ); + }; + std::string sigsStr = renderToString( renderSigs ); + const std::optional redactAfterSigs = redact != nullptr ? std::optional( *redact ) : std::nullopt; + // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs + // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr + // directly now rather than re-slicing it back out of the flushed memstream buffer. + // count-floor (CLI/MCP parity): the root prefix the CLI twin passes. sigsStr's rows are root-relative (flRootArg) and + // without the prefix this scan compared them against ABSOLUTE ing.files spellings, matched nothing and narrowed the + // whole block away (serialize.h narrowLegoToRenderedSigs' own R-E note): MCP `for` served no at all where the + // CLI served six rows. + const std::string mcpLegoRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); + if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsRowsAtTarget( sigsStr, mcpSigsCut ), mcpLegoRootPrefix ) ) // forbudget-soft + { + legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed + } + // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution + // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= + // is already CLI-flag-shaped everywhere else, e.g. the r=1 row's --expand=FILE:NAME, so the two + // surfaces do not need two spellings), and the SAME shared size gate (rw::priceSectionStub, + // serialize.h). This surface has no buffered/degrade-path split (renderToString always renders whole), + // so hasRenderedBytes is always true here and the size gate always has a true length to price. + // + // R2-L2p (independent review, 2026-09-19): mcpLegoWillStub/mcpComposeWillStub decide the SAME collapse + // the CLI twin discloses via kForSectionStubLegend (verbs_for.h finishForLensHeaderPriced), but this + // block used to just swap legoStr/composeStr for their stubs and never carried that clause anywhere — + // an MCP caller got a stubbed section with no legend explaining the cut. mcpSectionsWillStub survives + // this block's scope so the splice below (after `out` is assembled, mirroring priceForTaskRoot's own + // late-insertion technique for kOverCeilingLegend) knows whether to add it. + bool mcpSectionsWillStub = false; + { + // the CLI twin's one plan (rw::planForSectionStubs): this surface always renders whole, so rendered=true + const ForStubbableSection mcpLegoServedIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = true, + .renderedBytes = legoStr.size() }; + const ForStubbableSection mcpComposeServedIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, + .renderedBytes = composeStr.size() }; + // round C (reserve first, the CLI twin's rule): under budget_tokens the sections ride whole when they fit what the rows + // at the cut left of the budget + const std::size_t mcpSigsAtCut = sigsStr.size() - std::min( sigsStr.size(), mcpSigsCut.pastTargetBytes ); + const std::size_t mcpBudgetBytes = budgetTokens > 0 ? budgetBytesForTokens( budgetTokens ) : 0u; + const std::size_t mcpShare = mcpBudgetBytes - std::min( mcpBudgetBytes, mcpSigsAtCut + in.routeStr.size() ); + const ForSectionStubPlan plan = planForSectionStubs( budgetTokens > 0 ? forSoftSectionsOptIn( sections, legoStr.size(), composeStr.size(), mcpShare ) + : std::string_view( sections ), + task, mcpLegoServedIn, mcpComposeServedIn ); + // lego-charge (train 26c seam): the charge plan the budget follows (forTaskText's mcpSigChargePlan, the same + // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render + // above may have changed the section's size since forTaskText charged the budget, and the invariant below is the + // two planners' agreement on one input: the served stub never exceeds the stub the charge plan collapses it to. + const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, /*explicitCeiling=*/false, task, mcpLegoServedIn, mcpComposeServedIn ); + const bool mcpLegoWillStub = plan.legoWillStub; + const bool mcpComposeWillStub = plan.composeWillStub; + // postcondition of the rule itself, checked BEFORE the swap below discards the original size: + // whichever section actually collapses is, by construction, smaller than what it replaced. + ENSURES( !mcpLegoWillStub || plan.lego.stubXml.size() < legoStr.size(), + "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); + ENSURES( !mcpComposeWillStub || plan.compose.stubXml.size() < composeStr.size(), + "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); + ENSURES( !( mcpLegoWillStub && mcpSigChargePlan.legoWillStub ) || plan.lego.stubXml.size() <= mcpSigChargePlan.lego.stubXml.size(), + "lego-charge MCP twin: the served lego stub is longer than the stub the budget was charged for" ); + if( mcpLegoWillStub ) + { + legoStr = plan.lego.stubXml; + } + if( mcpComposeWillStub ) + { + composeStr = plan.compose.stubXml; + } + // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` + // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. + mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; + } + + // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are + // scoped and served from the rows at the target cut (above), and the rows past the cut fill what the sections served + // before the tail ( rows, , , ) leave of T — never below the cut. The tail rides on top here, + // as it always did on this twin. Re-rendered only when that narrows the rows; the first render's redaction count is + // replaced, never added to. + if( budgetTokens > 0 && mcpSigsCut.pastTarget > 0 ) + { + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "" (serialize.h forSigsBudgets) + const std::size_t mcpTargetBytes = budgetBytesForTokens( budgetTokens ); + const std::size_t mcpSectionsUsed = mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size(); + const std::size_t mcpRowDropFill = ( mcpTargetBytes > mcpSectionsUsed ? mcpTargetBytes - mcpSectionsUsed : 0u ) + kSigsWrapperBytes; + if( mcpRowDropFill < in.rowDropBudget ) + { + if( redact != nullptr && redactBeforeSigs && redactAfterSigs ) + { + for( std::size_t k = 0; k < redact->byKind.size(); ++k ) // take the first render's count back out + { + redact->byKind[k] -= redactAfterSigs->byKind[k] - redactBeforeSigs->byKind[k]; + } + } + mcpSigsCut = rw::SigsCutReport {}; + mcpSigsCut.continuationRequest = &mcpSigsNext; + mcpSigsCut.rowDropBudgetBytes = mcpRowDropFill; + sigsStr = renderToString( renderSigs ); + } + } // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between @@ -2047,105 +2172,19 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin } } std::fwrite( headerStr.data(), 1, headerStr.size(), mem ); - // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule - // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this - // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver - // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. - if( !mcpForHdrRows.empty() ) - { - const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string - { - return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); - }; - std::vector mcpHdrEsc; - std::string mcpHdrXml; - for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) - { - // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver - // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - mcpHdrXml += ""; - } - std::fwrite( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); - } - // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs - // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr - // directly now rather than re-slicing it back out of the flushed memstream buffer. - // count-floor (CLI/MCP parity): the root prefix the CLI twin passes. sigsStr's rows are root-relative (flRootArg) and - // without the prefix this scan compared them against ABSOLUTE ing.files spellings, matched nothing and narrowed the - // whole block away (serialize.h narrowLegoToRenderedSigs' own R-E note): MCP `for` served no at all where the - // CLI served six rows. - const std::string mcpLegoRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsRowsAtTarget( sigsStr, mcpSigsCut ), mcpLegoRootPrefix ) ) // forbudget-soft - { - legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed - } - // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution - // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= - // is already CLI-flag-shaped everywhere else, e.g. the r=1 row's --expand=FILE:NAME, so the two - // surfaces do not need two spellings), and the SAME shared size gate (rw::priceSectionStub, - // serialize.h). This surface has no buffered/degrade-path split (renderToString always renders whole), - // so hasRenderedBytes is always true here and the size gate always has a true length to price. - // - // R2-L2p (independent review, 2026-09-19): mcpLegoWillStub/mcpComposeWillStub decide the SAME collapse - // the CLI twin discloses via kForSectionStubLegend (verbs_for.h finishForLensHeaderPriced), but this - // block used to just swap legoStr/composeStr for their stubs and never carried that clause anywhere — - // an MCP caller got a stubbed section with no legend explaining the cut. mcpSectionsWillStub survives - // this block's scope so the splice below (after `out` is assembled, mirroring priceForTaskRoot's own - // late-insertion technique for kOverCeilingLegend) knows whether to add it. - bool mcpSectionsWillStub = false; - { - // the CLI twin's one plan (rw::planForSectionStubs): this surface always renders whole, so rendered=true - const ForStubbableSection mcpLegoServedIn{ .hasContent = !legoStr.empty(), .preCapTotal = legoPreCapCount, .rendered = true, - .renderedBytes = legoStr.size() }; - const ForStubbableSection mcpComposeServedIn{ .hasContent = !composeStr.empty(), .preCapTotal = composePreCapCount, .rendered = true, - .renderedBytes = composeStr.size() }; - const ForSectionStubPlan plan = planForSectionStubs( sections, task, mcpLegoServedIn, mcpComposeServedIn ); - // lego-charge (train 26c seam): the charge plan the budget follows (forTaskText's mcpSigChargePlan, the same - // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render - // above may have changed the section's size since forTaskText charged the budget, and the invariant below is the - // two planners' agreement on one input: the served stub never exceeds the stub the charge plan collapses it to. - const ForSectionStubPlan mcpSigChargePlan = forSectionsSigChargePlan( sections, /*explicitCeiling=*/false, task, mcpLegoServedIn, mcpComposeServedIn ); - const bool mcpLegoWillStub = plan.legoWillStub; - const bool mcpComposeWillStub = plan.composeWillStub; - // postcondition of the rule itself, checked BEFORE the swap below discards the original size: - // whichever section actually collapses is, by construction, smaller than what it replaced. - ENSURES( !mcpLegoWillStub || plan.lego.stubXml.size() < legoStr.size(), - "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); - ENSURES( !mcpComposeWillStub || plan.compose.stubXml.size() < composeStr.size(), - "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); - ENSURES( !( mcpLegoWillStub && mcpSigChargePlan.legoWillStub ) || plan.lego.stubXml.size() <= mcpSigChargePlan.lego.stubXml.size(), - "lego-charge MCP twin: the served lego stub is longer than the stub the budget was charged for" ); - if( mcpLegoWillStub ) - { - legoStr = plan.lego.stubXml; - } - if( mcpComposeWillStub ) - { - composeStr = plan.compose.stubXml; - } - // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` - // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. - mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; - } + std::fwrite( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); // R2-AF: rendered above, before the rows (round C prices it) const bool mcpLegoCountAttrs = legoStr.find( rw::kLegoCountAttrPrefix ) != std::string::npos; // false on a stub (its own attributes only) std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); std::fwrite( routeStr.data(), 1, routeStr.size(), mem ); // B6.3 - // DEEP-TAIL d2, MCP twin: the same shared walk + renderer the CLI --for uses (serialize.h), from the - // same resolved surface — the fixed default budget regime, so the tail rides on top (default row cap) - // and the ranked head above stays byte-identical to a tail-less bundle. + // DEEP-TAIL d2, MCP twin: the same model + renderer the CLI --for uses (forpage.h; FOLD C: page 0 of the file page) — + // it rides on top at the default row cap in every regime here, as it did before (this twin funds no trailing section + // from budget_tokens but /), and a cut tail carries the page after its last row as next=. { std::vector tailEsc; - const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), - kForFileTailShownCap, tailEsc, - nextAttrXml( forWidenNext( task ) ) ); // forbudget-soft D-4: the cap's next= + const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, in.evidence, mcpShownIds, flRootArg, task ), + kForFileTailShownCap, tailEsc ); std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } rw::emitRaw( mem, "" ); @@ -2601,6 +2640,9 @@ inline std::optional forTaskText( const std::string& root, const st // over the rows (one dialect here, so the soft tier's header is this one) const ForSigsBudgets mcpSigsBudgets = forSigsBudgets( budgetTokens > 0 ? forBudgetBytes : 0u, fixedBytes, fixedBytes, /*tierAtDefault=*/false ); // no bodies ride here const std::size_t sigsBudget = mcpSigsBudgets.target; + // round C (RESERVE SECTIONS FIRST): under budget_tokens the target is the retired ceiling's cut; assembleMcpFor scopes the + // sections at it and narrows the rows past it to what those sections leave of T (its refill) + // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO CLI argv here — an // MCP client continues by re-calling the tool: the machine form next_budget_tokens=T (re-call `for` with budget_tokens=T) @@ -2618,7 +2660,7 @@ inline std::optional forTaskText( const std::string& root, const st rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) && rw::docsAfterCodeFitsCeiling( budgetTokens, /*bodyCeiling=*/false ), rw::endLinesFitCeiling( budgetTokens, /*bodyCeiling=*/false ) ), // LB-A, e=, docs reorder - ix.g.implementors }; + ix.g.implementors, mcpEvidence }; // forbudget-soft: ONE assembly (the C3 unpaid/paid passes went with the hard ceiling they priced against) McpForAssembly chosen = assembleMcpFor( assemblyIn, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); if( !chosen.doc ) diff --git a/src/serialize.h b/src/serialize.h index 7cbf1c4ef..1b94285c7 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -955,16 +955,15 @@ inline std::string forRunawayGuardNote() // runaway guard's share (kForRunawayGuardBytes, the same quantity) — the whole ranked head is the answer. // • SOFT TARGET (an explicit --token-budget / MCP budget_tokens: T = softRowBytes): T is charged against the ROWS inside // only — never the header or a disclosure — so the compact and full dialects serve the same rows. Rows drop, -// tail-first, until they fit T (+ the "" wrapper); rank 1..4 always ship. Rows are therefore a rank prefix, -// monotone in T, and the next row would cross T. The text tier (what A..E aim at) is T itself — a wide target serves the -// rows uncut, which is what the cut's own next= (--signatures-only at a wide target) promises — EXCEPT on the default -// auto bundle (`tierAtDefault`: bodies or hops ride after the rows and no --pack-top-n names a sig posture), where it is -// the DEFAULT target's share as the full dialect's header prices it (`fullFixedBytes`) when T is wider: there a wider -// target must not re-inflate row text at the bodies' expense (classb-bytes-memo §2's N=8000 trap), so the rows shrink -// as the default's do and a target wider than the complete answer serves the default answer's rows at its tier. -// This replaced the auto bundle's frozen sig share and the trailing-section reserve of the hard-ceiling regime (classb-bytes- -// memo §2, lego-charge fix round 4): with no byte promise there is nothing for either to protect — the trailing sections take -// what the rows leave of T, in the default answer's priority order. +// tail-first, until they fit the row budget (+ the "" wrapper); rank 1..4 always ship. Rows are therefore a +// rank prefix, monotone in T. RESERVE SECTIONS FIRST (round C, owner decision 2026-10-14): the text tier and the TARGET +// CUT (what A..E aim at, and where the default regime's past_target= would start) is the retired hard ceiling's sig +// claim — T less the charged header and sections as the full dialect prices them (`fullFixedBytes`), and on the default +// auto bundle (`tierAtDefault`: bodies or hops ride after the rows and no --pack-top-n names a sig posture) no more than +// the DEFAULT target's share (classb-bytes-memo §2's N=8000 trap: a wider target must not re-inflate row text at the +// bodies' expense). The trailing sections (lego/compose scope, hops, bodies, the tail) are funded as if the rows stopped +// at that cut, so each keeps at least the share the hard ceiling gave it; the rows past the cut then fill what the +// sections leave of T (the caller's second rowDrop, runForLens) — and never fall below the cut. struct ForSigsBudgets { std::size_t target = 1; // >= 1: 0 means "no budget" to packSignatures @@ -977,7 +976,13 @@ inline ForSigsBudgets forSigsBudgets( std::size_t softRowBytes, std::size_t fixe ForSigsBudgets b; if( softRowBytes > 0 ) { - b.target = tierAtDefault ? std::min( softRowBytes, share( kForPayloadBudgetBytes, fullFixedBytes ) ) : softRowBytes; + // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the target is the retired hard ceiling's sig claim at this + // N — T less the bundle's charged header and sections as the FULL dialect prices them (one cut for both dialects), and + // on the default auto bundle no more than the default target's share — so the rows at the target are the rows that + // ceiling served, the trailing sections are funded as if the rows stopped there (the default regime's own rule), and + // the rows past it fill what the sections leave of T (the caller narrows rowDrop to that once they are rendered). + const std::size_t cut = share( softRowBytes, fullFixedBytes ); + b.target = tierAtDefault ? std::min( cut, share( kForPayloadBudgetBytes, fullFixedBytes ) ) : cut; b.rowDrop = softRowBytes + kSigsWrapperBytes; } else @@ -1059,137 +1064,36 @@ inline constexpr std::size_t kForAnchorBodyBudgetBytes = 22800; // The lens serves ranked SYMBOL heads concentrated in few files; a consumer that needs file-grain recall // 20+ deep (external re-measure: the pre-registered retrieval lanes score exactly that) runs out of files // long before that depth — measured externally as the ENTIRE recall gap on the one task family where the -// head's MRR already wins. The tail is the cheapest honest widening: the REMAINING candidate files (a -// positive lens score, not already in the served head's file set) as PATHS ONLY, in the deterministic -// best-symbol projection of the same (score desc, id asc) order the head selected with — a projection of -// the existing ranking, never a re-rank. Zero-score files are NOT served (the LB-A relevance-floor -// reasoning at file grain: a file whose symbols matched nothing has no evidence to stand on). total= -// counts every candidate file; shown= what this bundle printed; capped="1" when they differ. The legend -// labels it file-grain, weaker evidence than the ranked rows above it. -// • DEFAULT regime: rows cap at kForFileTailShownCap; the bytes ride ON TOP of the sig/body budgets -// (the kForAutoBodyBudgetBytes precedent) and are measured into est_tokens — the ranked head and the -// bodies stay byte-identical to a tail-less bundle by construction. -// • EXPLICIT --token-budget: the ceiling stays hard — the tail is funded LAST (weakest evidence pays -// first): rows fit into the residual the rendered bundle actually left, trimming down to the honest -// empty shell. kForFileTailShellReserve rides inside the committed bytes handed to the body walk so -// that shell always fits — the kAutoAttrReserve pattern, sized for ``. -inline constexpr std::size_t kForFileTailShownCap = 24; +// head's MRR already wins. Zero-score files are NOT served (the LB-A relevance-floor reasoning at file grain). +// The REMAINING candidate files (a positive lens score, not among the shown head's files) as PATHS ONLY, in the +// deterministic best-symbol projection of the same (score desc, id asc) order the head selected with — the rank rows. +// FOLD C (forbudget-soft round C, owner decision 2026-10-14): page 0 of the --for file page (forpage.h computeForFilePage: +// one row per positive-score file, file-first by the IDF share its top symbols cover) follows them, each row with sym= +// naming its top symbols, so the answer carries its file grain and a capped tail's next= is the page itself. Measured +// before the fold (arm M4, the page appended to the answer): 6 of 6 predicted rows reached an R0-missing item, none lost +// one. The model and its renderers live in forpage.h (they need the page's ranking and its next= composer); the constants +// and the legend stay here, where the --for header reads them. +// • DEFAULT regime: kForFileTailRankCap rank rows + kForFileTailPageRows page rows; the bytes ride ON TOP of the +// sig/body budgets and are measured into est_tokens. +// • EXPLICIT --token-budget (a soft target): the tail is a trailing section funded first at its share — the +// bytes left once the rows at the target cut and the sections before it are paid (runForLens) — and fills +// that share (its last row may cross it); every cut carries next=. kForFileTailShellReserve sizes the +// empty shell ``. +inline constexpr std::size_t kForFileTailRankCap = 24; // the rank rows (deep-tail d2): files outside the shown head +inline constexpr std::size_t kForFileTailPageRows = 40; // FOLD C: page 0 of the file page (forpage.h kForPageRowsDefault) +inline constexpr std::size_t kForFileTailShownCap = kForFileTailRankCap + kForFileTailPageRows; // the default row cap inline constexpr std::size_t kForFileTailShellReserve = 48; -// The candidate walk, shared by the XML and JSON dialects (and the MCP `for` twin) so the three surfaces -// cannot select differently. `headIds` is THE BUNDLE'S RESOLVED SURFACE (the exact set selects); -// paths are root-relativized exactly as every emitted p= is (R-R). paths.size() <= kForFileTailShownCap; -// `total` keeps counting past the cap so the disclosure pair is honest. -struct FileTail -{ - std::vector paths; // first kForFileTailShownCap tail files, ranker order, display form - std::size_t total = 0; // every remaining candidate file with a positive lens score -}; - -inline FileTail computeFileTail( const IngestResult& ing, const std::vector& rank, - const std::vector& headIds, std::string_view rootArg ) -{ - FileTail out; - std::vector inHead( ing.files.size(), 0 ); - for( NodeId sid : headIds ) - { - if( sid < ing.symbols.size() && ing.symbols[ sid ].fileId < inHead.size() ) - { - inHead[ ing.symbols[ sid ].fileId ] = 1; - } - } - - const std::size_t S = ing.symbols.size(); - std::vector order( S ); - for( NodeId i = 0; i < S; ++i ) - { - order[i] = i; - } - sortutil::radixSortByScoreDescId( order, rank ); - - const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); - std::vector seen( ing.files.size(), 0 ); - for( std::size_t k = 0; k < S; ++k ) - { - const NodeId id = order[k]; - if( id >= rank.size() || !( rank[id] > 0.0f ) ) - { - break; // (score desc) order: the zero-score tail is contiguous - } - const std::uint32_t f = ing.symbols[id].fileId; - if( f >= seen.size() || seen[f] || inHead[f] ) - { - continue; - } - seen[f] = 1; - ++out.total; - if( out.paths.size() < kForFileTailShownCap ) - { - out.paths.emplace_back( rootArg.empty() ? std::string( ing.files[f] ) - : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ) ); - } - } - return out; -} - -// The XML shape: `…` — §P8 vocabulary, always -// emitted (a total of 0 means genuinely none remain, never not-computed — the B1.4 rule for elements). -// `shownCap` lets the explicit-budget caller trim rows below the collected count; the JSON twin below -// renders the SAME decision so the dialects cannot diverge on what was served. -// `cutNextAttr` (a ready ` next="…"` from nextAttrXml, or empty) rides the open tag ONLY when the tail is cut -// (shown < total): the call that serves the files left out. Empty (every caller but the --for explicit-budget -// regime) ⇒ byte-identical to before. -inline std::string renderFileTailXml( const FileTail& t, std::size_t shownCap, std::vector& esc, std::string_view cutNextAttr = {} ) -{ - const std::size_t shown = std::min( shownCap, t.paths.size() ); - std::string x = ""; - } - x += ""; - return x; -} - -// The JSON twin: `"tail":{"total":T,"shown":K,"files":[…]}` (the caller supplies the leading comma). forbudget-soft (D-4): a -// capped tail carries `"next":"…"` — the XML twin's next= (the file page that lists what it left out) — before its files. -inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap, std::string_view cutNextInvocation = {} ) -{ - const std::size_t shown = std::min( shownCap, t.paths.size() ); - std::string j = "\"tail\":{\"total\":" + std::to_string( t.total ) - + ",\"shown\":" + std::to_string( shown ) - + ( shown < t.total ? nextFieldJson( cutNextInvocation ) : std::string() ) + ",\"files\":["; - for( std::size_t i = 0; i < shown; ++i ) - { - if( i > 0 ) - { - j += ","; - } - j += '"'; - jsonesc::escapeInto( t.paths[i], j, false, true, false ); // same core appendJsonStrField uses - j += '"'; - } - j += "]}"; - return j; -} - // The legend clause defining BOTH deep-tail surfaces (the r= rank fact and the element) — a named // constant, shared by the CLI --for header and the MCP `for` twin, so the sigs-budget exemption in each // dialect subtracts EXACTLY the bytes the clause adds (the kForAutoBundleLegend pattern). No "--" anywhere: // it rides inside an XML comment where "--" is ill-formed (G4). inline constexpr std::string_view kForFileTailLegend = - "; tail: file-grain tail, WEAKER evidence than the ranked rows (paths only): every positive-score file " - "NOT among the shown sigs rows — the files of trimmed rows first, best-symbol rank order; rows are t p=file; total=such files, " - "shown=printed, capped=1 when they differ. r= on a ranked row is its 1-based rank in this lens ranking, " - "rows in r= order, p= the file (a gap = a budget-trimmed row)"; + "; tail: file-grain rows, WEAKER evidence than the ranked rows: first t p= alone, the positive-score files NOT among the " + "shown sigs rows, the files of trimmed rows first, best-symbol rank order; then t p= sym=, the file page's first 40 rows " + "not listed above, file-first (the IDF share of the query's subtokens its top 8 symbols cover), sym= its top symbols; " + "total=every positive-score file, shown=printed, capped=1 when they differ, next= the page from its first unprinted row. " + "r= on a ranked row is its 1-based rank in this lens ranking, rows in r= order, p= the file (a gap = a budget-trimmed row)"; // R2-AF (round 2, S4): the legend clause defining `` — verbatim, round-2 amendment §R7. A // named constant, shared by the CLI --for header and the MCP `for` twin (rw::forNamedHeaderRows, @@ -1199,20 +1103,6 @@ inline constexpr std::string_view kForHdrLegend = "; hdr p= of=: the file the task names (of=) has exactly one same-directory, same-stem declaration/" "implementation partner (p=), listed first by name alone: a lookup, not a ranked or graph-derived row"; -// Explicit-budget row fit: the largest shown count whose rendered XML fits `budgetBytes` (0 rows always -// "fits" — the shell is reserved by the caller). Walks down from the collected count; deterministic. -inline std::size_t fileTailShownForBudget( const FileTail& t, std::size_t budgetBytes, std::vector& esc, std::string_view cutNextAttr = {} ) -{ - for( std::size_t shown = t.paths.size(); shown > 0; --shown ) - { - if( renderFileTailXml( t, shown, esc, cutNextAttr ).size() <= budgetBytes ) - { - return shown; - } - } - return 0; -} - // ── COMPACT CONCEPTUAL SERVING (pre-registered: docs/EVALS.md, the T3 route-narrowing round) ────────── // On the CONCEPTUAL route — the subtoken+body ranker, the route that names no anchor — the allowance // above is where this tool's bytes actually go and where they cost it the standing head-to-head: class B @@ -8635,6 +8525,18 @@ inline ForSectionStubPlan planForSectionStubs( std::string_view sections, std::s return p; } +// round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST): under a soft target a stubbable section is +// served whole when the two sections' measured renders fit what the rows at the target cut left of the bundle budget +// (`shareBytes`). The collapse exists to save bytes a hard ceiling could not spare; a section that fits its share keeps it, +// because a stub there serves less than the retired ceiling did (forbudget-soft §B5: s078/s083, a 1-interface became a +// stub once the soft target's wider rows widened its scope). Otherwise the caller's --sections= stands and the priced +// collapse (planForSectionStubs) decides. The default regime never calls this: its stub posture is unchanged. +inline std::string_view forSoftSectionsOptIn( std::string_view sections, std::size_t legoBytes, std::size_t composeBytes, + std::size_t shareBytes ) noexcept +{ + return legoBytes + composeBytes <= shareBytes ? std::string_view( "lego,compose" ) : sections; +} + // lego-charge (K18 root cause): what / cost the budget, computed BEFORE the sigs render. It used to be // the full pre-rendered bytes, while the default answer serves a collapsing section as a ~120 B stub: textual's 6.5 KB lego // left 71 B for (4 rows shipped in a 4.6 KB answer under a 7.5 KB ceiling). Now a section is charged as it is SERVED. @@ -8891,11 +8793,12 @@ inline std::vector> legoImplementorsOnSurface( const IngestR // was a ceiling and keep the shape they had (§1.3's section invariant: no section loses rows because the head was kept whole). // Selected by the r= values the ladder recorded for the past-target rows (cut.pastTargetRanks), not by emission position nor // by an r= threshold: the docs reorder (docs_after_code=) moves doc rows below the code rows AND renumbers them. -// The whole block on a soft target or an answer with nothing past its target. Only … rows are kept (a file's +// A soft target scopes them the same way at ITS target cut (round C, reserve first: the rows the retired ceiling served). +// The whole block on an answer with nothing past its target. Only … rows are kept (a file's // notes ride its rows and are scanned for p= the same way). inline std::string sigsRowsAtTarget( std::string_view sigsRendered, const SigsCutReport& cut ) { - if( !cut.pastTargetDisclosed || cut.pastTargetRanks.empty() || cut.pastTarget >= cut.shown ) + if( cut.pastTargetRanks.empty() || cut.pastTarget >= cut.shown ) { return std::string( sigsRendered ); } diff --git a/src/verbs_for.h b/src/verbs_for.h index 34af233a6..ca9340c6f 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -414,10 +414,11 @@ struct ForLensJsonInputs // The R-E harvest reached the XML --for bundle but left this JSON sibling's packSignaturesJson call // spelling `/*rootArg=*/{}` — the two dialects of ONE question disagreed about the spelling of a path. std::string_view rootArg; - // DEEP-TAIL d2: the file-grain tail candidates (computed once, above the dialect split, from the same - // resolved surface selects). REQUIRED, not defaulted — a defaulted pointer is how a dialect + // DEEP-TAIL d2: the file-grain tail (FOLD C: page 0 of the --for file page, computed once above the dialect split, so + // both dialects serve the same rows). REQUIRED, not defaulted — a defaulted pointer is how a dialect // silently loses a surface its XML twin serves. const rw::FileTail* fileTail; + const rw::LexTermEvidence* evidence; // FOLD C: the term evidence the tail's page rows rank by (the lens run's own) // L3 follow-up (CodeRabbit 4053600616): read BEFORE the caller nulls noteIndex for emptiness (d.notesDegraded, // MainDispatch) — REQUIRED like fileTail above, for the same reason: a defaulted bool is how a degrade // silently drops off this dialect while the XML twin keeps carrying it. @@ -429,7 +430,6 @@ struct ForLensJsonInputs std::string_view task; int packTopN; std::string_view rankFlags; // forRankShapingFlags( cfg ), owned by the caller for the call's duration - std::string_view tailCutNext; // forbudget-soft (D-4): the capped tail's continuation (rw::forWidenNext), REQUIRED }; // The lens bundle's opening keys. Every note is absent-unless-present — the same silence-means-nothing- @@ -705,7 +705,7 @@ inline std::size_t ownerLegendExemptBytes( const ForLensHeaderParts& p, bool com inline constexpr std::string_view kForCompactLegendBodies = "; b t= n= p= l= full bodies, c n= l= callee signatures"; inline constexpr std::string_view kForCompactLegendTail = - "; t p= file outside sigs (weaker), r= rank (gap = trimmed)"; + "; t p= file outside sigs (weaker), then t p= sym=: the file page's first 40 rows (file-first, sym= top symbols), r= rank (gap = trimmed)"; // R2-AF (round 2, S4): the COMPACT dialect's `` clause — verbatim, round-2 amendment §R7. // 96 B, present-only like every clause in this dialect (appendCompactForLegend below). inline constexpr std::string_view kForCompactLegendHdr = @@ -1304,30 +1304,19 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin return finishForLensHeaderPriced( std::move( header ), f ).header; } -// DEEP-TAIL d2, JSON dialect — the tail stanza and its explicit-regime fit, as a free function over -// emitForLensJson's locals (the ForLensHeaderParts precedent: that emitter is already -// carrying the whole envelope fixpoint). Default regime: the full row cap. Explicit regime: the hard -// ceiling stays hard — rows trim one by one into whatever the rendered bundle left, down to the honest -// empty shell (the always-present key is the disclosure; an absent key would read as "no tail exists"). -// forbudget-soft: a soft target funds the tail from what the ROWS left of it (`rowsAllowance` = T minus the rows served before -// it); the header is never charged. `cutNext` (D-4) rides a capped tail in every regime. -inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t rowsAllowance, std::string_view cutNext ) +// DEEP-TAIL d2, JSON dialect — the tail stanza and its explicit-regime fill, as a free function over +// emitForLensJson's locals (the ForLensHeaderParts precedent: that emitter is already carrying the whole envelope +// fixpoint). Default regime: the full row cap (page 0's 40 rows, FOLD C). Explicit regime (forbudget-soft round C, +// reserve first): the tail keeps at least its share — `shareBytes`, what the rows at the target cut left of the bundle +// budget — so rows are added until the stanza reaches it (rw::fileTailShownToFill); a cut tail carries "next". +inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t tokenBudget, std::size_t shareBytes ) { - std::string stanza = "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap, cutNext ); if( tokenBudget == 0 ) { - return stanza; + return "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap ); } - for( std::size_t shown = tail.paths.size(); ; --shown ) - { - stanza = "," + rw::renderFileTailJson( tail, shown, cutNext ); - if( stanza.size() <= rowsAllowance || shown == 0 ) - { - break; - } - } - return stanza; + const auto render = [ & ]( std::size_t shown ) { return "," + rw::renderFileTailJson( tail, shown ); }; + return render( rw::fileTailShownToFill( tail, shareBytes, render ) ); } // One RENDER of the --json bundle (emitForLensJson below). @@ -1416,7 +1405,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // row cap here (the degrade path below needs a stanza before any budget is knowable); the normal path // re-fits it after the sigs render (forLensJsonTailStanza above) — residual-funded, sigs untouched: // the reservation feeding sigsBudget above deliberately does NOT include these bytes. - std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap, in.tailCutNext ); + std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap ); bool sigsCapped = false; // the LADDER's verdict: decides the budget_bytes= stanza rw::SigsCutReport sigsCut; // cut-fix lane A: what the array cut (gate or ladder) — "capped" and the sigs_* keys @@ -1441,6 +1430,56 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const sigsJson.assign( block.bytes ); } } + // round C (RESERVE SECTIONS FIRST, the XML twin's rule): the tail is funded from the rows at the soft target's cut, and + // the rows past the cut fill what it left of T — never below the cut. Re-rendered only when that narrows the rows; the + // first render's redaction count and note tallies are replaced, and a failed re-render keeps the first render whole. + if( isSigsBuffered && in.tokenBudget > 0 && sigsCut.pastTarget > 0 ) + { + constexpr std::size_t kSigsWrapperBytes = 13; // the array's frame (serialize.h forSigsBudgets' wrapper allowance) + const std::size_t cutRows = sigsJson.size() - std::min( sigsJson.size(), sigsCut.pastTargetBytes ); + const rw::FileTail cutTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, rw::rowsAtTargetIds( jsonShownIds, in.rank, sigsCut.pastTarget ), + in.rootArg, in.task ); + const std::size_t tailBytes = forLensJsonTailStanza( cutTail, in.tokenBudget, bundleBudget > cutRows ? bundleBudget - cutRows : 0u ).size(); + const std::size_t rowDropFill = ( bundleBudget > tailBytes ? bundleBudget - tailBytes : 0u ) + kSigsWrapperBytes; + if( rowDropFill < sigsBudgets.rowDrop ) + { + const rw::SigsCutReport firstCut = sigsCut; + const bool firstCapped = sigsCapped; + const std::size_t firstDropped = sigsDroppedPositive; + const JsonSigNoteCounts firstNotes = noteCounts; + const std::vector firstShown = jsonShownIds; + const std::optional redactFirst = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; + if( redactBeforeSigs ) + { + *in.redact = *redactBeforeSigs; + } + noteCounts = JsonSigNoteCounts {}; + sigsCut = rw::SigsCutReport {}; + sigsCut.continuationRequest = &cutNext; + sigsCut.rowDropBudgetBytes = rowDropFill; + rw::MemoryStream refillStream; + bool refilled = false; + if( std::FILE* const rm = rw::openChargeStream( refillStream ) ) + { + packSigs( rm, sigsBudget, &sigsCapped, &sigsDroppedPositive, &jsonShownIds, &sigsCut ); + const rw::MemoryStreamBytes refill = refillStream.finish(); + if( refill.isWhole ) + { + sigsJson.assign( refill.bytes ); + refilled = true; + } + } + if( !refilled ) + { + sigsCut = firstCut; sigsCapped = firstCapped; sigsDroppedPositive = firstDropped; noteCounts = firstNotes; + jsonShownIds = firstShown; + if( redactFirst ) + { + *in.redact = *redactFirst; + } + } + } + } ForLensBlockCharge sigsCharge; // the sink: an unmeasured block takes the omitting path below if( !isSigsBuffered ) { @@ -1536,11 +1575,12 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ? ",\"budget_bytes\":" + std::to_string( kForPayloadBudgetBytes ) : std::string(); - // DEEP-TAIL, explicit-regime fit (forLensJsonTailStanza above): residual-funded, sigs untouched. - // lane 2: same rule as the XML twin — the tail excludes the files of the rows actually emitted, not the whole surface - const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, jsonShownIds, in.rootArg ); - tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, bundleBudget > sigsJson.size() ? bundleBudget - sigsJson.size() : 0u, - in.tailCutNext ); // soft: the rows' leftover funds the tail; D-4: its cut's continuation + // DEEP-TAIL (FOLD C: page 0 of the file page), the explicit-regime fill (forLensJsonTailStanza above): reserve first — funded + // from what the rows AT THE CUT left of the bundle budget (the same share the refill above priced the rows against) + // lane 2: the rank rows exclude the files of the rows emitted (after a refill: the rows it kept — the XML twin's rule) + const std::size_t jsonCutRows = sigsJson.size() - std::min( sigsJson.size(), sigsCut.pastTargetBytes ); + const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, jsonShownIds, in.rootArg, in.task ); + tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, bundleBudget > jsonCutRows ? bundleBudget - jsonCutRows : 0u ); const std::size_t bundleBytesBase = header.size() + sigsJson.size() + notesStanza.size() + surfaceCountsStanza.size() + tailStanza.size() + envelopeTextBytes + droppedPositiveStanza.size() // A2: 0 bytes on the (overwhelming) no-drop path @@ -2447,25 +2487,23 @@ ForAutoBodiesResult buildForEnrichment( const rw::Config& cfg, const rw::IngestR return out; } -// DEEP-TAIL d2 — the XML tail render + its explicit-regime fit, a free function over runForLens' locals -// (that function is already one of the largest in this file — the ForLensHeaderParts precedent). Default -// regime: the full row cap, bytes riding on top (est_tokens measures them). Explicit regime: rows fit the -// residual the rendered bundle actually left; the shell's bytes were reserved ahead of the body walk -// (kForFileTailShellReserve inside the enrichment's committed sum), so the disclosure always fits. -// fix round 4 (rule 5, review rv-wrap-recipe-k29 F1): under an explicit ceiling the cut tail carries `cutNextAttr` — -// the --for file page (forTailNext in runForLensPass) that lists every positive-score file — and the shell reserve the caller -// took (forTailShellReserve) includes it, so the disclosure still always fits. +// DEEP-TAIL d2 — the XML tail render + its explicit-regime fill, a free function over runForLens' locals (that function +// is already one of the largest in this file — the ForLensHeaderParts precedent). Default regime: page 0's full 40 rows +// (FOLD C), bytes riding on top (est_tokens measures them). Explicit regime (forbudget-soft round C, reserve first): the +// tail keeps at least its SHARE — the bundle budget less `spentBytes` (the rows at the target cut and the sections served +// before the tail), never below the empty shell — so rows are added until the element reaches it (its last row may cross +// it: a soft target makes no byte promise). A cut tail carries next= (the page after its last row) in every regime. inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t tokenBudget, - std::size_t bundleBudget, std::size_t spentBytes, std::string_view cutNextAttr = {} ) + std::size_t bundleBudget, std::size_t spentBytes ) { std::vector esc; if( tokenBudget == 0 ) { - return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc, cutNextAttr ); // forbudget-soft D-4: the cap's next= too + return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc ); } - const std::size_t tailAllowed = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, - rw::kForFileTailShellReserve + cutNextAttr.size() ); - return rw::renderFileTailXml( tail, rw::fileTailShownForBudget( tail, tailAllowed, esc, cutNextAttr ), esc, cutNextAttr ); + const std::size_t share = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, rw::kForFileTailShellReserve ); + const auto render = [ & ]( std::size_t shown ) { return rw::renderFileTailXml( tail, shown, esc ); }; + return render( rw::fileTailShownToFill( tail, share, render ) ); } @@ -2834,9 +2872,14 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { forConf.attrs += " budget_tokens=\"" + std::to_string( cfg.tokenBudget ) + "\""; } + // round C: the body ceiling's two disclosures (this attribute and its note below), measured so a soft target's cut can + // be priced without them — a body ceiling never moves a row (formaxtokenscheck (E)) + std::size_t forBodyCeilingDisclosureBytes = 0; if( forBodyCeiling ) { + const std::size_t before = forConf.attrs.size(); forConf.attrs += " max_tokens=\"" + std::to_string( cfg.maxTokens ) + "\""; + forBodyCeilingDisclosureBytes += forConf.attrs.size() - before; } // forbudget-soft (owner decision 2026-10-14): budget_tokens= is a SOFT target now — rows stop once it is reached and // nothing else is shaped by it — so its reading says so (kForSoftBudgetNote); max_tokens= stays the hard body ceiling. @@ -2848,7 +2891,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co } if( forBodyCeiling ) { + const std::size_t before = forConf.note.size(); forConf.note += " [max_tokens=: the token ceiling the detail bodies were shaped against]"; + forBodyCeilingDisclosureBytes += forConf.note.size() - before; } // §L10b: mention_anchored=/doc_mentions= — the machine form of mentionNote/docMentionNote, EACH @@ -3069,13 +3114,12 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // header above, and the body otherwise; "" keeps the --expand hint. const std::string forTopRowNext = forThin ? forWidenNext( cfg.forTask ) : std::string(); - // DEEP-TAIL d2: the file-grain tail candidates — one shared walk (serialize.h computeFileTail) for - // both dialects, computed from the SAME resolved surface selects, so the two dialects (and - // the MCP twin, which calls the same function) cannot select different tails. - const FileTail forFileTail = computeFileTail( ing, lensRank, lensSurfaceIds, flRootArg ); // the DEGRADE paths' tail (surface = head) - // H2H-Graft lane 2 (2026-09-07): the tail served must exclude only the files of the sigs rows actually - // EMITTED. Excluding the whole 40-candidate surface left every row the byte ladder trimmed (rank 5..40) - // in neither section: on rocksdb, three single-file answers at candidate rank 5/10/5 were served nowhere. + // DEEP-TAIL d2, FOLD C (forbudget-soft round C): the file-grain tail is page 0 of the --for file page — one shared + // model (forpage.h computeFileTail) for both dialects and the MCP twin, from the SAME ranking and term evidence the + // `--limit=40` page uses, so the tail, its next= page and the page itself cannot disagree. + const FileTail forFileTail = computeFileTail( ing, lensRank, lr.evidence, lensSurfaceIds, flRootArg, cfg.forTask ); // the DEGRADE paths' tail + // H2H-Graft lane 2 (2026-09-07): the tail's rank rows exclude only the files of the sigs rows actually EMITTED (a row the + // ladder trimmed must land in the tail, never nowhere). std::vector shownSigIds; // L2: --json — the ranking ("sigs") bundle, plus (§B1.4) a COUNT of what // @@ -3153,8 +3197,8 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co &forClone, testedPtr, ampPtr, redactPtr, cfg.packBudgetBytes, cfg.tokenBudget, notesPtr, legoTotal, composeTotal, routesTotal, flRootArg, - &forFileTail, d.notesDegraded, forDocsAfterCode, cfg.forTask, - cfg.packTopN, forRankShapingFlags( cfg ), rw::forWidenNext( cfg.forTask ) } ); + &forFileTail, &lr.evidence, d.notesDegraded, forDocsAfterCode, cfg.forTask, + cfg.packTopN, forRankShapingFlags( cfg ) } ); // §B0: this early return skipped the end-of-function tally below, so a --for --json run redacted // SILENTLY — the one stderr line that tells the user a secret was in their tree never appeared. reportRedactions( stderr, redactCounts ); @@ -3373,14 +3417,13 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // the first --detail body's floor under a soft target (head-cut with next= when it is over a quarter of the target): the // target fills the rows first, and the floor keeps the one body the caller asked for from being cut to nothing const std::size_t detailFloorBudget = forSoftTarget ? forDetailFirstBodyFloor( ing, detailIds, bundleBudget ) : 0u; - // D-4 (forbudget-soft; PROCESS rule 5: every cut recoverable): a capped carries next= — the --for file page (one row - // per positive-score file, pageable), the one call that lists the files the tail left out — in EVERY regime (the - // default's 24-row cap was the one default cut with no continuation; renderFileTailXml writes it on a capped tail only) - const std::string forTailNext = rw::nextAttrXml( rw::forWidenNext( cfg.forTask ) ); // THE TWO BUDGETS (forbudget-soft; serialize.h kForPayloadBudgetBytes / kForRunawayGuardBytes): the target the // text-shrink steps aim at, and where the row-drop step aims — the runaway guard (default), or the soft target's rows. + // round C (reserve first): a soft target's cut is priced with the full dialect's header and every fixed section — the + // graph block included, which the retired ceiling reserved out of the sig claim — so both dialects cut alike + const std::size_t cutHeaderBytes = fullChargedHeaderBytes - std::min( fullChargedHeaderBytes, forBodyCeilingDisclosureBytes ); const ForSigsBudgets sigsBudgets = forSigsBudgets( forSoftTarget ? bundleBudget : 0u, fixedBytes, - fullChargedHeaderBytes + sectionsChargedBytes, + cutHeaderBytes + sectionsChargedBytes + graphSection.xml.size(), /*tierAtDefault=*/autoBundleMode && cfg.packTopN == 0 ); const std::size_t sigsBudget = sigsBudgets.target; @@ -3448,7 +3491,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation forSigsCut.rowDropBudgetBytes = sigsBudgets.rowDrop; // forbudget-soft: IN — where step F aims (guard / soft target) forSigsCut.pastTargetDisclosed = !forSoftTarget; // …and the default's rows past its target are disclosed - sigsPreRendered = preRender( [ & ]( std::FILE* sm ) + // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); + // the redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it + const std::optional redactBeforeSigs = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; + const auto renderSigs = [ & ]( std::FILE* sm ) { packSignatures( sm, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, // Q3: churn/clone/tested/amp folded onto the blocks @@ -3458,14 +3504,15 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co flRootArg, // R-E: root-relative p= rw::forLensRules( forDocsAfterCode, forEndLines ), // LB-A floor, e= (endLinesFitCeiling), docs reorder &forDroppedPositive, // A2: exact count, see droppedPositiveCount - &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files + &shownSigIds, // lane 2: the rows actually emitted — the tail's rank rows exclude THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then forTopRowNext, // L-W: the widening page on a thin answer, else the body &forSigsCut, // cut-fix lane A: which cut readings the tag owes; // knob-honesty-068: carries forSigsNext IN SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: zero cx=/ccx=/in= omitted (legend: absent = 0) - }, - sigsStr ); + }; + sigsPreRendered = preRender( renderSigs, sigsStr ); + const std::optional redactAfterSigs = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; if( !sigsPreRendered ) { DISCLOSE( blockCharge, ForLensBlockCharge::DisclosureWhy::SigsUnmeasured, @@ -3545,7 +3592,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // only when the tag carries the case (serialize.h sigsCutLegendNotes), so the reserve below already covers them. // forbudget-soft: on an answer whose ranked head ran PAST the target (past_target=), the number shaped row text and // removed no row — its reading says "target" there; elsewhere the payload fit it and the ceiling reading stays true - const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( forSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ) + std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( forSigsCut.pastTarget > 0 ? rw::kForBudgetBytesTargetNote : rw::kForBudgetBytesNote ) : std::string() ) + rw::sigsCutReportLegend( forSigsCut ) // + the continuation's and the docs reorder's readings (docs_after_code=) + ( forGuardCut ? rw::forRunawayGuardNote() : std::string() ); // forbudget-soft: the runaway guard's reading, where it cut @@ -3600,8 +3647,13 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // the one rule both surfaces share, its ONE restoring invocation (used at every site below: the buffered substitution // and the degrade-path fallback), and the DEGRADE path it documents: no rendered bytes to gate on there, so the // section never collapses and is streamed whole below. + // round C (reserve first): a soft target serves the sections whole when they fit what the rows at its cut left + const std::size_t sigsAtCutBytes = sigsStr.size() - std::min( sigsStr.size(), forSigsCut.pastTargetBytes ); + const std::size_t sectionsShare = bundleBudget - std::min( bundleBudget, forHdrXml.size() + sigsAtCutBytes + routeStr.size() + graphSection.xml.size() ); const rw::ForSectionStubPlan stubPlan = rw::planForSectionStubs( - cfg.sections, cfg.forTask, + forSoftTarget && legoPreRendered && composePreRendered ? rw::forSoftSectionsOptIn( cfg.sections, legoStr.size(), composeStr.size(), sectionsShare ) + : std::string_view( cfg.sections ), + cfg.forTask, rw::ForStubbableSection{ .hasContent = legoPreRendered ? !legoStr.empty() : legoStubTotal > 0, .preCapTotal = legoStubTotal, .rendered = legoPreRendered, .renderedBytes = legoStr.size() }, rw::ForStubbableSection{ .hasContent = composePreRendered ? !composeStr.empty() : composeStubTotal > 0, .preCapTotal = composeStubTotal, @@ -3663,9 +3715,10 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co std::size_t detailBodyBudget = 0; // forbudget-soft: WHAT THE TRAILING SECTIONS ARE FUNDED FROM (bodies, hops, tail), in the default answer's priority // order. DEFAULT: the bundle as it stood at the 7.5 KB target — the rows served past it (past_target=) are not charged - // to the sections after them, so no section shrinks because the head was kept whole. SOFT TARGET: the rows already - // served (, the header rows, the fixed sections); the header and every disclosure are never charged. - const std::size_t sigsFundedBytes = sigsStr.size() - std::min( sigsStr.size(), forSoftTarget ? 0u : forSigsCut.pastTargetBytes ); + // to the sections after them, so no section shrinks because the head was kept whole. SOFT TARGET (round C, reserve + // first): the same rule at the soft target's cut — the rows the retired hard ceiling served at this N — so each section + // keeps at least the share that ceiling gave it; the header and every disclosure are never charged there. + const std::size_t sigsFundedBytes = sigsStr.size() - std::min( sigsStr.size(), forSigsCut.pastTargetBytes ); const std::size_t rowsSpentBytes = forHdrXml.size() + sigsFundedBytes + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size(); const std::size_t headerSpentBytes = forSoftTarget ? 0u : headerStr.size() + 6 + headerSpliceReserve + droppedPositiveSpliceReserve + sectionsStubSpliceReserve; @@ -3755,15 +3808,80 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co } } - // ── DEEP-TAIL d2: render the file-grain tail, funded LAST (see serialize.h kForFileTailShownCap) ── - // Rendered after the bodies decision so the explicit regime spends only the RESIDUAL the rendered - // bundle actually left — the sigs and the bodies are byte-identical to a tail-less bundle in the - // default regime by construction (nothing above charges these bytes), and under a hard ceiling the - // weakest-evidence section is the one that trims. The pre-ladder headerStr sizes the residual (a - // ladder rung can only SHRINK the header, so the fit stays conservative and deterministic). - const FileTail forFileTailShown = sigsPreRendered ? computeFileTail( ing, lensRank, shownSigIds, flRootArg ) : forFileTail; - const std::string tailStr = renderForFileTailXml( forFileTailShown, cfg.tokenBudget, bundleBudget, - rowsSpentBytes + detailSection.xml.size() + autoSection.xml.size(), forTailNext ); + // ── DEEP-TAIL d2: render the file-grain tail (FOLD C: page 0 of the file page; serialize.h kForFileTailShownCap) ── + // Rendered after the bodies decision. Default regime: the full 40 rows ride on top — the sigs and the bodies are + // byte-identical to a tail-less bundle by construction (nothing above charges these bytes). A soft target: the tail is + // the last trailing section, funded from what the rows AT THE CUT and the sections before it left (reserve first). + // its rank rows exclude the shown rows' files — under a soft target the rows AT THE CUT, the ones a refill below keeps + const FileTail forFileTailShown = sigsPreRendered ? computeFileTail( ing, lensRank, lr.evidence, + forSoftTarget ? rowsAtTargetIds( shownSigIds, lensRank, forSigsCut.pastTarget ) + : shownSigIds, + flRootArg, cfg.forTask ) + : forFileTail; + // round C: the --detail bodies are the caller's own request under their own ceiling (--max-tokens / the pack budget), so + // they neither fund nor are funded by the reserved sections: the tail's share and the rows' refill both leave them out, + // and a body ceiling never moves a row (formaxtokenscheck (E)) + const std::size_t tailSpentBytes = forSoftTarget ? rowsSpentBytes + autoSection.xml.size() + : rowsSpentBytes + detailSection.xml.size() + autoSection.xml.size(); + std::string tailStr = renderForFileTailXml( forFileTailShown, cfg.tokenBudget, bundleBudget, tailSpentBytes ); + + // ── round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the rows past the soft target's cut fill what the + // trailing sections left of T. The sections above were funded as if the rows stopped at the cut (sigsFundedBytes), so + // each kept at least the share the retired hard ceiling gave it; the rows are now narrowed to T less those sections' + // served bytes — never below the cut (a row budget at or under the target IS the cut: serialize.h sigsRowDropEffective). + // Re-rendered only when that budget is narrower than the first render's, which changes only rows past the cut: the + // sections' scope (sigsRowsAtTarget) and funding read the rows AT the cut. The first render's redaction count is + // replaced, never added to; a re-render whose buffer fails keeps the first render and its readings whole. + if( forSoftTarget && sigsPreRendered && forSigsCut.pastTarget > 0 ) + { + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "" (serialize.h forSigsBudgets) + const std::size_t sectionsServed = forHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() + + autoSection.xml.size() + tailStr.size(); // not the --detail bodies (above) + const std::size_t rowDropFill = ( bundleBudget > sectionsServed ? bundleBudget - sectionsServed : 0u ) + kSigsWrapperBytes; + if( rowDropFill < sigsBudgets.rowDrop ) + { + const rw::SigsCutReport firstCut = forSigsCut; + const std::size_t firstDropped = forDroppedPositive; + const bool firstCapped = forSigsCapped; + const std::vector firstShown = shownSigIds; + const std::optional redactNow = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; + if( redactPtr != nullptr && redactBeforeSigs && redactAfterSigs ) + { + for( std::size_t k = 0; k < redactPtr->byKind.size(); ++k ) // take the first render's count back out + { + redactPtr->byKind[k] -= redactAfterSigs->byKind[k] - redactBeforeSigs->byKind[k]; + } + } + forSigsCut = rw::SigsCutReport {}; + forSigsCut.continuationRequest = &forSigsNext; + forSigsCut.rowDropBudgetBytes = rowDropFill; + forSigsCut.pastTargetDisclosed = false; + std::string refill; + if( preRender( renderSigs, refill ) ) + { + sigsStr = std::move( refill ); + // the first render's spelling of the same attribute (no second fixed buffer: fixedbufsweep's census) + droppedPositiveNote = forDroppedPositive > 0 ? " dropped_positive=\"" + std::to_string( forDroppedPositive ) + "\"" + : std::string(); + sigsCeilingNote = rw::sigsCutReportLegend( forSigsCut ); // a soft target: no default ceiling, no guard reading + // lane 2: a row the refill dropped lands in the tail's rank rows — re-listed against the rows now shown, at + // the same share (the share, not the rows, is what the refill priced) + tailStr = renderForFileTailXml( computeFileTail( ing, lensRank, lr.evidence, shownSigIds, flRootArg, cfg.forTask ), + cfg.tokenBudget, bundleBudget, tailSpentBytes ); + } + else + { + forSigsCut = firstCut; + forDroppedPositive = firstDropped; + forSigsCapped = firstCapped; + shownSigIds = firstShown; + if( redactNow ) + { + *redactPtr = *redactNow; + } + } + } + } // PR #135: the priced root this header will carry — every late splice and the est_tokens fixpoint, in // finishForLensHeader (above runForLens, with each splice's rationale). From e6e379ec2200259c8bb5c2dac43b239deda43331 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 02:53:41 -0400 Subject: [PATCH 075/162] test: round C rule-4 updates (tail page rows, reserve-first twins, re-pins) deeptailcheck (new tail contract), forwidencheck (1), fordisclosurecheck #1c, forsectioncollapsecheck (13d), mcpforparitycheck (5) ST, ownerhopcheck (S) explicit / (B2), forrankordercheck (3) page-row allowance, forsigspancheck ST + body windows, estchargecheck (ST helper, A7 twin, A7B, A7R, A7R-K, A7D re-anchor, #18 clause text), estcalib for-named/for-budgeted re-pins, route/anchor/docdemote goldens (only the tail, its clause and est_tokens moved). Co-Authored-By: Claude Opus 5.5 --- test/anchorcheck.sh | 2 + test/anchorfix/golden_for.xml | 2 +- test/deeptailcheck.sh | 122 ++++++++++++++++++++++--------- test/docdemotecheck.sh | 4 + test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- test/estcalib.manifest | 9 ++- test/estchargecheck.sh | 67 +++++++++++------ test/fordisclosurecheck.sh | 10 ++- test/forrankordercheck.sh | 6 +- test/forsectioncollapsecheck.sh | 20 ++--- test/forsigspancheck.sh | 18 ++++- test/forwidencheck.sh | 26 +++++-- test/mcpforparitycheck.sh | 12 ++- test/ownerhopcheck.sh | 21 +++++- test/routecheck.sh | 2 + test/routefix/golden_for.xml | 2 +- 17 files changed, 235 insertions(+), 92 deletions(-) diff --git a/test/anchorcheck.sh b/test/anchorcheck.sh index 834f5ca0d..fc128b200 100755 --- a/test/anchorcheck.sh +++ b/test/anchorcheck.sh @@ -124,6 +124,8 @@ QUERY="frobnicate widget cache" # ranking, body or anchor byte moved (anchor-neutrality, this golden's purpose, untouched). # L1 (2026-09-19): the CLI default legend is compact; this arm compares against a golden recorded from the full default, so it asks for it. "$BIN" anchorfix --no-cache --for="$QUERY" --no-route --legend=full >"$TMP/plain_full.xml" 2>/dev/null +# RE-PIN 2026-10-15 (forbudget-soft round C, FOLD C; rule 4): golden_for.xml re-captured — the ONLY bytes that moved are the +# (page rows with sym= after the rank rows; total= every positive-score file), the tail legend clause and est_tokens=. diff -q "$TMP/plain_full.xml" "$ROOT/test/anchorfix/golden_for.xml" >/dev/null \ && ok "golden-neutral: plain --for --no-route byte-identical to the pre---anchor golden" \ || no "plain --for --no-route drifted from test/anchorfix/golden_for.xml (--anchor leaked into the default lens)" diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index 09364161d..249b7639b 100644 --- a/test/anchorfix/golden_for.xml +++ b/test/anchorfix/golden_for.xml @@ -1,4 +1,4 @@ -frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit() now lists the RANK rows above (paths only, +# ``, the files outside the shown head, files of trimmed rows first) and THEN page 0 of `--for=TASK --limit=40` +# minus the files already listed, each with sym= (``). total= counts every positive-score file (the page's +# total=), so the old "total = files outside the head" denominator is a by-design break: the arms below read the rank rows +# (no sym=) where they used to read the whole tail, and pin the page rows against the page itself. +# # RED-FIRST: every d1/d2 assertion below FAILS against the prebuilt d8e257d baseline (which emits neither # surface); each fuses presence with content bytes so a bare-exit arm cannot pass green-while-inert. # @@ -52,24 +58,53 @@ Q="widget frobnicate helper" XML="$( "$BIN" "$CORPUS" --no-cache --for="$Q" --pack-top-n=4 --legend=full 2>/dev/null )" printf '%s' "$XML" > "$TMP/for.xml" -# ── 1) d2: the file-grain tail — present, complete, disjoint from the head ───────────────────────────── +# ── 1) d2: the file-grain tail — present, complete; its rank rows disjoint from the head ─────────────── +# a python reader of the tail: "rank" rows (no sym=) and "page" rows (p, sym), in order +cat > "$TMP/tail.py" <<'PY' +import re, sys, html +s = open( sys.argv[ 1 ] ).read() +m = re.search( r"]*>(.*?)", s, re.S ) +rows = re.findall( r'', m.group( 1 ) if m else "" ) +mode = sys.argv[ 2 ] +for p, has, sym in rows: + if mode == "rank" and not has: print( html.unescape( p ) ) + if mode == "page" and has: print( html.unescape( p ) + "\t" + html.unescape( sym ) ) + if mode == "all": print( html.unescape( p ) + "\t" + html.unescape( sym ) ) +PY +# the page rows a tail must carry: page 0 of the file page, minus the rank rows, in page order +pagerows(){ "$BIN" "$CORPUS" --no-cache --for="$Q" --limit=40 2>/dev/null | python3 -c ' +import re, sys, html +rank = set( open( sys.argv[ 1 ] ).read().split( "\n" ) ) +for p, sym in re.findall( r"]*? sym=\"([^\"]*)\"", sys.stdin.read() ): + p = html.unescape( p ) + if p not in rank: print( p + "\t" + html.unescape( sym ) ) +' "$1"; } TAILTAG="$( printf '%s' "$XML" | grep -o ']*>' | head -1 )" case "$TAILTAG" in - *'total="11"'*'shown="11"'*'capped="0"'*) ok "tail element carries the exact split: $TAILTAG" ;; + *'total="12"'*'shown="12"'*'capped="0"'*) ok "tail element: every positive-score file (12), all shown: $TAILTAG" ;; '') no "no element in the --for bundle (d2 missing)" ;; - *) no "tail element has wrong counts: $TAILTAG (expected total=11 shown=11 capped=0)" ;; + *) no "tail element has wrong counts: $TAILTAG (expected total=12 shown=12 capped=0)" ;; esac -TROWS="$( printf '%s' "$XML" | grep -o '' | wc -l | tr -d ' ' )" -if [ "$TROWS" = "11" ]; then ok "tail serves 11 rows, no wrapper) -DUP="$( printf '%s' "$XML" | python3 -c ' +python3 "$TMP/tail.py" "$TMP/for.xml" rank > "$TMP/rank1.txt" +TROWS="$( grep -c . "$TMP/rank1.txt" )" +if [ "$TROWS" = "11" ]; then ok "tail serves 11 rank rows ( without sym=): the files outside the one-file head"; else no "tail rank rows: $TROWS (expected 11)"; fi +# disjoint: no rank row may also be a head row's p= (P7: the head is flat rows, no wrapper) +DUP="$( python3 - "$TMP/for.xml" "$TMP/rank1.txt" <<'PY' import re, sys -s = sys.stdin.read() -head = set( re.findall( r"]*?\bp=\"([^\"]*)\"", s ) ) -tails = re.findall( r"", s ) -print( sum( 1 for t in tails if t in head ) ) -' )" -if [ "$DUP" = "0" ]; then ok "tail files are disjoint from the head's files"; else no "$DUP tail file(s) duplicate a head file"; fi +s = open( sys.argv[ 1 ] ).read() +head = set( re.findall( r"]*?\bp=\"([^\"]*)\"", s ) ) +print( sum( 1 for t in open( sys.argv[ 2 ] ).read().split() if t in head ) ) +PY +)" +if [ "$DUP" = "0" ]; then ok "tail rank rows are disjoint from the head's files"; else no "$DUP tail rank row(s) duplicate a head file"; fi +# FOLD C: the page rows ARE the file page's first 40 rows not listed above, in page order, sym= included +python3 "$TMP/tail.py" "$TMP/for.xml" page > "$TMP/page1.txt" +pagerows "$TMP/rank1.txt" > "$TMP/page1_want.txt" +if [ -s "$TMP/page1_want.txt" ] && cmp -s "$TMP/page1.txt" "$TMP/page1_want.txt"; then + ok "FOLD C: the tail's page rows are the --limit=40 page's rows not listed above ($( grep -c . "$TMP/page1.txt" ), sym= equal)" +else + no "FOLD C: tail page rows differ from the file page's ($( grep -c . "$TMP/page1.txt" ) vs $( grep -c . "$TMP/page1_want.txt" ))" +fi # ── 2) d1: r= on every ranked row, exactly the ranks 1..K ────────────────────────────────────────────── RSEQ="$( printf '%s' "$XML" | grep -o '", xml ) +import html +xrows = [ ( html.unescape( p ), html.unescape( y ) ) for p, _, y in re.findall( r"", xml ) ] +xrows = [ ( p, y ) for p, _h, y in [ ( r[0], None, r[1] ) for r in xrows ] ] checks = [ bool( rs ) and rs == list( range( 1, len( rs ) + 1 ) ), - tail is not None and tail.get( "total" ) == 4 and tail.get( "shown" ) == 4, - tail is not None and tail.get( "files" ) == xtails, + tail is not None and tail.get( "total" ) == 12 and tail.get( "shown" ) == 12 + and [ f for f, y in zip( tail.get( "files", [] ), tail.get( "sym", [] ) ) if not y ] == [ "w09.py", "w10.py", "w11.py", "w12.py" ], + tail is not None and list( zip( tail.get( "files", [] ), tail.get( "sym", [] ) ) ) == xrows, ] print( " ".join( "Y" if c else "N" for c in checks ) ) EOF )" case "$JV" in - "Y Y Y") ok "JSON: r keys are contiguous 1..K; tail total/shown=4/4; files match the XML tail exactly" ;; + "Y Y Y") ok "JSON: r keys are contiguous 1..K; tail total/shown=12/12, rank rows w09..w12; files+sym match the XML tail exactly" ;; *) no "JSON dialect wrong (r-seq / tail-counts / xml-parity = $JV)" ;; esac @@ -121,28 +159,31 @@ if [ "$PTJ" = "Y" ]; then ok "pack-task JSON ranking rows carry r"; else no "pac TB="$( "$BIN" "$CORPUS" --no-cache --for="$Q" --pack-top-n=4 --token-budget=220 2>/dev/null )" TBTAG="$( printf '%s' "$TB" | grep -o ']*>' | head -1 )" case "$TBTAG" in - *'total="11"'*) ok "tight --token-budget keeps the tail disclosure: $TBTAG" ;; + *'total="12"'*) ok "tight --token-budget keeps the tail disclosure: $TBTAG" ;; '') no "tight --token-budget dropped the element entirely (disclosure lost)" ;; *) no "tight-budget tail lost its denominator: $TBTAG" ;; esac TBSHOWN="$( printf '%s' "$TBTAG" | grep -o 'shown="[0-9]*"' | grep -o '[0-9]*' )" TBCAP="$( printf '%s' "$TBTAG" | grep -o 'capped="[0-9]"' | grep -o '[0-9]' )" -if [ -n "$TBSHOWN" ] && [ "$TBSHOWN" -lt 11 ] && [ "$TBCAP" = "1" ]; then - ok "tight budget trimmed tail rows (shown=$TBSHOWN) and disclosed capped=1" +if [ -n "$TBSHOWN" ] && [ "$TBSHOWN" -lt 12 ] && [ "$TBCAP" = "1" ] && printf '%s' "$TBTAG" | grep -q ' next="'; then + ok "tight budget trimmed tail rows (shown=$TBSHOWN), disclosed capped=1 and named next=" else no "tight budget did not trim/disclose honestly (shown=$TBSHOWN capped=$TBCAP)" fi if printf '%s' "$TB" | xmllint --noout - 2>/dev/null; then ok "tight-budget bundle stays well-formed"; else no "tight-budget bundle fails xmllint"; fi -# ── 7) zero-tail honesty: a head that covers every file still emits the element (0 = none remain) ────── +# ── 7) zero-tail honesty: a head that covers every file leaves NO rank row (0 = none remain); the page rows still +# list every positive-score file (FOLD C), so total= = the 3 files and nothing is cut ───────────────────────── MINI="$TMP/mini" mkdir -p "$MINI" for i in 1 2 3; do printf 'def widget_frobnicate_%s():\n """widget frobnicate helper %s"""\n return 1\n' "$i" "$i" > "$MINI/m$i.py" done -Z="$( "$BIN" "$MINI" --no-cache --for="$Q" 2>/dev/null | grep -o ']*>' | head -1 )" +"$BIN" "$MINI" --no-cache --for="$Q" 2>/dev/null > "$TMP/mini.xml" +Z="$( grep -o ']*>' "$TMP/mini.xml" | head -1 )" +ZR="$( python3 "$TMP/tail.py" "$TMP/mini.xml" rank | grep -c . )" case "$Z" in - *'total="0"'*'shown="0"'*'capped="0"'*) ok "all-files-in-head bundle still emits (none remain, said out loud)" ;; + *'total="3"'*'shown="3"'*'capped="0"'*) [ "$ZR" = "0" ] && ok "all-files-in-head bundle: 0 rank rows (none remain), the 3 files as page rows, nothing cut ($Z)" || no "all-files-in-head bundle lists $ZR rank row(s) — a head file served as 'outside the head'" ;; *) no "zero-tail bundle wrong/missing: '$Z'" ;; esac @@ -159,11 +200,17 @@ T9_TAG="$( printf '%s' "$T9" | grep -o ']*>' | head -1 )" T9_TOTAL="$( printf '%s' "$T9_TAG" | grep -oE 'total="[0-9]+"' | grep -oE '[0-9]+' )" case "$T9_SIGS" in *'capped="1"'*) ok "(9) fixture guard: the 900-token budget trims sigs rows ($T9_SIGS; $T9_NSHOWN file(s) shown)" ;; *) no "(9) fixture guard: expected the ladder to trim at --token-budget=900, got '$T9_SIGS'" ;; esac -[ "$T9_TOTAL" = "$(( 12 - T9_NSHOWN ))" ] \ - && ok "(9a) tail total=$T9_TOTAL = 12 files minus the $T9_NSHOWN shown (trimmed surface rows are IN the tail)" \ - || no "(9a) tail total=$T9_TOTAL but $T9_NSHOWN file(s) are shown — $(( 12 - T9_NSHOWN - ${T9_TOTAL:-0} )) ranked file(s) served nowhere ($T9_TAG)" +printf '%s' "$T9" > "$TMP/t9.xml" +T9_RANK="$( python3 "$TMP/tail.py" "$TMP/t9.xml" rank | grep -c . )" +T9_SHOWNROWS="$( printf '%s' "$T9_TAG" | grep -oE 'shown="[0-9]+"' | grep -oE '[0-9]+' )" +# every file outside the shown rows is a rank row (12 - shown files), or the cut tail stops short of them and says so (next=) +if [ "$T9_TOTAL" = "12" ] && { [ "$T9_RANK" = "$(( 12 - T9_NSHOWN ))" ] || { [ "$T9_SHOWNROWS" -lt "$(( 12 - T9_NSHOWN ))" ] && printf '%s' "$T9_TAG" | grep -q ' next="'; }; }; then + ok "(9a) tail total=12; rank rows $T9_RANK = 12 files minus the $T9_NSHOWN shown, or a cut tail naming next= (trimmed rows land IN the tail)" +else + no "(9a) tail total=$T9_TOTAL, $T9_RANK rank row(s) but $T9_NSHOWN file(s) shown — a ranked file served nowhere ($T9_TAG)" +fi T9_EXPECT_FIRST="$( for i in 01 02 03 04 05 06 07 08 09 10 11 12; do printf '%s\n' "w$i.py"; done | grep -vxF -f <( printf '%s\n' "$T9_SHOWN_FILES" ) | head -1 )" -T9_FIRST="$( printf '%s' "$T9" | grep -o '/dev/null )" -T9J_TOTAL="$( printf '%s' "$T9J" | grep -o '"tail":{"total":[0-9]*' | grep -oE '[0-9]+$' )" -T9J_NSHOWN="$( printf '%s' "$T9J" | grep -o '"p":"w[0-9]*\.py"' | sort -u | grep -c . )" # sigs rows carry "p":; the tail carries "files":[] -[ -n "$T9J_TOTAL" ] && [ "$T9J_TOTAL" = "$(( 12 - T9J_NSHOWN ))" ] \ - && ok "(9c) JSON twin: tail total=$T9J_TOTAL = 12 minus its own $T9J_NSHOWN shown file(s)" \ - || no "(9c) JSON twin: tail total='$T9J_TOTAL' but its sigs show $T9J_NSHOWN file(s)" +T9JV="$( printf '%s' "$T9J" | python3 -c ' +import json, sys +d = json.load( sys.stdin ); t = d.get( "tail", {} ) +shown = { s.get( "p" ) for s in d.get( "sigs", [] ) } +rank = [ f for f, y in zip( t.get( "files", [] ), t.get( "sym", [] ) ) if not y ] +cut_ok = t.get( "shown", 0 ) < 12 - len( shown ) and "next" in t +print( "%s %d %d %d" % ( "Y" if t.get( "total" ) == 12 and ( len( rank ) == 12 - len( shown ) or cut_ok ) and not ( set( rank ) & shown ) else "N", + t.get( "total", -1 ), len( rank ), len( shown ) ) )' )" +case "$T9JV" in + Y*) ok "(9c) JSON twin: tail total=12, rank rows = 12 minus its own shown files, none a shown file ($T9JV)" ;; + *) no "(9c) JSON twin: tail wrong (ok total rank shown = $T9JV)" ;; +esac # ── 8) the legend defines both new surfaces where the reader meets them ──────────────────────────────── HDR="$( printf '%s' "$XML" | sed 's/-->.*//' )" -if printf '%s' "$HDR" | grep -q 'tail: file-grain tail'; then ok "legend defines the tail (file-grain, weaker evidence)"; else no "legend does not define the tail"; fi +if printf '%s' "$HDR" | grep -q 'tail: file-grain rows, WEAKER evidence' && printf '%s' "$HDR" | grep -q 'sym= its top symbols'; then ok "legend defines the tail (file-grain, weaker evidence; rank rows, then page rows with sym=)"; else no "legend does not define the tail"; fi if printf '%s' "$HDR" | grep -q 'r= on a ranked row'; then ok "legend defines r= (true ranker order)"; else no "legend does not define r="; fi # ── 9) determinism + well-formedness ─────────────────────────────────────────────────────────────────── @@ -204,7 +258,7 @@ import json, sys r = json.load( sys.stdin ) print( "" if "error" in r else r["result"]["content"][0]["text"] ) ' )" -if printf '%s' "$MCP_INNER" | grep -q ' or no page rows"; fi if printf '%s' "$MCP_INNER" | grep -q '"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): docdemotegolden_for.xml 5839 -> 6529 B (est_tokens "2340" -> +# "2616") and docdemotegolden_noroute.xml 9350 -> 10039 B ("3309" -> "3584"). Only the (page 0 of the file page after the +# rank rows, sym= on its rows, total= every positive-score file), the tail legend clause and est_tokens= moved (checked byte-equal +# with those three stripped). # RE-PIN 2026-10-08 (train 26b: lean-answers merged): docdemotegolden_for.xml 6167 -> 5851 B (est_tokens "2467" -> "2340") # and docdemotegolden_noroute.xml 9695 -> 9364 B ("3441" -> "3309"). Only the zero elision and its legend reading moved: # undoing it (zeros back, reading removed, est_tokens=/at= masked) gives the train-side goldens exactly, on both fixtures, diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 132e16a1f..44e642ef9 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/docdemotegolden_noroute.xml b/test/docdemotegolden_noroute.xml index e2216ec48..b24048a9c 100644 --- a/test/docdemotegolden_noroute.xml +++ b/test/docdemotegolden_noroute.xml @@ -1,4 +1,4 @@ -# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ): carries page 0 of the file page; +# 962->1031 / 898->967 o200k, measured with pin.py; the twins still differ by exactly the e= rows, 64 o200k). +for-named 1031 1026 1392 --for=billableTotal +for-named-noe 967 961 1298 RIPWIRE_FOR_ENDLINES=never --for=billableTotal # for-budgeted re-pinned 2026-10-14 (forbudget-soft: an explicit budget is a soft row target — the header is no longer shed to fit, measured with pin.py; other rows kept). -for-budgeted 855 849 1201 --for=dedupe --token-budget=400 +# …and 2026-10-15 (round C: reserve sections first — the tail keeps its share and carries page rows with sym=; 855->982 o200k). +for-budgeted 982 974 1376 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 4d2fcbc3a..9aafe7918 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -682,9 +682,20 @@ def verdict( path, n, refpath=None ): rr = ref[ 2 ] if k >= len( rr ): return fits, "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) - maximal = ab + rr[ k ][ 1 ] > T - 160 - return fits and maximal, "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d" % ( - "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T ) + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the target cut fill what they leave of T, so maximality is judged against T less their bytes (U: all + # after but , and the rows — less the --detail bodies, the caller's own request under its own ceiling: + # on a run without bundle= on the root the element is the --detail section) + s = open( path, encoding='utf-8', errors='replace' ).read() + U = ( len( s.split( '', 1 )[ 1 ].encode() ) - len( '' ) ) if '' in s else 0 + U += sum( len( h.encode() ) for h in re.findall( r']*/>', s.split( ']*>', s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r'', s, re.S ) + U -= len( bm.group( 0 ).encode() ) if bm else 0 + maximal = ab + rr[ k ][ 1 ] > T - U - 160 + return fits and maximal, "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d U=%d" % ( + "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T, U ) if __name__ == "__main__": good, msg = verdict( sys.argv[ 1 ], sys.argv[ 2 ], sys.argv[ 3 ] if len( sys.argv ) > 3 else None ) print( msg ); sys.exit( 0 if good else 1 ) @@ -720,10 +731,14 @@ for tw in "2000:--detail=20:f_tbp:f_for_detail" "2000:--with-graph:f_tbp:f_for_g tw_why="" { [ "$rc_tw" -eq 0 ] && [ -n "$tw_root" ]; } || tw_why="$tw_why exit-$rc_tw-or-no-root" [ -n "$tw_root" ] && [ "${tw_root#* over_ceiling=}" = "$tw_root" ] || tw_why="$tw_why over_ceiling-on-a-soft-target" - rows_prefix "$TMP/tw_bare.rows" "$TMP/tw.rows" || tw_why="$tw_why rows:$( wc -l < "$TMP/tw.rows" | tr -d ' ' )-lost-a-bare-row(bare $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' ))" + # round C (rule 4): SEC ("the trailing flag never removes a row") is the by-design break — a trailing section is + # reserved before the rows past the cut — so the rows clause is RP: the shape's rows are a rank prefix of its own + # no-budget answer's rows (the cut and ST above/below keep the rest) + soft_rows "$TMP/$tw_ref.out" >"$TMP/tw_ref.rows" 2>/dev/null + rows_prefix "$TMP/tw.rows" "$TMP/tw_ref.rows" || tw_why="$tw_why rows:$( wc -l < "$TMP/tw.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" [ "$tw_st_rc" -eq 0 ] || tw_why="$tw_why ST:[$tw_st]" [ -z "$tw_why" ] \ - && ok "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args: exit 0, no over_ceiling=, keeps all $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' ) bare rows ($tw_st)" \ + && ok "#11 A7 twin (forbudget-soft, round C) --for --token-budget=$tw_n $tw_args: exit 0, no over_ceiling=, $( wc -l < "$TMP/tw.rows" | tr -d ' ' ) rows a rank prefix of the no-budget answer (bare: $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' )) ($tw_st)" \ || no "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args —$tw_why" done # A7B — REPLACED (forbudget-soft (rule 4)). Was the owner-option-B contract for both flags OVER the budget: a premise past the @@ -738,7 +753,8 @@ soft_rows "$TMP/f_tb.out" >"$TMP/a7b.rows" 2>/dev/null; soft_rows "$TMP/f_tbp.ou a7b_n="$( wc -l < "$TMP/a7b.rows" | tr -d ' ' )"; a7b_pn="$( wc -l < "$TMP/a7b.plain" | tr -d ' ' )" a7b_why="" { [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ]; } || a7b_why="$a7b_why exit-$rc_ftb-or-no-root" -rows_prefix "$TMP/a7b.plain" "$TMP/a7b.rows" || a7b_why="$a7b_why rows:$a7b_n-lost-a-flagless-row($a7b_pn)" +# round C (rule 4): "keeps every flagless row" is the by-design break (the graph block and the first body are reserved before the +# rows past the cut); the uncapped answer's rank-prefix clause below is the rows invariant rows_prefix "$TMP/a7b.rows" "$TMP/a7b.unc" || a7b_why="$a7b_why rows:not-the-uncapped-answer's-rank-prefix" [ -n "$a7b_root" ] && [ "${a7b_root#* over_ceiling=}" = "$a7b_root" ] || a7b_why="$a7b_why over_ceiling-on-a-soft-target" { grep -aq ']*>"$A7R/n0.rows" 2>/dev/null + [ -s "$A7R/o.rows" ] && rows_prefix "$A7R/o.rows" "$A7R/n0.rows" \ + || r_why="$r_why rows:$( wc -l < "$A7R/o.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" r_st="$( soft_st "$A7R/o.xml" "$N" "$A7R/n0.xml" )" || r_why="$r_why ST:[$r_st]" [ "${r_root#* over_ceiling=}" = "$r_root" ] || r_why="$r_why over_ceiling" ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" @@ -1028,7 +1046,7 @@ for N in 1500 2000 2500 3000; do done done [ "$a7r_runs" -eq 8 ] && [ -z "$a7r_bad" ] \ - && ok "#11 A7R no reserve (forbudget-soft): 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) serve exactly the bare run's rows, fit T, unlabelled, first body served (cut ones carry next=)" \ + && ok "#11 A7R (forbudget-soft, round C): 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) serve a rank prefix of the no-budget rows, ST against T less the sections, unlabelled, first body served (cut ones carry next=)" \ || no "#11 A7R no reserve (forbudget-soft): a trailing section changed the rows or the soft target broke —$a7r_bad" # A7R-K — REPLACED in part (forbudget-soft (rule 4)). Was the pre-registered kill (reports/lego-charge.md fix round 4): with BOTH # --detail and --with-graph the reserve was off, so the rows were never FEWER than the --with-graph run's; a delivery past @@ -1043,7 +1061,8 @@ for N in 2000 2500 3000; do k_b1="$( grep -aoE ']*>' "$A7R/k.xml" | head -1 )" k_why="" [ "$k_rc" -eq 0 ] && [ -n "$k_root" ] || k_why="exit $k_rc / no root" - [ -s "$A7R/kb.rows" ] && cmp -s "$A7R/kb.rows" "$A7R/k.rows" || k_why="$k_why rows $( wc -l < "$A7R/k.rows" | tr -d ' ' )-vs-bare-$( wc -l < "$A7R/kb.rows" | tr -d ' ' )" + soft_rows "$A7R/n0.xml" >"$A7R/n0.rows" 2>/dev/null # round C (rule 4): RP instead of SEC, as A7R-1 + [ -s "$A7R/k.rows" ] && rows_prefix "$A7R/k.rows" "$A7R/n0.rows" || k_why="$k_why rows $( wc -l < "$A7R/k.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" k_st="$( soft_st "$A7R/k.xml" "$N" "$A7R/n0.xml" )" || k_why="$k_why ST:[$k_st]" [ "${k_root#* over_ceiling=}" = "$k_root" ] || k_why="$k_why over_ceiling" [ -n "$k_b1" ] && [ "${k_b1#* over_ceiling=}" = "$k_b1" ] || k_why="$k_why first-body-stub" @@ -1051,7 +1070,7 @@ for N in 2000 2500 3000; do [ -z "$k_why" ] || a7k_bad="$a7k_bad @$N:[${k_why# }]" done [ -z "$a7k_bad" ] \ - && ok "#11 A7R both sections (forbudget-soft): --detail=20 --with-graph at 2000..3000 serves exactly the bare rows, fits T, unlabelled, first body at its floor" \ + && ok "#11 A7R both sections (forbudget-soft, round C): --detail=20 --with-graph at 2000..3000 serves a rank prefix of the no-budget rows, ST, unlabelled, first body at its floor" \ || no "#11 A7R both sections (forbudget-soft): —$a7k_bad" # A7R-K2 — REPLACED in part (forbudget-soft (rule 4)). Was the A7B contract on this corpus: past the allowance with # over_ceiling="1" and the kept-rows reason (both gone with the ceiling), well-formed despite the note, and the head-cut first @@ -1129,7 +1148,9 @@ w0="$( a7r_sigs "$A7R/w0.xml" )"; w1="$( a7r_sigs "$A7R/w1.xml" )" # A7D-1: at 1500 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with no # budget — its legend clause rides the header, no over_ceiling=, and the next= RECOVERS the rows: that call (the default # answer) serves every one of the same total= hop rows. -a7r_out --token-budget=1500 >"$A7R/h.xml" +# round C (reserve first; rule 4): the hops are funded from the rows at the cut, so 1500 now serves the whole set — re-anchored +# to 600 (the marker) and 800 (the partial cut), measured on the round C build: 600 shown=0, 800 shown=3 of 6, 900+ whole +a7r_out --token-budget=600 >"$A7R/h.xml" h_tag="$( grep -aoE ']*>' "$A7R/h.xml" | head -1 )" h_root="$( ctx_root "$A7R/h.xml" )" h_tot="$( printf '%s' "$h_tag" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" @@ -1138,26 +1159,26 @@ hd_shown="$( printf '%s' "$hd_tag" | sed -nE 's/" ] && [ "${h_tot:-0}" -ge 1 ] 2>/dev/null \ && grep -aqF '; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)' "$A7R/h.xml" && [ "${h_root#* over_ceiling=}" = "$h_root" ] \ && [ -n "$h_root" ] && [ -n "$hd_shown" ] && [ "$hd_shown" = "$h_tot" ] && [ "$hd_tot" = "$h_tot" ]; } \ - && ok "#11 A7D hops: the spent-target at 1500 is the counted marker ($h_tag), defined, unlabelled; its next= serves all $hd_shown of $hd_tot rows" \ - || no "#11 A7D hops: spent-target at 1500 not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" + && ok "#11 A7D hops: the spent-target at 600 is the counted marker ($h_tag), defined, unlabelled; its next= serves all $hd_shown of $hd_tot rows" \ + || no "#11 A7D hops: spent-target at 600 not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" # A7D-1 (train 27): the compact dialect above defines the marker in its own short clause — it already reads total= shown= # capped=1 and next= in its un-droppable clauses (verbs_for.h kForHopsBudgetCutLegendCompact). The FULL dialect keeps the # long clause that defines both: the same marker, the same next=, with shown=0 capped=1 and next= read. -a7r_out --token-budget=1500 --legend=full >"$A7R/hf.xml" +a7r_out --token-budget=600 --legend=full >"$A7R/hf.xml" hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( ctx_root "$A7R/hf.xml" )" { [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] && [ -n "$hf_root" ] \ && grep -aqF '; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them' "$A7R/hf.xml" \ && [ "${hf_root#* over_ceiling=}" = "$hf_root" ] && ! grep -aqF '; hops: this budget' "$A7R/hf.xml"; } \ && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), unlabelled" \ || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause is missing or the root is labelled" -# A7D-2: a PARTIAL hops cut under the target carries the same next= (2000 tokens: some rows fit, not all). -a7r_out --token-budget=2000 >"$A7R/hp.xml" +# A7D-2: a PARTIAL hops cut under the target carries the same next= (800 tokens since round C: some rows fit, not all). +a7r_out --token-budget=800 >"$A7R/hp.xml" hp_tag="$( grep -aoE ']*>' "$A7R/hp.xml" | head -1 )" hp_sh="$( printf '%s' "$hp_tag" | sed -nE 's//dev/null \ - && ok "#11 A7D partial hops cut at 2000 tokens carries next= ($hp_tag)" \ - || no "#11 A7D partial hops cut at 2000 tokens: not a partial cut with next= (got '${hp_tag:-none}')" + && ok "#11 A7D partial hops cut at 800 tokens carries next= ($hp_tag)" \ + || no "#11 A7D partial hops cut at 800 tokens: not a partial cut with next= (got '${hp_tag:-none}')" # A7D-W (forbudget-soft, replaces the old 5000-token partial cut): a target wide enough for the rows AND the hop rows serves the # whole hop set, like the default answer — stop when complete, nothing left to continue (no capped=, so no next= owed). a7r_out --token-budget=5000 >"$A7R/hw.xml" @@ -2234,7 +2255,7 @@ rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" RZ_CLAUSE_ROUTE='route= name-exact(X)|subtoken+body' RZ_CLAUSES=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) -grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) if [ -z "$RZ_E" ] || [ "$RZ_WIDE_NOTE" != "0" ] || [ "$RZ_CLAUSES" != "3" ]; then no "#18 rung zero: the wide control (--token-budget=$RZ_WIDE) does not carry its legend (est='${RZ_E:-unreadable}' dropped-note=$RZ_WIDE_NOTE clauses=$RZ_CLAUSES/3) — there is no price to probe against; re-anchor the fixture" @@ -2246,7 +2267,7 @@ else rz_run "$RZ_PROBE"; RZ_PE="$( rz_est )"; RZ_PN="$( rz_note )"; RZ_PB="$( bytes_of "$RZ/o.xml" )" RZ_PC=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) # the ROOT's verdict only (read off the open tag): since lane/cutfix-bodies a marks # a --detail body whose first line alone exceeds the BODY allowance, which is not the root's est_tokens claim, @@ -2269,7 +2290,7 @@ else rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" RZ_CC=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) RZ_CO=0; grep -aoE '^]*>' "$RZ/o.xml" | grep -qF 'over_ceiling=' && RZ_CO=1 { [ -n "$RZ_CE" ] && [ "$RZ_CN" = "0" ] && [ "$RZ_CC" = "3" ] && [ "$RZ_CO" = "0" ]; } \ diff --git a/test/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index f0f9d8048..bab6faef8 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -128,9 +128,13 @@ cd_tail="$( grep -oE ']*>' "$TMP/cdef" | head -1 )" cd_ttot="$( printf '%s' "$cd_tail" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" if ! printf '%s' "$cd_tail" | grep -q ' capped="1"'; then no "#1c default regime premise: the default is not capped ('$cd_tail') — this arm measures nothing; re-anchor it on a capped tail" -elif printf '%s' "$cd_tail" | grep -qE "capped=\"1\" next=\"--for='$CONCTASK' --limit=40\">\$" && [ "$cp_rc" -eq 0 ] \ - && [ -n "$cp_tot" ] && [ -n "$cd_ttot" ] && [ "$cp_tot" -ge "$cd_ttot" ] 2>/dev/null; then - ok "#1c default regime: the capped carries the file-page next= (D-4), which pages $cp_tot files >= the tail's $cd_ttot" +# FOLD C (forbudget-soft round C; rule 4): the default tail carries page 0 of the file page, so its next= is the page FROM ITS +# FIRST UNPRINTED PAGE ROW (`--limit=40 --offset=K`); that call must answer with a page whose rows start at K. +elif cd_off="$( printf '%s' "$cd_tail" | sed -nE "s/.*capped=\"1\" next=\"--for='$CONCTASK' --limit=40( --offset=([0-9]+))?\">\$/\2/p" )"; \ + printf '%s' "$cd_tail" | grep -qE "capped=\"1\" next=\"--for='$CONCTASK' --limit=40( --offset=[0-9]+)?\">\$" && [ "$cp_rc" -eq 0 ] \ + && [ -n "$cp_tot" ] && [ -n "$cd_ttot" ] && [ "$cp_tot" -ge "$cd_ttot" ] 2>/dev/null \ + && "$BIN" src --for="$CONCTASK" --limit=40 ${cd_off:+--offset=$cd_off} --no-cache 2>/dev/null | grep -qE "^]* offset=\"${cd_off:-0}\""; then + ok "#1c default regime: the capped carries the file-page next= (D-4; offset=${cd_off:-0}, FOLD C) that answers; the page holds $cp_tot files >= the tail's $cd_ttot" else no "#1c default regime: the capped has no working file-page next= ('$cd_tail', page rc=$cp_rc total='${cp_tot:-none}')" fi diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index a3b6556b2..4e2f470aa 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -190,7 +190,8 @@ for q in "${REPO_Q[@]}"; do # forbudget-soft (stop when complete; PROCESS rule 5's stair-step target): rows served past the 7.5 KB target are disclosed # (: the last K rows) and are what the answer needs, so the band applies to the answer WITHOUT them — # every byte over 4% must be a disclosed past-target row, or a row past the old 1,000 B allowance (decision D-3: - # the default serves every hop row), never anything else + # the default serves every hop row), or — forbudget-soft round C, FOLD C (owner decision 2026-10-14) — a tail PAGE row + # (``: page 0 of the --for file page now rides the answer), never anything else past="$( printf '%s' "$out" | python3 -c ' import re, sys s = sys.stdin.read(); m = re.search( r"]*)>(.*?)", s, re.S ) @@ -199,7 +200,8 @@ rows = sorted( ( int( re.search( r"\br=\"([0-9]+)\"", d.group( 1 ) ).group( 1 ) for d in re.finditer( r"]*)>.*?", m.group( 2 ) if m else "", re.S ) ) past = sum( b for _, b in rows[ len( rows ) - int( k.group( 1 ) ): ] ) if k else 0 h = re.search( r"", s, re.S ) # D-3: the compact is no longer cut at its 1,000 B allowance -print( past + max( 0, len( h.group( 0 ).encode() ) - 1000 ) if h else past )' )" +page = sum( len( t.encode() ) for t in re.findall( r"", s ) ) # FOLD C: the tail page rows +print( past + page + ( max( 0, len( h.group( 0 ).encode() ) - 1000 ) if h else 0 ) )' )" pct="$( python3 -c "print( '%+.2f' % ( ( $now - $past - $base ) * 100.0 / $base ) )" )" printf ' q%-2d %-28s %5d → %5d - past-target/hops %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$past" "$pct" "$marker" if [ $(( now - past )) -gt $(( base * 104 / 100 )) ]; then diff --git a/test/forsectioncollapsecheck.sh b/test/forsectioncollapsecheck.sh index 4d23bc60e..943ee0439 100644 --- a/test/forsectioncollapsecheck.sh +++ b/test/forsectioncollapsecheck.sh @@ -407,8 +407,8 @@ lc_est(){ grep -o 'est_tokens="[0-9]*"' "$1" | head -1 | grep -o '[0-9][0-9]*'; lc_sigs(){ grep -o '' "$1"; } "$BIN" test/legochargefix --no-cache --for="$LCQ" >"$TMP/lc_def.xml" 2>/dev/null; rc_def=$? "$BIN" test/legochargefix --no-cache --for="$LCQ" --sections=lego,compose >"$TMP/lc_rest.xml" 2>/dev/null; rc_rest=$? -"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=3000 >"$TMP/lc_tb.xml" 2>/dev/null; rc_tb=$? -"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=3000 --sections=lego,compose >"$TMP/lc_tbrest.xml" 2>/dev/null; rc_tbrest=$? +"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=6000 >"$TMP/lc_tb.xml" 2>/dev/null; rc_tb=$? +"$BIN" test/legochargefix --no-cache --for="$LCQ" --token-budget=6000 --sections=lego,compose >"$TMP/lc_tbrest.xml" 2>/dev/null; rc_tbrest=$? LC_DEF="$( lc_shown "$TMP/lc_def.xml" )"; LC_REST="$( lc_shown "$TMP/lc_rest.xml" )" LC_TB="$( lc_shown "$TMP/lc_tb.xml" )"; LC_TBREST="$( lc_shown "$TMP/lc_tbrest.xml" )"; LC_TBEST="$( lc_est "$TMP/lc_tbrest.xml" )" if [ "$rc_def$rc_rest$rc_tb$rc_tbrest" != "0000" ] || ! [[ "$LC_DEF" =~ ^[0-9]+$ && "$LC_REST" =~ ^[0-9]+$ && "$LC_TB" =~ ^[0-9]+$ && "$LC_TBREST" =~ ^[0-9]+$ && "$LC_TBEST" =~ ^[0-9]+$ ]]; then @@ -427,13 +427,15 @@ else [[ "$LC_STUBTOTAL" =~ ^[0-9]+$ ]] && [ "$LC_IFACES" -eq "$(( LC_STUBTOTAL < 12 ? LC_STUBTOTAL : 12 ))" ] \ && ok "(13c) the stub's total=$LC_STUBTOTAL matches the restored ($LC_IFACES rows, display cap 12)" \ || no "(13c) the stub's total=\"$LC_STUBTOTAL\" vs $LC_IFACES restored rows (display cap 12)" - # (13d) forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget is a SOFT target charged against - # the rows only, so a section --sections= opts into is NOT charged to the rows any more — it rides on top (priced in - # est_tokens=) and costs no row (SEC); nothing is labelled over_ceiling. The old near-miss (the opted-in charged in - # full, shrinking) is the by-design break; what must hold now: the same rows with and without the opt-in. - [ "$LC_TBREST" -eq "$LC_TB" ] && ! grep -q 'over_ceiling="1"' "$TMP/lc_tbrest.xml" && grep -q ' costs no row ($LC_TBREST == $LC_TB), no over_ceiling (est_tokens=$LC_TBEST)" \ - || no "(13d) --token-budget=3000 --sections=lego,compose: $LC_TBREST vs $LC_TB without it (or over_ceiling, or no full ) — an opted-in section must not cost rows under a soft target" + # (13d) forbudget-soft (owner decisions 2026-10-14; rule 4): an explicit --token-budget is a SOFT target. Round C (RESERVE + # SECTIONS FIRST, owner decision 2026-10-14): the trailing sections are served first and the rows past the target cut fill + # what they leave of T — so a section --sections= opts into rides WHOLE and its bytes come out of the rows past the cut, + # never out of the section. By-design break of round B's "the same rows with and without the opt-in" (sections were funded + # from what the rows left): what holds now is the full , no over_ceiling, and rows that do not GROW with the opt-in. + # (Re-anchored from 3000 to 6000 tokens: at 3000 the rows at the cut name no implementing file, so is empty either way.) + [ "$LC_TBREST" -le "$LC_TB" ] && ! grep -q 'over_ceiling="1"' "$TMP/lc_tbrest.xml" && grep -q ' rides whole, reserved before the rows past the cut ($LC_TBREST <= $LC_TB rows), no over_ceiling (est_tokens=$LC_TBEST)" \ + || no "(13d) --token-budget=6000 --sections=lego,compose: $LC_TBREST vs $LC_TB without it (or over_ceiling, or no full ) — an opted-in section is reserved first under a soft target" fi if command -v python3 >/dev/null 2>&1; then lc_mcp(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"test/legochargefix","task":"%s"%s}}}\n' "$LCQ" "$1" \ diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 68aaeafad..72b213dd7 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -65,7 +65,8 @@ # --token-budget=8000 still reorder and carry docs_after_code= with its reading, XML, JSON and MCP; a question that # names docs keeps score order at 1200 too. # A BODY ceiling (--max-tokens=N --detail=K, XML only: --json refuses the pair, MCP `for` has no max_tokens) is the third tight -# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1600 and the Python+docs fixture (pydocs) at 1660 +# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1700 and the Python+docs fixture (pydocs) at 1760 +# (round C re-anchor: 1600 / 1660 before the tail carried the file page) # (RED at f4253d45: est 1634 > 1600 / 1725 > 1660, over_ceiling="1"); near miss --detail alone still reorders. # (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). # (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. @@ -460,12 +461,21 @@ if mode == "tight": return [ c for _, c in rows ], len( m2.group( 2 ).encode() ) + elided, 'capped="1"' in m2.group( 1 ), " next=" in m2.group( 1 ) a, ref = charged( A ), charged( body( sys.argv[ 7 ] ) ) if len( sys.argv ) > 7 else None T = int( budget * tb ) + # forbudget-soft round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served + # first and the rows past the target cut fill what they leave of T — maximality is judged against T less their bytes + # (U: XML, everything after but , plus the rows; JSON, the "tail" object) + if A.lstrip().startswith( "{" ): + try: U = len( json.dumps( json.loads( A ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: U = 0 + else: + U = ( len( A.split( "", 1 )[ 1 ].encode() ) - len( "" ) if "" in A else 0 ) + U += sum( len( h.encode() ) for h in re.findall( r']*/>', A.split( " 0: k = len( a[ 0 ] ); fits = a[ 1 ] <= T or k <= 4 if not fits: bad.append( "ST: rows charged %d B > target %d B with %d rows (floor 4)" % ( a[ 1 ], T, k ) ) if k < len( ref[ 0 ] ): - if a[ 1 ] + ref[ 0 ][ k ] <= T - 160: bad.append( "ST: cut at %d of %d rows with %d B charged, the next row (%d B) fits T=%d" % ( k, len( ref[ 0 ] ), a[ 1 ], ref[ 0 ][ k ], T ) ) + if a[ 1 ] + ref[ 0 ][ k ] <= T - U - 160: bad.append( "ST: cut at %d of %d rows with %d B charged, the next row (%d B) fits T=%d less the sections U=%d" % ( k, len( ref[ 0 ] ), a[ 1 ], ref[ 0 ][ k ], T, U ) ) if not ( a[ 2 ] and a[ 3 ] ): bad.append( "NX: rows cut (%d of %d) without capped + next=" % ( k, len( ref[ 0 ] ) ) ) print( " .... %s: ST rows=%d/%d charged=%d T=%d" % ( label, k, len( ref[ 0 ] ), a[ 1 ], T ) ) else: @@ -506,7 +516,9 @@ done # sit in the window where the uncharged note alone pushed a within-ceiling answer over (RED at f4253d45: ts 1534 -> 1634 with # over_ceiling="1" at 1600; pydocs 1626 -> 1725 at 1660). JSON refuses the pair and MCP `for` has no body-ceiling input. QP='How is a message delivered to its handler?' -for spec in "ts|$Q1|1600" "pydocs|$QP|1660"; do +# forbudget-soft round C (FOLD C; rule 4): the default answer's now carries page 0 of the file page (+94 / +87 tokens on +# these two answers), so the windows move with it: 1600 -> 1700, 1660 -> 1760 (est 1628 / 1713 on the round C binary). +for spec in "ts|$Q1|1700" "pydocs|$QP|1760"; do r="${spec%%|*}"; rest="${spec#*|}"; q="${rest%|*}"; mt="${rest##*|}" ON="$( onr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )"; OFF="$( offr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )" if ran_ok "$ON" "T/body/$r" && ran_ok "$OFF" "T/body/$r (twin)"; then diff --git a/test/forwidencheck.sh b/test/forwidencheck.sh index eb6e1d5b9..9132c6fc6 100755 --- a/test/forwidencheck.sh +++ b/test/forwidencheck.sh @@ -14,8 +14,8 @@ # lookup is how an answer is scored complete). # (2) The page is ranked file-first by a bounded union-coverage score: the fixture's gold file, whose four # symbols each match ONE query term, sits at file-rank 8..30 on the page and is ABSENT from the default -# --for answer (its best symbol is weaker than twenty single-term files that outrank it in best-symbol -# order, which is what the default's walks). +# --for answer's head and tail RANK rows (its best symbol is weaker than twenty single-term files that outrank it +# in best-symbol order, which is what those rows walk) — FOLD C: the tail's page rows carry it. # (3) The page is deterministic and well-formed; a cut page carries the house paging vocabulary # (shown= total= capped="1" has_more= next_offset=) and a next= naming the next page; the second page # has no row in common with the first and the pages concatenate to the wider page in order. @@ -126,10 +126,26 @@ print( " ".join( sorted( set( re.findall( r"\s([a-z_]+)=\"", m.group( 2 ) ) ) ) # ── (1) the default answer does NOT carry the gold; the page does, verbatim ───────────────────────────── run --for="$THIN" >"$TMP/default.xml"; rc=$? [ "$rc" = 0 ] || no "(1) default --for exited $rc: $( head -c 200 "$TMP/err" )" -if grep -q "p=\"$GOLD\"" "$TMP/default.xml"; then - no "(1) the default --for answer already names $GOLD — the fixture does not exercise widening" +# FOLD C (forbudget-soft round C, owner decision 2026-10-14; rule 4): the default answer's now carries page 0 of the +# file page (its rows with sym=), so the widening this fixture measures is IN the default answer. The by-design break: the +# old "the default answer does not name the gold" now reads the head and the tail's RANK rows (no sym=: the best-symbol walk +# the gold falls past) — the fixture still exercises widening — and the page rows must name the gold, exactly as the page does. +if python3 - "$TMP/default.xml" "$GOLD" <<'PY' +import re, sys +s = open( sys.argv[ 1 ] ).read(); g = sys.argv[ 2 ] +head = re.findall( r']*?\bp="([^"]*)"', s ) +rank = [ p for p, sym in re.findall( r'', s ) if not sym ] +sys.exit( 1 if g in head or g in rank else 0 ) +PY +then + ok "(1) the default --for answer's head and tail rank rows do not name $GOLD (the best-symbol walk it falls past)" +else + no "(1) the default --for answer's head or rank rows already name $GOLD — the fixture does not exercise widening" +fi +if grep -q " (the file-grain surface the gold fell past)" \ || no "(1) presence guard: no in the default answer — the fixture shape changed" diff --git a/test/mcpforparitycheck.sh b/test/mcpforparitycheck.sh index 3818d05c3..03778622a 100755 --- a/test/mcpforparitycheck.sh +++ b/test/mcpforparitycheck.sh @@ -214,15 +214,21 @@ def rows( s ): return m.group( 1 ), [ c for _, c in out ], len( m.group( 2 ).encode() ) + elided try: n, bpt = int( sys.argv[ 2 ] ), float( sys.argv[ 3 ] ) except ValueError: print( "BAD premise: no numeric budget / bytes-per-token constant" ); sys.exit( 0 ) -a, ref = rows( sys.stdin.read() ), rows( open( sys.argv[ 1 ] ).read() ) +ans = sys.stdin.read() +a, ref = rows( ans ), rows( open( sys.argv[ 1 ] ).read() ) +# round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the sections served before the tail +# ( rows, , , ) are reserved first and the rows past the cut fill what they leave of T — the +# maximality bound is T less those sections' bytes (U, read off the answer); the tail rides on top on this twin +after = ans.split( '', 1 )[ 1 ] if '' in ans else '' +U = len( after.split( ']*/>', ans.split( ' rows (answer or uncut reference)" ); sys.exit( 0 ) T = int( n * bpt ); attrs, ar, charged = a; k = len( ar ); bad = [] if not ( charged <= T or k <= 4 ): bad.append( "rows charged %d B > T=%d with %d rows (floor 4)" % ( charged, T, k ) ) if k < len( ref[ 1 ] ): - if charged + ref[ 1 ][ k ] <= T - 160: bad.append( "cut at %d with the next row (%d B) fitting T" % ( k, ref[ 1 ][ k ] ) ) + if charged + ref[ 1 ][ k ] <= T - U - 160: bad.append( "cut at %d with the next row (%d B) fitting T less the sections (U=%d)" % ( k, ref[ 1 ][ k ], U ) ) nb = re.search( r'\bnext_budget_tokens="([0-9]+)"', attrs ) if not ( 'capped="1"' in attrs and nb and int( nb.group( 1 ) ) > n ): bad.append( "NX: a cut without capped + next_budget_tokens > %d" % n ) -print( "%s ST rows=%d/%d charged=%d T=%d%s" % ( "BAD" if bad else "OK", k, len( ref[ 1 ] ), charged, T, ( ": " + "; ".join( bad ) ) if bad else "" ) ) +print( "%s ST rows=%d/%d charged=%d T=%d U=%d%s" % ( "BAD" if bad else "OK", k, len( ref[ 1 ] ), charged, T, U, ( ": " + "; ".join( bad ) ) if bad else "" ) ) PY for tb in 900 1200 1600 2000 6000; do b="$( mcp_for_budget "$INERT_Q" "$tb" )" diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 2b27ba3a6..137187f5e 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -413,7 +413,20 @@ for spec in "compact|$QTS|" "compact|$QV|" "full|$QTS|--legend=full" "compact|$Q if [ "$non" -ge 1 ] && [ "$noff" = 0 ] && grep -q 'qword="gateway"' "$on"; then : else no "(S/$dia '$q'${extra:+ $extra}) premise: via clause head=$non A/B=$noff (want >=1 / 0) with the gateway owner row"; continue; fi fi - if [ "$( SECTS "$on" )" = "$( SECTS "$off" )" ] && [ "$( SECTS "$off" )" != "[]" ]; then + # forbudget-soft round C (owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): under an explicit --token-budget the + # owner rows are hop rows — a trailing section reserved before the rows past the target cut — so they may cost rows PAST + # the cut, never a row at it: the rows are then a rank prefix of the A/B handle's (the clause itself, a header disclosure, + # is still never charged). The default-regime specs keep the byte-identity. + if [ -n "$extra" ] && [ "$extra" != "--legend=full" ]; then + if python3 - "$on" "$off" <<'PY' +import re, sys +rows = lambda f: re.findall( r']*?\bp="([^"]*)"[^>]*?\bn="([^"]*)"|]*?\bn="([^"]*)"[^>]*?\bp="([^"]*)"', open( f ).read().split( '' )[ 0 ] ) +a, b = rows( sys.argv[ 1 ] ), rows( sys.argv[ 2 ] ) +sys.exit( 0 if a and b and b[ :len( a ) ] == a and 'over_ceiling="1"' not in open( sys.argv[ 1 ] ).read() else 1 ) +PY + then ok "(S/$dia '$q' $extra) soft target: the owner rows (a section, reserved first) leave a rank prefix of the A/B handle's, no over_ceiling" + else no "(S/$dia '$q' $extra) soft target: is not a rank prefix of the A/B handle's: $( grep -o ']*>' "$on" | cut -c1-80 ) vs $( grep -o ']*>' "$off" | cut -c1-80 )"; fi + elif [ "$( SECTS "$on" )" = "$( SECTS "$off" )" ] && [ "$( SECTS "$off" )" != "[]" ]; then ok "(S/$dia '$q'${extra:+ $extra}) the owner rows' via=\"name\" clause costs / nothing (byte-identical to the A/B handle)" else no "(S/$dia '$q'${extra:+ $extra}) / differ from the A/B handle: $( grep -o ']*>' "$on" ) vs $( grep -o ']*>' "$off" )"; fi done @@ -445,9 +458,11 @@ if ran_ok "$f" "(B1b)"; then if [ "$got" = row ] || [ "$got" = cut ] || grep -q 'reason="budget"' "$f"; then ok "(B1b) --token-budget=4000: the owner is first or disclosed ($got)" else no "(B1b) --token-budget=4000: gateway neither the first owner row nor named by qword_cut/next= ($got)"; fi fi -on="$TMP/b2.on.xml"; off="$TMP/b2.off.xml"; run ts "$on" "--for=$QTS" --token-budget=1200; runoff ts "$off" "--for=$QTS" --token-budget=1200 +# forbudget-soft round C (rule 4): sections are funded from the rows at the soft target's cut (reserve first), so 1200 tokens +# now leaves the hops a share; re-anchored at 700, the widest budget that still exhausts it (measured: 700 budget, 900 not) +on="$TMP/b2.on.xml"; off="$TMP/b2.off.xml"; run ts "$on" "--for=$QTS" --token-budget=700; runoff ts "$off" "--for=$QTS" --token-budget=700 if ran_ok "$on" "(B2)" && ran_ok "$off" "(B2) A/B"; then - grep -q 'reason="budget"' "$off" || no "(B2) premise: --token-budget=1200 no longer exhausts the ceiling (no reason=\"budget\") — re-tune" + grep -q 'reason="budget"' "$off" || no "(B2) premise: --token-budget=700 no longer exhausts the ceiling (no reason=\"budget\") — re-tune" if cmp -s "$on" "$off"; then ok "(B2) exhausted ceiling: byte-identical to the A/B handle (owner rows add nothing)"; else no "(B2) exhausted-ceiling answer changed"; fi fi # (B3) a budget sweep on the 105-callee owner: no budget may drop callee names silently diff --git a/test/routecheck.sh b/test/routecheck.sh index ba040aa3b..33fc5ac94 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -67,6 +67,8 @@ CONCEPT="how does resolution work" # the pre-routing golden (captured from plain subtoken+body). The DEFAULT run only ADDS a "[routed: # subtoken+body …]" header note, so we compare the --no-route run (identical bytes) to the golden AND # assert the DEFAULT run's ranking body matches by stripping just the header comment. ────────────────── +# RE-PIN 2026-10-15 (forbudget-soft round C, FOLD C; rule 4): golden_for.xml re-captured — the ONLY bytes that moved are the +# (page rows with sym= after the rank rows; total= every positive-score file), the tail legend clause and est_tokens=. # RE-PIN 2026-07-30 (CA4 fixup wave, §F1): est_tokens="696" -> "703", the ONLY bytes that moved (the document # is 1757 B before and after; every ranking byte identical vs the pre-wave binary). Same cause as anchorcheck's # re-pin, and the two together are what identify it: BOTH goldens moved by exactly +7, so this is the diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index ae45429ee..73d002f6d 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Thu, 15 Oct 2026 03:01:57 -0400 Subject: [PATCH 076/162] fix(quality): the selector,memory legend sentence rides only with memory_skipped= (present-only, CHECKLIST 25) Co-Authored-By: Claude Opus 5.5 --- src/verbs_quality.h | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 1f20b72fe..c9432aa04 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -1174,6 +1174,7 @@ struct QualityDeltaLegendParts bool degraded = false; // ... ingest_scope= memory_limit= memory_need= also_skipped= kinds_next= bool memoryStopped = false; // ... memory_stop= (and memory_pressure= when it was pressure) bool memoryPeak = false; // ... memory_peak= (any range answer whose peak went over the limit) + bool memorySkippedShare = false; // ... memory_skipped= (skipped_why="selector,memory") }; // qd-memory-degrade — present-only: each rides only with the attributes it defines. No double dash anywhere in them @@ -1182,8 +1183,9 @@ inline constexpr const char* kQdKindsSkippedLegend = "kinds_skipped= names the kinds this answer did NOT run (alphabetical): no row of them is in this document, which is not a " "statement that none exists, and regressions=, gating= and every count cover only the kinds that ran; a count only a " "skipped kind feeds (register-macro-excluded=) is absent, never 0. skipped_why=\"selector\": the caller chose the kinds " - "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=; " - "skipped_why=\"selector,memory\": both, and memory_skipped= names memory's share (the rest the caller left out). "; + "(the kinds selector), and stale acks were judged for the chosen kinds only; skipped_why=\"memory\": see ingest_scope=. "; +inline constexpr const char* kQdMemorySkippedLegend = + "skipped_why=\"selector,memory\": both; memory_skipped= names memory's share (what kinds_next= runs), the rest the caller left out. "; inline constexpr const char* kQdDegradedLegend = "ingest_scope=\"changed-files\": the two full trees did not fit the memory guard's limit (memory_limit=, the max-memory value " "that sets it), so both commits' CHANGED files were ingested alone and the kinds those files decide ran with the same code and " @@ -1343,6 +1345,10 @@ inline void emitQualityDeltaLegend( const QualityDeltaLegendParts& p, std::FILE* { std::fputs( kQdDegradedLegend, out ); } + if( p.memorySkippedShare ) + { + std::fputs( kQdMemorySkippedLegend, out ); + } if( p.memoryStopped ) { std::fputs( kQdMemoryStopLegend, out ); @@ -2377,7 +2383,8 @@ std::optional runQualityDelta( const MainDispatch& d ) basis.registerMacroExcluded > 0, configDiag.total() > 0, regs, outOfScope, scope.active() || !foreignAcks.empty(), !foreignAcks.empty(), baselineAbsorbed, basis.headBasis, basis.declinedCallExcluded > 0, basis.valueRefExcluded > 0, collapse, - refs.kinds != quality::kAllKinds, refs.degraded, !refs.memoryStop.empty(), kd.peakOver != 0 }, out ); + refs.kinds != quality::kAllKinds, refs.degraded, !refs.memoryStop.empty(), kd.peakOver != 0, + kd.memorySkipped != 0 && refs.selected != quality::kAllKinds }, out ); const char* baseMarker = baseSel.marker; // R3: ditto — one seam decides staleness AND names it // 2026-09-06: what the sidecar readers skipped, on the root (absent means none) — see kQdBaseHeadUnreadable std::string sidecarHealthAttrs; From 5c7d92842718273a1fbb73006f372ee1cc39e633 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:05:00 -0400 Subject: [PATCH 077/162] refactor(for): round C's refill as helpers (qd) forLensJsonSoftRefill / mcpSoftSections / fileTailRankRows / fileTailPageRows, and the shared rw::forSoftRowDropFill / forSoftSectionsShare / redactTakeBack. Byte-identical on 9 probe argv (CLI XML default + explicit, JSON, --detail --with-graph, MCP for at 0/900/2000). Co-Authored-By: Claude Opus 5.5 --- src/forpage.h | 81 +++++++++++++++--------------- src/mcpverbs.h | 33 +++++++------ src/serialize.h | 29 +++++++++++ src/verbs_for.h | 128 +++++++++++++++++++++++++++--------------------- 4 files changed, 160 insertions(+), 111 deletions(-) diff --git a/src/forpage.h b/src/forpage.h index 4a0ba157e..6cc84857c 100644 --- a/src/forpage.h +++ b/src/forpage.h @@ -656,56 +656,49 @@ struct FileTail std::string task; // the --for task: a cut tail's next= names the page }; -inline FileTail computeFileTail( const IngestResult& ing, const std::vector& rank, const LexTermEvidence& ev, - const std::vector& headIds, std::string_view rootArg, std::string_view task ) +// 1. the rank rows: the best-symbol projection of the lens order, outside the shown head's files; marks each file it lists +inline void fileTailRankRows( FileTail& out, std::vector& listed, const IngestResult& ing, const std::vector& rank, + const std::vector& headIds, std::string_view rootArg ) { - FileTail out; - out.task = std::string( task ); const std::size_t F = ing.files.size(); - std::vector listed( F, 0 ); + std::vector inHead( F, 0 ); + for( NodeId sid : headIds ) { - // 1. the rank rows: the best-symbol projection of the lens order, outside the shown head's files - std::vector inHead( F, 0 ); - for( NodeId sid : headIds ) - { - if( sid < ing.symbols.size() && ing.symbols[ sid ].fileId < F ) - { - inHead[ ing.symbols[ sid ].fileId ] = 1; - } - } - const std::size_t S = std::min( ing.symbols.size(), rank.size() ); - std::vector order( S ); - for( NodeId i = 0; i < S; ++i ) + if( sid < ing.symbols.size() && ing.symbols[ sid ].fileId < F ) { - order[i] = i; + inHead[ ing.symbols[ sid ].fileId ] = 1; } - sortutil::radixSortByScoreDescId( order, rank ); - for( std::size_t k = 0; k < S && out.paths.size() < kForFileTailRankCap; ++k ) + } + const std::size_t S = std::min( ing.symbols.size(), rank.size() ); + std::vector order( S ); + for( NodeId i = 0; i < S; ++i ) + { + order[i] = i; + } + sortutil::radixSortByScoreDescId( order, rank ); + for( std::size_t k = 0; k < S && out.paths.size() < kForFileTailRankCap && rank[ order[k] ] > 0.0f; ++k ) // the zero-score tail is contiguous + { + const std::uint32_t f = ing.symbols[ order[k] ].fileId; + if( f >= F || listed[f] || inHead[f] ) { - const NodeId id = order[k]; - if( !( rank[id] > 0.0f ) ) - { - break; // (score desc) order: the zero-score tail is contiguous - } - const std::uint32_t f = ing.symbols[id].fileId; - if( f >= F || listed[f] || inHead[f] ) - { - continue; - } - listed[f] = 1; - out.paths.push_back( lensRowPath( ing, f, rootArg ) ); - out.syms.emplace_back(); - out.pageNext.push_back( 0 ); + continue; } + listed[f] = 1; + out.paths.push_back( lensRowPath( ing, f, rootArg ) ); + out.syms.emplace_back(); + out.pageNext.push_back( 0 ); } - // 2. the page rows: page 0 of the file page, minus the files already listed - const ForFilePage page = computeForFilePage( ing, rank, ev ); +} + +// 2. the page rows: page 0 of the file page, minus the files already listed, each with its sym= names +inline void fileTailPageRows( FileTail& out, std::vector& listed, const IngestResult& ing, const ForFilePage& page, std::string_view rootArg ) +{ out.total = page.rows.size(); out.pageSeen = std::min( page.rows.size(), kForFileTailPageRows ); - for( std::size_t r = 0; r < page.rows.size() && r < kForFileTailPageRows; ++r ) + for( std::size_t r = 0; r < out.pageSeen; ++r ) { const ForFileRow& row = page.rows[r]; - if( row.fileId >= F || listed[ row.fileId ] ) + if( row.fileId >= listed.size() || listed[ row.fileId ] ) { continue; } @@ -714,12 +707,22 @@ inline FileTail computeFileTail( const IngestResult& ing, const std::vector 0 ) { sym += ','; } + sym += k > 0 ? "," : ""; sym += ing.symbols[ row.top[k] ].name; } out.syms.push_back( std::move( sym ) ); out.pageNext.push_back( r + 1 ); } +} + +inline FileTail computeFileTail( const IngestResult& ing, const std::vector& rank, const LexTermEvidence& ev, + const std::vector& headIds, std::string_view rootArg, std::string_view task ) +{ + FileTail out; + std::vector listed( ing.files.size(), 0 ); + out.task = std::string( task ); + fileTailRankRows( out, listed, ing, rank, headIds, rootArg ); + fileTailPageRows( out, listed, ing, computeForFilePage( ing, rank, ev ), rootArg ); ENSURES( out.paths.size() == out.syms.size() && out.paths.size() == out.pageNext.size() && out.paths.size() <= out.total, "computeFileTail: the rows, their sym= and their continuations disagree, or more rows than positive-score files" ); return out; diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 74fd43112..fb1f2ec19 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1913,6 +1913,19 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by }; +// round C (RESERVE SECTIONS FIRST, the CLI twin's rule): the --sections= posture the stub plan reads — under budget_tokens the two +// sections ride whole when they fit what the rows at the cut (and ) left of the budget (serialize.h forSoftSectionsOptIn) +inline std::string_view mcpSoftSections( const McpForAssemblyInputs& in, const std::string& sigsStr, const rw::SigsCutReport& cut, + std::size_t sectionsBytes ) +{ + if( in.budgetTokens == 0 ) + { + return in.sections; + } + const std::size_t atCut = sigsStr.size() - std::min( sigsStr.size(), cut.pastTargetBytes ); + return forSoftSectionsOptIn( in.sections, sectionsBytes, 0u, + forSoftSectionsShare( budgetBytesForTokens( in.budgetTokens ), atCut + in.routeStr.size() ) ); +} inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::string headerStr, std::string legoStr, std::string composeStr, std::vector> legoScoped, std::size_t legoPreCapCount ) { @@ -2037,11 +2050,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin .renderedBytes = composeStr.size() }; // round C (reserve first, the CLI twin's rule): under budget_tokens the sections ride whole when they fit what the rows // at the cut left of the budget - const std::size_t mcpSigsAtCut = sigsStr.size() - std::min( sigsStr.size(), mcpSigsCut.pastTargetBytes ); - const std::size_t mcpBudgetBytes = budgetTokens > 0 ? budgetBytesForTokens( budgetTokens ) : 0u; - const std::size_t mcpShare = mcpBudgetBytes - std::min( mcpBudgetBytes, mcpSigsAtCut + in.routeStr.size() ); - const ForSectionStubPlan plan = planForSectionStubs( budgetTokens > 0 ? forSoftSectionsOptIn( sections, legoStr.size(), composeStr.size(), mcpShare ) - : std::string_view( sections ), + const ForSectionStubPlan plan = planForSectionStubs( mcpSoftSections( in, sigsStr, mcpSigsCut, legoStr.size() + composeStr.size() ), task, mcpLegoServedIn, mcpComposeServedIn ); // lego-charge (train 26c seam): the charge plan the budget follows (forTaskText's mcpSigChargePlan, the same // rule: rw::forSectionsSigChargePlan), recomputed here from the sections AS SERVED — the count-floor lego re-render @@ -2078,19 +2087,11 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin // replaced, never added to. if( budgetTokens > 0 && mcpSigsCut.pastTarget > 0 ) { - constexpr std::size_t kSigsWrapperBytes = 13; // "" + "" (serialize.h forSigsBudgets) - const std::size_t mcpTargetBytes = budgetBytesForTokens( budgetTokens ); - const std::size_t mcpSectionsUsed = mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size(); - const std::size_t mcpRowDropFill = ( mcpTargetBytes > mcpSectionsUsed ? mcpTargetBytes - mcpSectionsUsed : 0u ) + kSigsWrapperBytes; + const std::size_t mcpRowDropFill = forSoftRowDropFill( budgetBytesForTokens( budgetTokens ), + mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() ); if( mcpRowDropFill < in.rowDropBudget ) { - if( redact != nullptr && redactBeforeSigs && redactAfterSigs ) - { - for( std::size_t k = 0; k < redact->byKind.size(); ++k ) // take the first render's count back out - { - redact->byKind[k] -= redactAfterSigs->byKind[k] - redactBeforeSigs->byKind[k]; - } - } + redactTakeBack( redact, redactBeforeSigs, redactAfterSigs ); mcpSigsCut = rw::SigsCutReport {}; mcpSigsCut.continuationRequest = &mcpSigsNext; mcpSigsCut.rowDropBudgetBytes = mcpRowDropFill; diff --git a/src/serialize.h b/src/serialize.h index 1b94285c7..f9ea625f2 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -994,6 +994,35 @@ inline ForSigsBudgets forSigsBudgets( std::size_t softRowBytes, std::size_t fixe return b; } +// ── ROUND C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the shared arithmetic of the soft target's row refill ────── +// The rows past a soft target's cut fill what the trailing sections served before them left of T: this is the row-drop budget +// a refill render passes (SigsCutReport::rowDropBudgetBytes, the "" frame added as forSigsBudgets adds it). At or +// under the ladder's target it IS the cut (sigsRowDropEffective), so the rows never fall below it. CLI XML, JSON and MCP `for`. +inline std::size_t forSoftRowDropFill( std::size_t targetBytes, std::size_t sectionsServedBytes ) noexcept +{ + constexpr std::size_t kSigsWrapperBytes = 13; // "" + "", as forSigsBudgets + return ( targetBytes > sectionsServedBytes ? targetBytes - sectionsServedBytes : 0u ) + kSigsWrapperBytes; +} +// What a soft target's bundle budget leaves the trailing sections once `spentBytes` (the rows at the cut and the fixed +// sections) are paid — the share forSoftSectionsOptIn and the tail fill read. +inline std::size_t forSoftSectionsShare( std::size_t budgetBytes, std::size_t spentBytes ) noexcept +{ + return budgetBytes > spentBytes ? budgetBytes - spentBytes : 0u; +} +// A refill render replaces the first render's redaction count rather than adding to it: take the first render's own +// contribution (`after` less `before`, snapshotted around it) back out of the run's tally before rendering again. +inline void redactTakeBack( RedactCounts* tally, const std::optional& before, const std::optional& after ) noexcept +{ + if( tally == nullptr || !before || !after ) + { + return; + } + for( std::size_t k = 0; k < tally->byKind.size(); ++k ) + { + tally->byKind[k] -= after->byKind[k] - before->byKind[k]; + } +} + // ── THE --for LENS'S RANKED-HEAD CAP, OWNED ONCE (round-4 finding F-03) ────────────────────────────── // How many ranked symbols the --for lens considers before the relevance floor narrows it and the ladder // above trims it. The CLI lens (`--pack-top-n`, else this) and the MCP `for` verb MUST read the same diff --git a/src/verbs_for.h b/src/verbs_for.h index ca9340c6f..e080d093f 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1320,6 +1320,73 @@ inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t } // One RENDER of the --json bundle (emitForLensJson below). +// ── round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST): the JSON twin's row refill, a free function over +// emitForLensJsonPass' locals (the forLensJsonTailStanza precedent). The tail is funded from the rows at the soft target's cut; +// the rows past the cut then fill what it left of T — never below the cut (rw::forSoftRowDropFill). Re-rendered only when that +// narrows the rows; the first render's redaction count and note tallies are replaced, and a failed re-render keeps the first +// render (and every reading taken from it) whole. +struct ForJsonRefillState // the first render's outputs, by reference: the refill replaces them together or not at all +{ + std::string& json; + rw::SigsCutReport& cut; + bool& capped; + std::size_t& droppedPositive; + rw::JsonSigNoteCounts& notes; + std::vector& shown; +}; +struct ForJsonRefillBudgets +{ + std::size_t bundleBytes; // budgetBytesForTokens( N ) + std::size_t sigsTarget; // the ladder's target (the cut) + std::size_t firstRowDrop; // the first render's row budget (T plus the frame) +}; +template< class PackSigs > +inline void forLensJsonSoftRefill( const PackSigs& packSigs, const ForLensJsonInputs& in, ForJsonRefillState st, ForJsonRefillBudgets b, + const rw::SigsCutContinuation& cutNext, const std::optional& redactBeforeSigs ) +{ + const std::size_t cutRows = st.json.size() - std::min( st.json.size(), st.cut.pastTargetBytes ); + const rw::FileTail cutTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, rw::rowsAtTargetIds( st.shown, in.rank, st.cut.pastTarget ), + in.rootArg, in.task ); + const std::size_t tailBytes = forLensJsonTailStanza( cutTail, in.tokenBudget, rw::forSoftSectionsShare( b.bundleBytes, cutRows ) ).size(); + const std::size_t rowDrop = rw::forSoftRowDropFill( b.bundleBytes, tailBytes ); + if( rowDrop >= b.firstRowDrop ) + { + return; + } + const std::string firstJson = st.json; + const rw::SigsCutReport firstCut = st.cut; + const bool firstCap = st.capped; + const std::size_t firstDrop = st.droppedPositive; + const rw::JsonSigNoteCounts firstNote = st.notes; + const std::vector firstIds = st.shown; + const std::optional redactFirst = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; + if( redactBeforeSigs ) + { + *in.redact = *redactBeforeSigs; + } + st.notes = rw::JsonSigNoteCounts {}; + st.cut = rw::SigsCutReport {}; + st.cut.continuationRequest = &cutNext; + st.cut.rowDropBudgetBytes = rowDrop; + rw::MemoryStream refillStream; + std::FILE* const rm = rw::openChargeStream( refillStream ); + if( rm != nullptr ) + { + packSigs( rm, b.sigsTarget, &st.capped, &st.droppedPositive, &st.shown, &st.cut ); + } + const rw::MemoryStreamBytes refill = rm != nullptr ? refillStream.finish() : rw::MemoryStreamBytes {}; + if( rm != nullptr && refill.isWhole ) + { + st.json.assign( refill.bytes ); + return; + } + st.json = firstJson; st.cut = firstCut; st.capped = firstCap; st.droppedPositive = firstDrop; st.notes = firstNote; st.shown = firstIds; + if( redactFirst ) + { + *in.redact = *redactFirst; + } +} + inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) { using namespace rw; @@ -1430,55 +1497,11 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const sigsJson.assign( block.bytes ); } } - // round C (RESERVE SECTIONS FIRST, the XML twin's rule): the tail is funded from the rows at the soft target's cut, and - // the rows past the cut fill what it left of T — never below the cut. Re-rendered only when that narrows the rows; the - // first render's redaction count and note tallies are replaced, and a failed re-render keeps the first render whole. + // round C (RESERVE SECTIONS FIRST, the XML twin's rule): the rows past the soft target's cut fill what the tail left of T if( isSigsBuffered && in.tokenBudget > 0 && sigsCut.pastTarget > 0 ) { - constexpr std::size_t kSigsWrapperBytes = 13; // the array's frame (serialize.h forSigsBudgets' wrapper allowance) - const std::size_t cutRows = sigsJson.size() - std::min( sigsJson.size(), sigsCut.pastTargetBytes ); - const rw::FileTail cutTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, rw::rowsAtTargetIds( jsonShownIds, in.rank, sigsCut.pastTarget ), - in.rootArg, in.task ); - const std::size_t tailBytes = forLensJsonTailStanza( cutTail, in.tokenBudget, bundleBudget > cutRows ? bundleBudget - cutRows : 0u ).size(); - const std::size_t rowDropFill = ( bundleBudget > tailBytes ? bundleBudget - tailBytes : 0u ) + kSigsWrapperBytes; - if( rowDropFill < sigsBudgets.rowDrop ) - { - const rw::SigsCutReport firstCut = sigsCut; - const bool firstCapped = sigsCapped; - const std::size_t firstDropped = sigsDroppedPositive; - const JsonSigNoteCounts firstNotes = noteCounts; - const std::vector firstShown = jsonShownIds; - const std::optional redactFirst = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; - if( redactBeforeSigs ) - { - *in.redact = *redactBeforeSigs; - } - noteCounts = JsonSigNoteCounts {}; - sigsCut = rw::SigsCutReport {}; - sigsCut.continuationRequest = &cutNext; - sigsCut.rowDropBudgetBytes = rowDropFill; - rw::MemoryStream refillStream; - bool refilled = false; - if( std::FILE* const rm = rw::openChargeStream( refillStream ) ) - { - packSigs( rm, sigsBudget, &sigsCapped, &sigsDroppedPositive, &jsonShownIds, &sigsCut ); - const rw::MemoryStreamBytes refill = refillStream.finish(); - if( refill.isWhole ) - { - sigsJson.assign( refill.bytes ); - refilled = true; - } - } - if( !refilled ) - { - sigsCut = firstCut; sigsCapped = firstCapped; sigsDroppedPositive = firstDropped; noteCounts = firstNotes; - jsonShownIds = firstShown; - if( redactFirst ) - { - *in.redact = *redactFirst; - } - } - } + forLensJsonSoftRefill( packSigs, in, ForJsonRefillState{ sigsJson, sigsCut, sigsCapped, sigsDroppedPositive, noteCounts, jsonShownIds }, + ForJsonRefillBudgets{ bundleBudget, sigsBudget, sigsBudgets.rowDrop }, cutNext, redactBeforeSigs ); } ForLensBlockCharge sigsCharge; // the sink: an unmeasured block takes the omitting path below if( !isSigsBuffered ) @@ -3834,10 +3857,9 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // replaced, never added to; a re-render whose buffer fails keeps the first render and its readings whole. if( forSoftTarget && sigsPreRendered && forSigsCut.pastTarget > 0 ) { - constexpr std::size_t kSigsWrapperBytes = 13; // "" + "" (serialize.h forSigsBudgets) const std::size_t sectionsServed = forHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() + autoSection.xml.size() + tailStr.size(); // not the --detail bodies (above) - const std::size_t rowDropFill = ( bundleBudget > sectionsServed ? bundleBudget - sectionsServed : 0u ) + kSigsWrapperBytes; + const std::size_t rowDropFill = rw::forSoftRowDropFill( bundleBudget, sectionsServed ); if( rowDropFill < sigsBudgets.rowDrop ) { const rw::SigsCutReport firstCut = forSigsCut; @@ -3845,13 +3867,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co const bool firstCapped = forSigsCapped; const std::vector firstShown = shownSigIds; const std::optional redactNow = redactPtr != nullptr ? std::optional( *redactPtr ) : std::nullopt; - if( redactPtr != nullptr && redactBeforeSigs && redactAfterSigs ) - { - for( std::size_t k = 0; k < redactPtr->byKind.size(); ++k ) // take the first render's count back out - { - redactPtr->byKind[k] -= redactAfterSigs->byKind[k] - redactBeforeSigs->byKind[k]; - } - } + rw::redactTakeBack( redactPtr, redactBeforeSigs, redactAfterSigs ); // the first render's count comes back out forSigsCut = rw::SigsCutReport {}; forSigsCut.continuationRequest = &forSigsNext; forSigsCut.rowDropBudgetBytes = rowDropFill; From f8bff23cccda0af71182417588e7ca397d35377a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:21:28 -0400 Subject: [PATCH 078/162] =?UTF-8?q?test(quality):=20qddegradecheck=20(W)?= =?UTF-8?q?=20=E2=80=94=20the=20memory=5Fskipped=3D=20legend=20sentence=20?= =?UTF-8?q?is=20present-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- test/qddegradecheck.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/qddegradecheck.sh b/test/qddegradecheck.sh index 7af1a4de7..1ebaff4d0 100755 --- a/test/qddegradecheck.sh +++ b/test/qddegradecheck.sh @@ -601,6 +601,9 @@ else no "(W) complexity,dead-code: why=$( attr "$WORK/o" skipped_why ) memory_sk qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code --legend=full --no-cache if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) --legend=full defines memory_skipped= when it rides, and parses as XML" else no "(W) the legend lacks memory_skipped= (or invalid XML)"; fi +qd RIPWIRE_TEST_MEMGUARD=project:1 "$BIN" . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity --legend=full --no-cache +if [ -s "$WORK/o" ] && ! sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q 'memory_skipped=' && validxml "$WORK/o"; then ok "(W) present-only: no memory_skipped= sentence when only the selector skipped" +else no "(W) the selector-only legend defines memory_skipped= (or invalid XML / no output)"; fi # (no --json arm: --json is refused beside --quality-kinds, so memory_skipped= — which rides only beside a selector — has # no JSON surface yet; the JSON twin is written by the same kindsDisclosure as the XML) From 22728af8a37aa2258abdc886849c1e4fa8d1d7d7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:32:27 -0400 Subject: [PATCH 079/162] refactor(attrib): one of_def emitter for both dialects (quality-delta clone row) Co-Authored-By: Claude Opus 5.5 --- src/defattrib.h | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/defattrib.h b/src/defattrib.h index 863f3b602..a2260dc6e 100644 --- a/src/defattrib.h +++ b/src/defattrib.h @@ -672,29 +672,27 @@ inline bool defRowUnprinted( const DefGroups& groups, const RowDefTokens& tokens { return defTableCapped( groups ) && tokens.strongest( row ) <= DefMark::Name; } -// A row with no printed token on a cut table carries no of_def at all (its via="name" is the hedge); below the cap every row -// keeps the attribute, as before. -inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) +// A row's of_def in one dialect: `open`, the printed tokens (`sep`, `quote`), `close`. A row with no printed token on a cut +// table carries no of_def at all (its via="name" is the hedge); below the cap every row keeps the attribute, as before. +inline std::string ofDefIn( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row, std::string_view open, std::string_view sep, + std::string_view quote, char close ) { if( !groups.multi() || tokens.empty() || defRowUnprinted( groups, tokens, row ) ) { return {}; } - std::string out = " of_def=\""; - defTokensTo( out, groups, tokens.of( row ), " ", "" ); - out += '"'; + std::string out( open ); + defTokensTo( out, groups, tokens.of( row ), sep, quote ); + out += close; return out; } +inline std::string ofDefAttrXml( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) +{ + return ofDefIn( groups, tokens, row, " of_def=\"", " ", "", '"' ); +} inline std::string ofDefKeyJson( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) { - if( !groups.multi() || tokens.empty() || defRowUnprinted( groups, tokens, row ) ) - { - return {}; - } - std::string out = ",\"of_def\":["; - defTokensTo( out, groups, tokens.of( row ), ",", "\"" ); - out += ']'; - return out; + return ofDefIn( groups, tokens, row, ",\"of_def\":[", ",", "\"", ']' ); } // The columnar of_def column's value for one row: the printed tokens space-separated (the sites_at column's convention). inline std::string ofDefColumnValue( const DefGroups& groups, const RowDefTokens& tokens, std::size_t row ) From 3aae2b0b749bfbd556bf16fe436de66459f23148 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:38:34 -0400 Subject: [PATCH 080/162] test: two new verdicts as if/else (gateexitcheck G2) Co-Authored-By: Claude Opus 5.5 --- test/deeptailcheck.sh | 4 +++- test/forsoftbudgetcheck.sh | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/deeptailcheck.sh b/test/deeptailcheck.sh index 6262e9bb1..a7751afbd 100755 --- a/test/deeptailcheck.sh +++ b/test/deeptailcheck.sh @@ -183,7 +183,9 @@ done Z="$( grep -o ']*>' "$TMP/mini.xml" | head -1 )" ZR="$( python3 "$TMP/tail.py" "$TMP/mini.xml" rank | grep -c . )" case "$Z" in - *'total="3"'*'shown="3"'*'capped="0"'*) [ "$ZR" = "0" ] && ok "all-files-in-head bundle: 0 rank rows (none remain), the 3 files as page rows, nothing cut ($Z)" || no "all-files-in-head bundle lists $ZR rank row(s) — a head file served as 'outside the head'" ;; + *'total="3"'*'shown="3"'*'capped="0"'*) + if [ "$ZR" = "0" ]; then ok "all-files-in-head bundle: 0 rank rows (none remain), the 3 files as page rows, nothing cut ($Z)" + else no "all-files-in-head bundle lists $ZR rank row(s) — a head file served as 'outside the head'"; fi ;; *) no "zero-tail bundle wrong/missing: '$Z'" ;; esac diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 6b8d22493..d202bffa3 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -633,7 +633,8 @@ ok = list( zip( t.get( "files", [] ), t.get( "sym", [] ) ) ) == xr and str( t.ge print( "OK" if ok else "BAD json rows=%d xml rows=%d total %s/%s" % ( len( t.get( "files", [] ) ), len( xr ), t.get( "total" ), xa.get( "total" ) ) ) PY )" -[ "$C4" = OK ] && ok "(C4) --json tail == the XML tail (files + sym, total=, next)" || no "(C4) --json tail differs from the XML tail: $C4" +if [ "$C4" = OK ]; then ok "(C4) --json tail == the XML tail (files + sym, total=, next)" +else no "(C4) --json tail differs from the XML tail: $C4"; fi # (C5) the MCP twin serves the same tail rows as the CLI default mcp_call for "$Q" > "$TMP/mcp_def_tail.xml" if python3 "$TMP/tail.py" "$TMP/mcp_def_tail.xml" all > "$TMP/c5.mcp" && python3 "$TMP/tail.py" "$TMP/def.xml" all > "$TMP/c5.cli" \ From c0c6b93855e8b75d7de9aac6546e520e8630933f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:07:42 -0400 Subject: [PATCH 081/162] githarden: find the governing git config from the filesystem, no git child per call The startup trust scan ran `git rev-parse --absolute-git-dir --git-common-dir` once per crawl root on every call, only to learn which config files to scan. The scan now unions every config file git's discovery could pick (every ancestor's .git dir or gitdir: file, every ancestor that is itself a gitdir, and each one's commondir), walking the physical path. A superset can only trigger the authoritative `git config --get core.fsmonitor` probe, never hide a value. Where the filesystem cannot speak for git (GIT_DIR, GIT_COMMON_DIR or GIT_WORK_TREE set, a root that does not canonicalise, a pointer file behind a symlink) git is asked as before. Three shapes the old pre-check never disclosed now are: a bare repository as the root and a root reached through a symlink (the pre-check walked the logical path for a `.git` entry and never reached git), and a .git/config that is itself a symlink (the candidate is now treated as unknown, so git answers). githardencheck: arms (O) five discovery shapes, (Q) a config that gains the key between two calls, (P) a PATH-shim count of startup git children. Co-Authored-By: Claude Opus 5.5 --- src/githarden.h | 169 ++++++++++++++++++++++++++++++++++++++++- test/githardencheck.sh | 108 ++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 4 deletions(-) diff --git a/src/githarden.h b/src/githarden.h index fa2c944d6..e118c29ed 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -23,9 +23,13 @@ // git child now carries (gitcmd.h: --no-optional-locks -c core.fsmonitor=false, for every form of the key, in // every directory). What this header does is TELL the user when a root they pointed at carries the hook form, // and keep the env override as a second layer there. Because the governing config need not sit in `root/.git` -// — it can live up the parent chain, or in a per-worktree gitdir plus a shared commondir — `gitResolvedConfigDirs` -// asks git itself (`rev-parse --absolute-git-dir --git-common-dir`, the hook disabled on that probe's OWN -// command line) which dir governs the root, and the probe and disclosure run for THAT repo whatever its depth. +// — it can live up the parent chain, or in a per-worktree gitdir plus a shared commondir — the cheap tier scans +// every config file git's own discovery COULD pick for the root (`discoveryConfigSuperset`: every ancestor's +// `.git` dir or `gitdir:` file, every ancestor that is itself a gitdir, and each one's `commondir`), so the probe +// and disclosure run for the governing repo whatever its depth, with no git child unless a file could carry the +// key. Where the filesystem cannot speak for git (GIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE set, a root that does not +// canonicalise, a pointer file behind a symlink), `gitResolvedConfigDirs` asks git itself +// (`rev-parse --absolute-git-dir --git-common-dir`, the hook disabled on that probe's OWN command line). // // The trust decision is PER PROCESS: a long-lived --mcp server probes its roots once at start. A config edited // under a running server is not re-read — the same freshness contract every other startup-only fact has here. @@ -47,9 +51,11 @@ #include #include #include +#include #include #include #include +#include // std::errc — discoveryConfigSuperset tells "absent" from a stat it cannot judge #include #include @@ -220,11 +226,159 @@ inline std::vector gitResolvedConfigDirs( const std::stri return dirs; } +// ── the filesystem superset: every config file git's discovery could govern the root with, no git child ────── +// +// The `rev-parse` above is one git child per crawl root per process — on the critical path of EVERY call, and the +// answer it gives is a fact about the filesystem. With no GIT_DIR / GIT_COMMON_DIR / GIT_WORK_TREE in the +// environment, git (setup.c, 2.50.1) finds the governing gitdir by walking up from the PHYSICAL cwd: at each +// directory D it takes D/.git when that is a directory git accepts as a repository, or the path a `gitdir: X` FILE +// at D/.git names, or D itself when D is a gitdir (a bare repository, or a cwd inside .git — which needs a D/HEAD); +// a `.git` directory git rejects is skipped and the walk goes on up. The gitdir's `commondir` file, when present, +// names the dir whose `config` is shared (a linked worktree; git honours it in ANY gitdir). Local config is then +// gitdir/config, gitdir/config.worktree and commondir/config (+ includes, which the byte scan below reaches through +// the word "include"). So the union over EVERY ancestor-or-self D of those candidates — never stopping at the first +// `.git`, never judging which one git accepts — is a SUPERSET of the files git can read as local config. A superset +// is the safe direction: an extra candidate can only trigger the authoritative `git config --get` probe, never hide +// a value. Pointer files are read both as git reads them (the rest of the file, trailing CR/LF dropped, relative +// to D or to the gitdir) and as the first line trimmed of blanks; where the two differ both are kept. +// nullopt = the filesystem cannot speak for git here, and the caller asks git (gitResolvedConfigDirs) as before: +// an environment-named repository, a root that does not canonicalise, a stat that fails other than "absent", or a +// pointer file (.git, commondir) that is a symlink — git reads through it, the no-follow reader does not. +// test/githardencheck.sh (O) pins the shapes (an invalid .git skipped, a planted commondir, a bare root, a +// symlinked root) and (P) counts the git children: none for a root whose config cannot carry the key. +inline bool gitDiscoveryNamedByEnvironment() noexcept +{ + for( const char* name : { "GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE" } ) + { + if( const char* value = std::getenv( name ); value != nullptr && *value != '\0' ) + { + return true; + } + } + return false; +} + +// Both readings of a pointer file's path, each made absolute against `base`, appended to `out` once each. +inline void addPointerReadings( std::string_view exactRest, std::string_view looseRest, const std::filesystem::path& base, + std::vector& out ) +{ + while( !exactRest.empty() && ( exactRest.back() == '\n' || exactRest.back() == '\r' ) ) + { + exactRest.remove_suffix( 1 ); + } + const std::string loose = trimmedFirstLine( looseRest ); + for( const std::string_view reading : { exactRest, std::string_view( loose ) } ) + { + if( reading.empty() ) + { + continue; + } + std::filesystem::path p( reading ); + if( p.is_relative() ) + { + p = base / p; + } + if( std::find( out.begin(), out.end(), p ) == out.end() ) + { + out.push_back( std::move( p ) ); + } + } +} + +// true = absent (or not a directory on the way); false = a stat error git could see differently — doubt. +inline bool statAbsentOrOk( const std::error_code& ec ) noexcept +{ + return !ec || ec == std::errc::no_such_file_or_directory || ec == std::errc::not_a_directory; +} + +inline std::optional> discoveryConfigSuperset( const std::string& root ) +{ + if( gitDiscoveryNamedByEnvironment() ) + { + return std::nullopt; + } + std::error_code ec; + std::filesystem::path dir = std::filesystem::canonical( std::filesystem::path( root ), ec ); // git walks the physical cwd + if( ec || dir.empty() ) + { + return std::nullopt; + } + std::vector gitDirs; + for( ;; ) + { + const std::filesystem::path dotGit = dir / ".git"; + const std::filesystem::file_status st = std::filesystem::status( dotGit, ec ); // follows a link, as git's stat does + if( !statAbsentOrOk( ec ) ) + { + return std::nullopt; + } + ec.clear(); + if( std::filesystem::is_directory( st ) ) + { + if( std::find( gitDirs.begin(), gitDirs.end(), dotGit ) == gitDirs.end() ) { gitDirs.push_back( dotGit ); } + } + else if( std::filesystem::exists( std::filesystem::symlink_status( dotGit, ec ) ) ) + { + const std::optional head = pathguard::readRegularFileNoFollow( dotGit.string() ); + if( !head && pathguard::isSymlink( dotGit.string() ) ) + { + return std::nullopt; + } + if( head && head->starts_with( "gitdir:" ) ) + { + const std::string_view text( *head ); + addPointerReadings( text.substr( text.starts_with( "gitdir: " ) ? 8 : 7 ), text.substr( 7 ), dir, gitDirs ); + } + } + ec.clear(); + if( std::filesystem::exists( std::filesystem::symlink_status( dir / "HEAD", ec ) ) ) + { + if( std::find( gitDirs.begin(), gitDirs.end(), dir ) == gitDirs.end() ) { gitDirs.push_back( dir ); } + } + ec.clear(); + const std::filesystem::path parent = dir.parent_path(); + if( parent == dir || parent.empty() ) + { + break; + } + dir = parent; + } + std::vector files; + files.reserve( gitDirs.size() * 4 ); + for( const std::filesystem::path& gitDir : gitDirs ) + { + files.push_back( gitDir / "config" ); + files.push_back( gitDir / "config.worktree" ); + const std::string commondirFile = ( gitDir / "commondir" ).string(); + if( const std::optional common = pathguard::readRegularFileNoFollow( commondirFile ) ) + { + std::vector commonDirs; + addPointerReadings( *common, *common, gitDir, commonDirs ); + for( const std::filesystem::path& commonDir : commonDirs ) + { + files.push_back( commonDir / "config" ); + files.push_back( commonDir / "config.worktree" ); + } + } + else if( pathguard::isSymlink( commondirFile ) ) + { + return std::nullopt; + } + } + ENSURES( files.size() >= gitDirs.size() * 2, "two candidates per gitdir, plus its commondir's" ); + return files; +} + inline std::vector localConfigCandidates( const std::filesystem::path& root ) { + if( std::optional> superset = discoveryConfigSuperset( root.string() ) ) + { + return std::move( *superset ); // may be empty: no `.git` and no gitdir anywhere above — nothing git could read + } + DISCLOSE( Diagnostics::answerUnchanged, "githarden: the filesystem cannot speak for git's discovery at this root, so git names the governing dirs (one more git child); the scan and the authoritative probe after it are unchanged" ); std::vector out; - // Authoritative first: git resolves the governing gitdir/commondir for ANY root, subdirectory and + // Authoritative here: git resolves the governing gitdir/commondir for ANY root, subdirectory and // worktree and GIT_DIR included. Its config, its per-worktree config.worktree, and the shared common // config are the files a hook value could be spelled in (directly or through an include the cheap scan // also catches). @@ -280,6 +434,12 @@ inline bool localConfigMayCarryFsmonitor( const std::string& root ) std::optional bytes = pathguard::readRegularFileNoFollow( candidate.string() ); if( !bytes ) { + // git reads a config file THROUGH a symlink; the no-follow reader does not. A linked candidate is + // therefore unknown, not clean — git's own answer is asked for (arm (O) symlinked config). + if( pathguard::isSymlink( candidate.string() ) ) + { + return true; + } continue; } for( char& c : *bytes ) @@ -354,6 +514,7 @@ inline FsmonitorForm startupFormFor( std::string_view root ) noexcept // (a git URL, which git itself clones fresh — its config is never hostile) is skipped. inline const Report& hardenForRoots( std::span roots ) { + PROFILE_SCOPE_DESCRIBE( "githarden: hardenForRoots (startup trust probe, per crawl root)" ); Report& r = g_startup; r.forms.clear(); r.applied = false; diff --git a/test/githardencheck.sh b/test/githardencheck.sh index 8cb360843..83b43aec3 100755 --- a/test/githardencheck.sh +++ b/test/githardencheck.sh @@ -53,6 +53,14 @@ # regular-file-only open, so the run finishes, and nothing is disclosed from a file it could not read. A # blocking `cat` of the same FIFO under the same timeout harness is the contrast that shows the harness # detects a stall. +# (O) the startup scan finds the governing config from the filesystem alone, a superset of git's discovery: an +# invalid .git dir skipped for the hooked parent, a commondir planted in an ordinary .git, a bare root, a +# symlinked root (git walks the physical path), a symlinked .git/config (git reads through it). Each shape +# first proves plain git reads the hook value there. +# (Q) no stale answer: a repo whose config gains the hook (directly, then only via include.path) between two +# calls is disclosed on the next call. +# (P) the cost, counted through a PATH shim: a plain repo's run starts no startup-scan git child; with GIT_DIR +# set the same shim sees the rev-parse child (control). # (G) determinism: two `--situ` stdouts are byte-identical. # # Usage: RIPWIRE_BIN=build/ripwire bash test/githardencheck.sh @@ -423,6 +431,106 @@ else no "(N) the FIFO at .git/config.worktree was replaced or removed" fi +# ── (O) the startup scan finds the governing config without asking git (githarden.h discoveryConfigSuperset) ── +# The scan unions every config file git's discovery COULD pick — every ancestor's .git dir or gitdir: file, every +# ancestor that is itself a gitdir, each one's commondir — and runs `git config --get` only when one of them could +# carry the key. Each shape below first proves plain git governs the root with the hook value (presence), then +# requires the disclosure and zero firings. (O1) and (O2) also held when git resolved the dirs; (O3)-(O5) were +# missed before (the pre-check walked the LOGICAL path looking only for a `.git` entry, so a bare root and a +# symlinked root never reached git; a linked config was read no-follow and skipped) — red on the base binary. +# discloses TAG ROOT: one default-map run; rc 0, git_harden=fsmonitor-hook on stderr, the hook never ran. +discloses(){ + otag="$1"; oroot="$2" + rm -f "$TMP/MARK" + "$BIN" "$oroot" > "$TMP/o-$otag.out" 2> "$TMP/o-$otag.err" + orc=$? + if [ "$orc" -eq 0 ] && grep -q 'git_harden=fsmonitor-hook' "$TMP/o-$otag.err" && [ "$( fired )" -eq 0 ]; then + ok "(O) $otag: disclosed (git_harden=fsmonitor-hook), hook unfired, rc 0" + else + no "(O) $otag: rc=$orc disclosure=$( grep -c git_harden= "$TMP/o-$otag.err" ) fired=$( fired ): $( head -c 200 "$TMP/o-$otag.err" )" + fi + return 0 +} +# governs TAG ROOT: plain git reads the hook value at ROOT — the premise each (O) shape depends on. +governs(){ + gtag="$1"; groot="$2" + [ "$( "$REALGIT" -C "$groot" config --get core.fsmonitor 2>/dev/null )" = "$TMP/hook.sh" ] \ + && ok "(O) $gtag presence: plain git at the root reads core.fsmonitor = the hook" \ + || no "(O) $gtag presence: plain git at the root does not read the hook value — the shape cannot conclude" + return 0 +} +newrepo(){ ( mkdir -p "$1" && cd "$1" && "$REALGIT" init -q && printf 'int f;\n' > f.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm init ) >/dev/null 2>&1; } + +# (O1) an invalid .git directory in the root: git skips it and the walk goes on to the hooked parent repo. +FO1="$TMP/o1"; cp -R "$FX" "$FO1"; mkdir -p "$FO1/sub/.git" +governs "O1-invalid-dotgit" "$FO1/sub" +discloses "O1-invalid-dotgit" "$FO1/sub" +# (O2) a commondir planted in an ordinary .git: git takes the shared config from the dir it names. +newrepo "$TMP/o2"; newrepo "$TMP/o2common"; "$REALGIT" -C "$TMP/o2common" config core.fsmonitor "$TMP/hook.sh" +printf '../../o2common/.git\n' > "$TMP/o2/.git/commondir" +grep -qiE 'fsmonitor|include' "$TMP/o2/.git/config" \ + && no "(O) O2-commondir fixture: o2/.git/config itself spells the key — the arm would not test the commondir" \ + || ok "(O) O2-commondir fixture: o2/.git/config spells neither fsmonitor nor include" +governs "O2-commondir" "$TMP/o2" +discloses "O2-commondir" "$TMP/o2" +# (O3) a bare repository as the root: the root is the gitdir itself. +( "$REALGIT" init -q --bare "$TMP/o3.git" && "$REALGIT" -C "$TMP/o3.git" config core.fsmonitor "$TMP/hook.sh" ) >/dev/null 2>&1 +governs "O3-bare" "$TMP/o3.git" +discloses "O3-bare" "$TMP/o3.git" +# (O4) a symlink to a subdirectory of the hooked checkout: git walks the PHYSICAL path. +ln -s "$FX/sub" "$TMP/o4link" +governs "O4-symlinked-root" "$TMP/o4link" +discloses "O4-symlinked-root" "$TMP/o4link" +# (O5) .git/config is a symlink to the hooked file: git reads through it; the no-follow scan must not call it clean. +newrepo "$TMP/o5"; cp "$TMP/o5/.git/config" "$TMP/o5.cfg" +printf '[core]\n\tfsmonitor = %s\n' "$TMP/hook.sh" >> "$TMP/o5.cfg" +rm -f "$TMP/o5/.git/config"; ln -s "$TMP/o5.cfg" "$TMP/o5/.git/config" +[ -L "$TMP/o5/.git/config" ] && ok "(O) O5-symlinked-config fixture: .git/config is a symlink" || no "(O) O5-symlinked-config fixture: .git/config is not a symlink" +governs "O5-symlinked-config" "$TMP/o5" +discloses "O5-symlinked-config" "$TMP/o5" + +# ── (Q) no stale answer: a config that gains the key between two calls is hardened on the second ────────────── +# Nothing about the scan is kept across processes; this pins it. The first (warm-cache) call sees a plain repo; then +# the config gains the hook, directly and (on a third call) only through an include. +newrepo "$TMP/q" +"$BIN" "$TMP/q" >/dev/null 2> "$TMP/q1.err"; rc1=$? +[ "$rc1" -eq 0 ] && ! grep -q 'git_harden=' "$TMP/q1.err" \ + && ok "(Q) call 1 on the plain repo: rc 0, no disclosure (nothing to neutralise)" \ + || no "(Q) call 1 on the plain repo: rc=$rc1, disclosure=$( grep -c git_harden= "$TMP/q1.err" )" +"$REALGIT" -C "$TMP/q" config core.fsmonitor "$TMP/hook.sh" +governs "Q-gains-key" "$TMP/q" +discloses "Q-gains-key (call 2, same repo, warm cache)" "$TMP/q" +"$REALGIT" -C "$TMP/q" config --unset core.fsmonitor; "$REALGIT" -C "$TMP/q" config include.path "$TMP/hidden.inc" +governs "Q-gains-include" "$TMP/q" +discloses "Q-gains-include (call 3, the key only behind include.path)" "$TMP/q" + +# ── (P) the cost: no git child for a root whose config cannot carry the key, counted through a PATH shim ────── +# Timing-free: the startup scan used to run `git rev-parse --absolute-git-dir --git-common-dir` once per root on +# EVERY call. Control: with GIT_DIR set the filesystem cannot speak for git, so the same shim must see that child. +newrepo "$TMP/p" +mkdir -p "$TMP/shimp" +cat > "$TMP/shimp/git" <> "$TMP/shimp.log" +exec "$REALGIT" "\$@" +EOF +chmod +x "$TMP/shimp/git" +rm -f "$TMP/shimp.log"; PATH="$TMP/shimp:$PATH" "$BIN" "$TMP/p" >/dev/null 2>&1; prc=$? +PTOTAL="$( wc -l < "$TMP/shimp.log" 2>/dev/null | tr -d ' ' )"; PTOTAL="${PTOTAL:-0}" +PRES="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimp.log" 2>/dev/null )"; PRES="${PRES:-0}" +PCFG="$( grep -c 'config --get core.fsmonitor' "$TMP/shimp.log" 2>/dev/null )"; PCFG="${PCFG:-0}" +[ "$prc" -eq 0 ] && [ "$PTOTAL" -ge 1 ] \ + && ok "(P) presence: rc 0 and the shim logged $PTOTAL git child(ren) for the default map — it sees ripwire's git" \ + || no "(P) presence: rc=$prc, $PTOTAL git children logged — the arm cannot conclude" +[ "$PRES" -eq 0 ] && [ "$PCFG" -eq 0 ] \ + && ok "(P) the startup trust scan started no git child (rev-parse --absolute-git-dir: 0, config --get core.fsmonitor: 0)" \ + || no "(P) the startup trust scan still spawns git on a plain repo: rev-parse --absolute-git-dir x$PRES, config --get x$PCFG" +rm -f "$TMP/shimp.log"; PATH="$TMP/shimp:$PATH" GIT_DIR="$TMP/p/.git" GIT_WORK_TREE="$TMP/p" "$BIN" "$TMP/p" >/dev/null 2>&1 +PENV="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimp.log" 2>/dev/null )"; PENV="${PENV:-0}" +[ "$PENV" -ge 1 ] \ + && ok "(P) control: with GIT_DIR set git is asked (rev-parse --absolute-git-dir x$PENV) — the count above can see that child" \ + || no "(P) control: with GIT_DIR set no rev-parse --absolute-git-dir was logged — the shim cannot see the startup child" + # ── (G) determinism ───────────────────────────────────────────────────────────────────────────────────── "$BIN" "$FX" --situ > "$TMP/situ2.out" 2>/dev/null cmp -s "$TMP/situ.out" "$TMP/situ2.out" \ From 87e6718f3196c228d542a5397e5ab84245721f3e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:50:53 -0400 Subject: [PATCH 082/162] refactor(mcp): the for twin's rows and round C refill as helpers (qd vs the merge base) Byte-identical: MCP for on 2 tasks x budget_tokens 0/300/900/2000/6000. Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 94 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index fb1f2ec19..8a83afa64 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1913,6 +1913,57 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by }; +// R2-AF (round 2, S4): the rows — the CLI twin's exact rule (verbs_for.h renderForHdrRowsXml), duplicated here rather than +// shared because main.cpp includes this file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME +// resolver (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. "" when there is no row. +inline std::string mcpForHdrRowsXml( const IngestResult& ing, const std::vector& rows, std::string_view flRootArg ) +{ + std::string x; + const std::string rootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); + const auto rel = [ & ]( std::uint32_t f ) -> std::string + { + return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ); + }; + std::vector esc; + for( const rw::ForNamedHeaderRow& row : rows ) + { + // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver + // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. + ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); + x += ""; + } + return x; +} + +// round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are scoped and +// served from the rows at the target cut, and the rows past the cut fill what the sections served before the tail ( rows, +// , , : `sectionsBytes`) leave of T — never below the cut. The tail rides on top on this twin, as it always +// did. Re-rendered only when that narrows the rows; the first render's redaction count is replaced, never added to. +template< class Render > +inline void mcpForSoftRefill( const McpForAssemblyInputs& in, const Render& renderSigs, std::string& sigsStr, rw::SigsCutReport& cut, + const rw::SigsCutContinuation* next, std::size_t sectionsBytes, + const std::optional& before, const std::optional& after ) +{ + if( in.budgetTokens == 0 || cut.pastTarget == 0 ) + { + return; + } + const std::size_t rowDrop = forSoftRowDropFill( budgetBytesForTokens( in.budgetTokens ), sectionsBytes ); + if( rowDrop >= in.rowDropBudget ) + { + return; + } + redactTakeBack( in.redact, before, after ); + cut = rw::SigsCutReport {}; + cut.continuationRequest = next; + cut.rowDropBudgetBytes = rowDrop; + sigsStr = captureXml( renderSigs ); // assembleMcpFor's own render seam (its renderToString) +} + // round C (RESERVE SECTIONS FIRST, the CLI twin's rule): the --sections= posture the stub plan reads — under budget_tokens the two // sections ride whole when they fit what the rows at the cut (and ) left of the budget (serialize.h forSoftSectionsOptIn) inline std::string_view mcpSoftSections( const McpForAssemblyInputs& in, const std::string& sigsStr, const rw::SigsCutReport& cut, @@ -1968,31 +2019,9 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation mcpSigsCut.rowDropBudgetBytes = in.rowDropBudget; // forbudget-soft: IN — the CLI twin's two budgets mcpSigsCut.pastTargetDisclosed = budgetTokens == 0; - // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule - // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this - // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver - // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. - std::string mcpHdrXml; - if( !mcpForHdrRows.empty() ) - { - const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string - { - return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); - }; - std::vector mcpHdrEsc; - for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) - { - // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver - // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - mcpHdrXml += ""; - } - } + // R2-AF (round 2, S4): first rows inside the root, right after the legend (mcpForHdrRowsXml); rendered before the rows so the + // round C refill below prices them + const std::string mcpHdrXml = mcpForHdrRowsXml( ing, mcpForHdrRows, flRootArg ); std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail's rank rows exclude these files // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); the // redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it @@ -2085,19 +2114,8 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin // before the tail ( rows, , , ) leave of T — never below the cut. The tail rides on top here, // as it always did on this twin. Re-rendered only when that narrows the rows; the first render's redaction count is // replaced, never added to. - if( budgetTokens > 0 && mcpSigsCut.pastTarget > 0 ) - { - const std::size_t mcpRowDropFill = forSoftRowDropFill( budgetBytesForTokens( budgetTokens ), - mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() ); - if( mcpRowDropFill < in.rowDropBudget ) - { - redactTakeBack( redact, redactBeforeSigs, redactAfterSigs ); - mcpSigsCut = rw::SigsCutReport {}; - mcpSigsCut.continuationRequest = &mcpSigsNext; - mcpSigsCut.rowDropBudgetBytes = mcpRowDropFill; - sigsStr = renderToString( renderSigs ); - } - } + mcpForSoftRefill( in, renderSigs, sigsStr, mcpSigsCut, &mcpSigsNext, mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size(), + redactBeforeSigs, redactAfterSigs ); // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between From 31c054157b04c4c3ecd8b5e41e353cbcab6cd2ed Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 03:57:50 -0400 Subject: [PATCH 083/162] refactor(for): one rows render for the CLI and MCP twins (forpage.h forHdrRowsXml) The MCP twin duplicated the CLI render because of main.cpp's include order; forpage.h is included by both, so the one render lives there (qd duplication row). forhdrshapecheck ALL PASS. Co-Authored-By: Claude Opus 5.5 --- src/forpage.h | 22 ++++++++++++++++++++++ src/mcpverbs.h | 30 ++---------------------------- src/verbs_for.h | 20 +------------------- 3 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/forpage.h b/src/forpage.h index 6cc84857c..e21943193 100644 --- a/src/forpage.h +++ b/src/forpage.h @@ -39,6 +39,7 @@ // keeps `--expand=FILE:NAME`. The thresholds are a registered hypothesis (PLAN_OUTPUT_ROUTING_LOOP §1.5 L-N), // not a tuned number: the routing-loop ladder measures them, and a later round moves them with a measured reason. +#include "mention.h" // ForNamedHeaderRow — forHdrRowsXml below renders the resolver's rows #include "lexical.h" // LexTermEvidence — the term masks + df the BM25 pass already accumulated #include "model.h" #include "nextverb.h" // nextFlag / nextAttrXml — the ONE next= spelling @@ -632,6 +633,27 @@ inline std::string renderForFilePageXml( const IngestResult& ing, const ForFileP } +// R2-AF (round 2, S4): the rows of a --for answer (rw::forNamedHeaderRows, mention.h, resolves them) — ONE render +// for the CLI --for (verbs_for.h renderForHdrRowsXml) and the MCP `for` twin, root-relative like every other --for path. +// "" when the task names nothing with a unique partner. +inline std::string forHdrRowsXml( const IngestResult& ing, const std::vector& rows, std::string_view rootArg ) +{ + std::string x; + std::vector esc; + for( const ForNamedHeaderRow& row : rows ) + { + // local invariant: forNamedHeaderRows only ever returns fileIds it read out of ing.files itself (mention.h), so an + // out-of-range id here would mean that resolver's own contract broke — trapped where the bad index would be used + ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); + x += ""; + } + return x; +} + // ── FOLD C (forbudget-soft round C, owner decision 2026-10-14): the --for answer's carries page 0 of this page ── // One model and one renderer for the CLI --for (XML + JSON) and the MCP `for` twin. The tail lists, in this order: // 1. RANK rows (deep-tail d2, unchanged): the files of positive-score symbols NOT among the shown sigs rows, in the diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 8a83afa64..df95d4fee 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1913,32 +1913,6 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by }; -// R2-AF (round 2, S4): the rows — the CLI twin's exact rule (verbs_for.h renderForHdrRowsXml), duplicated here rather than -// shared because main.cpp includes this file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME -// resolver (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. "" when there is no row. -inline std::string mcpForHdrRowsXml( const IngestResult& ing, const std::vector& rows, std::string_view flRootArg ) -{ - std::string x; - const std::string rootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - const auto rel = [ & ]( std::uint32_t f ) -> std::string - { - return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ); - }; - std::vector esc; - for( const rw::ForNamedHeaderRow& row : rows ) - { - // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver - // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - x += ""; - } - return x; -} - // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are scoped and // served from the rows at the target cut, and the rows past the cut fill what the sections served before the tail ( rows, // , , : `sectionsBytes`) leave of T — never below the cut. The tail rides on top on this twin, as it always @@ -2019,9 +1993,9 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation mcpSigsCut.rowDropBudgetBytes = in.rowDropBudget; // forbudget-soft: IN — the CLI twin's two budgets mcpSigsCut.pastTargetDisclosed = budgetTokens == 0; - // R2-AF (round 2, S4): first rows inside the root, right after the legend (mcpForHdrRowsXml); rendered before the rows so the + // R2-AF (round 2, S4): first rows inside the root, right after the legend (forHdrRowsXml); rendered before the rows so the // round C refill below prices them - const std::string mcpHdrXml = mcpForHdrRowsXml( ing, mcpForHdrRows, flRootArg ); + const std::string mcpHdrXml = forHdrRowsXml( ing, mcpForHdrRows, flRootArg ); // forpage.h: the CLI twin's one render std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail's rank rows exclude these files // round C: the render is named so a soft target can narrow its rows past the cut once the sections are served (below); the // redaction tally around it is snapshotted so a second render replaces this one's count instead of adding to it diff --git a/src/verbs_for.h b/src/verbs_for.h index e080d093f..a11128c48 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2542,25 +2542,7 @@ inline std::string renderForHdrRowsXml( const rw::IngestResult& ing, const std:: { return {}; } - const std::string rootPrefix = rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( rootArg ); - const auto rel = [ & ]( std::uint32_t f ) -> std::string - { - return rootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], rootPrefix ) ); - }; - std::vector esc; - std::string x; - for( const rw::ForNamedHeaderRow& row : rows ) - { - // local invariant: forNamedHeaderRows only ever returns fileIds it read out of ing.files itself - // (mention.h), so an out-of-range id here would mean that resolver's own contract broke, not a - // renderer bug — worth trapping right at the point the bad index would otherwise index OOB below. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - x += ""; - } + std::string x = rw::forHdrRowsXml( ing, rows, rootArg ); // forpage.h: the one render, shared with the MCP twin // runForLens charges x.size() in every budget sum and emits x itself, so "no row" and "no bytes" must agree ENSURES( !x.empty(), "renderForHdrRowsXml: rows resolved but nothing rendered" ); return x; From f6127221f18bfe15a95eabbaea9983d242408aeb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 04:06:19 -0400 Subject: [PATCH 084/162] =?UTF-8?q?test:=20re-pin=20printffmtparity=20help?= =?UTF-8?q?=5Fall=20=E2=80=94=20the=20--quality-delta=3DA..B=20row's=20--q?= =?UTF-8?q?uality-ack=20sentence=20(review=20C2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intended text change, not a conversion: the help now says --quality-ack skips the projection and only a real memory stop refuses it. Co-Authored-By: Claude Opus 5.5 --- test/printf_parity.manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 87103e9a2..c58c785ef 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -38,5 +38,5 @@ safe_delete 0 e3535cdf695ad1d3e9ffbec44d529478059317b226fb60b2dfffaebc90b44218 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 3a8337704c1a55c31816e1bd43c5389186062503f35f1ecac548ca52ff37e84c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 8d48218a0cf5e7e2e7a0d907a539508c50c742f41efad21047bee7cf1fac3f92 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 d4e5d18cfc2c83acb4bb68d33e3ca89a37abb7a0e72b749c043e286b31e19589 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 0bb078bfb85a3965af9b81412397d601f13fce58 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 04:09:03 -0400 Subject: [PATCH 085/162] test(forrankordercheck): (3) also allows the growth of the tail's legend clause (FOLD C) Co-Authored-By: Claude Opus 5.5 --- test/forrankordercheck.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 4e2f470aa..364bd66fe 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -201,6 +201,9 @@ rows = sorted( ( int( re.search( r"\br=\"([0-9]+)\"", d.group( 1 ) ).group( 1 ) past = sum( b for _, b in rows[ len( rows ) - int( k.group( 1 ) ): ] ) if k else 0 h = re.search( r"", s, re.S ) # D-3: the compact is no longer cut at its 1,000 B allowance page = sum( len( t.encode() ) for t in re.findall( r"", s ) ) # FOLD C: the tail page rows +for pat, was in ( ( r"; tail: [^\]]*?budget-trimmed row\)", 391 ), ( r"; t p= file outside sigs \(weaker\)[^;]*?r= rank \(gap = trimmed\)", 57 ) ): + lm = re.search( pat, s ) # …and the growth of the clause that now defines them (full 391 B / compact 57 B before) + page += max( 0, len( lm.group( 0 ).encode() ) - was ) if lm else 0 print( past + page + ( max( 0, len( h.group( 0 ).encode() ) - 1000 ) if h else 0 ) )' )" pct="$( python3 -c "print( '%+.2f' % ( ( $now - $past - $base ) * 100.0 / $base ) )" )" printf ' q%-2d %-28s %5d → %5d - past-target/hops %5d (%s%%) %s\n' "$i" "'$q'" "$base" "$now" "$past" "$pct" "$marker" From 868b0f235b0f0506df5523a356bc7acaa23040b8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 04:23:08 -0400 Subject: [PATCH 086/162] githarden: split the discovery superset into one-level and one-gitdir helpers discoveryConfigSuperset measured complexity 39 / verbosity 77 in --quality-delta. The walk now calls addGitDirsAt per ancestor and addGitDirConfigFiles per gitdir; each returns false on doubt, which still sends the root to git. No behaviour change (githardencheck O/Q/P green). Co-Authored-By: Claude Opus 5.5 --- src/githarden.h | 118 +++++++++++++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/src/githarden.h b/src/githarden.h index e118c29ed..43d4e3421 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -258,6 +258,14 @@ inline bool gitDiscoveryNamedByEnvironment() noexcept return false; } +inline void addUniquePath( std::vector& out, const std::filesystem::path& p ) +{ + if( std::find( out.begin(), out.end(), p ) == out.end() ) + { + out.push_back( p ); + } +} + // Both readings of a pointer file's path, each made absolute against `base`, appended to `out` once each. inline void addPointerReadings( std::string_view exactRest, std::string_view looseRest, const std::filesystem::path& base, std::vector& out ) @@ -278,10 +286,7 @@ inline void addPointerReadings( std::string_view exactRest, std::string_view loo { p = base / p; } - if( std::find( out.begin(), out.end(), p ) == out.end() ) - { - out.push_back( std::move( p ) ); - } + addUniquePath( out, p ); } } @@ -291,6 +296,66 @@ inline bool statAbsentOrOk( const std::error_code& ec ) noexcept return !ec || ec == std::errc::no_such_file_or_directory || ec == std::errc::not_a_directory; } +// One level of git's discovery walk: the gitdirs directory `dir` could contribute — its `.git` directory, the +// target(s) of a `.git` gitdir: file, and `dir` itself when it holds a HEAD. false = doubt (a stat error that is not +// "absent", or a `.git` that is a symlink to something the no-follow reader cannot open). +inline bool addGitDirsAt( const std::filesystem::path& dir, std::vector& gitDirs ) +{ + std::error_code ec; + const std::filesystem::path dotGit = dir / ".git"; + const std::filesystem::file_status st = std::filesystem::status( dotGit, ec ); // follows a link, as git's stat does + if( !statAbsentOrOk( ec ) ) + { + return false; + } + ec.clear(); + if( std::filesystem::is_directory( st ) ) + { + addUniquePath( gitDirs, dotGit ); + } + else if( std::filesystem::exists( std::filesystem::symlink_status( dotGit, ec ) ) ) + { + const std::optional head = pathguard::readRegularFileNoFollow( dotGit.string() ); + if( !head && pathguard::isSymlink( dotGit.string() ) ) + { + return false; + } + if( head && head->starts_with( "gitdir:" ) ) + { + const std::string_view text( *head ); + addPointerReadings( text.substr( text.starts_with( "gitdir: " ) ? 8 : 7 ), text.substr( 7 ), dir, gitDirs ); + } + } + ec.clear(); + if( std::filesystem::exists( std::filesystem::symlink_status( dir / "HEAD", ec ) ) ) + { + addUniquePath( gitDirs, dir ); // a bare repository, or a directory inside a gitdir + } + return true; +} + +// The local config files one gitdir could govern with: its config and config.worktree, and the same pair in every +// reading of its `commondir`. false = doubt (a commondir behind a symlink, which git reads through). +inline bool addGitDirConfigFiles( const std::filesystem::path& gitDir, std::vector& files ) +{ + files.push_back( gitDir / "config" ); + files.push_back( gitDir / "config.worktree" ); + const std::string commondirFile = ( gitDir / "commondir" ).string(); + const std::optional common = pathguard::readRegularFileNoFollow( commondirFile ); + if( !common ) + { + return !pathguard::isSymlink( commondirFile ); + } + std::vector commonDirs; + addPointerReadings( *common, *common, gitDir, commonDirs ); + for( const std::filesystem::path& commonDir : commonDirs ) + { + files.push_back( commonDir / "config" ); + files.push_back( commonDir / "config.worktree" ); + } + return true; +} + inline std::optional> discoveryConfigSuperset( const std::string& root ) { if( gitDiscoveryNamedByEnvironment() ) @@ -304,38 +369,12 @@ inline std::optional> discoveryConfigSuperset return std::nullopt; } std::vector gitDirs; - for( ;; ) + for( ;; ) // every ancestor-or-self: never stop at the first `.git`, never judge which one git accepts { - const std::filesystem::path dotGit = dir / ".git"; - const std::filesystem::file_status st = std::filesystem::status( dotGit, ec ); // follows a link, as git's stat does - if( !statAbsentOrOk( ec ) ) + if( !addGitDirsAt( dir, gitDirs ) ) { return std::nullopt; } - ec.clear(); - if( std::filesystem::is_directory( st ) ) - { - if( std::find( gitDirs.begin(), gitDirs.end(), dotGit ) == gitDirs.end() ) { gitDirs.push_back( dotGit ); } - } - else if( std::filesystem::exists( std::filesystem::symlink_status( dotGit, ec ) ) ) - { - const std::optional head = pathguard::readRegularFileNoFollow( dotGit.string() ); - if( !head && pathguard::isSymlink( dotGit.string() ) ) - { - return std::nullopt; - } - if( head && head->starts_with( "gitdir:" ) ) - { - const std::string_view text( *head ); - addPointerReadings( text.substr( text.starts_with( "gitdir: " ) ? 8 : 7 ), text.substr( 7 ), dir, gitDirs ); - } - } - ec.clear(); - if( std::filesystem::exists( std::filesystem::symlink_status( dir / "HEAD", ec ) ) ) - { - if( std::find( gitDirs.begin(), gitDirs.end(), dir ) == gitDirs.end() ) { gitDirs.push_back( dir ); } - } - ec.clear(); const std::filesystem::path parent = dir.parent_path(); if( parent == dir || parent.empty() ) { @@ -347,20 +386,7 @@ inline std::optional> discoveryConfigSuperset files.reserve( gitDirs.size() * 4 ); for( const std::filesystem::path& gitDir : gitDirs ) { - files.push_back( gitDir / "config" ); - files.push_back( gitDir / "config.worktree" ); - const std::string commondirFile = ( gitDir / "commondir" ).string(); - if( const std::optional common = pathguard::readRegularFileNoFollow( commondirFile ) ) - { - std::vector commonDirs; - addPointerReadings( *common, *common, gitDir, commonDirs ); - for( const std::filesystem::path& commonDir : commonDirs ) - { - files.push_back( commonDir / "config" ); - files.push_back( commonDir / "config.worktree" ); - } - } - else if( pathguard::isSymlink( commondirFile ) ) + if( !addGitDirConfigFiles( gitDir, files ) ) { return std::nullopt; } From dcc7c8fe7393175f9e1545b009aa754f616e29c0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 04:27:50 -0400 Subject: [PATCH 087/162] fix(for): kForFileTailShownCap stays the rank rows' literal 24; kForFileTailPageRows 40; generated docs The capsweep and the limits generator read cap literals: the summed constant read as GONE. The renderers serve every row computeFileTail collected (<= 24 + 40). LIMITS.md / TUNING.md regenerated, README cap count 253 -> 254. Byte-identical on 5 probe argv (XML, JSON, MCP). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/LIMITS.md | 13 +++++++------ docs/TUNING.md | 16 ++++++++-------- src/forpage.h | 6 +++--- src/mcpverbs.h | 4 ++-- src/serialize.h | 5 ++--- src/verbs_for.h | 8 ++++---- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index bf380fff8..7bfedf631 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 253 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 254 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index b83ac804a..62387c578 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 253 | 98 | 149 | **104** | +| 254 | 98 | 150 | **104** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 253 + 7 is the 260 constants this generator parses out of `src/`. +are not counted as caps, and 254 + 7 is the 261 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 253 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 114 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 254 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 115 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 98 files that declare a cap — the 253 caps counted above, and no parameter. +One table for each of the 98 files that declare a cap — the 254 caps counted above, and no parameter. ### `src/abicheck.h` @@ -893,7 +893,8 @@ Discloses: `capped`, `calls_capped`, `entries_capped`, `files_capped`, `importer | `kForAutoBodyBudgetBytes` | `6000` | — | — | | `kForCapTailSigBytes` | `96` | — | — | | `kForCompactSurfaceBudgetBytes` | `1000` | — | — | -| `kForFileTailShownCap` | `24` | — | — | +| `kForFileTailPageRows` | `40` | — | FOLD C: page 0 of the file page (forpage.h kForPageRowsDefault) | +| `kForFileTailShownCap` | `24` | — | the rank rows (deep-tail d2): files outside the shown head | | `kForLensDefaultTopN` | `40` | — | — | | `kForPayloadBudgetBytes` | `7500` | — | — | | `kLegoClosureRowCap` | `200` | OUTPUT | — | diff --git a/docs/TUNING.md b/docs/TUNING.md index 8836074e1..e328dec7f 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 161 | 160 | 112 | 12 | **37** | 75 | +| 162 | 161 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **161 cap -declarations** under **160 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **162 cap +declarations** under **161 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 160 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **37 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 161 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **38 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, `kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+31 more). 111 + 12 + 37 = 160, and `emit` refuses to render a partition that does not add up. The ++32 more). 111 + 12 + 38 = 161, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 161" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 162" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 161-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 162-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/src/forpage.h b/src/forpage.h index e21943193..2df8511e5 100644 --- a/src/forpage.h +++ b/src/forpage.h @@ -658,7 +658,7 @@ inline std::string forHdrRowsXml( const IngestResult& ing, const std::vector`). H2H-Graft lane 2: a row the ranked head did not show lands here. +// kForFileTailShownCap; paths only (``). H2H-Graft lane 2: a row the ranked head did not show lands here. // 2. PAGE rows (FOLD C): page 0 of `--for=TASK --limit=40` (computeForFilePage, the same ranking and term evidence), // minus the files already listed above, each with sym= (its top symbols by lens rank). The measured arm M4 appended // this page to the answer: 6 of 6 predicted rows reached an R0-missing item; the rank rows stay because two graded rows @@ -698,7 +698,7 @@ inline void fileTailRankRows( FileTail& out, std::vector& listed, const In order[i] = i; } sortutil::radixSortByScoreDescId( order, rank ); - for( std::size_t k = 0; k < S && out.paths.size() < kForFileTailRankCap && rank[ order[k] ] > 0.0f; ++k ) // the zero-score tail is contiguous + for( std::size_t k = 0; k < S && out.paths.size() < kForFileTailShownCap && rank[ order[k] ] > 0.0f; ++k ) // the zero-score tail is contiguous { const std::uint32_t f = ing.symbols[ order[k] ].fileId; if( f >= F || listed[f] || inHead[f] ) @@ -836,7 +836,7 @@ inline std::string renderFileTailJson( const FileTail& t, std::size_t shownCap ) // The explicit-budget fill (reserve first, forbudget-soft round C): the FEWEST rows whose rendered size reaches // `shareBytes` — the tail keeps at least its share, so its last row may cross it — or every collected row. `render` -// is the dialect's renderer at a row count. Deterministic; at most kForFileTailShownCap + 1 renders. +// is the dialect's renderer at a row count. Deterministic; at most kForFileTailShownCap + kForFileTailPageRows + 1 renders. template< class Render > inline std::size_t fileTailShownToFill( const FileTail& t, std::size_t shareBytes, const Render& render ) { diff --git a/src/mcpverbs.h b/src/mcpverbs.h index df95d4fee..e360d637f 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2176,8 +2176,8 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin // from budget_tokens but /), and a cut tail carries the page after its last row as next=. { std::vector tailEsc; - const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, in.evidence, mcpShownIds, flRootArg, task ), - kForFileTailShownCap, tailEsc ); + const FileTail mcpTail = computeFileTail( ing, lensRank, in.evidence, mcpShownIds, flRootArg, task ); + const std::string tailStr = renderFileTailXml( mcpTail, mcpTail.paths.size(), tailEsc ); // every row computeFileTail collected std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } rw::emitRaw( mem, "" ); diff --git a/src/serialize.h b/src/serialize.h index f9ea625f2..ea8039187 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1102,15 +1102,14 @@ inline constexpr std::size_t kForAnchorBodyBudgetBytes = 22800; // before the fold (arm M4, the page appended to the answer): 6 of 6 predicted rows reached an R0-missing item, none lost // one. The model and its renderers live in forpage.h (they need the page's ranking and its next= composer); the constants // and the legend stay here, where the --for header reads them. -// • DEFAULT regime: kForFileTailRankCap rank rows + kForFileTailPageRows page rows; the bytes ride ON TOP of the +// • DEFAULT regime: kForFileTailShownCap rank rows + kForFileTailPageRows page rows; the bytes ride ON TOP of the // sig/body budgets and are measured into est_tokens. // • EXPLICIT --token-budget (a soft target): the tail is a trailing section funded first at its share — the // bytes left once the rows at the target cut and the sections before it are paid (runForLens) — and fills // that share (its last row may cross it); every cut carries next=. kForFileTailShellReserve sizes the // empty shell ``. -inline constexpr std::size_t kForFileTailRankCap = 24; // the rank rows (deep-tail d2): files outside the shown head +inline constexpr std::size_t kForFileTailShownCap = 24; // the rank rows (deep-tail d2): files outside the shown head inline constexpr std::size_t kForFileTailPageRows = 40; // FOLD C: page 0 of the file page (forpage.h kForPageRowsDefault) -inline constexpr std::size_t kForFileTailShownCap = kForFileTailRankCap + kForFileTailPageRows; // the default row cap inline constexpr std::size_t kForFileTailShellReserve = 48; // The legend clause defining BOTH deep-tail surfaces (the r= rank fact and the element) — a named diff --git a/src/verbs_for.h b/src/verbs_for.h index a11128c48..a01b276e7 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1313,7 +1313,7 @@ inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t { if( tokenBudget == 0 ) { - return "," + rw::renderFileTailJson( tail, rw::kForFileTailShownCap ); + return "," + rw::renderFileTailJson( tail, tail.paths.size() ); // every row computeFileTail collected } const auto render = [ & ]( std::size_t shown ) { return "," + rw::renderFileTailJson( tail, shown ); }; return render( rw::fileTailShownToFill( tail, shareBytes, render ) ); @@ -1472,7 +1472,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // row cap here (the degrade path below needs a stanza before any budget is knowable); the normal path // re-fits it after the sigs render (forLensJsonTailStanza above) — residual-funded, sigs untouched: // the reservation feeding sigsBudget above deliberately does NOT include these bytes. - std::string tailStanza = "," + renderFileTailJson( *in.fileTail, kForFileTailShownCap ); + std::string tailStanza = "," + renderFileTailJson( *in.fileTail, in.fileTail->paths.size() ); bool sigsCapped = false; // the LADDER's verdict: decides the budget_bytes= stanza rw::SigsCutReport sigsCut; // cut-fix lane A: what the array cut (gate or ladder) — "capped" and the sigs_* keys @@ -2522,7 +2522,7 @@ inline std::string renderForFileTailXml( const rw::FileTail& tail, std::size_t t std::vector esc; if( tokenBudget == 0 ) { - return rw::renderFileTailXml( tail, rw::kForFileTailShownCap, esc ); + return rw::renderFileTailXml( tail, tail.paths.size(), esc ); // every row computeFileTail collected } const std::size_t share = std::max( bundleBudget > spentBytes ? bundleBudget - spentBytes : 0u, rw::kForFileTailShellReserve ); const auto render = [ & ]( std::size_t shown ) { return rw::renderFileTailXml( tail, shown, esc ); }; @@ -3813,7 +3813,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co } } - // ── DEEP-TAIL d2: render the file-grain tail (FOLD C: page 0 of the file page; serialize.h kForFileTailShownCap) ── + // ── DEEP-TAIL d2: render the file-grain tail (FOLD C: page 0 of the file page; serialize.h kForFileTailShownCap / kForFileTailPageRows) ── // Rendered after the bodies decision. Default regime: the full 40 rows ride on top — the sigs and the bodies are // byte-identical to a tail-less bundle by construction (nothing above charges these bytes). A soft target: the tail is // the last trailing section, funded from what the rows AT THE CUT and the sections before it left (reserve first). From ea45ac38028d58ffc3fc2a966cabb4eeb362086a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 04:37:42 -0400 Subject: [PATCH 088/162] githardencheck: report the (O5) fixture check through if/else (gateexitcheck G2) Co-Authored-By: Claude Opus 5.5 --- test/githardencheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/githardencheck.sh b/test/githardencheck.sh index 83b43aec3..408b33c43 100755 --- a/test/githardencheck.sh +++ b/test/githardencheck.sh @@ -485,7 +485,7 @@ discloses "O4-symlinked-root" "$TMP/o4link" newrepo "$TMP/o5"; cp "$TMP/o5/.git/config" "$TMP/o5.cfg" printf '[core]\n\tfsmonitor = %s\n' "$TMP/hook.sh" >> "$TMP/o5.cfg" rm -f "$TMP/o5/.git/config"; ln -s "$TMP/o5.cfg" "$TMP/o5/.git/config" -[ -L "$TMP/o5/.git/config" ] && ok "(O) O5-symlinked-config fixture: .git/config is a symlink" || no "(O) O5-symlinked-config fixture: .git/config is not a symlink" +if [ -L "$TMP/o5/.git/config" ]; then ok "(O) O5-symlinked-config fixture: .git/config is a symlink"; else no "(O) O5-symlinked-config fixture: .git/config is not a symlink"; fi governs "O5-symlinked-config" "$TMP/o5" discloses "O5-symlinked-config" "$TMP/o5" From c16eee8745bb96877a9023ff6e236cc3b0db3deb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 05:08:04 -0400 Subject: [PATCH 089/162] githarden: a NUL inside a .git gitdir file or commondir defers to git git reads both pointer paths as C strings, cut at the first NUL; the filesystem walk kept the NUL in the path, opened nothing and called the candidate clean, so the disclosure, env layer and doctor trust row were lost (the per-child -c core.fsmonitor=false still held). Treat a NUL in either file as doubt, which falls back to git's own resolution. githardencheck: (O6) NUL in a gitdir file and (O7) NUL in commondir, red on ea45ac38; (O8) a symlinked .git pointer file exercises the git fallback and counts its rev-parse child through a shim. Co-Authored-By: Claude Opus 5.5 --- src/githarden.h | 19 ++++++++++++++----- test/githardencheck.sh | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/githarden.h b/src/githarden.h index 43d4e3421..3d0221981 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -28,7 +28,7 @@ // `.git` dir or `gitdir:` file, every ancestor that is itself a gitdir, and each one's `commondir`), so the probe // and disclosure run for the governing repo whatever its depth, with no git child unless a file could carry the // key. Where the filesystem cannot speak for git (GIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE set, a root that does not -// canonicalise, a pointer file behind a symlink), `gitResolvedConfigDirs` asks git itself +// canonicalise, a pointer file behind a symlink or holding a NUL), `gitResolvedConfigDirs` asks git itself // (`rev-parse --absolute-git-dir --git-common-dir`, the hook disabled on that probe's OWN command line). // // The trust decision is PER PROCESS: a long-lived --mcp server probes its roots once at start. A config edited @@ -242,8 +242,9 @@ inline std::vector gitResolvedConfigDirs( const std::stri // a value. Pointer files are read both as git reads them (the rest of the file, trailing CR/LF dropped, relative // to D or to the gitdir) and as the first line trimmed of blanks; where the two differ both are kept. // nullopt = the filesystem cannot speak for git here, and the caller asks git (gitResolvedConfigDirs) as before: -// an environment-named repository, a root that does not canonicalise, a stat that fails other than "absent", or a -// pointer file (.git, commondir) that is a symlink — git reads through it, the no-follow reader does not. +// an environment-named repository, a root that does not canonicalise, a stat that fails other than "absent", a +// pointer file (.git, commondir) that is a symlink — git reads through it, the no-follow reader does not — or a +// pointer file with a NUL byte in it (git cuts the path there). // test/githardencheck.sh (O) pins the shapes (an invalid .git skipped, a planted commondir, a bare root, a // symlinked root) and (P) counts the git children: none for a root whose config cannot carry the key. inline bool gitDiscoveryNamedByEnvironment() noexcept @@ -298,7 +299,7 @@ inline bool statAbsentOrOk( const std::error_code& ec ) noexcept // One level of git's discovery walk: the gitdirs directory `dir` could contribute — its `.git` directory, the // target(s) of a `.git` gitdir: file, and `dir` itself when it holds a HEAD. false = doubt (a stat error that is not -// "absent", or a `.git` that is a symlink to something the no-follow reader cannot open). +// "absent", a `.git` that is a symlink to something the no-follow reader cannot open, or a `.git` file holding a NUL). inline bool addGitDirsAt( const std::filesystem::path& dir, std::vector& gitDirs ) { std::error_code ec; @@ -320,6 +321,10 @@ inline bool addGitDirsAt( const std::filesystem::path& dir, std::vectorfind( '\0' ) != std::string::npos ) + { + return false; // git reads the path as a C string, cut at the first NUL — let git answer + } if( head && head->starts_with( "gitdir:" ) ) { const std::string_view text( *head ); @@ -335,7 +340,7 @@ inline bool addGitDirsAt( const std::filesystem::path& dir, std::vector& files ) { files.push_back( gitDir / "config" ); @@ -346,6 +351,10 @@ inline bool addGitDirConfigFiles( const std::filesystem::path& gitDir, std::vect { return !pathguard::isSymlink( commondirFile ); } + if( common->find( '\0' ) != std::string::npos ) + { + return false; // as for a `.git` file: git cuts the path at the NUL — let git answer + } std::vector commonDirs; addPointerReadings( *common, *common, gitDir, commonDirs ); for( const std::filesystem::path& commonDir : commonDirs ) diff --git a/test/githardencheck.sh b/test/githardencheck.sh index 408b33c43..a354eee63 100755 --- a/test/githardencheck.sh +++ b/test/githardencheck.sh @@ -55,8 +55,10 @@ # detects a stall. # (O) the startup scan finds the governing config from the filesystem alone, a superset of git's discovery: an # invalid .git dir skipped for the hooked parent, a commondir planted in an ordinary .git, a bare root, a -# symlinked root (git walks the physical path), a symlinked .git/config (git reads through it). Each shape -# first proves plain git reads the hook value there. +# symlinked root (git walks the physical path), a symlinked .git/config (git reads through it), a NUL byte +# inside a .git gitdir file or a commondir file (git cuts the path at the NUL, so the scan defers to git), and a +# .git pointer file behind a symlink (the scan defers to git; the shim sees that child). Each shape first proves +# plain git reads the hook value there. # (Q) no stale answer: a repo whose config gains the hook (directly, then only via include.path) between two # calls is disclosed on the next call. # (P) the cost, counted through a PATH shim: a plain repo's run starts no startup-scan git child; with GIT_DIR @@ -488,6 +490,35 @@ rm -f "$TMP/o5/.git/config"; ln -s "$TMP/o5.cfg" "$TMP/o5/.git/config" if [ -L "$TMP/o5/.git/config" ]; then ok "(O) O5-symlinked-config fixture: .git/config is a symlink"; else no "(O) O5-symlinked-config fixture: .git/config is not a symlink"; fi governs "O5-symlinked-config" "$TMP/o5" discloses "O5-symlinked-config" "$TMP/o5" +# (O6) a .git gitdir: file with a NUL inside the path: git reads it as a C string, cut at the NUL, so it governs with +# the hooked gitdir before the NUL; a scan that keeps the NUL in the path opens nothing and must not call it clean. +newrepo "$TMP/o6src"; "$REALGIT" -C "$TMP/o6src" config core.fsmonitor "$TMP/hook.sh" +mkdir -p "$TMP/o6" && printf 'int f;\n' > "$TMP/o6/f.c" && printf 'gitdir: %s/o6src/.git\000junk\n' "$TMP" > "$TMP/o6/.git" +if od -An -c "$TMP/o6/.git" | grep -q '\\0'; then ok "(O) O6-nul-gitdir fixture: o6/.git holds a NUL byte"; else no "(O) O6-nul-gitdir fixture: o6/.git holds no NUL byte"; fi +governs "O6-nul-gitdir" "$TMP/o6" +discloses "O6-nul-gitdir" "$TMP/o6" +# (O7) a commondir with a NUL inside the path: the same cut, for the shared config. +newrepo "$TMP/o7"; newrepo "$TMP/o7common"; "$REALGIT" -C "$TMP/o7common" config core.fsmonitor "$TMP/hook.sh" +printf '%s/o7common/.git\000junk\n' "$TMP" > "$TMP/o7/.git/commondir" +if od -An -c "$TMP/o7/.git/commondir" | grep -q '\\0'; then ok "(O) O7-nul-commondir fixture: commondir holds a NUL byte"; else no "(O) O7-nul-commondir fixture: commondir holds no NUL byte"; fi +governs "O7-nul-commondir" "$TMP/o7" +discloses "O7-nul-commondir" "$TMP/o7" +# (O8) the root's .git is a symlink to a gitdir: file: git reads through it; the scan defers to git, so the disclosure +# comes from the git-resolved fallback — the shim must see its rev-parse child. +newrepo "$TMP/o8src"; "$REALGIT" -C "$TMP/o8src" config core.fsmonitor "$TMP/hook.sh" +printf 'gitdir: %s/o8src/.git\n' "$TMP" > "$TMP/o8.ptr" +mkdir -p "$TMP/o8" && printf 'int f;\n' > "$TMP/o8/f.c" && ln -s "$TMP/o8.ptr" "$TMP/o8/.git" +if [ -L "$TMP/o8/.git" ]; then ok "(O) O8-symlinked-pointer fixture: o8/.git is a symlink"; else no "(O) O8-symlinked-pointer fixture: o8/.git is not a symlink"; fi +governs "O8-symlinked-pointer" "$TMP/o8" +discloses "O8-symlinked-pointer" "$TMP/o8" +mkdir -p "$TMP/shimo8" +printf '#!/bin/bash\nprintf "%%s\\n" "$*" >> "%s/shimo8.log"\nexec "%s" "$@"\n' "$TMP" "$REALGIT" > "$TMP/shimo8/git" +chmod +x "$TMP/shimo8/git" +rm -f "$TMP/shimo8.log"; PATH="$TMP/shimo8:$PATH" "$BIN" "$TMP/o8" >/dev/null 2>&1; o8rc=$? +O8RES="$( grep -c 'rev-parse --absolute-git-dir' "$TMP/shimo8.log" 2>/dev/null )"; O8RES="${O8RES:-0}" +[ "$o8rc" -eq 0 ] && [ "$O8RES" -ge 1 ] \ + && ok "(O) O8-symlinked-pointer: the scan deferred to git (rev-parse --absolute-git-dir x$O8RES), rc 0" \ + || no "(O) O8-symlinked-pointer: rc=$o8rc, rev-parse --absolute-git-dir x$O8RES — the git fallback was not taken" # ── (Q) no stale answer: a config that gains the key between two calls is hardened on the second ────────────── # Nothing about the scan is kept across processes; this pins it. The first (warm-cache) call sees a plain repo; then From 7d05c7cd417c8467ed6f074139bc63fba9dfa664 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 05:16:30 -0400 Subject: [PATCH 090/162] githarden: read a .git gitdir file in its own helper addGitDirsAt went over the complexity bar (16 > 15) with the NUL check; addGitFileTargets now holds the pointer-file reading. Behaviour-identical. Co-Authored-By: Claude Opus 5.5 --- src/githarden.h | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/githarden.h b/src/githarden.h index 3d0221981..4b7017ddd 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -297,9 +297,31 @@ inline bool statAbsentOrOk( const std::error_code& ec ) noexcept return !ec || ec == std::errc::no_such_file_or_directory || ec == std::errc::not_a_directory; } +// The target(s) a `.git` file at `dir` names with `gitdir:`. false = doubt (a `.git` that is a symlink to something +// the no-follow reader cannot open, or a file holding a NUL: git reads the path as a C string, cut at the first NUL). +inline bool addGitFileTargets( const std::filesystem::path& dir, const std::filesystem::path& dotGit, + std::vector& gitDirs ) +{ + const std::optional head = pathguard::readRegularFileNoFollow( dotGit.string() ); + if( !head ) + { + return !pathguard::isSymlink( dotGit.string() ); + } + if( head->find( '\0' ) != std::string::npos ) + { + return false; + } + if( head->starts_with( "gitdir:" ) ) + { + const std::string_view text( *head ); + addPointerReadings( text.substr( text.starts_with( "gitdir: " ) ? 8 : 7 ), text.substr( 7 ), dir, gitDirs ); + } + return true; +} + // One level of git's discovery walk: the gitdirs directory `dir` could contribute — its `.git` directory, the // target(s) of a `.git` gitdir: file, and `dir` itself when it holds a HEAD. false = doubt (a stat error that is not -// "absent", a `.git` that is a symlink to something the no-follow reader cannot open, or a `.git` file holding a NUL). +// "absent", or a `.git` file addGitFileTargets cannot speak for). inline bool addGitDirsAt( const std::filesystem::path& dir, std::vector& gitDirs ) { std::error_code ec; @@ -314,22 +336,9 @@ inline bool addGitDirsAt( const std::filesystem::path& dir, std::vector head = pathguard::readRegularFileNoFollow( dotGit.string() ); - if( !head && pathguard::isSymlink( dotGit.string() ) ) - { - return false; - } - if( head && head->find( '\0' ) != std::string::npos ) - { - return false; // git reads the path as a C string, cut at the first NUL — let git answer - } - if( head && head->starts_with( "gitdir:" ) ) - { - const std::string_view text( *head ); - addPointerReadings( text.substr( text.starts_with( "gitdir: " ) ? 8 : 7 ), text.substr( 7 ), dir, gitDirs ); - } + return false; } ec.clear(); if( std::filesystem::exists( std::filesystem::symlink_status( dir / "HEAD", ec ) ) ) From 754925abc138a7414a037954c22829281ef20fb5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 05:41:45 -0400 Subject: [PATCH 091/162] test(declinedsite): gate named rows for the call sites the resolver declined A call the resolver declines (tier 3, the builtin-method gate, the C++ standard-member gate) has no edge and is counted as declined_calls=, but no answer says which call it was: buildGraph keeps only the interned candidate list, shared by every call with the same candidates. "What does doPackage call" then misses pkg.Package() through an interface value, and "who calls TaskManager#define_task" misses every test that calls it on an untyped receiver. This adds the gate for the fix (red on the current binary). test/declinedsitecheck.sh (new, 84 arms; the 59 [P] arms fail on 22728af8, the 25 [C] controls pass) over the in-repo corpus test/declinedsitefix (a Go interface value, a Rust &dyn trait call, Ruby test-DSL calls from three test files, a TypeScript interface-typed receiver, a C++ template-parameter receiver, Python declines that share one candidate list) plus the 16 ladder languages of test/declinefix and the gate fixtures test/builtinbindfix and test/stdmembergatefix. It pins: one row per declined call on --callees and --callers (and --safe-delete), never inside count= or the rows; gate="builtin" / gate="std" on gate declines; candidates only with --legend=full; hedged K~d attribution on multi-definition callers; the cap (dc_shown=, dc_next=, file-first); CLI --json, columnar and the MCP twins; a legend that says what a row does NOT mean; risk="possible-callers" instead of none-found on --safe-delete; no rows on --impact or --for. Registered in test/regression.sh and the shard weights; gate count 685 -> 686. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/declinedsitecheck.sh | 382 ++++++++++++++++++ test/declinedsitefix/cpp/alpha/engine.h | 4 + test/declinedsitefix/cpp/beta/engine.h | 4 + test/declinedsitefix/cpp/caller/drive.cpp | 10 + test/declinedsitefix/go/cmd/package.go | 29 ++ test/declinedsitefix/go/deb/deb.go | 17 + test/declinedsitefix/go/go.mod | 3 + test/declinedsitefix/go/pkgr/pkgr.go | 26 ++ test/declinedsitefix/go/rpm/rpm.go | 17 + test/declinedsitefix/py/alpha/store.py | 6 + test/declinedsitefix/py/beta/store.py | 6 + test/declinedsitefix/py/caller/use.py | 13 + test/declinedsitefix/py/other/loader.py | 2 + .../rb/lib/rake/application.rb | 9 + test/declinedsitefix/rb/lib/rake/task.rb | 16 + .../rb/lib/rake/task_manager.rb | 11 + .../rb/test/test_application.rb | 11 + .../rb/test/test_name_space.rb | 8 + .../rb/test/test_task_manager.rb | 16 + test/declinedsitefix/rs/Cargo.toml | 4 + .../rs/src/builtin/index/mod.rs | 13 + .../rs/src/builtin/links/mod.rs | 13 + test/declinedsitefix/rs/src/builtin/mod.rs | 2 + test/declinedsitefix/rs/src/driver/mod.rs | 23 ++ test/declinedsitefix/rs/src/lib.rs | 3 + test/declinedsitefix/rs/src/preprocess/mod.rs | 18 + test/declinedsitefix/ts/src/app/dispatch.ts | 9 + .../ts/src/routers/regexp/router.ts | 7 + test/declinedsitefix/ts/src/routers/router.ts | 3 + .../ts/src/routers/trie/router.ts | 7 + test/regression.sh | 2 +- 35 files changed, 702 insertions(+), 9 deletions(-) create mode 100755 test/declinedsitecheck.sh create mode 100644 test/declinedsitefix/cpp/alpha/engine.h create mode 100644 test/declinedsitefix/cpp/beta/engine.h create mode 100644 test/declinedsitefix/cpp/caller/drive.cpp create mode 100644 test/declinedsitefix/go/cmd/package.go create mode 100644 test/declinedsitefix/go/deb/deb.go create mode 100644 test/declinedsitefix/go/go.mod create mode 100644 test/declinedsitefix/go/pkgr/pkgr.go create mode 100644 test/declinedsitefix/go/rpm/rpm.go create mode 100644 test/declinedsitefix/py/alpha/store.py create mode 100644 test/declinedsitefix/py/beta/store.py create mode 100644 test/declinedsitefix/py/caller/use.py create mode 100644 test/declinedsitefix/py/other/loader.py create mode 100644 test/declinedsitefix/rb/lib/rake/application.rb create mode 100644 test/declinedsitefix/rb/lib/rake/task.rb create mode 100644 test/declinedsitefix/rb/lib/rake/task_manager.rb create mode 100644 test/declinedsitefix/rb/test/test_application.rb create mode 100644 test/declinedsitefix/rb/test/test_name_space.rb create mode 100644 test/declinedsitefix/rb/test/test_task_manager.rb create mode 100644 test/declinedsitefix/rs/Cargo.toml create mode 100644 test/declinedsitefix/rs/src/builtin/index/mod.rs create mode 100644 test/declinedsitefix/rs/src/builtin/links/mod.rs create mode 100644 test/declinedsitefix/rs/src/builtin/mod.rs create mode 100644 test/declinedsitefix/rs/src/driver/mod.rs create mode 100644 test/declinedsitefix/rs/src/lib.rs create mode 100644 test/declinedsitefix/rs/src/preprocess/mod.rs create mode 100644 test/declinedsitefix/ts/src/app/dispatch.ts create mode 100644 test/declinedsitefix/ts/src/routers/regexp/router.ts create mode 100644 test/declinedsitefix/ts/src/routers/router.ts create mode 100644 test/declinedsitefix/ts/src/routers/trie/router.ts diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index fc6a54872..b03cdf941 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -112,6 +112,7 @@ "deadprecisioncheck.sh": 0.8, "deckcheck.sh": 54.8, "deckclaimcheck.sh": 0.1, +"declinedsitecheck.sh": 25.0, "deeptailcheck.sh": 1.0, "defaultceilingcheck.sh": 20.1, "defectshapecheck.sh": 14.2, diff --git a/README.md b/README.md index 7bfedf631..5eef8409a 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +686 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **685 gate scripts** and is the authoritative list; +`test/regression.sh` names **686 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 11acf723b..791e77946 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 686 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **685 gate scripts**, all of which exist on disk. +naming **686 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 686. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index db11d6c46..a0678335b 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["686 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "686", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["686 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/declinedsitecheck.sh b/test/declinedsitecheck.sh new file mode 100755 index 000000000..141043a10 --- /dev/null +++ b/test/declinedsitecheck.sh @@ -0,0 +1,382 @@ +#!/usr/bin/env bash +# declinedsitecheck.sh — the call SITES the resolver declined are NAMED, as hedged rows that are never a resolved edge. +# +# test/declinedsitecheck.sh [BIN] # BIN defaults to $RIPWIRE_BIN, then build/ripwire +# +# THE GAP. A tier-3 decline (test/declinecheck.sh) or a builtin/standard-member gate decline leaves a call with no +# edge and counts it: --callees answers declined_calls="6", --callers declined_calls="33". The count is honest, but +# it names nothing: buildGraph interns only the candidate LIST (graph.h internDeclinedList, shared by every call with +# the same list), so no answer can say WHICH call, on which line, in which caller. A question "what does doPackage +# call" then misses `pkg.Package( info, f )` through an interface value, and "who calls TaskManager#define_task" +# misses every test that calls it on an untyped receiver — both one call away from the answer. +# +# THE CONTRACT (design: the lane's Phase A note). +# * At every decline site buildGraph also records ( caller, list index, line ): one record per declined CALL. +# * --callees=X: one row per declined call X makes, after the +# rows; a gate decline says which gate: gate="builtin" (the builtin-method name gate) or gate="std" (the C++ +# standard-member gate); a tier-3 decline carries no gate=. +# * --callers=T (and --safe-delete=T): one row per declined call whose +# candidate list holds a definition of T. +# * NEVER an edge: count=, the rows, hop_tested=/hop_untested= and every MCP "count" are exactly what the +# pre-change binary printed; a row is its own element and never carries via="name" or prov=. +# * Capped AND counted: declined_calls= stays the total; past the cap the root says dc_shown="N" (rows printed) and +# dc_next= (the pasteable call that lists every site); rows past the cap are chosen file-first (each file's first +# site before any file's second), so a cut still names every file it can. +# * Candidates are named only on --legend=full (cand="FILE:LINE ...", the definitions the call could have meant); +# the default rows carry the count alone (cands=). +# * Multi-definition selectors (attrib-multidef): a callees-form carries of_def="K" (the definition that makes +# the call, a fact); a callers-form carries ONLY hedged tokens K~d ("this declined call's candidates include +# group K"), never a bare K. +# * Legend: an answer with rows defines them, saying what they do NOT mean ("the resolver saw K definitions of +# this name and proved none; not a claim this call reaches any of them"); an answer without carries no clause. +# * Dialects: CLI --json "dc":[{"n","l","cands","via"}] / [{"in","p","cands"}]; --format=columnar keeps the +# rows verbatim; the MCP twins (find_symbol, find_referencing_symbols) carry "declined_sites":[...] with the same +# sites ({"name","line","cands","via"} / {"in","file","line","cands"}) and an unchanged "count". +# * --safe-delete: a definition with no caller and no use but a declined call that could have meant it reads +# risk="possible-callers", never "none-found", and lists the rows; --impact and --for print no row. +# +# ARMS (fixture test/declinedsitefix: go/ py/ rs/ ts/ cpp/ rb/, plus test/declinefix, test/builtinbindfix, +# test/stdmembergatefix and test/lpinfix): +# (A) the three shapes the rows ask about: a Go interface value (pkg.ConventionalFileName twice, pkg.Package), a Rust +# &dyn trait call (preprocessor.run), Ruby test-DSL calls of TaskManager#define_task from three test files — +# every site named with its line, every candidate with --legend=full, none by default +# (B) language neutrality: the 16 ladder languages of test/declinefix (callees and callers form), plus TS (an +# interface-typed receiver), C++ (a template parameter receiver) and Python (two declines sharing ONE list keep +# two rows); Elixir's lexical refusal is not a decline and gets no row +# (C) near misses: a resolved call, a via="name" row and an external call never get a ; a builtin-gate and a +# std-gate decline stay labelled as such; count= and the rows are the pre-change binary's (pinned) +# (D) multi-definition labels (of_def on ) +# (E) dialects: --json, columnar, the MCP twins (same sites, same count), and every answer parses as XML +# (F) legend: defined where rows ride, absent elsewhere (and on the zero-decline corpus test/lpinfix) +# (G) the cap, on generated trees: callers and callees past the cap are counted, file-first, with dc_next= +# (H) safe-delete never reads none-found beside a declined possible caller; impact and for print no +# (I) determinism; (J) the predicates can fail +# +# RED ON BASE: every [P] arm fails on a binary without the record (no row anywhere); [C] arms are controls that +# pass on both. Exits non-zero on any failure. + +set -u +export PYTHONDONTWRITEBYTECODE=1 +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$PWD/$BIN" # a relative binary is relative to the caller's cwd +echo "declinedsitecheck: BIN=$BIN" +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +for d in declinedsitefix declinefix builtinbindfix stdmembergatefix lpinfix; do + [ -d "$ROOT/test/$d" ] || { echo "fixture missing: test/$d"; exit 2; } +done +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +# Every arm runs in the Python below; its exit status (1 on any FAIL arm, anything else on a crash) is the accumulator's input. +python3 - "$BIN" "$ROOT" "$TMP" <<'PY' || no "the arms above did not all pass (or the checker itself failed): python rc != 0" +import json, os, re, subprocess, sys +import xml.etree.ElementTree as ET + +BIN, ROOT, TMP = sys.argv[1], sys.argv[2], sys.argv[3] +T = os.path.join( ROOT, "test" ) +FIX, DF = os.path.join( T, "declinedsitefix" ), os.path.join( T, "declinefix" ) +BB, SM, CLEAN = os.path.join( T, "builtinbindfix" ), os.path.join( T, "stdmembergatefix" ), os.path.join( T, "lpinfix" ) +npass = nfail = 0 + +def say( good, tag, msg, why = "" ): + global npass, nfail + if good: + npass += 1; print( " PASS %s: %s" % ( tag, msg ) ) + else: + nfail += 1; print( " FAIL %s: %s%s" % ( tag, msg, ( " -- " + why ) if why else "" ) ) + +def run( root, *args ): + """( rc, stdout ) of one CLI answer; rc 124 on a timeout.""" + try: + p = subprocess.run( [ BIN, root, "--no-cache" ] + list( args ), capture_output = True, text = True, timeout = 240 ) + return p.returncode, p.stdout + except subprocess.TimeoutExpired: + return 124, "" + +def mcp( tool, arguments ): + """The MCP tool's text payload parsed as JSON, or None (initialize + one tools/call over stdio).""" + req = json.dumps( { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } ) + "\n" + json.dumps( + { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": tool, "arguments": arguments } } ) + "\n" + try: + p = subprocess.run( [ BIN, "--mcp" ], input = req, capture_output = True, text = True, timeout = 240 ) + except subprocess.TimeoutExpired: + return None + for line in p.stdout.splitlines(): + try: + r = json.loads( line ) + except ValueError: + continue + if r.get( "id" ) == 2 and "result" in r: + try: + return json.loads( r[ "result" ][ "content" ][ 0 ][ "text" ] ) + except ( ValueError, KeyError, IndexError, TypeError ): + return None + return None + +class Answer: + """One XML answer: its root element, the / rows, the rows (anywhere), the leading legend comments.""" + def __init__( self, rc, text, tag ): + self.rc, self.text, self.tag = rc, text, tag + self.root, self.err = None, "" + m = re.match( r"(\s*)+", text, re.S ) + self.legend = m.group( 0 ) if m else "" + try: + doc = ET.fromstring( "" + text + "" ) + self.root = doc.find( tag ) + except ET.ParseError as e: + self.err = "not well-formed XML: %s" % e + self.dc = [ dict( e.attrib ) for e in self.root.iter( "dc" ) ] if self.root is not None else [] + rowtag = "c" if tag == "safe-delete" else "s" + self.rows = [ dict( e.attrib ) for e in self.root.findall( rowtag ) ] if self.root is not None else [] + @property + def ok( self ): + return self.rc == 0 and self.root is not None + def a( self, k ): + return self.root.get( k ) if self.root is not None else None + def why( self ): + if self.rc != 0: return "rc=%s" % self.rc + if self.root is None: return self.err or "no <%s> root" % self.tag + return "root %s; dc=%s" % ( dict( self.root.attrib ), self.dc ) + +def ask( root, verb, sel, *more ): + rc, out = run( root, "--%s=%s" % ( verb, sel ), *more ) + return Answer( rc, out, verb if verb != "safe-delete" else "safe-delete" ) + +def dc_sites( ans, key ): + return sorted( ( d.get( key[ 0 ] ), d.get( key[ 1 ] ), d.get( "cands" ) ) for d in ans.dc ) + +def line_of( path, needle ): + with open( path, encoding = "utf-8" ) as f: + for i, line in enumerate( f, 1 ): + if needle in line: + return str( i ) + return "?" + +LEG_DONT = "proved none" +LEG_NOT = "not a claim this call reaches any of them" + +# ── (A) the three shapes ──────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (A) the declined sites the rows ask about are named ===" ) +GO, RS, RB = os.path.join( FIX, "go" ), os.path.join( FIX, "rs" ), os.path.join( FIX, "rb" ) +a = ask( GO, "callees", "doPackage" ) +want = WANT_GO = [ ( "ConventionalFileName", "16", "2" ), ( "ConventionalFileName", "18", "2" ), ( "Package", "24", "2" ) ] +say( a.ok and dc_sites( a, ( "n", "l" ) ) == want, "[P] a_go", + "go --callees=doPackage names pkg.ConventionalFileName@16, @18 and pkg.Package@24 (cands=2 each)", a.why() ) +say( a.ok and all( d.get( "via" ) == "declined" and "gate" not in d for d in a.dc ) and len( a.dc ) == 3, "[P] a_go_via", + "each tier-3 row reads via=\"declined\" and carries no gate=", a.why() ) +say( a.ok and a.a( "count" ) == "2" and a.a( "declined_calls" ) == "3" and sorted( r.get( "n" ) for r in a.rows ) == [ "Get", "WithDefaults" ], + "[C] a_go_count", "count=\"2\" and the two resolved rows are the pre-change answer; declined_calls=\"3\" stays the total", a.why() ) +say( a.ok and not ( { d.get( "n" ) for d in a.dc } & { "Get", "WithDefaults", "Create", "Stat" } ), "[C] a_go_neg", + "the resolved (pkgr.Get, pkgr.WithDefaults) and external (os.Create) calls get no ", a.why() ) +say( a.ok and not any( "cand" in d for d in a.dc ), "[C] a_go_nocand", "default rows carry no candidate names (cand=)", a.why() ) +af = ask( GO, "callees", "doPackage", "--legend=full" ) +cands = { ( d.get( "n" ), d.get( "l" ) ): set( d.get( "cand", "" ).split() ) for d in af.dc } +say( af.ok and cands.get( ( "Package", "24" ) ) == { "deb/deb.go:15", "rpm/rpm.go:15" } + and cands.get( ( "ConventionalFileName", "16" ) ) == { "deb/deb.go:11", "rpm/rpm.go:11" }, "[P] a_go_full", + "--legend=full lists each row's candidates as file:line (Deb and RPM; not the interface's bodyless signature)", af.why() ) + +r = ask( RS, "callees", "preprocess_book" ) +say( r.ok and dc_sites( r, ( "n", "l" ) ) == [ ( "run", "18", "2" ) ], "[P] a_rs", + "rust --callees=preprocess_book names preprocessor.run@18 through Box (cands=2)", r.why() ) +say( r.ok and r.a( "count" ) == "2" and sorted( x.get( "n" ) for x in r.rows ) == [ "new", "preprocessor_should_run" ], "[C] a_rs_count", + "count=\"2\" (PreprocessorContext::new, preprocessor_should_run) unchanged", r.why() ) +rf = ask( RS, "callees", "preprocess_book", "--legend=full" ) +say( rf.ok and len( rf.dc ) == 1 and set( rf.dc[ 0 ].get( "cand", "" ).split() ) == { "src/builtin/index/mod.rs:10", "src/builtin/links/mod.rs:10" }, + "[P] a_rs_full", "--legend=full names LinkPreprocessor::run and IndexPreprocessor::run", rf.why() ) + +b = ask( RB, "callers", "lib/rake/task_manager.rb:define_task" ) +want = WANT_RB = sorted( [ ( "test_create_task_manager", "test/test_task_manager.rb:9", "2" ), ( "test_define_two", "test/test_task_manager.rb:13", "2" ), + ( "test_define_two", "test/test_task_manager.rb:14", "2" ), ( "test_load_rakefile", "test/test_application.rb:5", "2" ), + ( "test_top_level", "test/test_application.rb:9", "2" ), ( "test_namespace_lookup", "test/test_name_space.rb:6", "2" ) ] ) +say( b.ok and dc_sites( b, ( "in", "p" ) ) == want, "[P] a_rb", + "ruby --callers=TaskManager#define_task names the six test-DSL sites in all three test files", b.why() ) +say( b.ok and b.a( "count" ) == "2" and b.a( "declined_calls" ) == "6" and len( b.rows ) == 2, "[C] a_rb_count", + "count=\"2\" (Task.define_task, synthesize_file_task) and declined_calls=\"6\" unchanged", b.why() ) + +# ── (B) language neutrality ──────────────────────────────────────────────────────────────────────────────────────── +print( "=== (B) every ladder language names its declined site ===" ) +# lang | caller | callee | call file (relative to test/declinefix) | one definition selector +table = [ + ( "java", "javaDeclined", "jbody", "java/caller/JavaCaller.java", "java/alpha/Alpha.java:jbody" ), + ( "cpp", "cppDeclined", "crender", "cpp/caller/caller.cpp", "cpp/alpha/alpha.cpp:crender" ), + ( "py", "py_declined", "pyfetch", "py/caller/caller.py", "py/alpha/alpha.py:pyfetch" ), + ( "rust", "rust_declined", "rfetch", "rust/caller/caller.rs", "rust/alpha/alpha.rs:rfetch" ), +] +for lang, caller, callee in [ ( "ts", "tsDeclined", "tsHop" ), ( "js", "jsDeclined", "jsHop" ), ( "go", "GoDeclined", "GoHop" ), + ( "swift", "swiftDeclined", "swiftHop" ), ( "objc", "objcDeclined", "objcHop" ), + ( "bash", "bash_declined", "bash_hop" ), ( "ruby", "ruby_declined", "ruby_hop" ), + ( "csharp", "CsDeclined", "CsHop" ), ( "c", "c_declined", "c_hop" ), ( "php", "phpDeclined", "phpHop" ), + ( "lua", "lua_declined", "lua_hop" ), ( "dart", "dartDeclined", "dartHop" ) ]: + cdir, adir = os.path.join( DF, "sweep", lang, "c" ), os.path.join( DF, "sweep", lang, "a" ) + cfile = [ f for f in sorted( os.listdir( cdir ) ) ][ 0 ] + afile = [ f for f in sorted( os.listdir( adir ) ) ][ 0 ] + table.append( ( lang, caller, callee, "sweep/%s/c/%s" % ( lang, cfile ), "sweep/%s/a/%s:%s" % ( lang, afile, callee ) ) ) +for lang, caller, callee, cfile, defsel in table: + call_line = line_of( os.path.join( DF, cfile ), callee ) + x = ask( DF, "callees", caller ) + say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( callee, call_line, "2" ) ] and x.a( "count" ) == "0", "[P] b_%s" % lang, + "%s: --callees=%s names %s@%s (cands=2), count=\"0\"" % ( lang, caller, callee, call_line ), x.why() ) + y = ask( DF, "callers", defsel ) + say( y.ok and dc_sites( y, ( "in", "p" ) ) == [ ( caller, "%s:%s" % ( cfile, call_line ), "2" ) ] and y.a( "count" ) == "0", + "[P] b_%s_callers" % lang, "%s: --callers=%s names %s at %s:%s" % ( lang, defsel, caller, cfile, call_line ), y.why() ) +x = ask( DF, "callees", "ex_declined" ) +say( x.ok and x.dc == [] and x.a( "graph_unresolved" ) == "2", "[C] b_elixir", + "elixir: the lexical refusal is counted unresolved, not declined, and gets no ", x.why() ) +TS, CPP, PYF = os.path.join( FIX, "ts" ), os.path.join( FIX, "cpp" ), os.path.join( FIX, "py" ) +x = ask( TS, "callees", "dispatch" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "match", "8", "2" ) ] and x.a( "count" ) == "1", "[P] b_ts_iface", + "ts: router.match@8 on an interface-typed receiver is named; clean() stays the one row", x.why() ) +x = ask( CPP, "callees", "drive" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "run", "9", "2" ) ] and x.a( "count" ) == "1", "[P] b_cpp_tmpl", + "c++: engine.run@9 on a template-parameter receiver is named; twice() stays the one row", x.why() ) +x = ask( PYF, "callees", "caller/use.py:load_twice" ) +say( x.ok and dc_sites( x, ( "n", "l" ) ) == [ ( "fetch", "11", "2" ), ( "fetch", "12", "2" ) ], "[P] b_py_shared", + "python: two declines sharing ONE interned candidate list keep two rows, one per line", x.why() ) + +# ── (C) near misses ──────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (C) near misses: no where nothing was declined; gate declines stay labelled ===" ) +x = ask( GO, "callees", "onlyResolved" ) +say( x.ok and x.dc == [] and x.a( "declined_calls" ) is None and not any( k.startswith( "dc_" ) for k in x.root.attrib ) + and ", no dc_ attribute, no legend clause", x.why() ) +x = ask( PYF, "callees", "fetch_all" ) +say( x.ok and x.dc == [] and len( x.rows ) == 2 and all( r.get( "via" ) == "name" for r in x.rows ), "[C] c_byname", + "via=\"name\" rows (a same-directory by-name binding) are edges, never ", x.why() ) +x = ask( BB, "callees", "py/plain.py:param_get" ) +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "get" and x.dc[ 0 ].get( "gate" ) == "builtin" and x.a( "count" ) == "0", + "[P] c_gate_builtin", "the builtin-method gate's decline (config.get) reads gate=\"builtin\"", x.why() ) +x = ask( SM, "callees", "stdAppend" ) +stdm = x.root.find( "stdm" ) if x.root is not None else None +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "append" and x.dc[ 0 ].get( "gate" ) == "std" and stdm is not None, + "[P] c_gate_std", "the C++ standard-member gate's decline reads gate=\"std\", beside its unchanged line", x.why() ) +for tag, root, verb, sel in [ ( "go", GO, "callees", "doPackage" ), ( "rs", RS, "callees", "preprocess_book" ), + ( "rb", RB, "callers", "lib/rake/task_manager.rb:define_task" ), ( "py", PYF, "callers", "fetch" ) ]: + x = ask( root, verb, sel ) + say( x.ok and len( x.rows ) == int( x.a( "count" ) or -1 ) and not any( r.get( "via" ) == "declined" for r in x.rows ) + and not any( "prov" in d or d.get( "via" ) == "name" for d in x.dc ), "[C] c_count_%s" % tag, + "%s --%s=%s: rows == count=, no row reads via=\"declined\", no reads like an edge" % ( tag, verb, sel ), x.why() ) + +# ── (D) multi-definition labels ──────────────────────────────────────────────────────────────────────────────────── +print( "=== (D) of_def on rows ===" ) +x = ask( PYF, "callees", "load" ) +got = sorted( ( d.get( "l" ), d.get( "of_def" ) ) for d in x.dc ) +say( x.ok and x.a( "defs" ) == "2" and got == [ ( "2", "2" ), ( "7", "1" ) ], "[P] d_callees", + "callees of two definitions: each names the definition that makes the call (of_def=\"1\" / \"2\", a fact)", x.why() ) +x = ask( PYF, "callers", "fetch" ) +toks = [ set( d.get( "of_def", "" ).split() ) for d in x.dc ] +say( x.ok and len( x.dc ) == 4 and all( t == { "1~d", "2~d" } for t in toks ), "[P] d_callers", + "callers of two definitions: every reads of_def=\"1~d 2~d\" (hedged: its candidates hold both), never a bare K", x.why() ) +say( x.ok and "~d" in x.legend, "[P] d_legend", "the legend defines the ~d mark", x.why() ) + +# ── (E) dialects ─────────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (E) --json, columnar, MCP ===" ) +rc, out = run( GO, "--callees=doPackage", "--json" ) +try: + j = json.loads( out ) +except ValueError: + j = {} +jd = sorted( ( d.get( "n" ), str( d.get( "l" ) ), str( d.get( "cands" ) ) ) for d in j.get( "dc", [] ) ) +say( rc == 0 and jd == WANT_GO and len( j.get( "callees", [] ) ) == 2 and j.get( "count" ) == 2, "[P] e_json_callees", + "--json carries \"dc\" with the XML sites; \"callees\" and \"count\" unchanged", "rc=%s dc=%s" % ( rc, j.get( "dc" ) ) ) +rc, out = run( RB, "--callers=lib/rake/task_manager.rb:define_task", "--json" ) +try: + j = json.loads( out ) +except ValueError: + j = {} +jd = sorted( ( d.get( "in" ), d.get( "p" ), str( d.get( "cands" ) ) ) for d in j.get( "dc", [] ) ) +say( rc == 0 and jd == WANT_RB and j.get( "count" ) == 2, "[P] e_json_callers", + "callers --json carries the same six sites", "rc=%s dc=%s" % ( rc, j.get( "dc" ) ) ) +rc, out = run( GO, "--callees=doPackage", "--format=columnar" ) +c = Answer( rc, out, "callees" ) +say( c.ok and dc_sites( c, ( "n", "l" ) ) == WANT_GO, "[P] e_columnar", "columnar keeps the rows verbatim", c.why() ) +m = mcp( "find_symbol", { "path": GO, "symbol": "doPackage" } ) or {} +md = sorted( ( d.get( "name" ), str( d.get( "line" ) ), str( d.get( "cands" ) ) ) for d in m.get( "declined_sites", [] ) ) +say( md == WANT_GO and m.get( "count" ) == 2 and all( d.get( "via" ) == "declined" for d in m.get( "declined_sites", [] ) ), + "[P] e_mcp_callees", "MCP find_symbol: \"declined_sites\" == the CLI rows, \"count\" unchanged", "declined_sites=%s count=%s" % ( m.get( "declined_sites" ), m.get( "count" ) ) ) +say( m != {} and not any( str( x.get( "via", "" ) ) == "declined" for x in m.get( "calls", [] ) ), "[C] e_mcp_calls", + "MCP \"calls\" holds no declined site", str( m.get( "calls" ) ) ) +m = mcp( "find_referencing_symbols", { "path": RB, "symbol": "lib/rake/task_manager.rb:define_task" } ) or {} +md = sorted( ( d.get( "in" ), "%s:%s" % ( d.get( "file" ), d.get( "line" ) ), str( d.get( "cands" ) ) ) for d in m.get( "declined_sites", [] ) ) +say( md == WANT_RB and m.get( "count" ) == 2, "[P] e_mcp_callers", + "MCP find_referencing_symbols: the same six sites, \"count\" unchanged", "declined_sites=%s count=%s" % ( m.get( "declined_sites" ), m.get( "count" ) ) ) +for tag, ans in [ ( "full_go", af ), ( "full_rs", rf ) ]: + say( ans.ok and ans.err == "", "[C] e_xml_%s" % tag, "the --legend=full answer with rows is well-formed XML", ans.why() ) + +# ── (F) legend ───────────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (F) the legend says what a row does NOT mean ===" ) +say( a.ok and " with 'proved none' and 'not a claim this call reaches any of them'", a.legend[ -400: ] ) +say( b.ok and " the same way", b.legend[ -400: ] ) +for sel, verb in [ ( "other", "callers" ), ( "go", "callees" ), ( "other", "impact" ) ]: + x = ask( CLEAN, verb, sel ) + say( x.ok and x.dc == [] and x.a( "declined_calls" ) is None and " row and no clause" % verb, x.why() ) + +# ── (G) the cap, on generated trees ──────────────────────────────────────────────────────────────────────────────── +print( "=== (G) capped AND counted ===" ) +G = os.path.join( TMP, "cap" ) +for d in ( "alpha", "beta", "callers", "big" ): + os.makedirs( os.path.join( G, d ), exist_ok = True ) +open( os.path.join( G, "alpha", "a.py" ), "w" ).write( "class A:\n def poll(self):\n return 1\n" ) +open( os.path.join( G, "beta", "b.py" ), "w" ).write( "class B:\n def poll(self):\n return 2\n" ) +for f in range( 3 ): + with open( os.path.join( G, "callers", "f%d.py" % f ), "w" ) as h: + for k in range( 12 ): + h.write( "def use_%d_%d(x):\n return x.poll()\n\n\n" % ( f, k ) ) +with open( os.path.join( G, "big", "big.py" ), "w" ) as h: + h.write( "def big(x):\n" + "".join( " x.poll()\n" for _ in range( 40 ) ) + " return x\n" ) +x = ask( G, "callers", "alpha/a.py:poll" ) +shown = int( x.a( "dc_shown" ) or 0 ) if x.ok else 0 +files = { d.get( "p", "" ).rsplit( ":", 1 )[ 0 ] for d in x.dc } +say( x.ok and x.a( "declined_calls" ) == "76" and 8 <= shown <= 24 and len( x.dc ) == shown and x.a( "dc_next" ), + "[P] g_callers_cut", "76 declined callers: dc_shown=N (8..24) rows printed, declined_calls=\"76\" the total, dc_next= present", x.why() ) +say( x.ok and files == { "callers/f0.py", "callers/f1.py", "callers/f2.py", "big/big.py" }, "[P] g_callers_files", + "the cut is file-first: all four calling files are named", "files=%s" % sorted( files ) ) +x = ask( G, "callees", "big" ) +shown = int( x.a( "dc_shown" ) or 0 ) if x.ok else 0 +ls = [ int( d.get( "l", "0" ) ) for d in x.dc ] +say( x.ok and x.a( "declined_calls" ) == "40" and 8 <= shown <= 24 and len( x.dc ) == shown and ls == sorted( ls ) and len( set( ls ) ) == len( ls ) + and x.a( "dc_next" ), "[P] g_callees_cut", "40 declined calls in one body: dc_shown rows in line order, dc_next= present", x.why() ) +x = ask( G, "callees", "use_0_0" ) +say( x.ok and len( x.dc ) == 1 and x.a( "dc_shown" ) is None and x.a( "dc_next" ) is None, "[P] g_uncut", + "under the cap: every row, no dc_shown= and no dc_next=", x.why() ) + +# ── (H) the other verbs ──────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (H) safe-delete, impact, for ===" ) +x = ask( GO, "safe-delete", "deb/deb.go:Package" ) +say( x.ok and x.a( "risk" ) == "possible-callers" and x.a( "callers" ) == "0", "[P] h_sd_risk", + "safe-delete of a definition only a declined call could mean: risk=\"possible-callers\", never none-found; callers=\"0\" unchanged", x.why() ) +say( x.ok and dc_sites( x, ( "in", "p" ) ) == [ ( "doPackage", "cmd/package.go:24", "2" ) ], "[P] h_sd_rows", + "safe-delete lists the declined possible caller as a row", x.why() ) +x = ask( PYF, "safe-delete", "beta/store.py:fetch" ) +say( x.ok and x.a( "risk" ) == "untested-radius" and x.a( "callers" ) == "1", "[C] h_sd_ctrl", + "a definition with a real caller keeps its risk= (untested-radius) and callers=\"1\"", x.why() ) +x = ask( GO, "impact", "deb/deb.go:Package" ) +say( x.ok and x.dc == [] and x.a( "reaches" ) == "0" and x.a( "declined_calls" ) == "1", "[C] h_impact", + "impact prints no row (declined_calls= stays its disclosure); reaches=\"0\" unchanged", x.why() ) +rc, out = run( GO, "--for=doPackage" ) +say( rc == 0 and " 0, "[C] h_for", "--for prints no row", "rc=%s" % rc ) + +# ── (I) determinism ──────────────────────────────────────────────────────────────────────────────────────────────── +print( "=== (I) determinism ===" ) +rc1, o1 = run( GO, "--callees=doPackage", "--legend=full" ) +rc2, o2 = run( GO, "--callees=doPackage", "--legend=full" ) +say( rc1 == 0 and rc2 == 0 and o1 == o2 and len( o1 ) > 0, "[C] i_det", "two runs byte-identical" ) + +# ── (J) the predicates can fail ──────────────────────────────────────────────────────────────────────────────────── +print( "=== (J) the predicates reject what they must ===" ) +fake = Answer( 0, '', "callers" ) +say( fake.ok and not all( set( d.get( "of_def", "" ).split() ) <= { "1~d", "2~d" } for d in fake.dc ), "[C] j_bare_k", + "a callers-form (a bare K) is caught" ) +fake = Answer( 0, '', "callees" ) +say( fake.ok and ( len( fake.rows ) != int( fake.a( "count" ) ) or any( r.get( "via" ) == "declined" for r in fake.rows ) ), "[C] j_in_count", + "a declined site folded into the rows / count= is caught" ) +fake = Answer( 0, '', "callees" ) +say( not fake.ok, "[C] j_malformed", "a malformed answer is not read as an empty one" ) + +print( "declinedsitecheck: %d PASS / %d FAIL" % ( npass, nfail ) ) +sys.exit( 1 if nfail else 0 ) +PY +if [ "$fail" = 0 ]; then echo "declinedsitecheck: PASS"; else echo "declinedsitecheck: FAIL"; exit 1; fi diff --git a/test/declinedsitefix/cpp/alpha/engine.h b/test/declinedsitefix/cpp/alpha/engine.h new file mode 100644 index 000000000..aabb6bccb --- /dev/null +++ b/test/declinedsitefix/cpp/alpha/engine.h @@ -0,0 +1,4 @@ +struct AlphaEngine +{ + int run( int n ) { return n + 1; } +}; diff --git a/test/declinedsitefix/cpp/beta/engine.h b/test/declinedsitefix/cpp/beta/engine.h new file mode 100644 index 000000000..1a73a030b --- /dev/null +++ b/test/declinedsitefix/cpp/beta/engine.h @@ -0,0 +1,4 @@ +struct BetaEngine +{ + int run( int n ) { return n - 1; } +}; diff --git a/test/declinedsitefix/cpp/caller/drive.cpp b/test/declinedsitefix/cpp/caller/drive.cpp new file mode 100644 index 000000000..3b1448ce4 --- /dev/null +++ b/test/declinedsitefix/cpp/caller/drive.cpp @@ -0,0 +1,10 @@ +static int twice( int n ) +{ + return n * 2; +} + +template +int drive( Engine& engine, int n ) +{ + return engine.run( twice( n ) ); +} diff --git a/test/declinedsitefix/go/cmd/package.go b/test/declinedsitefix/go/cmd/package.go new file mode 100644 index 000000000..6da148932 --- /dev/null +++ b/test/declinedsitefix/go/cmd/package.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "os" + + "example.com/dsf/pkgr" +) + +func doPackage(target string, intoDir bool) error { + info := pkgr.WithDefaults(&pkgr.Info{}) + pkg, err := pkgr.Get("deb") + if err != nil { + return err + } + if target == "" { + target = pkg.ConventionalFileName(info) + } else if intoDir { + target = target + "/" + pkg.ConventionalFileName(info) + } + f, err := os.Create(target) + if err != nil { + return err + } + return pkg.Package(info, f) +} + +func onlyResolved() *pkgr.Info { + return pkgr.WithDefaults(&pkgr.Info{}) +} diff --git a/test/declinedsitefix/go/deb/deb.go b/test/declinedsitefix/go/deb/deb.go new file mode 100644 index 000000000..7b80d5650 --- /dev/null +++ b/test/declinedsitefix/go/deb/deb.go @@ -0,0 +1,17 @@ +package deb + +import ( + "io" + + "example.com/dsf/pkgr" +) + +type Deb struct{} + +func (Deb) ConventionalFileName(info *pkgr.Info) string { + return info.Target + ".deb" +} + +func (Deb) Package(info *pkgr.Info, w io.Writer) error { + return nil +} diff --git a/test/declinedsitefix/go/go.mod b/test/declinedsitefix/go/go.mod new file mode 100644 index 000000000..95c29d763 --- /dev/null +++ b/test/declinedsitefix/go/go.mod @@ -0,0 +1,3 @@ +module example.com/dsf + +go 1.21 diff --git a/test/declinedsitefix/go/pkgr/pkgr.go b/test/declinedsitefix/go/pkgr/pkgr.go new file mode 100644 index 000000000..5d11d4209 --- /dev/null +++ b/test/declinedsitefix/go/pkgr/pkgr.go @@ -0,0 +1,26 @@ +package pkgr + +import "io" + +// Info is what every packager reads. +type Info struct { + Target string +} + +// Packager is implemented once per package format. +type Packager interface { + ConventionalFileName(info *Info) string + Package(info *Info, w io.Writer) error +} + +var registry = map[string]Packager{} + +// Get looks a packager up by format name. +func Get(format string) (Packager, error) { + return registry[format], nil +} + +// WithDefaults fills the defaults in. +func WithDefaults(info *Info) *Info { + return info +} diff --git a/test/declinedsitefix/go/rpm/rpm.go b/test/declinedsitefix/go/rpm/rpm.go new file mode 100644 index 000000000..832f16dd1 --- /dev/null +++ b/test/declinedsitefix/go/rpm/rpm.go @@ -0,0 +1,17 @@ +package rpm + +import ( + "io" + + "example.com/dsf/pkgr" +) + +type RPM struct{} + +func (RPM) ConventionalFileName(info *pkgr.Info) string { + return info.Target + ".rpm" +} + +func (RPM) Package(info *pkgr.Info, w io.Writer) error { + return nil +} diff --git a/test/declinedsitefix/py/alpha/store.py b/test/declinedsitefix/py/alpha/store.py new file mode 100644 index 000000000..3473b6c37 --- /dev/null +++ b/test/declinedsitefix/py/alpha/store.py @@ -0,0 +1,6 @@ +class AlphaStore: + def fetch(self, key): + return key + + def fetch_all(self, keys, peer): + return [peer.fetch(k) for k in keys] diff --git a/test/declinedsitefix/py/beta/store.py b/test/declinedsitefix/py/beta/store.py new file mode 100644 index 000000000..377ae63ed --- /dev/null +++ b/test/declinedsitefix/py/beta/store.py @@ -0,0 +1,6 @@ +class BetaStore: + def fetch(self, key): + return None + + def fetch_all(self, keys, peer): + return {k: peer.fetch(k) for k in keys} diff --git a/test/declinedsitefix/py/caller/use.py b/test/declinedsitefix/py/caller/use.py new file mode 100644 index 000000000..e4db33ea1 --- /dev/null +++ b/test/declinedsitefix/py/caller/use.py @@ -0,0 +1,13 @@ +def normalise(key): + return key.strip() + + +def load(store, key): + k = normalise(key) + return store.fetch(k) + + +def load_twice(store, key): + first = store.fetch(key) + second = store.fetch(normalise(key)) + return first, second diff --git a/test/declinedsitefix/py/other/loader.py b/test/declinedsitefix/py/other/loader.py new file mode 100644 index 000000000..05dc1948e --- /dev/null +++ b/test/declinedsitefix/py/other/loader.py @@ -0,0 +1,2 @@ +def load(store, key): + return store.fetch(key) diff --git a/test/declinedsitefix/rb/lib/rake/application.rb b/test/declinedsitefix/rb/lib/rake/application.rb new file mode 100644 index 000000000..798a467b0 --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/application.rb @@ -0,0 +1,9 @@ +module Rake + class Application + include TaskManager + end + + def self.application + @application ||= Application.new + end +end diff --git a/test/declinedsitefix/rb/lib/rake/task.rb b/test/declinedsitefix/rb/lib/rake/task.rb new file mode 100644 index 000000000..141c9535a --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/task.rb @@ -0,0 +1,16 @@ +module Rake + class Task + def initialize(name) + @name = name + end + + class << self + def define_task(*args, &block) + Rake.application.define_task(self, *args, &block) + end + end + end + + class FileTask < Task + end +end diff --git a/test/declinedsitefix/rb/lib/rake/task_manager.rb b/test/declinedsitefix/rb/lib/rake/task_manager.rb new file mode 100644 index 000000000..f83d0d0d3 --- /dev/null +++ b/test/declinedsitefix/rb/lib/rake/task_manager.rb @@ -0,0 +1,11 @@ +module Rake + module TaskManager + def define_task(task_class, *args, &block) + task_class.new(args.first) + end + + def synthesize_file_task(task_name) + define_task(Rake::FileTask, task_name) + end + end +end diff --git a/test/declinedsitefix/rb/test/test_application.rb b/test/declinedsitefix/rb/test/test_application.rb new file mode 100644 index 000000000..d7844b854 --- /dev/null +++ b/test/declinedsitefix/rb/test/test_application.rb @@ -0,0 +1,11 @@ +require "minitest/autorun" + +class TestApplication < Minitest::Test + def test_load_rakefile + @app.define_task(Rake::Task, :default) + end + + def test_top_level + @app.define_task(Rake::FileTask, "a.o") + end +end diff --git a/test/declinedsitefix/rb/test/test_name_space.rb b/test/declinedsitefix/rb/test/test_name_space.rb new file mode 100644 index 000000000..90138500b --- /dev/null +++ b/test/declinedsitefix/rb/test/test_name_space.rb @@ -0,0 +1,8 @@ +require "minitest/autorun" + +class TestNameSpace < Minitest::Test + def test_namespace_lookup + mgr = fresh_manager + mgr.define_task(Rake::Task, "n:t") + end +end diff --git a/test/declinedsitefix/rb/test/test_task_manager.rb b/test/declinedsitefix/rb/test/test_task_manager.rb new file mode 100644 index 000000000..e3127df17 --- /dev/null +++ b/test/declinedsitefix/rb/test/test_task_manager.rb @@ -0,0 +1,16 @@ +require "minitest/autorun" + +class TestTaskManager < Minitest::Test + def setup + @tm = make_manager + end + + def test_create_task_manager + @tm.define_task(Rake::Task, :t) + end + + def test_define_two + @tm.define_task(Rake::Task, :a) + @tm.define_task(Rake::Task, :b) + end +end diff --git a/test/declinedsitefix/rs/Cargo.toml b/test/declinedsitefix/rs/Cargo.toml new file mode 100644 index 000000000..6d7de0d00 --- /dev/null +++ b/test/declinedsitefix/rs/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "dsf" +version = "0.1.0" +edition = "2021" diff --git a/test/declinedsitefix/rs/src/builtin/index/mod.rs b/test/declinedsitefix/rs/src/builtin/index/mod.rs new file mode 100644 index 000000000..bdae3c7b9 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/index/mod.rs @@ -0,0 +1,13 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct IndexPreprocessor; + +impl Preprocessor for IndexPreprocessor { + fn name(&self) -> &str { + "index" + } + + fn run(&self, _ctx: &PreprocessorContext, book: Book) -> Book { + book + } +} diff --git a/test/declinedsitefix/rs/src/builtin/links/mod.rs b/test/declinedsitefix/rs/src/builtin/links/mod.rs new file mode 100644 index 000000000..44278e061 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/links/mod.rs @@ -0,0 +1,13 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct LinkPreprocessor; + +impl Preprocessor for LinkPreprocessor { + fn name(&self) -> &str { + "links" + } + + fn run(&self, _ctx: &PreprocessorContext, book: Book) -> Book { + book + } +} diff --git a/test/declinedsitefix/rs/src/builtin/mod.rs b/test/declinedsitefix/rs/src/builtin/mod.rs new file mode 100644 index 000000000..16595b463 --- /dev/null +++ b/test/declinedsitefix/rs/src/builtin/mod.rs @@ -0,0 +1,2 @@ +pub mod index; +pub mod links; diff --git a/test/declinedsitefix/rs/src/driver/mod.rs b/test/declinedsitefix/rs/src/driver/mod.rs new file mode 100644 index 000000000..2cd320991 --- /dev/null +++ b/test/declinedsitefix/rs/src/driver/mod.rs @@ -0,0 +1,23 @@ +use crate::preprocess::{Book, Preprocessor, PreprocessorContext}; + +pub struct MDBook { + pub root: String, + pub preprocessors: Vec>, +} + +fn preprocessor_should_run(root: &str) -> bool { + !root.is_empty() +} + +impl MDBook { + pub fn preprocess_book(&self, book: Book) -> Book { + let ctx = PreprocessorContext::new(self.root.clone()); + let mut out = book; + for preprocessor in self.preprocessors.iter() { + if preprocessor_should_run(&self.root) { + out = preprocessor.run(&ctx, out); + } + } + out + } +} diff --git a/test/declinedsitefix/rs/src/lib.rs b/test/declinedsitefix/rs/src/lib.rs new file mode 100644 index 000000000..55cd32324 --- /dev/null +++ b/test/declinedsitefix/rs/src/lib.rs @@ -0,0 +1,3 @@ +pub mod builtin; +pub mod driver; +pub mod preprocess; diff --git a/test/declinedsitefix/rs/src/preprocess/mod.rs b/test/declinedsitefix/rs/src/preprocess/mod.rs new file mode 100644 index 000000000..0d0f0e1b4 --- /dev/null +++ b/test/declinedsitefix/rs/src/preprocess/mod.rs @@ -0,0 +1,18 @@ +pub struct Book { + pub chapters: Vec, +} + +pub struct PreprocessorContext { + pub root: String, +} + +impl PreprocessorContext { + pub fn new(root: String) -> PreprocessorContext { + PreprocessorContext { root } + } +} + +pub trait Preprocessor { + fn name(&self) -> &str; + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Book; +} diff --git a/test/declinedsitefix/ts/src/app/dispatch.ts b/test/declinedsitefix/ts/src/app/dispatch.ts new file mode 100644 index 000000000..3a5e86908 --- /dev/null +++ b/test/declinedsitefix/ts/src/app/dispatch.ts @@ -0,0 +1,9 @@ +import { Router } from "../routers/router"; + +function clean(path: string): string { + return path.trim(); +} + +export function dispatch(router: Router, method: string, path: string): string[] { + return router.match(method, clean(path)); +} diff --git a/test/declinedsitefix/ts/src/routers/regexp/router.ts b/test/declinedsitefix/ts/src/routers/regexp/router.ts new file mode 100644 index 000000000..76d087519 --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/regexp/router.ts @@ -0,0 +1,7 @@ +import { Router } from "../router"; + +export class RegExpRouter implements Router { + match(method: string, path: string): string[] { + return [path, method]; + } +} diff --git a/test/declinedsitefix/ts/src/routers/router.ts b/test/declinedsitefix/ts/src/routers/router.ts new file mode 100644 index 000000000..00d13f3dc --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/router.ts @@ -0,0 +1,3 @@ +export interface Router { + match(method: string, path: string): string[]; +} diff --git a/test/declinedsitefix/ts/src/routers/trie/router.ts b/test/declinedsitefix/ts/src/routers/trie/router.ts new file mode 100644 index 000000000..a622eb109 --- /dev/null +++ b/test/declinedsitefix/ts/src/routers/trie/router.ts @@ -0,0 +1,7 @@ +import { Router } from "../router"; + +export class TrieRouter implements Router { + match(method: string, path: string): string[] { + return [method, path]; + } +} diff --git a/test/regression.sh b/test/regression.sh index 4aef777ea..dab8c0941 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From b758c6bfffa98176266968da1e3ce6e834020859 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 06:09:23 -0400 Subject: [PATCH 092/162] test(declinedsite): arms for the wrong implementations the first gate let through The early gates review found three plausible wrong implementations that passed every arm, and no arm on the three Ruby decline sites. New arms, red on the pre-change binary (26 PASS / 87 FAIL; every new [P] arm fails because no row or possible-callers reading exists yet): - Ruby: a declined `new` names the called name (never its initialize candidates); the two side-cut declines read gate="side"; callers of Widget.helper / Gadget#helper each name only their own caller (rows join by candidate-list membership, never by the called name). New fixture test/declinedsitefix/rbside. - The declinedlistcheck shapes: the language filter and the workspace-root filter split one name into two lists; each definition names only its own declined caller. - Rows == declined_calls= on twelve uncut answers (every decline site records), and == declined_iface= on the TypeScript interface answers. - A default attribute whitelist for both row forms, no candidates under another key in --json or MCP, via="declined" on every form. - Every other root attribute equals the pre-change literal. - dc_next= runs and names every declined line; the cut is file-first in path order. - safe-delete: a control keeps none-found; the TypeScript interface method reads possible-callers. - The legend drops "no count or row" where rows ride and reads the review's hedge wording. builtinbindcheck (R): re-anchored by design. It pinned risk="none-found" beside declined_calls=; the definition only a declined call could mean now reads risk="possible-callers" with its row. The qualifying-sentence assert is kept as its twin (the risk sentence says it is not a safety reading). Co-Authored-By: Claude Opus 5.5 --- test/builtinbindcheck.sh | 16 +- test/declinedsitecheck.sh | 176 +++++++++++++++++++++- test/declinedsitefix/rbside/lib/base.rb | 5 + test/declinedsitefix/rbside/lib/child.rb | 5 + test/declinedsitefix/rbside/lib/gadget.rb | 9 ++ test/declinedsitefix/rbside/lib/widget.rb | 9 ++ 6 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 test/declinedsitefix/rbside/lib/base.rb create mode 100644 test/declinedsitefix/rbside/lib/child.rb create mode 100644 test/declinedsitefix/rbside/lib/gadget.rb create mode 100644 test/declinedsitefix/rbside/lib/widget.rb diff --git a/test/builtinbindcheck.sh b/test/builtinbindcheck.sh index 3c504faa4..fcc277bd6 100755 --- a/test/builtinbindcheck.sh +++ b/test/builtinbindcheck.sh @@ -202,11 +202,21 @@ for spec in "edit-check|--edit-check=mono/py/e/pool.py:popitem" "safe-delete|--s no "(R) $flag: no declined_calls=\"1\" or no definition: ${R:-no <$tag> root}" fi done +# Re-anchored by design (lane declined-sites, gates review R11): this arm pinned risk="none-found" beside declined_calls=, +# qualified by a legend sentence. A definition with no caller and no use that a declined call could have meant now reads +# risk="possible-callers" and the declined call is listed as a row; the qualifying-sentence assert is kept as the twin: +# the risk value's own sentence still says it is not a safety reading. rw --safe-delete=mono/py/e/pool.py:popitem --legend=full >"$TMP/sd.xml" -if [ "$( attr "$( root_tag "$TMP/sd.xml" safe-delete )" risk )" = none-found ] && legend_of "$TMP/sd.xml" | grep -q 'none-found beside declined_calls= is not a safety reading'; then - ok "(R) --safe-delete: risk=none-found is qualified by the declined_calls= sentence" +SDR="$( root_tag "$TMP/sd.xml" safe-delete )" +if [ "$( attr "$SDR" risk )" = possible-callers ] && [ "$( attr "$SDR" declined_calls )" = 1 ] && grep -q ' row" else - no "(R) --safe-delete: risk=none-found stands unqualified beside declined_calls=" + no "(R) --safe-delete: a definition only a declined call could mean should read risk=possible-callers with its row: ${SDR:-no root}" +fi +if legend_of "$TMP/sd.xml" | grep -q 'possible-callers: .*not a safety reading'; then + ok "(R) --safe-delete: risk=possible-callers is qualified in the legend (not a safety reading)" +else + no "(R) --safe-delete: risk=possible-callers stands unqualified in the legend" fi QD="$TMP/qd"; rm -rf "$QD"; mkdir -p "$QD"; cp -R mono "$QD/" 2>/dev/null ( cd "$QD" && git init -q && git -c user.name=t -c user.email=t@t add -A && git -c user.name=t -c user.email=t@t commit -qm base ) >/dev/null 2>&1 diff --git a/test/declinedsitecheck.sh b/test/declinedsitecheck.sh index 141043a10..8b392454d 100755 --- a/test/declinedsitecheck.sh +++ b/test/declinedsitecheck.sh @@ -15,8 +15,10 @@ # * --callees=X: one row per declined call X makes, after the # rows; a gate decline says which gate: gate="builtin" (the builtin-method name gate) or gate="std" (the C++ # standard-member gate); a tier-3 decline carries no gate=. -# * --callers=T (and --safe-delete=T): one row per declined call whose -# candidate list holds a definition of T. +# * --callers=T (and --safe-delete=T): one row per declined +# call whose candidate LIST holds a definition of T (membership, never the called name: two lists can share a name). +# * Ruby: a call the side cut declined (in reach, on the other side of self) reads gate="side"; a declined `new` names +# the called name (n="new"), never its `initialize` candidates. # * NEVER an edge: count=, the rows, hop_tested=/hop_untested= and every MCP "count" are exactly what the # pre-change binary printed; a row is its own element and never carries via="name" or prov=. # * Capped AND counted: declined_calls= stays the total; past the cap the root says dc_shown="N" (rows printed) and @@ -50,6 +52,14 @@ # (F) legend: defined where rows ride, absent elsewhere (and on the zero-decline corpus test/lpinfix) # (G) the cap, on generated trees: callers and callees past the cap are counted, file-first, with dc_next= # (H) safe-delete never reads none-found beside a declined possible caller; impact and for print no +# (K) the gates review's amendments: R1 the three Ruby decline sites (new, side, side over the assembled set) and +# the W1 discriminator (callers by candidate list, never by name); R2 rows == declined_calls= on every uncut +# answer (== declined_iface= on the TS interface answers); R3 W1 on the declinedlistcheck shapes (language and +# root filters split one name into two lists); R4 the default attribute whitelist (no candidates leak under +# another key, CLI JSON or MCP); R5 every other root attribute is the pre-change literal; R6 dc_next= runs and +# names every declined line; R7 the cut is file-first in path order; R8 safe-delete's possible-callers is +# per definition (control none-found; the TS interface method flips); R9/R10 the legend drops "no count or +# row" where rows ride and reads the review's hedge wording # (I) determinism; (J) the predicates can fail # # RED ON BASE: every [P] arm fails on a binary without the record (no row anywhere); [C] arms are controls that @@ -359,6 +369,168 @@ say( x.ok and x.dc == [] and x.a( "reaches" ) == "0" and x.a( "declined_calls" ) rc, out = run( GO, "--for=doPackage" ) say( rc == 0 and " 0, "[C] h_for", "--for prints no row", "rc=%s" % rc ) +# ── (K) the gates review's amendments (R1–R11): the wrong implementations that passed every arm above ──────────────── +print( "=== (K) R1 Ruby decline sites: new, and the two side cuts (gate=\"side\") ===" ) +RBS = os.path.join( FIX, "rbside" ) +x = ask( RBS, "callees", "build" ) +say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "new" and x.dc[ 0 ].get( "l" ) == "3" and x.dc[ 0 ].get( "cands" ) == "1" + and "gate" not in x.dc[ 0 ], "[P] k_rb_new", + "ruby --callees=build names the declined `new` at base.rb:3 as n=\"new\" (the CALLED name, never the candidate's initialize), cands=1, no gate=", x.why() ) +x = ask( RBS, "callers", "lib/child.rb:initialize" ) +say( x.ok and dc_sites( x, ( "in", "p" ) ) == [ ( "build", "lib/base.rb:3", "1" ) ], "[P] k_rb_new_callers", + "ruby --callers=lib/child.rb:initialize names the declined `new` in build at lib/base.rb:3", x.why() ) +for sel in ( "run", "make" ): + x = ask( RBS, "callees", sel ) + say( x.ok and len( x.dc ) == 1 and x.dc[ 0 ].get( "n" ) == "helper" and x.dc[ 0 ].get( "l" ) == "7" and x.dc[ 0 ].get( "gate" ) == "side", + "[P] k_rb_side_%s" % sel, "ruby --callees=%s names helper@7 with gate=\"side\" (in reach, other side of self)" % sel, x.why() ) +for sel, only in ( ( "lib/widget.rb:helper", "run" ), ( "lib/gadget.rb:helper", "make" ) ): + x = ask( RBS, "callers", sel ) + say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ only ], "[P] k_rb_w1_%s" % only, + "W1: --callers=%s names ONLY %s (candidate-list membership, never the called name)" % ( sel, only ), x.why() ) + +print( "=== (K) R3 W1 on the declinedlistcheck shapes: lists, never names ===" ) +ONE, WS = os.path.join( TMP, "one" ), os.path.join( TMP, "ws" ) +def cfn( path, name, body ): + os.makedirs( os.path.dirname( path ), exist_ok = True ) + open( path, "w" ).write( "int %s()\n{\n return %s;\n}\n" % ( name, body ) ) +def pyfn( path, name, body ): + os.makedirs( os.path.dirname( path ), exist_ok = True ) + open( path, "w" ).write( "def %s():\n return %s\n" % ( name, body ) ) +cfn( os.path.join( ONE, "a", "size.cpp" ), "size", "1" ); cfn( os.path.join( ONE, "b", "size.cpp" ), "size", "2" ) +pyfn( os.path.join( ONE, "c", "size.py" ), "size", "3" ); pyfn( os.path.join( ONE, "d", "size.py" ), "size", "4" ) +cfn( os.path.join( ONE, "e", "caller.cpp" ), "e_call", "size()" ); pyfn( os.path.join( ONE, "f", "caller.py" ), "f_call", "size()" ) +for r in ( "r1", "r2" ): + cfn( os.path.join( WS, r, "x.cpp" ), "foo", "1" ); cfn( os.path.join( WS, r, "y", "foo.cpp" ), "foo", "2" ) + cfn( os.path.join( WS, r, "z", "caller.cpp" ), "%s_call" % r, "foo()" ) +for sel, only in ( ( "a/size.cpp:size", "e_call" ), ( "c/size.py:size", "f_call" ) ): + x = ask( ONE, "callers", sel ) + say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ only ] and x.a( "declined_calls" ) == "1", "[P] k_list_%s" % only, + "W1: --callers=%s names only %s (the language filter splits `size` into two lists)" % ( sel, only ), x.why() ) +rc, out = 124, "" +try: + p = subprocess.run( [ BIN, "r1", "r2", "--no-cache", "--callers=r1/x.cpp:foo" ], cwd = WS, capture_output = True, text = True, timeout = 240 ) + rc, out = p.returncode, p.stdout +except subprocess.TimeoutExpired: + pass +x = Answer( rc, out, "callers" ) +say( x.ok and [ d.get( "in" ) for d in x.dc ] == [ "r1_call" ], "[P] k_list_roots", + "W1: two roots: --callers=r1/x.cpp:foo names only r1's caller (the root filter splits `foo`)", x.why() ) + +print( "=== (K) R2 rows == declined_calls= on every uncut answer; TS: == declined_iface= too ===" ) +uncut = [ ( "go callees doPackage", ask( GO, "callees", "doPackage" ) ), ( "rb callers define_task", ask( RB, "callers", "lib/rake/task_manager.rb:define_task" ) ), + ( "py callers fetch", ask( PYF, "callers", "fetch" ) ), ( "py callees load", ask( PYF, "callees", "load" ) ), + ( "go safe-delete Package", ask( GO, "safe-delete", "deb/deb.go:Package" ) ), ( "rbside callees build", ask( RBS, "callees", "build" ) ), + ( "rbside callers widget helper", ask( RBS, "callers", "lib/widget.rb:helper" ) ), ( "bb callees param_get", ask( BB, "callees", "py/plain.py:param_get" ) ), + ( "sm callees stdAppend", ask( SM, "callees", "stdAppend" ) ), ( "ts callees dispatch", ask( TS, "callees", "dispatch" ) ), + ( "cpp callees drive", ask( CPP, "callees", "drive" ) ), ( "df callers java", ask( DF, "callers", "java/alpha/Alpha.java:jbody" ) ) ] +bad = [ "%s: rows=%d declined_calls=%s" % ( tag, len( x.dc ), x.a( "declined_calls" ) ) for tag, x in uncut + if not ( x.ok and x.a( "dc_shown" ) is None and len( x.dc ) > 0 and str( len( x.dc ) ) == x.a( "declined_calls" ) ) ] +say( not bad, "[P] k_rows_eq_declined", "on %d uncut answers the row count equals declined_calls= (every decline site records)" % len( uncut ), "; ".join( bad ) ) +for sel in ( "src/routers/trie/router.ts:match", "src/routers/router.ts:match" ): + x = ask( TS, "callers", sel ) + say( x.ok and len( x.dc ) == 1 and x.a( "declined_calls" ) == "1" and x.a( "declined_iface" ) == "1" and x.dc[ 0 ].get( "in" ) == "dispatch", + "[P] k_ts_iface_%s" % sel.split( "/" )[ -2 ], "ts --callers=%s: one (dispatch) == declined_calls=\"1\" == declined_iface=\"1\"" % sel, x.why() ) + +print( "=== (K) R4 attribute whitelist; R11 via=\"declined\" on every form ===" ) +CALLEE_KEYS, CALLER_KEYS = { "n", "l", "cands", "via", "gate", "of_def" }, { "in", "p", "cands", "via", "gate", "of_def" } +ce = [ x for tag, x in uncut if x.tag == "callees" ] + [ ask( G, "callees", "big" ) ] +cr = [ x for tag, x in uncut if x.tag != "callees" ] + [ ask( G, "callers", "alpha/a.py:poll" ) ] +say( all( x.ok and x.dc for x in ce ) and all( set( d ) <= CALLEE_KEYS and d.get( "via" ) == "declined" for x in ce for d in x.dc ), "[P] k_keys_callees", + "default callees keys stay within {n,l,cands,via,gate,of_def}, via=\"declined\" on each", str( [ d for x in ce for d in x.dc if set( d ) - CALLEE_KEYS ][ :3 ] ) ) +say( all( x.ok and x.dc for x in cr ) and all( set( d ) <= CALLER_KEYS and d.get( "via" ) == "declined" for x in cr for d in x.dc ), "[P] k_keys_callers", + "default callers and safe-delete keys stay within {in,p,cands,via,gate,of_def}, via=\"declined\" on each (Q2)", + str( [ d for x in cr for d in x.dc if set( d ) - CALLER_KEYS or d.get( "via" ) != "declined" ][ :3 ] ) ) +jbad = [] +for root, flag in ( ( GO, "--callees=doPackage" ), ( RB, "--callers=lib/rake/task_manager.rb:define_task" ) ): + rc, out = run( root, flag, "--json" ) + try: + j = json.loads( out ) + except ValueError: + j = {} + if rc != 0 or not j.get( "dc" ) or any( "cand" in d or "candidates" in d for d in j[ "dc" ] ): + jbad.append( "%s rc=%s dc=%s" % ( flag, rc, j.get( "dc" ) ) ) +say( not jbad, "[P] k_json_nocand", "--json default \"dc\" entries carry no \"cand\" (candidates only on legend=full)", "; ".join( jbad ) ) +mbad = [] +for tool, root, sym in ( ( "find_symbol", GO, "doPackage" ), ( "find_referencing_symbols", RB, "lib/rake/task_manager.rb:define_task" ) ): + m = mcp( tool, { "path": root, "symbol": sym } ) or {} + ds = m.get( "declined_sites", [] ) + if not ds or any( "candidates" in d or "cand" in d or d.get( "via" ) != "declined" for d in ds ): + mbad.append( "%s declined_sites=%s" % ( tool, ds ) ) +say( not mbad, "[P] k_mcp_nocand", "MCP default \"declined_sites\" entries carry via=\"declined\" and no \"candidates\"", "; ".join( mbad ) ) + +print( "=== (K) R5 the root keeps every base attribute (dc_shown/dc_next aside) ===" ) +BASE_ROOTS = [ + ( "a_go", GO, "callees", "doPackage", { "schema": "ripwire.callees/v1", "of": "doPackage", "defs": "1", "count": "2", "hop_tested": "0", "hop_untested": "2", + "declined_calls": "3", "graph_ambiguous": "0", "graph_unresolved": "0", "graph_unindexed": "1", "counts_floor": "1", "next": "--expand=doPackage" } ), + ( "a_rb", RB, "callers", "lib/rake/task_manager.rb:define_task", { "schema": "ripwire.callers/v1", "of": "lib/rake/task_manager.rb:define_task", "defs": "1", + "count": "2", "hop_tested": "0", "hop_untested": "2", "declined_calls": "6", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", + "next": "--uses=define_task" } ), + ( "d_callers", PYF, "callers", "fetch", { "schema": "ripwire.callers/v1", "of": "fetch", "defs": "2", "count": "2", "hop_tested": "0", "hop_untested": "2", + "declined_calls": "4", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", "next": "--uses=fetch" } ), + ( "g_callers", G, "callers", "alpha/a.py:poll", { "schema": "ripwire.callers/v1", "of": "alpha/a.py:poll", "defs": "1", "count": "0", "hop_tested": "0", + "hop_untested": "0", "declined_calls": "76", "graph_ambiguous": "0", "graph_unresolved": "0", "counts_floor": "1", "next": "--uses=poll" } ), +] +for tag, root, verb, sel, want in BASE_ROOTS: + x = ask( root, verb, sel ) + got = { k: v for k, v in ( x.root.attrib.items() if x.root is not None else [] ) if k not in ( "root", "dc_shown", "dc_next" ) } + say( x.ok and x.dc and got == want, "[P] k_root_%s" % tag, "%s --%s=%s: rows ride and every other root attribute is the base literal" % ( tag, verb, sel ), + "got %s" % { k: ( got.get( k ), want.get( k ) ) for k in set( got ) | set( want ) if got.get( k ) != want.get( k ) } ) + +print( "=== (K) R6 dc_next is a call that lists every site; R7 the cut is file-first in path order ===" ) +import shlex +G_CALLER_LINES = { "big/big.py:%d" % l for l in range( 2, 42 ) } | { "callers/f%d.py:%d" % ( f, 2 + 4 * k ) for f in range( 3 ) for k in range( 12 ) } +for verb, sel, want_lines in ( ( "callers", "alpha/a.py:poll", G_CALLER_LINES ), ( "callees", "big", { str( l ) for l in range( 2, 42 ) } ) ): + x = ask( G, verb, sel ) + nxt = x.a( "dc_next" ) or "" + rc, out = run( G, *shlex.split( nxt ) ) if nxt else ( 1, "" ) + if verb == "callers": + named = set( re.findall( r' p="([^"]+)"', out ) ) + else: + named = { m for m in re.findall( r']*\bl="(\d+)"', out ) } + say( x.ok and nxt and rc == 0 and want_lines <= named, "[P] k_dc_next_%s" % verb, + "the printed dc_next= (%s) runs (rc 0) and names all %d declined call lines" % ( nxt or "absent", len( want_lines ) ), + "rc=%s missing=%d" % ( rc, len( want_lines - named ) ) ) +x = ask( G, "callers", "alpha/a.py:poll" ) +first4 = [ d.get( "p", "" ).rsplit( ":", 1 )[ 0 ] for d in x.dc[ :4 ] ] +say( x.ok and first4 == [ "big/big.py", "callers/f0.py", "callers/f1.py", "callers/f2.py" ], "[P] k_cut_order", + "the first four callers rows of the cut are four distinct files in path order (each file's first site before any file's second)", "first4=%s" % first4 ) + +print( "=== (K) R8 safe-delete: possible-callers only where a declined call could mean THIS definition ===" ) +x = ask( GO, "safe-delete", "cmd/package.go:onlyResolved" ) +say( x.ok and x.a( "risk" ) == "none-found" and x.dc == [] and x.a( "declined_calls" ) is None, "[C] k_sd_ctrl", + "a definition no declined call could mean keeps risk=\"none-found\" on a tree that has declines", x.why() ) +x = ask( TS, "safe-delete", "src/routers/trie/router.ts:match" ) +say( x.ok and x.a( "risk" ) == "possible-callers" and [ d.get( "in" ) for d in x.dc ] == [ "dispatch" ], "[P] k_sd_ts", + "ts --safe-delete=src/routers/trie/router.ts:match reads risk=\"possible-callers\" with the dispatch row (base: none-found)", x.why() ) + +print( "=== (K) R9 + R10 legend: no \"no count or row\" where rides; the hedge in the review's words ===" ) +R10_EDGE = "NOT an edge and NOT a caller or callee claim" +R10_K1 = "(K may be 1)" +lbad = [] +for tag, x in uncut + [ ( "g callers", ask( G, "callers", "alpha/a.py:poll" ) ) ]: + if not x.ok or not x.dc: + lbad.append( "%s: no rows" % tag ) + elif "no count or row" in x.legend: + lbad.append( "%s: says 'no count or row'" % tag ) +say( not lbad, "[P] k_legend_drift", "no answer carrying rows also says 'no count or row' (compact)", "; ".join( lbad[ :4 ] ) ) +lbad = [] +for root, verb, sel in ( ( GO, "callees", "doPackage" ), ( RB, "callers", "lib/rake/task_manager.rb:define_task" ), ( GO, "safe-delete", "deb/deb.go:Package" ), + ( RBS, "callees", "run" ) ): + x = ask( root, verb, sel, "--legend=full" ) + if not x.ok or not x.dc: + lbad.append( "%s: no rows" % sel ) + elif "no count or row" in x.legend or R10_EDGE not in x.legend or R10_K1 not in x.legend or LEG_DONT not in x.legend or LEG_NOT not in x.legend: + lbad.append( "%s: legend lacks the R10 wording or keeps 'no count or row'" % sel ) + elif x.err: + lbad.append( "%s: %s" % ( sel, x.err ) ) +say( not lbad, "[P] k_legend_full", "--legend=full: the clause reads 'NOT an edge and NOT a caller or callee claim', '(K may be 1)', 'proved none', " + "'not a claim this call reaches any of them'; no 'no count or row'; well-formed XML", "; ".join( lbad ) ) +x = ask( RBS, "callees", "run", "--legend=full" ) +say( x.ok and x.dc and "gate=side" in x.legend, "[P] k_legend_side", "the gate=side reading rides where a gate=\"side\" row does", x.why() ) +x = ask( GO, "safe-delete", "deb/deb.go:Package", "--legend=full" ) +say( x.ok and "possible-callers:" in x.legend and "not a safety reading" in x.legend and "none-found beside declined_calls=" not in x.legend, + "[P] k_legend_sd", "safe-delete's risk sentence defines possible-callers and says it is not a safety reading", x.why() ) + # ── (I) determinism ──────────────────────────────────────────────────────────────────────────────────────────────── print( "=== (I) determinism ===" ) rc1, o1 = run( GO, "--callees=doPackage", "--legend=full" ) diff --git a/test/declinedsitefix/rbside/lib/base.rb b/test/declinedsitefix/rbside/lib/base.rb new file mode 100644 index 000000000..8aea9d10a --- /dev/null +++ b/test/declinedsitefix/rbside/lib/base.rb @@ -0,0 +1,5 @@ +class Base + def self.build + new + end +end diff --git a/test/declinedsitefix/rbside/lib/child.rb b/test/declinedsitefix/rbside/lib/child.rb new file mode 100644 index 000000000..e42e7788f --- /dev/null +++ b/test/declinedsitefix/rbside/lib/child.rb @@ -0,0 +1,5 @@ +class Child < Base + def initialize + @x = 1 + end +end diff --git a/test/declinedsitefix/rbside/lib/gadget.rb b/test/declinedsitefix/rbside/lib/gadget.rb new file mode 100644 index 000000000..8ac6c528c --- /dev/null +++ b/test/declinedsitefix/rbside/lib/gadget.rb @@ -0,0 +1,9 @@ +class Gadget + def helper + 2 + end + + def self.make + helper + end +end diff --git a/test/declinedsitefix/rbside/lib/widget.rb b/test/declinedsitefix/rbside/lib/widget.rb new file mode 100644 index 000000000..e3fa9a534 --- /dev/null +++ b/test/declinedsitefix/rbside/lib/widget.rb @@ -0,0 +1,9 @@ +class Widget + def self.helper + 1 + end + + def run + helper + end +end From 43ffced92ab01b47ff5f1e14eca74954ba8b5f0f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 06:10:01 -0400 Subject: [PATCH 093/162] fix(for): a terser compact tail clause; CI round 1 rule-4 updates Compact clause 136 -> 99 B (ripwire.for/v1 (U) re-pinned 775 -> 820, measured 802; (L) 7,871 B under its 7,900). fornotesbudgetcheck / compactlegendcheck ST: maximality against T less the trailing sections. testrowruncheck census: forpage.h renders the FILE tail rows (not tests_to_run). relevancefloorcheck (4) normalises the tail page rows and the clause growth out (2875 B < 3000), (4b) 3300 -> 3400 (3366). Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 2 +- test/compactlegendcheck.sh | 25 ++++++++++++++++++++++--- test/fornotesbudgetcheck.sh | 17 ++++++++++++++++- test/relevancefloorcheck.sh | 11 ++++++++--- test/testrowruncheck.sh | 5 +++-- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index a01b276e7..31659d0ff 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -705,7 +705,7 @@ inline std::size_t ownerLegendExemptBytes( const ForLensHeaderParts& p, bool com inline constexpr std::string_view kForCompactLegendBodies = "; b t= n= p= l= full bodies, c n= l= callee signatures"; inline constexpr std::string_view kForCompactLegendTail = - "; t p= file outside sigs (weaker), then t p= sym=: the file page's first 40 rows (file-first, sym= top symbols), r= rank (gap = trimmed)"; + "; t p= file outside sigs (weaker), t sym= a file-page row: its top symbols, r= rank (gap = trimmed)"; // R2-AF (round 2, S4): the COMPACT dialect's `` clause — verbatim, round-2 amendment §R7. // 96 B, present-only like every clause in this dialect (appendCompactForLegend below). inline constexpr std::string_view kForCompactLegendHdr = diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 98efa4a6b..0aadb9866 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -546,6 +546,10 @@ probeFor() # callers prose minus that reading to the OLD 330; the existing (U-twin) holds safe-delete minus its reading to 720. # the pins follow the definitions, measured + 10 rounded up to 10. # schema pin measured +# RE-PINNED 2026-10-15 (lane forbudget-soft round C, FOLD C): ripwire.for/v1 775 -> 820 (measured 802; 761 at the last pin). +# The now carries page-0 rows of the file page with sym=, and the compact tail clause reads them: "; t p= file +# outside sigs (weaker), r= rank (gap = trimmed)" (58 B) -> "...(weaker), t sym= a file-page row: its top symbols, r= ..." +# (99 B, +41 B), nothing else moved. (L) stays under its 7,900 B bill (7,871 B). # RE-PINNED 2026-10-04 (lane/lean-answers-068), two new definitions, each measured on this gate's fixture against the # base binary (255dc199) — no prose was added to any reading that already rode: # ripwire.for/v1 738->761: " (absent cx/ccx/in = 0)" (+23 B) — the --for lens omits a zero cx=/ccx=/in=. @@ -645,7 +649,7 @@ ripwire.pack-task/v1 990 974 ripwire.pack-top-n/v1 760 745 ripwire.expand/v1 280 265 ripwire.expand-file/v1 510 508 -ripwire.for/v1 775 761 +ripwire.for/v1 820 802 ' pinFor() { @@ -1128,7 +1132,7 @@ fi # softp.py FILE N TASK ROWSOUT [REF] prints ONE line "OK|BAD rows= total= charged= T= est= over= next= fits= maximal= rp= hl="; # exit 3 = no root or no block (a missing premise: the caller FAILs it, never skips). ROWSOUT gets the rows. cat > "$TMP/softp.py" <<'PY' -import html, re, sys +import html, json, re, sys def read( path ): try: s = open( path, encoding = "utf-8", errors = "replace" ).read() except OSError: return None @@ -1150,9 +1154,24 @@ def read( path ): or "[over_ceiling=1 also when the sigs next= rides unpaid" in s, hl = "(ceiling)" not in s and "task_echo: dropped" not in s and ' route="' in rt and tk is not None and html.unescape( tk.group( 1 ) ) == sys.argv[ 3 ] ) +def sections_u( s ): + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the cut fill what they leave of T, so maximality reads T less their bytes (U): XML, all after but + # plus the rows, less the --detail bodies (a root without bundle=); JSON, the "tail" object + if s.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( s ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + u = ( len( s.split( "", 1 )[ 1 ].encode() ) - len( "" ) ) if "" in s else 0 + u += sum( len( h.encode() ) for h in re.findall( r"]*/>", s.split( "]*>", s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r"", s, re.S ) + u -= len( bm.group( 0 ).encode() ) if bm else 0 + return u a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ); k = len( a[ "rows" ] ) +U = sections_u( a[ "s" ] ) open( sys.argv[ 4 ], "w" ).write( "".join( x[ 1 ] + "\n" for x in a[ "rows" ] ) ) cut = k < a[ "total" ] fits = ( a[ "charged" ] <= X or k <= 4 ) and k >= min( 4, a[ "total" ] ) @@ -1161,7 +1180,7 @@ maximal = rp = True if len( sys.argv ) > 5: ref = read( sys.argv[ 5 ] ) if ref is None: sys.exit( 3 ) - if cut and k < len( ref[ "rows" ] ): maximal = a[ "charged" ] + ref[ "rows" ][ k ][ 2 ] > X - 160 + if cut and k < len( ref[ "rows" ] ): maximal = a[ "charged" ] + ref[ "rows" ][ k ][ 2 ] > X - U - 160 rp = k > 0 and [ x[ 1 ] for x in a[ "rows" ] ] == [ x[ 1 ] for x in ref[ "rows" ] ][ :k ] ok = a[ "est" ] != "-" and fits and maximal and rp and nxt and not a[ "over" ] and a[ "hl" ] print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d next=%d fits=%d maximal=%d rp=%d hl=%d" diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index b91096f21..971562790 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -311,9 +311,24 @@ def read( path ): b = re.search( r'\bbudget_tokens="(\d+)"', rt ); bt = int( b.group( 1 ) ) if b else None rows.sort() return dict( rows=rows, charged=charged, total=total, next=nxt, over=over, est=est, bt=bt ) +def sections_u( s ): + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the cut fill what they leave of T, so maximality reads T less their bytes (U): XML, all after but + # plus the rows, less the --detail bodies (a root without bundle=); JSON, the "tail" object + if s.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( s ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + u = ( len( s.split( "", 1 )[ 1 ].encode() ) - len( "" ) ) if "" in s else 0 + u += sum( len( h.encode() ) for h in re.findall( r"]*/>", s.split( "]*>", s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r"", s, re.S ) + u -= len( bm.group( 0 ).encode() ) if bm else 0 + return u a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ) +U = sections_u( open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() ) open( sys.argv[ 3 ], 'w' ).write( "".join( x[ 1 ] + "\n" for x in a[ 'rows' ] ) ) k = len( a[ 'rows' ] ) fits = ( a[ 'charged' ] <= X or k <= 4 ) and k >= min( 4, a[ 'total' ] ) @@ -324,7 +339,7 @@ if len( sys.argv ) > 4: ref = read( sys.argv[ 4 ] ) if ref is None: sys.exit( 3 ) if cut and k < len( ref[ 'rows' ] ): - maximal = a[ 'charged' ] + ref[ 'rows' ][ k ][ 2 ] > X - 160 + maximal = a[ 'charged' ] + ref[ 'rows' ][ k ][ 2 ] > X - U - 160 mine = [ x[ 1 ] for x in a[ 'rows' ] ] rp = len( sys.argv ) > 4 and k > 0 and mine == [ x[ 1 ] for x in ref[ 'rows' ] ][ :k ] if rp and len( sys.argv ) > 5 and sys.argv[ 5 ]: diff --git a/test/relevancefloorcheck.sh b/test/relevancefloorcheck.sh index efab47e7a..8b1d57305 100755 --- a/test/relevancefloorcheck.sh +++ b/test/relevancefloorcheck.sh @@ -177,12 +177,17 @@ echo "=== (4) byte ceiling on the fixture ===" # answer with exactly those two additions normalised out (4), and the whole answer gets its own ceiling of # 3300 B (4b) — still far below the 5307 B padded answer this arm exists to keep out. a_bytes="$( printf '%s' "$A_OUT" | wc -c | tr -d ' ' )" -a_norm_bytes="$( printf '%s' "$A_OUT" | sed -E 's/ e="[0-9]+"//g; s/; e= on a d row: the 1-based line where that definition ends, body-inclusive; absent when the extent is not known \(extent_suspect, docs, config\), never 0; l= is the line of the definition.s name, so a definition can start above l=//' | wc -c | tr -d ' ' )" +# RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): the now carries page 0 of the --for file page — rows +# `` — and its full legend clause grew 391 -> 598 B to define them. (4) normalises those out too (the page rows, +# and the clause back to its old 391 B), so its 3000 B assertion still measures what it measured; (4b), the whole answer, +# moves 3300 -> 3400 B (measured 3366): still far below the 5307 B padded answer this arm exists to keep out. +a_norm_bytes="$( printf '%s' "$A_OUT" | sed -E 's/ e="[0-9]+"//g; s/; e= on a d row: the 1-based line where that definition ends, body-inclusive; absent when the extent is not known \(extent_suspect, docs, config\), never 0; l= is the line of the definition.s name, so a definition can start above l=//' \ + | python3 -c 'import re,sys; s=sys.stdin.read(); s=re.sub(r"","",s); m=re.search(r"; tail: [^\]]*?budget-trimmed row\)",s); s=s.replace(m.group(0),"x"*391) if m and len(m.group(0))>391 else s; sys.stdout.write(s)' | wc -c | tr -d ' ' )" case "$a_bytes$a_norm_bytes" in *[!0-9]*|'') no "(4) byte counts are not numeric ('$a_bytes' / '$a_norm_bytes')"; a_bytes=999999; a_norm_bytes=999999 ;; esac [ "$a_norm_bytes" -lt 3000 ] && ok "(4) the floored bundle without e= and its clause is $a_norm_bytes B (< 3000; pre-fix was 5307 B on this fixture)" \ || no "(4) the floored bundle without e= and its clause is $a_norm_bytes B, over the 3000 B ceiling" -[ "$a_bytes" -lt 3300 ] && ok "(4b) the whole floored bundle is $a_bytes B (< 3300 with e= and its legend clause)" \ - || no "(4b) the whole floored bundle is $a_bytes B, over the 3300 B ceiling" +[ "$a_bytes" -lt 3400 ] && ok "(4b) the whole floored bundle is $a_bytes B (< 3400 with e=, the tail page rows and their legend clauses)" \ + || no "(4b) the whole floored bundle is $a_bytes B, over the 3400 B ceiling" if printf '%s' "$A_OUT" | grep -q ' e="'; then [ "$a_norm_bytes" -lt "$a_bytes" ] \ && ok "(4c) the normalisation in (4) removed e= bytes ($a_bytes -> $a_norm_bytes B), so the 3000 B twin is live" \ diff --git a/test/testrowruncheck.sh b/test/testrowruncheck.sh index c87277d05..612846a92 100755 --- a/test/testrowruncheck.sh +++ b/test/testrowruncheck.sh @@ -48,10 +48,11 @@ echo "testrowruncheck: BIN=$BIN" # ── ARM 0 — the emitter census, derived from src/ ────────────────────────────────────────────────────── # Every site that prints a tests_to_run row. The list is the CONTRACT: a site added to src/ and not added -# here fails, which is the only way a family gate stays a family gate. +# here fails, which is the only way a family gate stays a family gate. forpage.h (forbudget-soft round C) renders the --for +# answer's FILE tail rows (``, moved there from serialize.h, already excluded): file rows, not tests_to_run. EXPECTED_SITES="src/verbs_change.h src/situ.h src/prcontext.h src/packtask.h src/handoff.h src/flipimpact.h src/mcpverbs.h src/mcpedit.h" FOUND_SITES="$( cd "$ROOT" && grep -lE 'testRows(Rendered|Joined|List)\(|"<(t|test) p=\\"|\{\\"test\\":|\{\\"p\\":\\"%s\\"%s\}' src/*.h src/*.cpp 2>/dev/null \ - | grep -vE 'src/(serialize|testmap)\.h' | sort | tr '\n' ' ' | sed 's/ $//' )" + | grep -vE 'src/(serialize|testmap|forpage)\.h' | sort | tr '\n' ' ' | sed 's/ $//' )" WANT_SITES="$( printf '%s\n' $EXPECTED_SITES | sort | tr '\n' ' ' | sed 's/ $//' )" [ "$FOUND_SITES" = "$WANT_SITES" ] \ && ok "(0) the tests_to_run row emitters are exactly the census this gate drives" \ From 3c9886e28f3e35ad90985fe36298ac153d7e686d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 06:21:24 -0400 Subject: [PATCH 094/162] feat(declined): name the call sites the resolver declined, as hedged rows A declined call (tier 3, the builtin-method and C++ standard-member name gates, Ruby's side cut and `new`) gets no edge; declined_calls= counted it without naming it. buildGraph now keeps one record per declined call (caller, interned candidate list, reference index, kind; 13 B per call), written at all seven decline sites through one helper, so the records are exactly the calls declined_calls= counts. The kind is an enum with a value reserved for interface-typed dispatch rows. --callees lists the declines the definitions made as ; --callers and --safe-delete list the declined calls whose candidate LIST holds the selector's definition (list membership, never the called name) as . gate="builtin"|"std"|"side" names the gate; of_def= is the defining group on the callees form and hedged K~d on the callers form; candidates (cand=) only with --legend=full. Rows are never in count=, the rows or any reach. Capped at 16 (file-first on the callers form; --limit raises it) with dc_shown= and dc_next=. The same rows ride --json ("dc"), columnar (verbatim) and the MCP twins ("declined_sites"). --safe-delete reads risk="possible-callers" instead of none-found when a declined call could have meant the definition. The legend defines (what a row does NOT mean), and the declined_calls= clause no longer says "no count or row" where rows ride. --impact prints no rows; its legend names the callers verb as where the first-hop sites are listed. The Graph is rebuilt from the ingest every run (never cached), so no cache version moves. Co-Authored-By: Claude Opus 5.5 --- src/callhierarchy.h | 7 + src/columnar.h | 4 +- src/compactlegend.h | 17 +- src/declinedsites.h | 367 +++++++++++++++++++++++++++++++++++++++++++ src/graph.h | 155 +++++++++++++++--- src/graphlegend.h | 64 ++++++++ src/mcpverbs.h | 13 +- src/verbs_navigate.h | 64 +++++--- 8 files changed, 649 insertions(+), 42 deletions(-) create mode 100644 src/declinedsites.h diff --git a/src/callhierarchy.h b/src/callhierarchy.h index b6e90a26a..f2d821c05 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -20,6 +20,7 @@ // surface-specific by contract (the CLI has a legend, the MCP arm speaks JSON-RPC errors), and folding them // in would trade one drift for another. +#include "declinedsites.h" // the rows: the declined call sites, ordered here once for every surface #include "defattrib.h" // the per-row definition attribution every surface of these rows serves (and tsContractSignatures) #include "filter.h" // pathTierIndexOver / compareTierThenPath — the tier-then-path row order both surfaces serve #include "graph.h" // resolveAllByNameQualified, the CSR, testSymbolForwardReach / countTestedIn / isTestedByReach @@ -91,6 +92,9 @@ struct CallHierarchyRows ValueRefRows valueRefs; // reference-as-value round: rows, never in `rows` or any count above StdMemberCallsMade stdCalls; // callees only: of declinedCalls, the C++ standard-member ones (graph.h stdMemberCallsMadeBy) OneHopAttribution attr; // attrib-multidef (defattrib.h): the definition groups + each row's ties, parallel to `rows` + // lane declined-sites (declinedsites.h): the records behind declinedCalls, in served order (callers file-first, callees + // file and line) — the rows, never in `rows` or any count above; uncut, declinedSites.size() == declinedCalls. + std::vector declinedSites; }; // The callees answer's line (graph.h StdMemberGate, stdMemberCallsMadeBy), one spelling per dialect — the JSON keys @@ -295,6 +299,9 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph } out.declinedCalls = wantCallers ? declinedCallsNaming( g, out.matches ) : declinedCallsMadeBy( g, out.matches ); + out.declinedSites = wantCallers ? declinedSitesNaming( g, out.matches ) : declinedSitesMadeBy( g, out.matches ); + orderDeclinedSites( ing, g, out.declinedSites, /*fileFirst=*/wantCallers ); + ENSURES( out.declinedSites.size() == out.declinedCalls, "a row per declined call the answer counts" ); out.declinedIface = wantCallers ? declinedIfaceCallsNaming( ing, g, out.matches ) : 0; out.stdCalls = wantCallers ? StdMemberCallsMade{} : stdMemberCallsMadeBy( g, out.matches ); diff --git a/src/columnar.h b/src/columnar.h index d9847d89f..09015b289 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -25,6 +25,7 @@ #include #include +#include #include namespace rw @@ -241,6 +242,7 @@ struct ColumnarRowExtras const std::vector* ofDef = nullptr; const std::string* defRows = nullptr; const std::vector* splitArm = nullptr; + const std::string* tailRows = nullptr; // XML rows kept verbatim after (lane declined-sites: ) }; inline std::string columnarExtraFields( const ColumnarRowExtras& extras ) { @@ -391,7 +393,7 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, // A6 / 0.6.5 / FE-B: each present only when the caller passed it — see the wrapper banner and emitColumnarOptionalColumns. emitColumnarOptionalColumns( out, ing, rows, testReach, depth, extras ); - rw::emitTo( out, "", wrapperTag ); + rw::emitTo( out, "{}", extras.tailRows != nullptr ? std::string_view( *extras.tailRows ) : std::string_view(), wrapperTag ); } // COLUMNAR form of the --uses verb: use-site rows carry (fileId, line, role, enclosing-symbol name `in`) diff --git a/src/compactlegend.h b/src/compactlegend.h index 9b14cbb51..9663e06b1 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -304,6 +304,8 @@ struct CompactCompletenessTerm // item: naming tested carries the tested column, not every columnar answer std::string_view onKey = {}; // read ONLY on an answer of this schema key (ripwire./v1): the same NAME on the same // element means different things on two verbs ( on context-ratio vs ensemble) + std::string_view unlessTag = {}; // with unlessAttr: NOT read on a payload where this element carries that attribute — the + std::string_view unlessAttr = {}; // row a richer one replaces there (declined_calls= beside a row says "in no row") }; // #60: the escaped spelling of the module-scope owner's name, as it reaches a rendered document, and the @@ -335,7 +337,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // first two at once. { "bodyless_defs", "bodyless_defs=K: K of defs= have no body, so no callees to read" }, { "unproven_defs", "unproven_defs=K: K same-named defs not tied to that file, in no count or row (bare name shows them)" }, - { "declined_calls", "declined_calls=K: K call sites left unbound (no evidence chose one def), in no count or row" }, + { "declined_calls", "declined_calls=K: K call sites left unbound (no evidence chose one def), in no count or row", false, {}, MapHeaderRead::No, {}, {}, "dc", "via" }, { "cross_kind", "cross_kind=kind:N,...: the defs= definitions differ in kind; rows union all of them (narrow with file:name)" }, // lane recall: absent at zero, callers and impact only (graphlegend.h kDeclinedIfaceLegend). { "declined_iface", "declined_iface=K: K declined TS calls sharing a name with an interface/abstract signature, by name only (MAY go through it); not a subset of declined_calls=" }, @@ -591,6 +593,15 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "of_def", "of_def=K..: the def ix=K groups the row belongs to; K resolved edge, K~s edge labelled split, K~n by name alone, K~t into a primary template whose specialisation is listed; NOT a claim the edge is false", true, "s" }, { "of_def", "c of_def=K..: the def ix=K groups that caller belongs to (K~s split label, K~n by name, K~t primary template); NOT a claim the edge is false", true, "c" }, { "of_def", "rf of_def=K..: the def ix=K groups its file's reached symbols belong to, strongest tie per group", true, "rf" }, + // lane declined-sites (declinedsites.h): the rows, read exactly where one rides. The first row also reads + // declined_calls=, whose plain row above steps aside there (its "in no count or row" is false beside a row). + { "via", "declined_calls=K: K call sites left unbound, in no count; : one such call SITE " + "(callees: called name, line; callers: calling def, file:line); NOT an edge or a caller/callee claim; cands=K: " + "K in-tree defs it could have meant (K may be 1), the resolver proved none; not a claim this call reaches any of them", + true, "dc", MapHeaderRead::No, "declined" }, + { "gate", "dc gate=: builtin or std, a name gate declined it; side, a Ruby call whose in-reach defs answer on the other side of self", true, "dc" }, + { "of_def", "dc of_def=K: the def ix=K making the call; K~d: its candidates include group K, never a claim it calls K", true, "dc" }, + { "dc_shown", "dc_shown=N: dc rows printed (callers: file-first); dc_next= lists every site" }, { "fields", " column: each row's def ix=K groups (K~s split label, K~n by name, K~t primary template), space-separated", true, "cols", MapHeaderRead::No, "of_def" }, { "fields", " column: 1 where the row's edge to of= carries the resolver's split label (may be another arm's)", true, "cols", MapHeaderRead::No, "split" }, { "fields", " column: 1 = via=name, the row's edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "cols", MapHeaderRead::No, "via" }, @@ -1792,6 +1803,10 @@ inline bool isCompletenessTermPresent( const CompactCompletenessTerm& t, std::st { return false; } + if( !t.unlessTag.empty() && payloadHasAnyAttr( doc, t.unlessAttr, t.unlessTag ) ) + { + return false; + } if( !t.onTag.empty() ) { return payloadHasAnyAttr( doc, t.attr, t.onTag, t.valueItem ); diff --git a/src/declinedsites.h b/src/declinedsites.h new file mode 100644 index 000000000..e437b53c5 --- /dev/null +++ b/src/declinedsites.h @@ -0,0 +1,367 @@ +#pragma once +// declinedsites.h — the rows: the call SITES the resolver declined to bind, named. One row builder and its three +// spellings (XML, the CLI's --json, the MCP twins' JSON), shared by --callers/--callees (callhierarchy.h selects the +// records once for both surfaces) and --safe-delete. +// +// THE GAP IT CLOSES. A declined call (tier 3, the builtin-method and C++ standard-member name gates, Ruby's side cut and +// `new`) gets no edge, and declined_calls= counted it without naming it: "what does doPackage call" missed +// `pkg.Package( info, f )` through an interface value, "who calls TaskManager#define_task" missed every test that calls +// it on an untyped receiver. buildGraph now keeps one record per declined call (graph.h Graph::declinedSite*), and these +// rows read it back. +// +// WHAT A ROW IS, AND IS NOT. The call site — its enclosing definition, file:line and the CALLED name — is a fact. Its +// link to any definition is not: cands=K counts the in-tree definitions the call could have meant (K may be 1), the +// resolver proved none, and the call may reach none of them (an out-of-tree, builtin or standard-library method). So a +// row is its own element, never an row, never inside count=, hop_tested=/hop_untested=, the rank or any reach; +// via="declined" rides every row so a row read without its legend (a graded packet, a columnar or MCP answer) still +// says so. Candidates are named (cand=) only on --legend=full. +// +// WHICH ROWS. Callees form: the records the selector's definitions MADE (graph.h declinedSitesMadeBy). Callers and +// safe-delete form: the records whose stored candidate LIST holds one of the selector's definitions (declinedSitesNaming) +// — membership, never the called name: the language and root filters split one name into several lists, and a Ruby +// `new` declines with `initialize` candidates. Either way the rows are exactly the calls the answer's declined_calls= +// counts, so uncut, the row count equals it. +// +// CAPPED AND COUNTED. kDeclinedSiteCap rows by default (a raisable default: --limit=N lifts it, as every primary row cap +// does); past it the root says dc_shown= (rows printed) and dc_next= (the same question with --limit raised to list every +// site). The callers form is cut FILE-FIRST — each calling file's first site, in path order, before any file's second — +// so a cut still names every file it can; the callees form reads in file then line order, as the body does. + +#include "cli.h" // kPageValueMax — dc_next='s --limit saturates at the flag's own ceiling +#include "defattrib.h" // DefGroups — of_def on a multi-definition answer +#include "graph.h" // Graph::declinedSite*, DeclinedSiteKind, kNoDeclinedList +#include "model.h" +#include "nextverb.h" // nextFlag — dc_next= pastes as one argv element +#include "valuerefs.h" // VrRender / vrPath — the rows' path spelling; escapeXml / jsonStr through serialize.h + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +inline constexpr std::size_t kDeclinedSiteCap = 16; // rows per answer by default; --limit=N raises it (dc_next= does) +inline constexpr std::size_t kDeclinedSiteCandCap = 8; // cand= entries per row on --legend=full; cand_more= counts the rest + +// gate= on a row: which name gate (or Ruby's side cut) declined the call; nothing for the ladder's own tier-3 decline and +// for Ruby's `new` (its candidates are `initialize`, the row's n= is the called `new`). +inline const char* declinedSiteGate( DeclinedSiteKind kind ) noexcept +{ + switch( kind ) + { + case DeclinedSiteKind::Builtin: return "builtin"; + case DeclinedSiteKind::Std: return "std"; + case DeclinedSiteKind::RubySide: return "side"; + case DeclinedSiteKind::Ladder: + case DeclinedSiteKind::RubyNew: + case DeclinedSiteKind::Iface: return nullptr; + } + return nullptr; +} + +// The served order, in place. Every form first sorts by the call's file (path order), line, then record (resolve order: +// two calls on one line keep their source order). The callers form is then interleaved FILE-FIRST: a stable sort on each +// site's rank within its file, so rank 0 of every file (in path order) precedes rank 1 of any. +inline void orderDeclinedSites( const IngestResult& ing, const Graph& g, std::vector& sites, bool fileFirst ) +{ + const auto refOf = [ & ]( std::uint32_t site ) -> const Reference& { return ing.references[ g.declinedSiteRef[ site ] ]; }; + for( const std::uint32_t site : sites ) + { + EXPECTS( site < g.declinedSiteRef.size() && g.declinedSiteRef[ site ] < ing.references.size(), + "the records and the references come from the same build (an edit preview rebuilds both)" ); + } + std::sort( sites.begin(), sites.end(), [ & ]( std::uint32_t a, std::uint32_t b ) + { + const Reference& ra = refOf( a ); + const Reference& rb = refOf( b ); + if( ra.fileId != rb.fileId ) + { + return ing.files[ ra.fileId ] < ing.files[ rb.fileId ]; + } + return ra.line != rb.line ? ra.line < rb.line : a < b; + } ); + if( !fileFirst || sites.size() < 2 ) + { + return; + } + std::vector rankInFile( sites.size(), 0 ); + for( std::size_t i = 1; i < sites.size(); ++i ) + { + rankInFile[i] = refOf( sites[i] ).fileId == refOf( sites[i - 1] ).fileId ? rankInFile[i - 1] + 1 : 0; + } + std::vector order( sites.size() ); + std::iota( order.begin(), order.end(), 0u ); + std::stable_sort( order.begin(), order.end(), [ & ]( std::uint32_t a, std::uint32_t b ) { return rankInFile[a] < rankInFile[b]; } ); + std::vector interleaved; + interleaved.reserve( sites.size() ); + for( const std::uint32_t i : order ) + { + interleaved.push_back( sites[i] ); + } + sites.swap( interleaved ); +} + +// One row's facts, surface-neutral: every dialect below spells these and nothing else. +struct DeclinedSiteView +{ + std::string_view name; // the CALLED name (the reference's own; never a candidate's) + std::uint32_t line = 0; // the call's line + std::string file; // the call's file (root-relative on one root) + std::string_view caller; // the enclosing definition's name + std::size_t cands = 0; // the candidate list's size + bool candsKnown = false; // false only for a list internDeclinedList could not store + const char* gate = nullptr; + std::vector ofDef; // callees: {"K"}, the definition making the call; callers: {"K~d", ...} + std::vector cand; // --legend=full: the candidates as file:line, at most kDeclinedSiteCandCap + std::size_t candMore = 0; +}; + +// The rows of one answer: `shown` (served order, already cut) as views. `matches`/`groups` give the multi-definition +// labels (of_def; none on a one-group answer, nor on a definition table cut past its cap, whose numbers would name groups +// the table does not list). +inline std::vector declinedSiteViews( const IngestResult& ing, const Graph& g, std::span shown, bool callersForm, + std::span matches, const DefGroups& groups, const VrRender& vr, bool candidates ) +{ + std::vector out; + out.reserve( shown.size() ); + // match → its group, sorted by node once per answer (a row looks its caller or candidates up by binary search) + std::vector> groupOf; + const bool labelled = groups.multi() && !defTableCapped( groups ) && groups.ofMatch.size() == matches.size(); + if( labelled ) + { + for( std::size_t i = 0; i < matches.size(); ++i ) + { + if( groups.ofMatch[i] < groups.size() ) + { + groupOf.emplace_back( matches[i], groups.ofMatch[i] ); + } + } + std::sort( groupOf.begin(), groupOf.end() ); + } + const auto groupOfNode = [ & ]( NodeId n ) -> std::uint32_t + { + const auto it = std::lower_bound( groupOf.begin(), groupOf.end(), std::pair( n, 0u ) ); + return it != groupOf.end() && it->first == n ? it->second : UINT32_MAX; + }; + std::vector groupHit( groups.size(), 0 ); + for( const std::uint32_t site : shown ) + { + const Reference& r = ing.references[ g.declinedSiteRef[ site ] ]; + DeclinedSiteView v; + v.name = r.calleeName; + v.line = r.line; + v.file = vrPath( ing, r.fileId, vr ); + v.caller = ing.symbols[ g.declinedSiteCaller[ site ] ].name; + v.gate = declinedSiteGate( g.declinedSiteKind[ site ] ); + const std::uint32_t list = g.declinedSiteList[ site ]; + std::span cand; + if( list != kNoDeclinedList ) + { + EXPECTS( list + 1 < g.declinedListOff.size(), "a stored list has its closing offset" ); + cand = std::span( g.declinedListCand.data() + g.declinedListOff[ list ], g.declinedListOff[ list + 1 ] - g.declinedListOff[ list ] ); + v.cands = cand.size(); + v.candsKnown = true; + } + if( labelled && !callersForm ) + { + if( const std::uint32_t grp = groupOfNode( g.declinedSiteCaller[ site ] ); grp != UINT32_MAX ) + { + v.ofDef.push_back( std::to_string( grp + 1 ) ); + } + } + else if( labelled ) + { + std::fill( groupHit.begin(), groupHit.end(), 0 ); + for( const NodeId c : cand ) + { + if( const std::uint32_t grp = groupOfNode( c ); grp != UINT32_MAX ) + { + groupHit[ grp ] = 1; + } + } + for( std::size_t k = 0; k < groupHit.size(); ++k ) + { + if( groupHit[k] ) + { + v.ofDef.push_back( std::to_string( k + 1 ) + "~d" ); // hedged: its candidates include group K, never "it calls K" + } + } + } + if( candidates ) + { + for( const NodeId c : cand ) + { + if( v.cand.size() == kDeclinedSiteCandCap ) + { + ++v.candMore; + continue; + } + const Symbol& cs = ing.symbols[c]; + v.cand.push_back( vrPath( ing, cs.fileId, vr ) + ":" + std::to_string( cs.line ) ); + } + } + out.push_back( std::move( v ) ); + } + return out; +} + +// XML: (callees) or (callers, safe-delete), in the views' order; "" when there is none. +inline std::string declinedSitesXml( std::span views, bool callersForm ) +{ + std::string out; + std::vector esc; + const auto ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; + for( const DeclinedSiteView& v : views ) + { + out += callersForm ? " 0 ) + { + out += " cand_more=\"" + std::to_string( v.candMore ) + "\""; + } + out += "/>"; + } + return out; +} + +// JSON, one array: the CLI --json `"dc"` (the XML attribute names as keys) or the MCP twins' `"declined_sites"` +// (name/line or in/file/line, candidates). "" when there is none, so an answer without a decline keeps its bytes. +inline std::string declinedSitesJson( std::span views, bool callersForm, bool mcp ) +{ + if( views.empty() ) + { + return {}; + } + std::string out = mcp ? ",\"declined_sites\":[" : ",\"dc\":["; + const auto list = [ & ]( std::string_view key, const std::vector& items ) + { + out += ",\"" + std::string( key ) + "\":["; + for( std::size_t i = 0; i < items.size(); ++i ) + { + out += ( i == 0 ? "\"" : ",\"" ) + jsonStr( items[i] ) + "\""; + } + out += "]"; + }; + for( std::size_t i = 0; i < views.size(); ++i ) + { + const DeclinedSiteView& v = views[i]; + out += i == 0 ? "{" : ",{"; + if( callersForm ) + { + out += mcp ? "\"in\":\"" + jsonStr( v.caller ) + "\",\"file\":\"" + jsonStr( v.file ) + "\",\"line\":" + std::to_string( v.line ) + : "\"in\":\"" + jsonStr( v.caller ) + "\",\"p\":\"" + jsonStr( v.file ) + ":" + std::to_string( v.line ) + "\""; + } + else + { + out += std::string( mcp ? "\"name\":\"" : "\"n\":\"" ) + jsonStr( v.name ) + ( mcp ? "\",\"line\":" : "\",\"l\":" ) + std::to_string( v.line ); + } + if( v.candsKnown ) + { + out += ",\"cands\":" + std::to_string( v.cands ); + } + out += ",\"via\":\"declined\""; + if( v.gate != nullptr ) + { + out += std::string( ",\"gate\":\"" ) + v.gate + "\""; + } + if( !v.ofDef.empty() ) + { + list( "of_def", v.ofDef ); + } + if( !v.cand.empty() ) + { + list( mcp ? "candidates" : "cand", v.cand ); + } + if( v.candMore > 0 ) + { + out += ",\"cand_more\":" + std::to_string( v.candMore ); + } + out += "}"; + } + out += "]"; + return out; +} + +// The cut: how many rows print (the default cap, or --limit=N) and, past it, the call that lists every site — the same +// question with --limit raised to the total (never below the answer's own row window, so the rows it pages do not shrink). +struct DeclinedSiteCut +{ + std::size_t shown = 0; + std::string next; // "" when nothing was cut +}; +inline DeclinedSiteCut declinedSiteCut( std::size_t total, int pageLimit, std::string_view verbFlag, std::string_view selector, std::size_t rowWindow ) +{ + DeclinedSiteCut cut; + const std::size_t cap = pageLimit > 0 ? std::size_t( pageLimit ) : kDeclinedSiteCap; + cut.shown = std::min( total, cap ); + if( cut.shown < total ) + { + // saturated at --limit's own ceiling, so the call it names is one the parser accepts (a billion sites is past any tree) + const std::size_t limit = std::min( std::max( total, rowWindow ), std::size_t( kPageValueMax ) ); + cut.next = nextFlag( verbFlag, selector ) + " --limit=" + std::to_string( limit ); + } + ENSURES( cut.shown <= total && ( cut.shown == total ) == cut.next.empty() ); + return cut; +} +inline std::string declinedSiteCutAttrXml( const DeclinedSiteCut& cut ) +{ + if( cut.next.empty() ) + { + return {}; + } + std::vector esc; + return " dc_shown=\"" + std::to_string( cut.shown ) + "\" dc_next=\"" + std::string( escapeXml( cut.next, esc ) ) + "\""; +} +// The JSON twin of the two root attributes, the same names on the CLI --json and the MCP twins (the attribute-parity rule). +inline std::string declinedSiteCutKeyJson( const DeclinedSiteCut& cut ) +{ + if( cut.next.empty() ) + { + return {}; + } + return ",\"dc_shown\":" + std::to_string( cut.shown ) + ",\"dc_next\":\"" + jsonStr( cut.next ) + "\""; +} + +// What the legend must define, read off the rows an answer prints (graphlegend.h declinedSitesLegend). +inline DeclinedSitesLegendFlags declinedSitesLegendFlags( std::span views, bool callersForm, const DeclinedSiteCut& cut ) +{ + DeclinedSitesLegendFlags f; + f.any = !views.empty(); + f.callersForm = callersForm; + f.cut = !cut.next.empty(); + for( const DeclinedSiteView& v : views ) + { + const std::string_view gate = v.gate != nullptr ? std::string_view( v.gate ) : std::string_view(); + f.side = f.side || gate == "side"; + f.nameGate = f.nameGate || gate == "builtin" || gate == "std"; + f.ofDef = f.ofDef || !v.ofDef.empty(); + f.cand = f.cand || !v.cand.empty(); + } + return f; +} + +} // namespace rw diff --git a/src/graph.h b/src/graph.h index 0c4bb09a5..16b495f5a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -45,6 +45,21 @@ namespace rw // ComposeEdge defined in model.h (so serialize.h can use it without including graph.h). +// Which rule declined a call (Graph::declinedSiteKind): what a row's gate= says, and what train 29's interface-typed +// dispatch rows (L6) will add a value to. One byte per declined call; the value is never ranked or counted, only read back. +enum class DeclinedSiteKind : std::uint8_t +{ + Ladder, // tier 3: two or more candidates, none in the caller's file or directory, no evidence chose one (no gate=) + Builtin, // the builtin-method name gate (BuiltinMethodGate): gate="builtin" + Std, // the C++ standard-member gate (StdMemberGate): gate="std" + RubySide, // Ruby: in reach of self's method lookup, but on the other side of self (class vs instance): gate="side" + RubyNew, // Ruby: `new` on a class object, the reachable `initialize` definitions unpicked (no gate=; the row names `new`) + Iface, // RESERVED for L6 (interface-typed dispatch rows); nothing writes it yet +}; +// A declined call whose candidate list internDeclinedList could not store (its uint32 offset ceiling): the record keeps the +// call, with no list (no cands=, and no callers row — declinedCallsNaming cannot count it either). +inline constexpr std::uint32_t kNoDeclinedList = UINT32_MAX; + struct Graph { sparseCsr inEdges; // N×N, row = target's in-neighbours (for PageRank) @@ -139,6 +154,18 @@ struct Graph std::vector declinedListOff{ 0u }; // the leading offset lives here, so a default Graph is already a valid zero-list CSR std::vector declinedListCand; std::vector declinedListCallCount; + // lane declined-sites: ONE record per declined CALL, written beside every ++declinedOut (buildGraph's recordDecline), so + // the record count is the header's declined= and the records a selector's definitions made are its declined_calls=. + // Parallel columns in resolve-loop order (deterministic): the caller; the interned candidate list (kNoDeclinedList when + // internDeclinedList could not store it); the call's index into ing.references, from which the line, the file and the + // CALLED name are read (never off the candidates: a Ruby `new` declines with `initialize` candidates), and receiver and + // qualifier stay reachable for L6; and the rule that declined it. 13 B per declined call. Read by declinedSitesMadeBy / + // declinedSitesNaming below, the rows of the callers, callees and safe-delete answers. Built from the ingest each + // run like the rest of the Graph (never cached), so no cache version moves with it. + std::vector declinedSiteCaller; + std::vector declinedSiteList; + std::vector declinedSiteRef; + std::vector declinedSiteKind; // 1 for a definition some call the builtin-method name gate DECLINED could have meant (BuiltinMethodGate below); empty // on a corpus with no such decline. Read by the dead-code predicate (quality.h isDeadCandidate): a definition a // declined `pool.get( k )` could have meant is not provably uncalled, which is what it was before the gate, when @@ -6136,7 +6163,7 @@ inline void fillImplementorBound( Graph& g, const std::vector>& listsByHash, std::span cand ); // defined beside declinedCallsNaming, its one reader +inline std::uint32_t internDeclinedList( Graph& g, HashMap>& listsByHash, std::span cand ); // defined beside declinedCallsNaming, its one reader inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = nullptr, bool census = false ) { PROFILE_SCOPE_DESCRIBE( "buildGraph: resolve refs + build CSR" ); @@ -6477,6 +6504,19 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::vector cand, tier; HashMap> declinedListsByHash; // tier-3 declines: list hash → list numbers (internDeclinedList) declinedListsByHash.reserve( 4096 ); + // Every decline site goes through here: the caller's count (declinedOut, the header's declined=), the candidate list + // (internDeclinedList, the callers answers' declined_calls=) and ONE site record (Graph::declinedSite*) — so the rows a + // callers or callees answer prints are exactly the calls its declined_calls= counts. + const auto recordDecline = [ & ]( const Reference& r, std::span declinedCand, DeclinedSiteKind kind ) + { + EXPECTS( &r >= ing.references.data() && &r < ing.references.data() + ing.references.size(), "the resolve loop's own reference" ); + EXPECTS( r.fromSymbol < g.declinedOut.size(), "a declined call has a caller" ); + ++g.declinedOut[ r.fromSymbol ]; + g.declinedSiteList.push_back( internDeclinedList( g, declinedListsByHash, declinedCand ) ); + g.declinedSiteCaller.push_back( r.fromSymbol ); + g.declinedSiteRef.push_back( std::uint32_t( &r - ing.references.data() ) ); + g.declinedSiteKind.push_back( kind ); + }; std::vector bindingTier; // A4-R5 reused FFI-alias fallback candidate buffer std::string bindKey; // A4-R5 reused "#var" key buffer for the ctypes-handle gate HashMap bindingEdges; // A4-R5 (from<<32|to) keys of edges resolved via an FFI alias — @@ -7515,8 +7555,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null disposition = vetoExternal( r ); continue; } - ++g.declinedOut[ r.fromSymbol ]; - internDeclinedList( g, declinedListsByHash, std::span( rubyReachable.data(), rubyReachable.size() ) ); + recordDecline( r, std::span( rubyReachable.data(), rubyReachable.size() ), DeclinedSiteKind::RubyNew ); disposition = CallDisposition::Declined; continue; } @@ -7711,8 +7750,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null : RubyCut::Unchanged; if( rubyCut == RubyCut::Declined ) { - ++g.declinedOut[ r.fromSymbol ]; // in reach, but on the other side of self: no edge, and no claim it is outside - internDeclinedList( g, declinedListsByHash, std::span( rubyReachable.data(), rubyReachable.size() ) ); + // in reach, but on the other side of self: no edge, and no claim it is outside + recordDecline( r, std::span( rubyReachable.data(), rubyReachable.size() ), DeclinedSiteKind::RubySide ); disposition = CallDisposition::Declined; continue; } @@ -7843,8 +7882,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null cand.swap( filtScratch ); if( cut == RubyCut::Declined ) { - ++g.declinedOut[ r.fromSymbol ]; - internDeclinedList( g, declinedListsByHash, cand ); + recordDecline( r, cand, DeclinedSiteKind::RubySide ); disposition = CallDisposition::Declined; continue; } @@ -8069,8 +8107,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } else { - ++g.declinedOut[ r.fromSymbol ]; - internDeclinedList( g, declinedListsByHash, cand ); + recordDecline( r, cand, DeclinedSiteKind::Ladder ); if( stdIndex != StdMemberGate::kNotStd && !stdReceiverKeeps( stdRecv, r.fromSymbol, cand ) ) { g.stdMemberDeclines.emplace_back( r.fromSymbol, stdIndex ); // the line names it too @@ -8131,8 +8168,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // DECLINED. Still no edge and still no guess — that precision rule is the ladder's point. What is gone is the silence: the decline counts on the // caller (declinedOut → the header's declined=, the callees answer's declined_calls=) and records the candidate list it could equally have meant // (internDeclinedList → the callers and impact answers' declined_calls=), so a count="0" there says a call was declined. test/declinecheck.sh (A), (B). - ++g.declinedOut[ r.fromSymbol ]; - internDeclinedList( g, declinedListsByHash, cand ); + recordDecline( r, cand, DeclinedSiteKind::Ladder ); disposition = CallDisposition::Declined; continue; } @@ -8369,9 +8405,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null continue; } std::sort( gateRefused.begin(), gateRefused.end() ); // one sequence per set, as internDeclinedList keys it - ++g.declinedOut[ r.fromSymbol ]; ++g.gateDeclinedCalls; - internDeclinedList( g, declinedListsByHash, gateRefused ); + recordDecline( r, gateRefused, DeclinedSiteKind::Builtin ); g.gateDeclinedTarget.resize( N, 0 ); // no-op after the first decline for( const NodeId c : gateRefused ) { @@ -8410,9 +8445,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null if( !gateRefused.empty() && byName >= StdMemberGate::kMinInTreeCandidates ) { std::sort( gateRefused.begin(), gateRefused.end() ); // one sequence per set, as internDeclinedList keys it - ++g.declinedOut[ r.fromSymbol ]; ++g.stdMemberDeclinedCalls; - internDeclinedList( g, declinedListsByHash, gateRefused ); + recordDecline( r, gateRefused, DeclinedSiteKind::Std ); g.gateDeclinedTarget.resize( N, 0 ); // no-op after the first decline: a definition a declined call could have meant is not dead for( const NodeId c : gateRefused ) { @@ -8685,6 +8719,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } DASSERT( verifyCsr( g.inEdges, N ) ); // structural, so CHECKED in debug and not promised in release — see Diagnostics.h DASSERT( verifyOffsetCsr( g.declinedListOff, g.declinedListCand, g.declinedListCallCount.size(), N ) ); + // One site record per declined call (recordDecline): at most one per reference, so the uint32 record and reference + // indices cannot wrap, and the four columns stay parallel. + ENSURES( g.declinedSiteCaller.size() <= ing.references.size() && g.declinedSiteList.size() == g.declinedSiteCaller.size() + && g.declinedSiteRef.size() == g.declinedSiteCaller.size() && g.declinedSiteKind.size() == g.declinedSiteCaller.size() ); } // inheritance edges (Lego view): isInherit refs (derived → base name) → implementors[base] += derived. @@ -13033,12 +13071,14 @@ inline GaugeClauses graphGaugeClauses( const Graph& g ) noexcept // it, so a collision costs a compare, never a wrong share. List numbers are first-seen in the sequential resolve loop, and the // map is only probed and inserted into — neither reaches output. // +// Returns the call's list number (Graph::declinedSiteList), or kNoDeclinedList when the list could not be stored. +// // DEGRADE: the offsets are uint32. A new list that would carry the candidate array past UINT32_MAX entries is not recorded: -// the call stays counted on its caller (declinedOut, the header's declined=), and declined_calls= on the callers and impact -// answers can under-count, as the counts_floor="1" those answers carry already allows. A declined set holds at least one +// the call stays counted on its caller (declinedOut, the header's declined=) and keeps its site record with no list, and +// declined_calls= on the callers and impact answers can under-count, as the counts_floor="1" those answers carry already allows. A declined set holds at least one // candidate — tier 3's two or more, or the ONE lone method the builtin-method name gate (BuiltinMethodGate) refused to // bind by name — so the list count never exceeds the entry count and the uint32 list numbers cannot wrap first. -inline void internDeclinedList( Graph& g, HashMap>& listsByHash, std::span cand ) +inline std::uint32_t internDeclinedList( Graph& g, HashMap>& listsByHash, std::span cand ) { EXPECTS( !cand.empty(), "tier 3 declines a set it could not narrow to one; the builtin-method gate a non-empty set it admitted none of" ); std::uint64_t hash = 14695981039346656037ull; // the FNV-1a 64-bit offset basis @@ -13055,19 +13095,21 @@ inline void internDeclinedList( Graph& g, HashMap kOffsetCeiling - g.declinedListCand.size() ) { DISCLOSE( "graph: the declined candidate lists would overflow their uint32 offsets — this call's list is not recorded, so declined_calls= can under-count" ); - return; + return kNoDeclinedList; } - bucket.push_back( std::uint32_t( g.declinedListCallCount.size() ) ); + const std::uint32_t listIndex = std::uint32_t( g.declinedListCallCount.size() ); + bucket.push_back( listIndex ); g.declinedListCand.insert( g.declinedListCand.end(), cand.begin(), cand.end() ); g.declinedListOff.push_back( std::uint32_t( g.declinedListCand.size() ) ); g.declinedListCallCount.push_back( 1 ); + return listIndex; } // declined_calls= on the callers and impact answers: how many tier-3 declines named at least one of `targets` @@ -13120,6 +13162,77 @@ inline std::size_t declinedCallsMadeBy( const Graph& g, std::span return callCount; } +// lane declined-sites: the site records (Graph::declinedSite*) behind the two counts above, for the rows +// (declinedsites.h). declinedSitesMadeBy: the declines `sources` made — one record per call declinedCallsMadeBy counts. +inline std::vector declinedSitesMadeBy( const Graph& g, std::span sources ) +{ + std::vector sites; + if( g.declinedSiteCaller.empty() || sources.empty() ) + { + return sites; + } + std::vector isSource( g.declinedOut.size(), 0 ); + for( const NodeId s : sources ) + { + if( s < isSource.size() ) + { + isSource[ s ] = 1; + } + } + for( std::uint32_t site = 0; site < g.declinedSiteCaller.size(); ++site ) + { + if( isSource[ g.declinedSiteCaller[ site ] ] ) + { + sites.push_back( site ); + } + } + ENSURES( sites.size() == declinedCallsMadeBy( g, sources ), "every declined call a source made has its record, and no other does" ); + return sites; +} + +// declinedSitesNaming: the declined calls whose stored candidate LIST holds one of `targets` — list membership, never the +// called name (the language and root filters split one name into several lists) — one record per call declinedCallsNaming +// counts. Which lists hold a target is decided once per list (a bit), then the records are one pass: O(lists + records). +inline std::vector declinedSitesNaming( const Graph& g, std::span targets ) +{ + std::vector sites; + if( g.declinedListCallCount.empty() || targets.empty() ) + { + return sites; + } + std::vector isTarget( g.declinedOut.size(), 0 ); + for( const NodeId t : targets ) + { + if( t < isTarget.size() ) + { + isTarget[ t ] = 1; + } + } + std::vector listHolds( g.declinedListCallCount.size(), 0 ); + std::size_t callCount = 0; // declinedCallsNaming's own sum, for the postcondition + for( std::size_t listIndex = 0; listIndex < listHolds.size(); ++listIndex ) + { + for( std::uint32_t slot = g.declinedListOff[ listIndex ]; slot < g.declinedListOff[ listIndex + 1 ]; ++slot ) + { + if( isTarget[ g.declinedListCand[ slot ] ] ) + { + listHolds[ listIndex ] = 1; + callCount += g.declinedListCallCount[ listIndex ]; + break; + } + } + } + for( std::uint32_t site = 0; site < g.declinedSiteList.size(); ++site ) + { + if( const std::uint32_t list = g.declinedSiteList[ site ]; list != kNoDeclinedList && listHolds[ list ] ) + { + sites.push_back( site ); + } + } + ENSURES( sites.size() == callCount, "each stored list counts exactly the calls whose records name it" ); + return sites; +} + // The callees answer's line: of the declines `sources` made (declinedCallsMadeBy), the C++ calls named like a // standard container or string member on a receiver no rule tied to an in-tree class (Graph::stdMemberDeclines) — the names, // each once in table (byte) order and comma-joined, and how many calls. Empty on a graph with none, so every other answer diff --git a/src/graphlegend.h b/src/graphlegend.h index 93b502fcd..3acf2f7f5 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -646,6 +646,70 @@ inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, boo return declinedCallsLegendWithGate( on, gateDeclined ) + declinedCallsStdLegend( on && stdDeclined ); } +// ── lane declined-sites: the rows (declinedsites.h) ──────────────────────────────────────────────────────────── +// Where rows ride, the clause above cannot say "no count or row here includes them": the rows name those calls. This +// twin says so instead (gates review R9) and is the one such an answer carries; everything else in it is the clause above. +inline constexpr const char* kDeclinedCallsRowsLegend = + "declined_calls=K (absent when 0) counts call SITES the resolver declined to bind: the called name has two or more same-language definitions, none in the caller's file or directory, and no qualifier, receiver type or include chose one, so no edge exists and no count here includes them; the rows below name them (the map header's declined=). Callers form: declined calls that could have meant this selector's definitions; impact form: that could have reached SYM or a symbol in its radius; callees form: declined calls these definitions make. Each call counts once however many candidates it had; the uses verb on the called name lists the sites. "; +inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, bool stdDeclined, bool dcRows ) +{ + if( !dcRows ) + { + return declinedCallsLegendWithGates( on, gateDeclined, stdDeclined ); + } + return std::string( kDeclinedCallsRowsLegend ) + declinedCallsGateLegend( gateDeclined ) + declinedCallsStdLegend( stdDeclined ); +} +// The clause, in the gates review's words (R10): what a row is, and above all what it is NOT. Each later sentence rides +// only where its attribute does, so an answer pays for the vocabulary it prints. +inline constexpr const char* kDeclinedSitesLegend = + ": a call SITE the resolver declined to bind; NOT an edge and NOT a caller or callee claim (counted in declined_calls=, never in count= or a row above). cands=K: K in-tree definitions the call could have meant (K may be 1); the resolver proved none; not a claim this call reaches any of them (it may reach an out-of-tree, builtin or standard-library method). "; +inline constexpr const char* kDeclinedSitesCalleesForm = + ": the called name and the line of the call, in the body of one of these definitions. "; +inline constexpr const char* kDeclinedSitesCallersForm = + ": the definition making the call and the call's file:line, listed because its candidates include a definition of this selector. "; +inline constexpr const char* kDeclinedSitesSideClause = + "gate=side: a Ruby call whose in-reach definitions answer on the other side of self. "; +inline constexpr const char* kDeclinedSitesNameGateClause = + "gate=builtin|std: declined by the builtin-method or the C++ standard-member name gate. "; +inline constexpr const char* kDeclinedSitesOfDefCallees = + "of_def=K on a : the definition (ix=K) making the call, a fact. "; +inline constexpr const char* kDeclinedSitesOfDefCallers = + "of_def=K~d on a : its candidates include definition group K; never a claim it calls K. "; +inline constexpr const char* kDeclinedSitesCutClause = + "dc_shown=N: the rows printed (the callers form cuts file-first: each file's first site before any file's second); dc_next= is the call that lists every site. "; +inline constexpr const char* kDeclinedSitesCandClause = + "cand= (legend full only): the candidate definitions as file:line, cand_more= how many more. "; +// The impact answer prints no row (gates review Q4): where its declined_calls= rides, it names where the sites are listed. +inline constexpr const char* kDeclinedSitesImpactPointer = + "The callers verb on SYM lists its declined first-hop calls as rows. "; +inline const char* declinedSitesImpactPointer( bool on ) noexcept { return on ? kDeclinedSitesImpactPointer : ""; } +// What one answer's rows carry, read off the rows it prints (never re-derived). +struct DeclinedSitesLegendFlags +{ + bool any = false; // a row is printed + bool callersForm = false; // in= p= rows (callers, safe-delete) rather than n= l= (callees) + bool side = false; // a row says gate="side" + bool nameGate = false; // a row says gate="builtin" or gate="std" + bool ofDef = false; // a row carries of_def= + bool cut = false; // the root carries dc_shown= / dc_next= + bool cand = false; // a row carries cand= (legend full) +}; +inline std::string declinedSitesLegend( const DeclinedSitesLegendFlags& f ) +{ + if( !f.any ) + { + return {}; + } + std::string out( kDeclinedSitesLegend ); + out += f.callersForm ? kDeclinedSitesCallersForm : kDeclinedSitesCalleesForm; + out += f.side ? kDeclinedSitesSideClause : ""; + out += f.nameGate ? kDeclinedSitesNameGateClause : ""; + out += f.ofDef ? ( f.callersForm ? kDeclinedSitesOfDefCallers : kDeclinedSitesOfDefCallees ) : ""; + out += f.cut ? kDeclinedSitesCutClause : ""; + out += f.cand ? kDeclinedSitesCandClause : ""; + return out; +} + // ── reference-as-value round (src/valuerefs.h, src/ingest_valuerefs.h) ───────────────────────────────────────── // Each clause rides ONLY a document that carries the attribute or rows it defines (value_refs= / ), so an answer // with no value reference keeps its bytes. What a row does NOT mean is said in the same breath: it is matched by name, diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f30d5e5e4..e7692c144 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -782,6 +782,14 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& calledBySites = rowCallSites( ing, chRows.matches, std::span( calledBy ).subspan( calledByWin.begin, calledByWin.end - calledByWin.begin ), VrRender{ sqSingleRoot, sqRootPrefix } ); char pab[ kPageDisclosureCap ]; + // lane declined-sites (declinedsites.h): the CLI root's rows for the direction count= describes (find_symbol: the + // declines its definitions make; find_referencing_symbols: the declined calls that could have meant them), cut by the + // same `limit` the rows take — one computation (callhierarchy.h ordered them), the CLI's own row builder. + const DeclinedSiteCut sqDcCut = declinedSiteCut( chRows.declinedSites.size(), page.limit, referencingOnly ? "--callers=" : "--callees=", name, + std::size_t( rowCap ) ); + const std::vector sqDc = declinedSiteViews( ing, g, std::span( chRows.declinedSites ).first( sqDcCut.shown ), + referencingOnly, chRows.matches, chRows.attr.groups, VrRender{ sqSingleRoot, sqRootPrefix }, + /*candidates=*/false ); std::string out = "{"; if( sqSingleRoot ) @@ -803,6 +811,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + ",\"hop_tested\":" + std::to_string( chTested.tested ) + ",\"hop_untested\":" + std::to_string( chTested.untested ) + declinedCallsKeyJson( chRows.declinedCalls ) // the CLI root's declined_calls=, for the direction count= describes + + declinedSiteCutKeyJson( sqDcCut ) // the CLI root's dc_shown= / dc_next=, absent when nothing was cut + declinedIfaceKeyJson( chRows.declinedIface ) // the CLI root's declined_iface= (callers direction only) + stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ) // the CLI callees answer's line (callees direction only; absent when none) + nextFieldJson( nextFlag( referencingOnly ? "--uses=" : "--expand=", chNextSelector ) ); // P3 (L7): the CLI root's next= (mcpattrparitycheck) @@ -855,6 +864,8 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& out += rw::viaNameNoteJson(); // FE-B: the reading of "via":"name", only in an answer that carries it } out += nextFieldJson( splitArmLegend( anySplit ), "split_note" ); // attrib-multidef Q1: likewise for "prov":"split" + out += declinedSitesJson( sqDc, referencingOnly, /*mcp=*/true ); // lane declined-sites: the CLI rows, never in calls/calledBy or count + out += nextFieldJson( declinedSitesLegend( declinedSitesLegendFlags( sqDc, referencingOnly, sqDcCut ) ), "declined_note" ); // their reading, only when they ride out += nextFieldJson( callSitesAtLegend( calledBySites.any, calledBySites.anyCut, kCallSitesPerRowCap ), "sites_note" ); // CALLSITE-AT: its reading, only when a row carries it (nextFieldJson: "" -> no key, JSON-escaped) // Reference-as-value round: the CLI's window, the same rows (callhierarchy.h computed them once): valueRefs = // where the symbol is USED AS A VALUE (the --callers side), valueCallees = what it stores/passes and may call @@ -3030,7 +3041,7 @@ inline std::optional impactText( const std::string& root, const std kTestedRowLegend, kImpactTestedPartitionLegend, // A6 kTestedLensBlindSpotLegend, // F-02: rides with the partition, byte-identical to the CLI twin unprovenDefsVerbLegend( UnprovenDefsVerb::Impact, unprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs=, as on the CLI - declinedCallsLegendWithGates( declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI + declinedCallsLegendWithGates( declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ) + declinedSitesImpactPointer( declinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI (+ where its sites are listed) declinedIfaceLegend( declinedIface > 0 ), // likewise declined_iface=, as on the CLI ( std::string( valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs=, as on the CLI + valueRefsDepthLegendFor( imValueRefs ) // ... and the depth disclosure, as on the CLI diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 6c966fe67..f9b3c8cb8 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -225,6 +225,14 @@ std::optional runCallHierarchy( const MainDispatch& d ) { chHasSplit = chAttr.splitArm( i ); } + // lane declined-sites (declinedsites.h): the rows — the declined call SITES declined_calls= counts, in callhierarchy.h's + // served order, cut at the default cap (--limit raises it; dc_next= names that call) — computed once for every dialect below. + const rw::DeclinedSiteCut chDcCut = rw::declinedSiteCut( chRows.declinedSites.size(), cfg.pageLimit, wantCallers ? "--callers=" : "--callees=", sym, + std::size_t( effectiveRowCap( cfg.pageLimit, rw::kCallHierarchyRowCap ) ) ); + const std::vector chDc = + rw::declinedSiteViews( ing, g, std::span( chRows.declinedSites ).first( chDcCut.shown ), wantCallers, matches, chAttr.groups, + rw::VrRender{ chSingleRoot, chRootPrefix }, cfg.legend == "full" ); + const std::string chDcRows = rw::declinedSitesXml( chDc, wantCallers ); // §H4 §3.4: the FIRST legend these two verbs have ever shipped (0 bytes before — which is why every // one of their root attributes sits in test/legendcoverage_baseline.txt), and the floor marker that @@ -255,7 +263,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), - rw::declinedCallsLegendWithGates( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendWithGates( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !chDc.empty() ) // exactly when the root carries declined_calls= + + rw::declinedSitesLegend( rw::declinedSitesLegendFlags( chDc, wantCallers, chDcCut ) ), // and the rows, exactly when one rides std::string( rw::declinedIfaceLegend( chRows.declinedIface > 0 ) ) // likewise, exactly when declined_iface= is there + rw::stdMemberCallsLegend( chRows.stdCalls.calls > 0 && !cfg.columnar ), // and when the line is (XML only) ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there @@ -275,7 +284,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) // Callees keep expand on the original selector; nextFlag retains quoting and the 120-byte contract. const std::string chNextAttr = rw::nextAttrXml( rw::nextFlag( wantCallers ? "--uses=" : "--expand=", chNextSelector ) ); // The tier-3 declines count= does not include (callhierarchy.h), on every dialect; absent at zero. - const std::string chDeclinedAttr = rw::declinedCallsAttrXml( chRows.declinedCalls ) + rw::declinedIfaceAttrXml( chRows.declinedIface ); + const std::string chDeclinedAttr = rw::declinedCallsAttrXml( chRows.declinedCalls ) + rw::declinedSiteCutAttrXml( chDcCut ) // + the cut, beside the count it cuts + + rw::declinedIfaceAttrXml( chRows.declinedIface ); // Reference-as-value round: value_refs= beside count= (never in it), absent at zero; the window after the rows. const rw::VrRender chVr { chSingleRoot, chRootPrefix }; const std::string chValueRefsAttr = ( rw::valueRefsCountAttrXml( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, chRows.valueRefs.depthCut, chVr ) ); // + the depth cut that makes it a floor @@ -322,7 +332,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) const std::string chDefRows = rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ); emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals, // CALLSITE-AT: sites_at only when a row has sites - chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit } ); + chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit, &chDcRows } ); // + the rows verbatim return 0; } @@ -343,7 +353,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) // R-E: the JSON twin of the XML root= below — right after the leading identifying fields. if( chSingleRoot ) { rw::emitTo( stdout, ",\"root\":\"{}\"", jsonStr( cfg.roots[0] ).c_str() ); } rw::emitTo( stdout, ",\"hop_tested\":{},\"hop_untested\":{}{}{}", chTested.tested, chTested.untested, - rw::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedIfaceKeyJson( chRows.declinedIface ) // A6; then the XML root's declined_calls=/declined_iface= + rw::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedSiteCutKeyJson( chDcCut ) + + rw::declinedIfaceKeyJson( chRows.declinedIface ) // A6; then the XML root's declined_calls=/dc_shown=/dc_next=/declined_iface= + rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ), // and the XML line ( rw::valueRefsCountKeyJson( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthKeyJson( ing, chRows.valueRefs.depthCut, chVr ) ) ); rw::emitTo( stdout, "{}{}", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, @@ -362,7 +373,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) chAtKeys[ i ] += rw::ofDefKeyJson( chAttr.groups, chAttr.tokens, i ) + rw::splitArmKeyJson( chAttr.splitArm( i ) ); // attrib-multidef } printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia, &chAtKeys ); - rw::emitTo( stdout, "]{}}}", rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); + rw::emitTo( stdout, "]{}{}}}", rw::declinedSitesJson( chDc, wantCallers, /*mcp=*/false ), // the XML rows, keys = its attribute names + rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); return 0; } @@ -394,7 +406,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) rw::splitArmAttrXml( chAttr.splitArm( i ) ), rw::ofDefAttrXml( chAttr.groups, chAttr.tokens, i ), // attrib-multidef chSites.any ? rw::callSitesAttrXml( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ) : std::string() ); // CALLSITE-AT } - rw::emitTo( stdout, "{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows + rw::emitTo( stdout, "{}{}{}", rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/false ), // the declined C++ standard-member calls, after the rows + chDcRows, // and each declined call SITE () rw::valueRefsXml( ing, chRows.valueRefs, wantCallers, chVr, chVrNext ), tag ); return 0; } @@ -936,6 +949,7 @@ struct SafeDeleteLegendFlags bool defTable = false; // attrib-multidef: the table + of_def= rows (a multi-group selector) bool defTableCut = false; // ... and the table lists only the groups this page's rows name (defs_listed=) bool splitArms = false; // a row is on this page (a one-group selector's split arm, Q1) + rw::DeclinedSitesLegendFlags declinedSites{}; // lane declined-sites: what the rows on this answer carry (declinedsites.h) }; // count-floor (countfloor.h) + CALLSITE-AT: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and @@ -987,12 +1001,15 @@ inline constexpr SafeDeleteRiskReading kSafeDeleteRiskReadings[] = { { "uses-exist", "uses-exist: callers or uses exist, and at least part of the radius is test-covered. " }, { "unmodelled", "unmodelled: zero callers AND zero uses were READ, but this kind is used by reading or naming it and those " "counts cannot see that — no reading either way, never a none-found. " }, + // lane declined-sites (gates review Q3): what none-found read beside declined_calls= before the rows named the calls + { "possible-callers", "possible-callers: zero proven callers and zero uses, but declined_calls=K call sites (the rows) could have " + "meant this definition; not a safety reading. " }, }; inline const char* safeDeleteRiskSentence( std::string_view risk ) noexcept { const auto it = std::ranges::find( kSafeDeleteRiskReadings, risk, &SafeDeleteRiskReading::risk ); const auto end = std::end( kSafeDeleteRiskReadings ); // outside the promise: a self-check names values, it calls nothing - ASSUME( it != end, "safeDeleteRiskSentence: runSafeDelete assigns only the four values listed" ); + ASSUME( it != end, "safeDeleteRiskSentence: runSafeDelete assigns only the five values listed" ); return it != end ? it->sentence : ""; } @@ -1005,11 +1022,11 @@ inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs const std::size_t declinedCalls = flags.declinedCalls; // declined_calls=: its definition, and — beside risk=none-found — the sentence that keeps none-found from reading as // a safety verdict about a definition some declined call may have meant (graph.h declinedCallsNaming). - std::string declinedClause = rw::declinedCallsLegendWithGates( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined ); - if( declinedCalls > 0 && risk == "none-found" ) - { - declinedClause += "none-found beside declined_calls= is not a safety reading: those calls may reach this definition. "; - } + // Since lane declined-sites a definition some declined call could have meant never reads none-found: it reads + // possible-callers, whose own sentence says it is not a safety reading, and its rows name the calls. + ASSUME( declinedCalls == 0 || risk != "none-found", "runSafeDelete reads possible-callers (or unmodelled) wherever declined_calls= rides" ); + std::string declinedClause = rw::declinedCallsLegendWithGates( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined, flags.declinedSites.any ) + + rw::declinedSitesLegend( flags.declinedSites ); declinedClause += rw::valueRefsSafeDeleteLegend( flags.valueRefs > 0 ); // exactly when the root carries value_refs= declinedClause += rw::valueRefsDepthLegend( flags.valueRefDepthCut ); // exactly when it carries the depth disclosure declinedClause += safeDeleteFloorLegend( flags, risk ); // count-floor + CALLSITE-AT: present-only clauses @@ -1230,7 +1247,13 @@ std::optional runSafeDelete( const MainDispatch& d ) ENSURES( !usesFloor || sdFloor.isFloor ); // a NotCalls definition always floors callers= too (callerFloors' first rule) const bool anyEvidence = !callerIds.empty() || !sites.empty() || !sdValueRefs.rows.empty(); // a decorator row is not a use site, still evidence + // lane declined-sites: declined calls whose candidate lists hold a definition (as --callers counts and lists them) — the + // rows, file-first, cut at the callers verb's own cap (one constant, gates review Q6). + const std::size_t sdDeclinedCalls = declinedCallsNaming( g, defs ); + std::vector sdDeclinedSites = declinedSitesNaming( g, defs ); + orderDeclinedSites( ing, g, sdDeclinedSites, /*fileFirst=*/true ); const char* risk = ( !anyEvidence && usesFloor ) ? "unmodelled" + : !anyEvidence && sdDeclinedCalls > 0 ? "possible-callers" // gates review Q3: never none-found beside a declined possible caller : !anyEvidence ? "none-found" : ( !reach.empty() && radiusTested == 0 ) ? "untested-radius" : "uses-exist"; @@ -1253,7 +1276,8 @@ std::optional runSafeDelete( const MainDispatch& d ) // #60: exactly when an owner is one of the rows this page prints. pageWindow is pure, so recomputing // it here (the legend streams BEFORE the rows) cannot disagree with the window the rows below take. const PageWindow sdLw = pageWindow( callerIds.size(), effectiveRowCap( cfg.pageLimit, 40 ), cfg.pageOffset ); - const std::size_t sdDeclinedCalls = declinedCallsNaming( g, defs ); // declined calls that could have meant a def (as --callers counts them) + const rw::DeclinedSiteCut sdDcCut = rw::declinedSiteCut( sdDeclinedSites.size(), cfg.pageLimit, "--safe-delete=", cfg.safeDeleteSym, + std::size_t( effectiveRowCap( cfg.pageLimit, 40 ) ) ); // CALLSITE-AT: a row's p= is where the CALLER is defined; the lines to open are its call sites, as pasteable // file:line tokens (editcheck.h rowCallSites: the uses verb's call-role filters, one scan of the reference table, the @@ -1271,13 +1295,17 @@ std::optional runSafeDelete( const MainDispatch& d ) { sdHasSplit = sdAttr.splitArm( i ); } + const std::vector sdDc = + rw::declinedSiteViews( ing, g, std::span( sdDeclinedSites ).first( sdDcCut.shown ), /*callersForm=*/true, defs, sdAttr.groups, + sdVr, cfg.legend == "full" ); emitSafeDeleteLegend( defs.size(), sdUnprovenDefs, ambiguousCallers, risk, SafeDeleteLegendFlags{ .singleRoot = sdSingleRoot, .hasUnindexed = rw::graphGaugeClauses( g ), .hasModScope = anyModuleScopeRow( ing, std::span( callerIds ).subspan( sdLw.begin, sdLw.end - sdLw.begin ) ), .declinedCalls = sdDeclinedCalls, .gateDeclined = g.gateDeclinedCalls > 0, .stdDeclined = g.stdMemberDeclinedCalls > 0, .valueRefs = sdValueRefs.rows.size(), .callersFloor = sdFloor.isFloor, .usesFloor = usesFloor, .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0, - .defTable = sdAttr.groups.multi(), .defTableCut = sdListing.cut, .splitArms = sdHasSplit } ); + .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= @@ -1301,7 +1329,7 @@ std::optional runSafeDelete( const MainDispatch& d ) defs.size(), callerIds.size(), sdCallersFloorAttr, ambiguousCallers, reach.size(), sites.size(), sdUsesFloorAttr, testedSelf ? 1 : 0, radiusTested, radiusUntested, deadCodeCandidate ? 1 : 0, risk, rw::unprovenDefsAttrXml( sdUnprovenDefs ).c_str(), // H1: beside the verdict it qualifies; absent at zero - rw::declinedCallsAttrXml( sdDeclinedCalls ).c_str(), // beside risk= too: declined calls that may reach it; absent at zero + ( rw::declinedCallsAttrXml( sdDeclinedCalls ) + rw::declinedSiteCutAttrXml( sdDcCut ) ).c_str(), // beside risk= too: declined calls that may reach it (+ the cut); absent at zero ( rw::valueRefsCountAttrXml( sdValueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, sdValueRefs.depthCut, sdVr ) ), // value uses: in uses=, never in callers=/impact_reaches=; absent at zero pageDisclosure( cab, sizeof( cab ), cw.end - cw.begin, callerIds.size(), cw.end, cfg.pageLimit, cfg.pageOffset, true ), rw::graphCountFloorAttrXml( g ).c_str(), sdRootAttr.c_str(), sdNextAttr.c_str(), @@ -1320,8 +1348,8 @@ std::optional runSafeDelete( const MainDispatch& d ) } rw::emitTo( stdout, "{}{}/>", rw::splitArmAttrXml( sdAttr.splitArm( i ) ), rw::ofDefAttrXml( sdAttr.groups, sdAttr.tokens, i ) ); // attrib-multidef } - rw::emitTo( stdout, "{}", rw::valueRefsXml( ing, sdValueRefs, true, sdVr, - "--uses=" + std::string( cfg.safeDeleteSym ) ) ); + rw::emitTo( stdout, "{}{}", rw::declinedSitesXml( sdDc, /*callersForm=*/true ), // lane declined-sites: each declined possible caller, after the rows + rw::valueRefsXml( ing, sdValueRefs, true, sdVr, "--uses=" + std::string( cfg.safeDeleteSym ) ) ); return 0; } @@ -2806,7 +2834,7 @@ std::optional runImpact( const MainDispatch& d ) rw::testedLensLegend( cfg.columnar ), rw::kImpactTestedPartitionLegend, // A6: the columnar form reads its dense column rw::kTestedLensBlindSpotLegend, // F-02: rides with the partition rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Impact, imUnprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs= - rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ) + rw::declinedSitesImpactPointer( imDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( imDeclinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::modScopeLegend( imHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ) ) // idea #6; fix27 From dc162c47b108bde55d3b95fb84ec3c562991f89a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 07:02:00 -0400 Subject: [PATCH 095/162] test(compactlegendcheck): A-PIN for.xml re-anchored (FOLD C tail only); P4 floor twin at 900 under reserve-first Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 14 +++++++++++--- test/compactlegendfix/pre_l1_full/for.xml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 0aadb9866..67ab994ae 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -313,6 +313,10 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # sites_at= clause says the match is by LEAF name ("its call sites spelled like the called name" -> "its calls of the called # LEAF name, any receiver or qualifier"), 3890 -> 3907 B. Checked mechanically: the reverse replacement of that one phrase # gives the previous pin byte for byte (at= masked). The six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-15 (lane forbudget-soft round C, FOLD C, by design), for.xml only: the carries page 0 of the +# --for file page (rows with sym=), its full legend clause defines them, and est_tokens= prices them, 4111 -> 4500 B. Checked +# mechanically: with the element, the tail clause and est_tokens= masked, the new answer equals the previous pin byte for +# byte (at= masked). The six other pins are unchanged. PIN_DIR="$ROOT/test/compactlegendfix/pre_l1_full" # the one normalisation, in python on BOTH sides so no sed dialect decides it (BSD sed appends a final newline, GNU # sed does not): at="…" masked, trailing newlines dropped. @@ -1367,9 +1371,13 @@ for tb in 500 900; do fi fi else - case "$sigs" in *' capped="1"'*|*' next="'*) capd=1 ;; *) capd=0 ;; esac - if [ "$r" -ne "$t" ] || [ "$capd" -ne 0 ] || ! cmp -s "$TMP/sp.$lg.rows" "$TMP/sp.ref.p4.$lg.rows"; then - p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=900 --legend=$lg: $r of $t rows, capped/next=$capd — a target wider than the answer must serve the no-budget answer's rows uncut" + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are + # reserved before the rows past the cut, so 900 no longer serves the whole 12-row head (11 rows measured) — by design. + # What holds: the rows are a rank prefix of the no-budget answer's, and a cut carries capped + next=. + case "$sigs" in *' capped="1"'*) capd=1 ;; *) capd=0 ;; esac + case "$sigs" in *' next="'*) nxd=1 ;; *) nxd=0 ;; esac + if ! head -n "$r" "$TMP/sp.ref.p4.$lg.rows" | cmp -s - "$TMP/sp.$lg.rows" || { [ "$r" -lt "$t" ] && [ $(( capd * nxd )) -ne 1 ]; }; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=900 --legend=$lg: $r of $t rows, capped=$capd next=$nxd — the rows must be a rank prefix of the no-budget answer's, and a cut must carry capped + next=" fi fi p4facts="$p4facts $tb/$lg: $r of $t rows est $pe;" diff --git a/test/compactlegendfix/pre_l1_full/for.xml b/test/compactlegendfix/pre_l1_full/for.xml index ba0c0ada4..ed7922c7c 100644 --- a/test/compactlegendfix/pre_l1_full/for.xml +++ b/test/compactlegendfix/pre_l1_full/for.xml @@ -1 +1 @@ -double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file +double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file From fde5074723f03f99561f5280fad4de92c678288b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 07:09:49 -0400 Subject: [PATCH 096/162] docs(declined): help, change-check skill and CHANGELOG name the rows; compact cand= reading --callers/--callees help says what a row is (a call site the resolver declined, never an edge or a count= row) and --safe-delete's help lists risk="possible-callers"; the change-check skill's risk= list likewise. The compact legend gains the cand= reading compactlegendcheck (S) requires for every conditional clause. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ++++++++++++++++ skills/ripwire-change-check/SKILL.md | 4 ++-- src/cli.h | 10 +++++++--- src/compactlegend.h | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2909d172a..ffdfe1c2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,22 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — the call sites the resolver declined are named, as hedged `` rows + +A call the resolver declines to bind (two or more same-language candidates and no evidence to choose one, the builtin-method +and C++ standard-member name gates, Ruby's class-versus-instance side cut and `new`) gets no edge, and `declined_calls=` counted +it without naming it: "what does doPackage call" missed a call through a Go interface value, and "who calls +TaskManager#define_task" missed the tests that call it on an untyped receiver. `--callees` now lists each such call the +definitions make as ``, and `--callers` lists each declined call whose candidates include the +selector's definition as `` (by the stored candidate list, never by the called name). +`gate="builtin"|"std"|"side"` names the rule that declined it; candidates (`cand=`) print only with `--legend=full`. A row is a +call SITE, never an edge: it is not in `count=`, the `` rows, `hop_tested=`/`hop_untested=` or any reach, and the legend says +the call may reach none of its `cands=` definitions. 16 rows by default (`--limit` raises it; the callers form is cut +file-first), with `dc_shown=`/`dc_next=` past the cap. The same rows ride `--json` (`"dc"`), `--format=columnar` and the MCP +twins (`"declined_sites"`). `--safe-delete` now reads `risk="possible-callers"` (not `none-found`) for a definition only a +declined call could have meant, and lists those calls; `--impact` prints no rows and its legend names `--callers` as where the +first-hop sites are listed. One record per declined call (13 B); the graph is rebuilt every run, so no cache version moves. + ### Changed — a multi-definition answer says which definition each row belongs to A selector that names several same-named definitions (`--callers=empty` over two classes' `empty`) used to answer with the diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index 8ba08cc06..de8522d3d 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -195,8 +195,8 @@ emits a flat `` top-K — identity + score + signatu 1-hop `callers=`, the transitive `--impact` blast radius (`impact_reaches=`), every `--uses` read/write/import/call/extends site (`uses=`), how much of that radius the `tested=` lens covers (`radius_tested=`/`radius_untested=`), and `--dead-code`'s own zero-caller/internal-linkage shape at - `defs=1` (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `uses-exist` / `unmodelled` / - `untested-radius` — never a go/no-go verdict; `radius_untested=` equal to `impact_reaches=` is the + `defs=1` (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `possible-callers` (a declined call could have + meant it; its `` rows name the sites) / `uses-exist` / `unmodelled` / `untested-radius` — never a go/no-go verdict; `radius_untested=` equal to `impact_reaches=` is the strongest signal ("nothing downstream is test-covered"). `ambiguous_callers=`/per-row `amb="1"` disclose the same call-graph resolution limit `--edit-check`'s `incompatible=` and `--for`'s `amb=` already carry. diff --git a/src/cli.h b/src/cli.h index cb6c9acbc..0a34d8277 100644 --- a/src/cli.h +++ b/src/cli.h @@ -343,7 +343,7 @@ struct Config // computes — 1-hop callers, the transitive --impact blast radius, every --uses // read/write/import/call/extends site, how much of the radius the tested= lens // covers, and --dead-code's own zero-caller/internal-linkage shape at defs=1. FACTS only: - // risk= names what was found (none-found/uses-exist/untested-radius/unmodelled), never a + // risk= names what was found (none-found/possible-callers/uses-exist/untested-radius/unmodelled), never a // go/no-go verdict. file:name disambiguates like --around/--lego. std::string_view sliceSpec; // --slice=SYM[:VAR] (lane/paper-slice): NAME-BASED intra-procedural def-use // slice of VAR inside the ONE uniquely-resolved definition SYM (multiple defs @@ -1143,7 +1143,10 @@ inline constexpr char kHelpHead[] = " who calls SYM (1-hop in-edges). file:name disambiguates a same-named symbol across files (like --around/--lego);\n" " Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere.\n" " Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=)\n" - " --callees=SYM what SYM calls (1-hop out-edges). file:name disambiguates like --callers\n" + " A call the resolver DECLINED to bind whose candidates include SYM is a row:\n" + " a call site, never an edge or a count= row (16 at most, file-first: dc_shown=, dc_next=)\n" + " --callees=SYM what SYM calls (1-hop out-edges). file:name disambiguates like --callers; each call it makes that the\n" + " resolver declined to bind is a row, never an edge or a count= row\n" " --uses=SYM show every place SYM is used, not just called — reads, writes, imports, extends\n" " the statically resolvable use-sites of SYM (role=call|macro|read|write|import|extends|type, file:line); external=\"1\" if SYM has no in-corpus def.\n" " file:name or a \"::\" spelling narrows defs= AND the role=\"call\" sites (kept only where the call RESOLVES to a chosen def —\n" @@ -1944,7 +1947,8 @@ inline constexpr char kHelpHead[] = " zero-caller/internal-linkage shape at defs=1 (dead_code_candidate=). ambiguous_callers= names callers\n" " whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of\n" " proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND\n" - " zero uses), untested-radius (a radius exists and none of it is test-covered), or\n" + " zero uses), possible-callers (none of either, but a call the resolver declined could have meant\n" + " SYM: its rows), untested-radius (a radius exists and none of it is test-covered), or\n" " uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; unmodelled\n" " when nothing was found for a kind used by reading or naming it (a variable, class, struct)\n" " whose uses those counts cannot see. callers_floor=/uses_floor= mark a count the index holds\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index 9663e06b1..b9357e4be 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -602,6 +602,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "gate", "dc gate=: builtin or std, a name gate declined it; side, a Ruby call whose in-reach defs answer on the other side of self", true, "dc" }, { "of_def", "dc of_def=K: the def ix=K making the call; K~d: its candidates include group K, never a claim it calls K", true, "dc" }, { "dc_shown", "dc_shown=N: dc rows printed (callers: file-first); dc_next= lists every site" }, + { "cand", "dc cand=: the candidate defs as file:line (legend full only); cand_more= how many more", true, "dc" }, { "fields", " column: each row's def ix=K groups (K~s split label, K~n by name, K~t primary template), space-separated", true, "cols", MapHeaderRead::No, "of_def" }, { "fields", " column: 1 where the row's edge to of= carries the resolver's split label (may be another arm's)", true, "cols", MapHeaderRead::No, "split" }, { "fields", " column: 1 = via=name, the row's edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "cols", MapHeaderRead::No, "via" }, From 48fd7b7847899a37421fe7c5a5625b36f02f7537 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 07:29:13 -0400 Subject: [PATCH 097/162] fix(declined): the callees record query checks its count on the no-record path too declinedSitesMadeBy returned early on a graph with no site record before its postcondition ran, so a decline site that counted a call without recording it went unnoticed there (a throwaway mutant that recorded only the tier-3 sites reached callHierarchyRows' check instead). The early return now states that no record means no declined call. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graph.h b/src/graph.h index 16b495f5a..a8f54cf6c 100644 --- a/src/graph.h +++ b/src/graph.h @@ -13169,6 +13169,7 @@ inline std::vector declinedSitesMadeBy( const Graph& g, std::span std::vector sites; if( g.declinedSiteCaller.empty() || sources.empty() ) { + ENSURES( declinedCallsMadeBy( g, sources ) == 0, "no record, so no declined call: every decline site writes one" ); return sites; } std::vector isSource( g.declinedOut.size(), 0 ); From 658fc77b253cab34cb2e89f3ceb08095b617098d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 07:32:28 -0400 Subject: [PATCH 098/162] harden git subprocesses Every git child now carries options that keep a repository's own configuration from naming a program git runs during ripwire's read-only calls: - filter drivers defined in the repository's local/worktree config are probed once per root (and per request-named root in the MCP/LSP servers) and pinned to an empty clean/smudge/process command on every git child; a name -c cannot spell makes the children refuse instead; - diff, log, show and blame carry --no-ext-diff --no-textconv; - status and diff carry --ignore-submodules=dirty; - log.showSignature is pinned off in the common prefix. Disclosed on stderr as git_harden=filter-driver. New gate test/gitchildoptscheck.sh (behavioural, PATH-shim argv/env, structural). Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +- SECURITY.md | 2 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/binstale.h | 2 +- src/crossref.h | 6 +- src/gitcmd.h | 108 ++++++++++- src/githarden.h | 190 +++++++++++++++++++- src/gitmine.h | 16 +- src/gitstamp.h | 2 +- src/handoff.h | 2 +- src/mcpindex.h | 2 + src/mergescout.h | 2 +- src/planlint.h | 2 +- src/prcontext.h | 2 +- src/quality.h | 20 +-- src/slicediff.h | 2 +- test/gitchildoptscheck.sh | 315 +++++++++++++++++++++++++++++++++ test/regression.sh | 2 +- 19 files changed, 642 insertions(+), 49 deletions(-) create mode 100755 test/gitchildoptscheck.sh diff --git a/README.md b/README.md index bf380fff8..0d1761fba 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +686 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **685 gate scripts** and is the authoritative list; +`test/regression.sh` names **686 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/SECURITY.md b/SECURITY.md index acd274412..32118b881 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,7 +18,7 @@ ripwire is a command-line indexing tool with the following security model: - **Input:** Arbitrary source code repositories on the local filesystem - **Output:** XML summaries and analysis results streamed to stdout or written to cache - **Trust boundary:** The tool operates only on local files with the user's own permissions. It does not communicate over the network, with two named exceptions: a root given as a git URL is cloned into the cache (`git clone --depth=1`, with `protocol.ext.allow=never` and `protocol.file.allow=user` pinned on the command), and the tree-sitter registry is listed during initial setup -- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself +- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. The repository's own `filter..clean/smudge/process` drivers are pinned to an empty command on every git child ripwire starts (a stderr line carrying `git_harden=filter-driver`), its diff programs are refused with `--no-ext-diff --no-textconv`, signature display is pinned off, and `status`/`diff` do not enter submodule repositories (`--ignore-submodules=dirty`). `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself Security vulnerabilities relevant to this tool include: diff --git a/docs/EVALS.md b/docs/EVALS.md index 11acf723b..791e77946 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 686 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **685 gate scripts**, all of which exist on disk. +naming **686 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 686. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index db11d6c46..a0678335b 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["686 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "686", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["686 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/binstale.h b/src/binstale.h index 40970e896..6251ac773 100644 --- a/src/binstale.h +++ b/src/binstale.h @@ -120,7 +120,7 @@ struct BinaryStaleResult // (no history for that path, git error) — a hard "don't know" that skips the pair rather than guess. inline std::string lastTouchingCommit( const std::string& root, const std::string& path ) { - return quality::gitOneLine( root, "log -1 --format=%H -- " + shSingleQuote( path ) + " 2>/dev/null" ); + return quality::gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%H -- " + shSingleQuote( path ) + " 2>/dev/null" ); } inline BinaryStaleResult computeBinaryStaleness( const std::string& root ) diff --git a/src/crossref.h b/src/crossref.h index aa87c483b..fba0c64f3 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -881,7 +881,7 @@ inline std::vector diffRaw( const std::string& root, const std::string& DISCLOSE( "crossref: refusing a diff whose revision arguments are not resolved object names" ); return {}; } - const std::string raw = gitCapture( root, "diff --raw --no-abbrev --no-renames " + shSingleQuote( a ) + " " + shSingleQuote( b ) + " -- 2>/dev/null" ); + const std::string raw = gitCapture( root, "diff --no-ext-diff --no-textconv --ignore-submodules=dirty --raw --no-abbrev --no-renames " + shSingleQuote( a ) + " " + shSingleQuote( b ) + " -- 2>/dev/null" ); std::vector out; for( std::string_view line : splitLines( raw ) ) { @@ -2642,7 +2642,7 @@ inline GitListing worktreeChangedPaths( const std::string& root, const std::stri { return {}; } - GitListing changed = gitListChecked( root, "diff --name-only --no-renames --relative " + shSingleQuote( headSha ) + " --" ); + GitListing changed = gitListChecked( root, "diff --no-ext-diff --no-textconv --ignore-submodules=dirty --name-only --no-renames --relative " + shSingleQuote( headSha ) + " --" ); GitListing others = gitListChecked( root, "ls-files --others --exclude-standard" ); changed.ok = changed.ok && others.ok; changed.lines.insert( changed.lines.end(), others.lines.begin(), others.lines.end() ); @@ -2947,7 +2947,7 @@ inline RenameEvidence renameEvidenceOf( const std::string& root, const std::stri { return ev; // a git-quoted spelling is not the path's name: no evidence either way } - ev.headCopy = gitCapture( root, "show " + shSingleQuote( "HEAD:./" + path ) + " 2>/dev/null" ); + ev.headCopy = gitCapture( root, "show --no-ext-diff --no-textconv " + shSingleQuote( "HEAD:./" + path ) + " 2>/dev/null" ); std::string work; const WorktreeCopy copy = readWorktreeCopy( root, path, work ); ev.definitionLeft = ( copy == WorktreeCopy::Text || copy == WorktreeCopy::Gone ) && definitionLeftCopy( ev.headCopy, work, missing ); diff --git a/src/gitcmd.h b/src/gitcmd.h index 17ee3b6aa..e3ff10652 100644 --- a/src/gitcmd.h +++ b/src/gitcmd.h @@ -3,7 +3,7 @@ // gitcmd.h — THE one spelling of how ripwire starts a git child. // // Every git command this tool runs begins with gitCmd(), so the git-config trust boundary (githarden.h) is a -// property of the command line rather than of anything a caller has to detect first. The prefix is two +// property of the command line rather than of anything a caller has to detect first. The prefix is three // policy options, applied to every invocation: // // --no-optional-locks ripwire's git calls are read-only. Without this, a `git status` that refreshes @@ -15,6 +15,28 @@ // otherwise apply, and git passes it on to any git it spawns. The builtin daemon // is not consulted by ripwire's own few calls either — their cost is a stat pass // of the same order as the crawl ripwire already makes. +// -c log.showSignature=false a repository's config can turn signature display on for every `git log`, and +// signature display runs the configured gpg.program. ripwire's log formats never +// ask for a signature (and its parsers would misread the extra lines), so the pin +// changes no answer. Version-safe: a git that predates the key ignores it. +// +// FILTER DRIVERS. A repository's config may name `filter..clean/smudge/process` programs, and git runs +// them whenever it re-hashes a working-tree file (`status`, a diff against the working tree) or exports one +// (`archive`). The driver NAME is chosen by the repository, so there is no single key to pin. githarden.h asks +// git, once per root, which filter drivers the repository-local config (local + worktree scope, includes +// followed) defines and registers them here; every later gitCmd() then carries +// -c 'filter..clean=' -c 'filter..smudge=' -c 'filter..process=' -c 'filter..required=false' +// for each of them. An empty command is git's own "no filter" (convert.c), so the file is read as stored. A +// name that `-c` cannot spell (empty, or holding '=', which `-c` splits on) is not pinned: the process is +// FAIL-CLOSED instead — every later child carries kGitRefusedOption, an option no git accepts, so no git +// child runs at all rather than one run with a program the pins could not reach (githarden.h discloses it). +// +// DIFF PROGRAMS are pinned at the call site, because they are subcommand options, not global ones: every +// executed `diff`, `log`, `show` and `blame` carries `--no-ext-diff --no-textconv` (no diff.external, +// diff..command or diff..textconv program), and every `status` and `diff` carries +// `--ignore-submodules=dirty`, so git never starts a second git inside a submodule's OWN repository, whose +// config the per-root filter pins above do not cover. test/gitchildoptscheck.sh observes all of it from the +// child through a PATH shim. // // The ONE git command that must not carry the prefix is githarden's `git config --get core.fsmonitor` probe: // a command-line value would BE the answer it reads back. A config read refreshes no index and runs no hook. @@ -24,23 +46,99 @@ // Gated by test/githardencheck.sh: behavioural arms run the git-backed verbs through several root shapes and // the MCP and --plan-lint entry points and assert a hook-form core.fsmonitor stays unfired; a PATH shim arm // asserts every git child carries the prefix; a structural arm refuses an executed git command spelled -// outside gitCmd(). +// outside gitCmd(). test/gitchildoptscheck.sh gates the filter pins and the diff-program options. +#include "infra/shquote.h" // rw::shSingleQuote — a driver name is the repository's text, quoted for the shell + +#include +#include #include #include +#include namespace rw { -inline constexpr std::string_view kGitCmdPrefix = "git --no-optional-locks -c core.fsmonitor=false"; +inline constexpr std::string_view kGitCmdPrefix = "git --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false"; + +// An option no git version accepts: git exits 129 before it reads a config file or a working tree. Carried by +// every child once a repository names a filter driver the -c pins cannot spell (see FILTER DRIVERS above). +inline constexpr std::string_view kGitRefusedOption = "--ripwire-refused-unpinnable-filter-driver"; + +namespace gitcmddetail +{ +// The registered pins. Written by githarden (at startup, and by an MCP/LSP server when a request names a root +// it has not probed) and read by every gitCmd() call, possibly from a worker thread — hence the mutex. Append-only. +struct DriverPins +{ + std::mutex lock; + std::vector names; // every filter driver name pinned so far, in registration order + std::string options; // the -c options (and the refusal, once set), each with its leading space + bool refused = false; +}; + +inline DriverPins& driverPins() +{ + static DriverPins pins; + return pins; +} +} // namespace gitcmddetail + +// What one registration did: how many names were new, and whether any of them could not be pinned (then every +// later child carries kGitRefusedOption). +struct DriverPinResult +{ + std::size_t added = 0; + bool unpinnable = false; +}; + +// Register filter driver names. A name already pinned is skipped, so the options grow once per distinct name +// however many roots name it. `-c` takes `key=value` and splits at the FIRST '=', so a name holding '=' cannot be +// spelled; an empty name is not a subsection git can be told about either. Newline and NUL never reach here (git +// config cannot hold them in a subsection name). +inline DriverPinResult pinGitFilterDrivers( const std::vector& names ) +{ + gitcmddetail::DriverPins& pins = gitcmddetail::driverPins(); + const std::lock_guard guard( pins.lock ); + DriverPinResult result; + for( const std::string& name : names ) + { + if( std::find( pins.names.begin(), pins.names.end(), name ) != pins.names.end() ) + { + continue; + } + pins.names.push_back( name ); + ++result.added; + if( name.empty() || name.contains( '=' ) ) + { + result.unpinnable = true; + if( !pins.refused ) + { + pins.refused = true; + pins.options += ' '; + pins.options += kGitRefusedOption; + } + continue; + } + for( std::string_view key : { ".clean=", ".smudge=", ".process=", ".required=false" } ) + { + pins.options += " -c "; + pins.options += shSingleQuote( "filter." + name + std::string( key ) ); + } + } + return result; +} -// `rest` is everything after the program name and the two policy options, starting with its own leading space +// `rest` is everything after the program name and the policy options, starting with its own leading space // (" -c core.quotepath=false -C "), exactly as the call site spelled it after "git" before this header existed. inline std::string gitCmd( std::string_view rest ) { + gitcmddetail::DriverPins& pins = gitcmddetail::driverPins(); + const std::lock_guard guard( pins.lock ); std::string cmd; - cmd.reserve( kGitCmdPrefix.size() + rest.size() ); + cmd.reserve( kGitCmdPrefix.size() + pins.options.size() + rest.size() ); cmd += kGitCmdPrefix; + cmd += pins.options; cmd += rest; return cmd; } diff --git a/src/githarden.h b/src/githarden.h index fa2c944d6..6da68dad0 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -30,6 +30,16 @@ // The trust decision is PER PROCESS: a long-lived --mcp server probes its roots once at start. A config edited // under a running server is not re-read — the same freshness contract every other startup-only fact has here. // +// FILTER DRIVERS (2026-10-15). The same trust class reaches git through `filter..clean/smudge/process`: a +// repository-local config can name a program that git runs whenever it re-hashes a stale working-tree file (`git +// status`, a diff against the working tree) or exports one (`git archive`), and the driver name is the repository's +// choice, so no fixed -c key covers it. hardenFilterDriversFor() asks git which drivers the local/worktree scopes +// define (cheap-tier gated on the same candidate files: "filter" or "include") and pins each to an empty command on +// every later git child (gitcmd.h FILTER DRIVERS), disclosing git_harden=filter-driver. A server that meets a root +// only in a request (MCP/LSP with no startup root) probes it on first sighting (hardenLateRoot, from getIndex). +// Diff programs (diff.external, diff..command/textconv) are refused at the call site instead, with +// --no-ext-diff --no-textconv; submodule repositories are not entered (--ignore-submodules=dirty). +// // NOT covered, and measured NOT to fire here: a `post-index-change` hook under `git status --porcelain` (git 2.50.1, // the same fixture). Hooks are the same trust class as config, but an arm that cannot be observed to fire is not // asserted (CONTRIBUTING §2) — so nothing is claimed for hooks. Revisit when a git version is shown to run one on @@ -47,6 +57,8 @@ #include #include #include +#include +#include #include #include #include @@ -273,8 +285,18 @@ inline std::vector localConfigCandidates( const std::file return out; } -inline bool localConfigMayCarryFsmonitor( const std::string& root ) +// One read of the local config candidates, two questions: could a file carry core.fsmonitor, and could one carry a +// filter driver? "include" answers yes to both — an include can bring in either key. Same soundness argument as above: +// a key git honours must be spelled in one of these files or reached through an include they name. +struct LocalConfigNeedles { + bool fsmonitor = false; + bool filter = false; +}; + +inline LocalConfigNeedles localConfigNeedles( const std::string& root ) +{ + LocalConfigNeedles found; for( const std::filesystem::path& candidate : localConfigCandidates( root ) ) { std::optional bytes = pathguard::readRegularFileNoFollow( candidate.string() ); @@ -286,12 +308,162 @@ inline bool localConfigMayCarryFsmonitor( const std::string& root ) { if( c >= 'A' && c <= 'Z' ) { c = static_cast( c + ( 'a' - 'A' ) ); } } - if( bytes->find( "fsmonitor" ) != std::string::npos || bytes->find( "include" ) != std::string::npos ) + const bool include = bytes->find( "include" ) != std::string::npos; + found.fsmonitor = found.fsmonitor || include || bytes->find( "fsmonitor" ) != std::string::npos; + found.filter = found.filter || include || bytes->find( "filter" ) != std::string::npos; + } + return found; +} + +// ── filter drivers the REPOSITORY's own config names (gitcmd.h FILTER DRIVERS) ───────────────────────────── +// +// `git config --show-scope -z --get-regexp` lists every filter..clean/smudge/process with the scope it came +// from; an include is reported with the scope of the file that included it. Only `local` and `worktree` are the +// repository's own writing: `global`/`system` are the user's and the machine's (git-lfs installs its driver +// there), and `command` is this process's own -c pins and the caller's GIT_CONFIG_* block. The read is a config +// read — it refreshes no index and starts no program. -z keeps a name with any byte but NUL/newline intact: +// each entry is "scope\0key\nvalue\0". +// +// A git older than 2.26 has no --show-scope and rejects the option (exit 129). The fallback reads the same +// entries WITHOUT scope and pins every one of them, the user's global drivers included: over-neutralising is the +// safe direction, and the disclosure says scope="all" so the difference is visible. +struct FilterDriverProbe +{ + std::vector names; // distinct driver names, in the order git listed them + bool allScopes = false; // the pre-2.26 fallback: scope could not be read, every scope pinned +}; + +inline bool readConfigListing( const std::string& root, bool withScope, std::string& body ) +{ + const std::string out = popenTrimmed( hardenedGitPrefix( root ) + " config" + ( withScope ? " --show-scope" : "" ) + + " -z --get-regexp '^filter\\..*\\.(clean|smudge|process)$' 2>/dev/null; printf '\\nrc=%s' \"$?\"" ); + const std::size_t rcAt = out.rfind( "\nrc=" ); + if( rcAt == std::string::npos ) + { + return false; + } + const std::string_view rc = std::string_view( out ).substr( rcAt + 4 ); + if( rc != "0" && rc != "1" ) + { + return false; // 1 is "no match"; anything else is git refusing the command line or the config + } + body = out.substr( 0, rcAt ); + return true; +} + +// "filter..\n" → . The name is everything between the section's dot and the LAST dot, so a +// name holding dots survives; nullopt for anything that is not a filter driver key. +inline std::optional filterDriverNameOf( std::string_view entry ) +{ + entry = entry.substr( 0, entry.find( '\n' ) ); // the key; the value follows the newline + constexpr std::string_view kSection = "filter."; + const std::size_t lastDot = entry.rfind( '.' ); + if( !entry.starts_with( kSection ) || lastDot == std::string_view::npos || lastDot < kSection.size() ) + { + return std::nullopt; + } + return std::string( entry.substr( kSection.size(), lastDot - kSection.size() ) ); +} + +inline FilterDriverProbe probeLocalFilterDrivers( const std::string& root ) +{ + FilterDriverProbe probe; + std::string body; + bool withScope = true; + if( !readConfigListing( root, true, body ) ) + { + withScope = false; + if( !readConfigListing( root, false, body ) ) { - return true; + return probe; // git cannot read this config at all — then no git command here gets as far as a filter } + probe.allScopes = true; + } + std::vector fields; + for( std::size_t start = 0; start < body.size(); ) + { + const std::size_t nul = body.find( '\0', start ); + const std::size_t stop = nul == std::string::npos ? body.size() : nul; + fields.push_back( std::string_view( body ).substr( start, stop - start ) ); + start = stop + 1; + } + const std::size_t stride = withScope ? 2 : 1; + for( std::size_t i = 0; i + stride <= fields.size(); i += stride ) + { + if( withScope && fields[i] != "local" && fields[i] != "worktree" ) + { + continue; + } + std::optional name = filterDriverNameOf( fields[i + stride - 1] ); + if( name && std::find( probe.names.begin(), probe.names.end(), *name ) == probe.names.end() ) + { + probe.names.push_back( std::move( *name ) ); + } + } + return probe; +} + +// Probe one root's filter drivers, pin them (gitcmd.h), and disclose on stderr. The names are the repository's +// text and are never printed — only their count. Safe to call from a server thread: the pins are mutex-guarded. +inline void hardenFilterDriversFor( const std::string& root ) +{ + const FilterDriverProbe probe = probeLocalFilterDrivers( root ); + if( probe.names.empty() ) + { + return; + } + if( pinGitFilterDrivers( probe.names ).unpinnable ) + { + emitTo( stderr, "ripwire: git: {} configures a filter driver whose name git's -c option cannot spell — every git call this run is refused instead (git_harden=filter-driver-refused): history, churn and the dirty stamp are unavailable\n", root ); + return; + } + emitTo( stderr, "ripwire: git: {} configures {} filter driver(s) in {} — neutralised for this run (git_harden=filter-driver): every git call ripwire makes runs them with an empty clean/smudge/process command, so no configured filter program starts\n", + root, probe.names.size(), probe.allScopes ? "its config (scope=all: this git cannot report scope, so the user's own drivers are neutralised too)" : "the repository's own config" ); +} + +// Roots a server meets only in a REQUEST (an MCP or LSP server started with no root): the startup pass never saw +// them, so the first request that names one probes it here. One probe per distinct spelling; the startup roots +// are recorded too, so they are not probed twice. +namespace detail +{ +struct ProbedRoots +{ + std::mutex lock; + std::vector roots; +}; +inline ProbedRoots& probedRoots() +{ + static ProbedRoots probed; + return probed; +} +inline bool firstSighting( const std::string& root ) +{ + ProbedRoots& probed = probedRoots(); + const std::lock_guard guard( probed.lock ); + if( std::find( probed.roots.begin(), probed.roots.end(), root ) != probed.roots.end() ) + { + return false; + } + probed.roots.push_back( root ); + return true; +} +} // namespace detail + +inline void hardenLateRoot( const std::string& root ) +{ + if( root.empty() || !detail::firstSighting( root ) ) + { + return; + } + std::error_code ec; + if( !std::filesystem::is_directory( std::filesystem::path( root ), ec ) || ec ) + { + return; + } + if( localConfigNeedles( root ).filter ) + { + hardenFilterDriversFor( root ); } - return false; } // ── the environment override block (git's own mechanism, git-config(1) ENVIRONMENT) ─────────────────────── @@ -365,10 +537,16 @@ inline const Report& hardenForRoots( std::span roots ) { continue; } - const std::string rootStr = std::string( root ); - const FsmonitorForm form = localConfigMayCarryFsmonitor( rootStr ) ? probeFsmonitorForm( rootStr ) : FsmonitorForm::Unset; + const std::string rootStr = std::string( root ); + const LocalConfigNeedles needles = localConfigNeedles( rootStr ); + const FsmonitorForm form = needles.fsmonitor ? probeFsmonitorForm( rootStr ) : FsmonitorForm::Unset; r.forms.emplace_back( rootStr, form ); anyHook = anyHook || form == FsmonitorForm::Hook; + detail::firstSighting( rootStr ); + if( needles.filter ) + { + hardenFilterDriversFor( rootStr ); + } } if( !anyHook ) { diff --git a/src/gitmine.h b/src/gitmine.h index e3dcbe013..1bdc1ba4e 100644 --- a/src/gitmine.h +++ b/src/gitmine.h @@ -1500,7 +1500,7 @@ inline std::vector> gitLogFileSets( const std::string // open --name-only pipe would leave a filled pipe buffer waiting on us while we wait on the probe. const GitPathIndex byGitPath = gitPathIndexOfFiles( ing, onlyRoot ); - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log " + kMergeDiffArgs + windowArgs + "--name-only --format=tformat:__C__ 2>/dev/null"; + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log --no-ext-diff --no-textconv " + kMergeDiffArgs + windowArgs + "--name-only --format=tformat:__C__ 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { @@ -1623,7 +1623,7 @@ inline RawCommitStream gitLogNameOnlyRaw( const std::string& root, const std::st PROFILE_SCOPE_DESCRIBE( "gitmine: gitLogNameOnlyRaw (git log --name-only popen)" ); RawCommitStream out; const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) - + " log " + kMergeDiffArgs + "--since=" + shSingleQuote( defaultWindowSince( root, coSince ) ) // F1: HEAD-anchored when `coSince` is a default month window + + " log --no-ext-diff --no-textconv " + kMergeDiffArgs + "--since=" + shSingleQuote( defaultWindowSince( root, coSince ) ) // F1: HEAD-anchored when `coSince` is a default month window + " --name-only --format=tformat:__C__%x20%ct 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) @@ -1676,7 +1676,7 @@ inline std::string gitWindowBoundarySha( const std::string& root, const std::str // G3: the shared reader, not a private `char buf[128]` + fgets accumulate. `| tail -1` already reduces // the output to one line, so `.back()` is that line; gitCommandLines has already stripped its CR/LF tail. const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) - + " log --since=" + shSingleQuote( defaultWindowSince( root, coSince ) ) // F1: the probe must resolve the window it guards, by the same rule + + " log --no-ext-diff --no-textconv --since=" + shSingleQuote( defaultWindowSince( root, coSince ) ) // F1: the probe must resolve the window it guards, by the same rule + " --format=%H 2>/dev/null | tail -1"; const GitCommandLines res = gitCommandLines( cmd ); if( !res.isStarted || res.lines.empty() ) @@ -1798,7 +1798,7 @@ inline std::int64_t gitHeadCommitEpoch( const std::string& root ) { // G3: the shared reader. `log -1 --format=%ct` prints exactly one line, so the first is the whole // answer; gitCommandLines strips the CR/LF tail, and the trailing-space strip is kept for the value. - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log -1 --format=%ct HEAD 2>/dev/null"; + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log --no-ext-diff --no-textconv -1 --format=%ct HEAD 2>/dev/null"; const GitCommandLines res = gitCommandLines( cmd ); if( !res.isStarted || res.lines.empty() ) { @@ -1841,7 +1841,7 @@ inline std::vector gitFileCommitCountsInDayWindow( const std::str // Stream `git log --name-only` with each commit's epoch on its __C__ marker line: "__C__ ". // A commit counts once per file it touches, iff its epoch is within [cutoff, headEpoch]. const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) - + " log " + kMergeDiffArgs + "--name-only --format=tformat:__C__%x20%ct 2>/dev/null"; + + " log --no-ext-diff --no-textconv " + kMergeDiffArgs + "--name-only --format=tformat:__C__%x20%ct 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { @@ -2069,7 +2069,7 @@ inline DecayedChurnMined gitLogDecayedFileMining( const std::string& root, const // built BEFORE the log pipe opens, for the reason gitLogFileSets states: it runs a git probe of its own. const GitPathIndex byGitPath = gitPathIndexOfFiles( ing, onlyRoot ); - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log " + kMergeDiffArgs + windowArgs + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log --no-ext-diff --no-textconv " + kMergeDiffArgs + windowArgs + "--name-only --format=tformat:__C__%x20%ct 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) @@ -2433,7 +2433,7 @@ inline std::vector gitFileAuthors( // -- ): one "__C__email|unix-ts" header per commit, then its changed files, one per line, // until a blank line / the next header. Mirrors gitCommitFileSets' exact invocation style. std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) - + " log " + kMergeDiffArgs + "--name-only --format=tformat:__C__%ae\\|%at"; + + " log --no-ext-diff --no-textconv " + kMergeDiffArgs + "--name-only --format=tformat:__C__%ae\\|%at"; if( singleFile ) { const std::string& fp = ing.files[ onlyFileId ]; @@ -3362,7 +3362,7 @@ inline bool gitChurnCounts( const std::string& root, const rw::IngestResult& ing { const std::string windowArgs = rw::historyWindowArgs( root, scope, since ); const rw::GitCommandLines touched = rw::gitCommandLines( - gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log " + rw::kMergeDiffArgs + windowArgs + "--name-only --format= 2>/dev/null" ); + gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " log --no-ext-diff --no-textconv " + rw::kMergeDiffArgs + windowArgs + "--name-only --format= 2>/dev/null" ); if( !touched.isStarted ) { return false; diff --git a/src/gitstamp.h b/src/gitstamp.h index 8a8f0f41f..b23cb894e 100644 --- a/src/gitstamp.h +++ b/src/gitstamp.h @@ -123,7 +123,7 @@ inline std::string stampAt( const std::string& root ) { return {}; // not a git repo / no HEAD } - const bool dirty = !quality::gitOneLine( root, "status --porcelain 2>/dev/null" ).empty(); + const bool dirty = !quality::gitOneLine( root, "status --porcelain --ignore-submodules=dirty 2>/dev/null" ).empty(); return sha.substr( 0, 9 ) + ( dirty ? "+dirty" : "" ) + ( isShallow( root ) ? "+shallow" : "" ); } diff --git a/src/handoff.h b/src/handoff.h index 48b9d6ce1..98265c854 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -248,7 +248,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In }; const std::string branch = gitOneLine( root, "rev-parse --abbrev-ref HEAD" ); - const std::string subject = gitOneLine( root, "log -1 --format=%s" ); + const std::string subject = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%s" ); const std::string at = gitstamp::atAttr( root ); auto [ changedMask, gitOk ] = gitDiffChangedMask( root, ing ); diff --git a/src/mcpindex.h b/src/mcpindex.h index 6315a4f97..7325cfa7e 100644 --- a/src/mcpindex.h +++ b/src/mcpindex.h @@ -19,6 +19,7 @@ #include "serialize.h" #include "search.h" #include "gitmine.h" +#include "githarden.h" // githarden::hardenLateRoot — a request-named root's filter drivers are pinned before its first git child #include "lexical.h" #include "recall.h" #include "valuerefs.h" // reference-as-value round: the per-index ValueRefIndex cache (valueRefIndexOf) @@ -1164,6 +1165,7 @@ inline const ValueRefIndex& valueRefIndexOf( const McpIndex& ix ) // the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no graph rebuild). inline const McpIndex& getIndex( const std::string& root ) { + githarden::hardenLateRoot( root ); // a root first named in a request: pin its filter drivers before any git child runs there McpIndex& ix = mcpIndexSlot(); // hot path. The FS-event watcher (Feature 1) lets us SKIP the directory-mtime sweep when it proves no diff --git a/src/mergescout.h b/src/mergescout.h index e23478dc8..f2343ed1f 100644 --- a/src/mergescout.h +++ b/src/mergescout.h @@ -637,7 +637,7 @@ inline ScoutResult computeMergeScout( const std::string& root, std::string_view baseShas[i] = resolveMergeBase( root, refShas[i], result.headSha ); } - const bool dirty = !quality::gitOneLine( root, "status --porcelain 2>/dev/null" ).empty(); // dirty ⇒ the implicit extra arm + const bool dirty = !quality::gitOneLine( root, "status --porcelain --ignore-submodules=dirty 2>/dev/null" ).empty(); // dirty ⇒ the implicit extra arm HeadChangedByBase headChangedByBase = planHeadConflictLane( baseShas, result.headSha ); diff --git a/src/planlint.h b/src/planlint.h index 9f2ff1028..028deb61c 100644 --- a/src/planlint.h +++ b/src/planlint.h @@ -385,7 +385,7 @@ inline std::string gitBlameLineSha( const std::string& repoRoot, const std::stri return {}; } const std::string cmd = gitCmd( " -c core.quotepath=false" ) + quality::gitBlameConfigPins( repoRoot ) - + " -C " + shSingleQuote( repoRoot ) + " blame --porcelain -L " + + " -C " + shSingleQuote( repoRoot ) + " blame --no-ext-diff --no-textconv --porcelain -L " + std::to_string( lineNo1 ) + ",+1 HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) diff --git a/src/prcontext.h b/src/prcontext.h index 4b7ad300b..08ff9d619 100644 --- a/src/prcontext.h +++ b/src/prcontext.h @@ -220,7 +220,7 @@ inline NumstatDiff numstatChangedPaths( const std::string& root, const std::stri // revision this file builds is a rev-parse-resolved sha, see resolveDiffAnchor), so nothing downstream // can be re-read as an option or as an ambiguous pathspec. Belt to the resolver's braces; an empty // pathspec list after `--` means "all paths", so the diff itself is unchanged. - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --numstat " + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --numstat " + revArgs + " -- 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); diff --git a/src/quality.h b/src/quality.h index 0a0b746df..095f29225 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1974,16 +1974,16 @@ inline RenameMap gitRenameMap( const std::string& root, const std::string& span // silently dropped the most common rename an agent ever makes — and the gate would still have passed, // because the content route rescues that same ack by a different mechanism. That near-miss is why the // gate asserts the ROUTE (acked_by_rename) and not merely the outcome. - absorb( popenTrimmed( pinned + "diff -M50% --diff-filter=R --name-status --cached 2>/dev/null" ) ); - absorb( popenTrimmed( pinned + "diff -M50% --diff-filter=R --name-status HEAD 2>/dev/null" ) ); - absorb( popenTrimmed( pinned + "log -M50% --diff-filter=R --name-status --format= -n " + absorb( popenTrimmed( pinned + "diff --no-ext-diff --no-textconv --ignore-submodules=dirty -M50% --diff-filter=R --name-status --cached 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "diff --no-ext-diff --no-textconv --ignore-submodules=dirty -M50% --diff-filter=R --name-status HEAD 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "log --no-ext-diff --no-textconv -M50% --diff-filter=R --name-status --format= -n " + std::to_string( kRenameWindowCommits ) + " 2>/dev/null" ) ); } else { rm.truncatedWindow = false; // the range IS the scope; nothing outside it was in question rm.commitsScanned = 0; - absorb( popenTrimmed( pinned + "log -M50% --diff-filter=R --name-status --format= " + shSingleQuote( span ) + " 2>/dev/null" ) ); + absorb( popenTrimmed( pinned + "log --no-ext-diff --no-textconv -M50% --diff-filter=R --name-status --format= " + shSingleQuote( span ) + " 2>/dev/null" ) ); } return rm; } @@ -2005,7 +2005,7 @@ inline std::string gitHeadSha( const std::string& root ) // dashes at 4 and 7) — the caller degrades to a fixed epoch date. Read-only: `git log` never mutates the repo. inline std::string gitCommitterDateIso( const std::string& root ) { - std::string out = gitOneLine( root, "log -1 --format=%cs HEAD 2>/dev/null" ); // not const: a const local cannot be moved out on return + std::string out = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%cs HEAD 2>/dev/null" ); // not const: a const local cannot be moved out on return if( out.size() != 10 || out[4] != '-' || out[7] != '-' ) { return {}; @@ -2052,7 +2052,7 @@ inline bool gitIsAncestor( const std::string& root, const std::string& ancestor, // (HEAD epoch anchors the window — wall-clock is never consulted). "" on any failure (degrade: churn silent). inline std::string gitWindowRefSha( const std::string& root, std::uint32_t days ) { - const std::string epochStr = gitOneLine( root, "log -1 --format=%ct HEAD 2>/dev/null" ); + const std::string epochStr = gitOneLine( root, "log --no-ext-diff --no-textconv -1 --format=%ct HEAD 2>/dev/null" ); if( epochStr.empty() ) { return {}; @@ -5013,7 +5013,7 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // (not a repo, unborn HEAD, git absent) reads as "not identical" and falls back to the archived baseline. inline bool workingTreeMatchesHead( const std::string& root ) { - const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --quiet HEAD -- . >/dev/null 2>&1"; + const std::string cmd = gitCmd( " -c core.quotepath=false -C " ) + shSingleQuote( root ) + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --quiet HEAD -- . >/dev/null 2>&1"; return os::system( cmd.c_str() ) == 0; } @@ -5927,7 +5927,7 @@ inline void gitBlameRangeWindowCommits( const std::string& root, const std::stri return; } const std::string cmd = gitCmd( " -c core.quotepath=false" ) + gitBlameConfigPins( root ) + " -C " + shSingleQuote( root ) - + " blame --porcelain -L " + std::to_string( startLine ) + ",+" + std::to_string( lineCount ) + + " blame --no-ext-diff --no-textconv --porcelain -L " + std::to_string( startLine ) + ",+" + std::to_string( lineCount ) + " HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) @@ -6001,7 +6001,7 @@ inline std::vector gitDiffHunksVsHead( const std::string& root, const { std::vector hunks; const std::string cmd = gitCmd( " -c core.quotepath=false -c diff.algorithm=myers -C " ) + shSingleQuote( root ) - + " diff --no-ext-diff --unified=0 --no-color HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; + + " diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- " + shSingleQuote( relPath ) + " 2>/dev/null"; std::FILE* pipe = os::popen( cmd.c_str(), "r" ); if( !pipe ) { DISCLOSE( "quality: churn hunk diff could not be spawned" ); return hunks; } @@ -9254,7 +9254,7 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // degrades every symbol below to AMBIENT (churnEditWindowCommitCount is gated on `> 0`). std::int64_t churnCutoffEpoch = 0; { - const std::string epochStr = gitOneLine( std::string( root ), "log -1 --format=%ct HEAD 2>/dev/null" ); + const std::string epochStr = gitOneLine( std::string( root ), "log --no-ext-diff --no-textconv -1 --format=%ct HEAD 2>/dev/null" ); if( !epochStr.empty() ) { const std::int64_t headEpoch = std::strtoll( epochStr.c_str(), nullptr, 10 ); diff --git a/src/slicediff.h b/src/slicediff.h index 996c16d86..195bb510f 100644 --- a/src/slicediff.h +++ b/src/slicediff.h @@ -329,7 +329,7 @@ inline std::string blobAtRev( const std::string& root, const std::string& sha, c { return {}; } - return quality::gitOneLine( root, "show " + shSingleQuote( sha + ":" + p ) + " 2>/dev/null" ); + return quality::gitOneLine( root, "show --no-ext-diff --no-textconv " + shSingleQuote( sha + ":" + p ) + " 2>/dev/null" ); }; std::string blob = read( rel ); if( !blob.empty() ) diff --git a/test/gitchildoptscheck.sh b/test/gitchildoptscheck.sh new file mode 100755 index 000000000..8d4ed856c --- /dev/null +++ b/test/gitchildoptscheck.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +# gitchildoptscheck.sh — every git child ripwire starts carries the options that keep a REPOSITORY's own config +# from naming a program git runs (src/gitcmd.h, src/githarden.h). +# +# githardencheck.sh covers core.fsmonitor. This gate covers the rest of that trust class on ripwire's read-only +# calls: a filter driver (`filter..clean` with a matching `filter=` attribute) that git runs when it +# re-hashes a working-tree file whose stat data is stale — the state a copied or extracted tree is in — and the diff +# programs (diff.external, diff..command/textconv). +# +# The fixture is the review's F12b shape (tmp/rv-githarden-speed/fx/mkfx.sh): one commit, `*.c filter=ev`, a local +# filter.ev.clean that appends a line to MARK and passes its input through, the content UNCHANGED and only the file's +# mtime moved. A ripwire run can leave the index refreshed, which makes the next run a no-op, so EVERY behavioural +# row rebuilds the fixture first. +# +# Arms: +# (A) presence: on a fresh fixture, plain `git --no-optional-locks status` runs the filter — the fixture is live. +# If this git does not, the gate cannot conclude and exits 2 rather than pass vacuously. +# (B) red, only when RIPWIRE_BASE_BIN names a pre-fix binary: ` --situ` runs the filter. +# (C) green: map, --situ, --quality-delta, --hotspots, --rank-by=churn and --doctor each run the filter 0 times, +# each on a fresh fixture, and each must have answered (exit status + a stdout marker) so a run that stops +# before git cannot pass as "0 firings". +# (D) disclosure: stderr carries git_harden=filter-driver; stdout does not. +# (E) an MCP server started with NO root, whose request names the fixture (the late-root path): 0 firings, and +# the request was answered. +# (F) a PATH shim that records each git child's argv and GIT_* environment and then execs the real git. Over the +# same verbs, on the fixture and on a plain multi-commit repository with a working-tree edit: +# F1 every child starts with --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false +# (the `config --get core.fsmonitor` probe excepted, as in githardencheck K); +# F2 on the fixture, every child after the filter-driver probe carries the four filter.ev pins; +# F3 every diff/log/show/blame child carries --no-ext-diff and --no-textconv, and every status/diff child +# carries --ignore-submodules=dirty; +# F4 no child saw a GIT_EXTERNAL_DIFF, GIT_ATTR_SOURCE or GIT_CONFIG_PARAMETERS ripwire did not inherit. +# A per-verb table (children, subcommands, F1-F3 violations) is printed. Controls: a synthetic log with one +# child lacking each option must be flagged by the same checker, and with RIPWIRE_BASE_BIN the base binary's +# logs must fail F2 and F3. +# (G) structural: every source line that spells a git log/diff/show/blame invocation carries --no-textconv, and +# every `status --porcelain` carries --ignore-submodules=dirty; a mutation control on a COPY of src/ must be +# found by the same scan. +# +# Usage: RIPWIRE_BIN=build/ripwire [RIPWIRE_BASE_BIN=/path/to/pre-fix/ripwire] bash test/gitchildoptscheck.sh + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +# shellcheck source=lib/clean-env.sh +. "$ROOT/test/lib/clean-env.sh" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +BASE="${RIPWIRE_BASE_BIN:-}" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; return 0; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +REALGIT="$( command -v git )" +[ -n "$REALGIT" ] || { echo "git not on PATH — this gate needs it"; exit 2; } +echo "gitchildoptscheck: BIN=$BIN git=$( "$REALGIT" --version )${BASE:+ BASE=$BASE}" + +TMP="$( mktemp -d )" +trap 'rm -rf "$TMP"' EXIT +TMP="$( cd "$TMP" && pwd -P )" +export XDG_CACHE_HOME="$TMP/cache"; mkdir -p "$XDG_CACHE_HOME" +MARK="$TMP/MARK" +CMD="$TMP/cmd.sh" +printf '#!/bin/sh\necho "cmd $* in $PWD" >> %s\ncat\n' "$MARK" > "$CMD"; chmod +x "$CMD" +FX="$TMP/f12b" +fired(){ [ -f "$MARK" ] && wc -l < "$MARK" | tr -d ' ' || echo 0; } + +# The F12b recipe, verbatim in shape: clean filter, content unchanged, only the stat data stale. +mkfx(){ + rm -rf "$FX" "$MARK" + mkdir -p "$FX" + ( cd "$FX" && "$REALGIT" init -q && printf 'int f(void){return 1;}\n' > f.c && printf '*.c filter=ev\n' > .gitattributes \ + && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm i \ + && "$REALGIT" config filter.ev.clean "$CMD clean" && touch -t 202001010000 f.c ) >/dev/null 2>&1 \ + || { echo "fixture build failed"; exit 2; } +} + +# ── (A) presence ──────────────────────────────────────────────────────────────────────────────────────── +mkfx +"$REALGIT" --no-optional-locks -C "$FX" status --porcelain >/dev/null 2>&1 +if [ "$( fired )" -ge 1 ]; then + ok "(A) presence: plain git status runs the fixture's clean filter ($( fired ) call) — the fixture is live" +else + echo " SKIP (A) this git does not run the clean filter on a stale-stat file here — the gate cannot conclude (exit 2)"; exit 2 +fi + +# run_row TAG RCS MARKER CMD…: fresh fixture, run, count. Sets RVERDICT (ok|rc|marker|fired) and RFIRED. +run_row(){ + rtag="$1"; rrcs="$2"; rmarker="$3"; shift 3 + mkfx + "$@" > "$TMP/r-$rtag.out" 2> "$TMP/r-$rtag.err" + RRC=$? + RFIRED="$( fired )" + case " $rrcs " in *" $RRC "*) ;; *) RVERDICT=rc; return 0 ;; esac + if [ -n "$rmarker" ] && ! grep -qF -- "$rmarker" "$TMP/r-$rtag.out"; then RVERDICT=marker; return 0; fi + if [ "$RFIRED" -eq 0 ]; then RVERDICT=ok; else RVERDICT=fired; fi + return 0 +} + +# ── (B) red on the pre-fix binary, when one is named ──────────────────────────────────────────────────── +if [ -n "$BASE" ]; then + run_row "base-situ" "0" "" "$BASE" "$FX" --situ + if [ "$RVERDICT" = fired ]; then + ok "(B) red: the pre-fix binary's --situ ran the clean filter $RFIRED time(s)" + else + no "(B) the pre-fix binary did not run the filter (verdict $RVERDICT) — the red cannot be shown" + fi +else + echo " NOTE (B) RIPWIRE_BASE_BIN not set — the red arm is skipped" +fi + +# ── (C) green across the git-backed verbs ─────────────────────────────────────────────────────────────── +VERBS='map||0|< +situ|--situ|0|situational-awareness +quality-delta|--quality-delta|0 2|< +hotspots|--hotspots|0|< +churn|--rank-by=churn|0|< +doctor|--doctor|0 1| "$TMP/mcp.in" +run_row "e-mcp" "0" '"id":2,"result"' sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp.in" +case "$RVERDICT" in + ok) ok "(E) MCP request-named root: answered (id 2 result) and the clean filter did not run" ;; + *) no "(E) MCP request-named root: verdict $RVERDICT, exit $RRC, firings $RFIRED: $( head -c 200 "$TMP/r-e-mcp.err" )" ;; +esac + +# ── (F) the PATH shim: argv + GIT_* environment of every child ────────────────────────────────────────── +mkdir -p "$TMP/shim" +cat > "$TMP/shim/git" <> "\${GITCHILD_LOG:?}" +exec "$REALGIT" "\$@" +EOF +chmod +x "$TMP/shim/git" + +# A plain repository with history, a rename and a working-tree edit, so log/diff/blame paths run too. +RICH="$TMP/rich" +( mkdir -p "$RICH" && cd "$RICH" && "$REALGIT" init -q \ + && printf 'int a(void){return 1;}\nint b(void){return a();}\n' > a.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm one \ + && printf 'int c(void){return 3;}\n' > c.c && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm two \ + && "$REALGIT" mv c.c d.c && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm three \ + && printf '### T1\nIn progress.\n\xe2\x8f\xb3 pending\n' > wave.md && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm four \ + && printf 'int e(void){return b();}\n' >> a.c ) >/dev/null 2>&1 || no "(F) could not build the plain history repository" +PLAINVERBS="$VERBS +plan-lint|--plan-lint=$RICH/wave.md|0|plan-lint" + +# check_log FILE PINS: prints one line per violation, "F1|F2|F3|F4 ". PINS=1 enforces F2 on every +# child after the filter-driver probe; PINS=2 on every child but a config read (the control for a binary with no probe). +subcmd_of(){ # argv fields (\x1f-separated, leading empty field) → the subcommand: skip global options and their values + printf '%s' "$1" | awk -F '\037' '{ for( i = 2; i <= NF; ++i ) { if( $i == "-c" || $i == "-C" ) { ++i; continue } if( $i ~ /^-/ ) { continue } print $i; exit } }' +} +check_log(){ + clog="$1"; cpins="$2"; seenprobe=0 + while IFS= read -r line; do + case "$line" in + ENV*) + for v in GIT_EXTERNAL_DIFF GIT_ATTR_SOURCE GIT_CONFIG_PARAMETERS; do + case "$line" in *"$v="*) printf 'F4 env %s\n' "$v" ;; esac + done + continue ;; + esac + argv="${line#ARGV}" + sub="$( subcmd_of "$argv" )" + flat="$( printf '%s' "$argv" | tr '\037' ' ' )" + case "$flat" in *" config --get core.fsmonitor") continue ;; esac + case "$flat" in + " --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false"*) ;; + *) printf 'F1 %s %s\n' "$sub" "$flat" ;; + esac + if [ "$sub" = config ]; then + case "$flat" in *"--show-scope"*|*"filter\\."*) seenprobe=1 ;; esac + continue + fi + if { [ "$cpins" = 1 ] && [ "$seenprobe" = 1 ]; } || [ "$cpins" = 2 ]; then + for k in clean= smudge= process= required=false; do + case "$flat" in *" -c filter.ev.$k"*) ;; *) printf 'F2 %s %s\n' "$sub" "$flat"; break ;; esac + done + fi + case "$sub" in + diff|log|show|blame) + case "$flat" in *" --no-ext-diff"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac + case "$flat" in *" --no-textconv"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac ;; + esac + case "$sub" in + diff|status) + case "$flat" in *" --ignore-submodules=dirty"*) ;; *) printf 'F3 %s %s\n' "$sub" "$flat" ;; esac ;; + esac + done < "$clog" + return 0 +} + +# shim_sweep BINARY LABEL REPO PINS VERBLIST: one log per verb; prints the table rows; accumulates violations in $TMP/viol-LABEL. +shim_sweep(){ + sbin="$1"; slabel="$2"; srepo="$3"; spins="$4"; slist="$5" + : > "$TMP/viol-$slabel" + while IFS='|' read -r vtag vflag vrcs vmark; do + [ "$srepo" = "$FX" ] && mkfx + GITCHILD_LOG="$TMP/shim-$slabel-$vtag.log"; : > "$GITCHILD_LOG"; export GITCHILD_LOG + # shellcheck disable=SC2086 + PATH="$TMP/shim:$PATH" "$sbin" "$srepo" $vflag >/dev/null 2>&1 + unset GITCHILD_LOG + n="$( grep -c '^ARGV' "$TMP/shim-$slabel-$vtag.log" )" + subs="$( grep '^ARGV' "$TMP/shim-$slabel-$vtag.log" | while IFS= read -r l; do subcmd_of "${l#ARGV}"; done | sort | uniq -c | awk '{ printf "%s%s×%s", sep, $2, $1; sep=" " }' )" + check_log "$TMP/shim-$slabel-$vtag.log" "$spins" > "$TMP/viol-$slabel-$vtag" + cat "$TMP/viol-$slabel-$vtag" >> "$TMP/viol-$slabel" + f1="$( grep -c '^F1' "$TMP/viol-$slabel-$vtag" )"; f2="$( grep -c '^F2' "$TMP/viol-$slabel-$vtag" )" + f3="$( grep -c '^F3' "$TMP/viol-$slabel-$vtag" )"; f4="$( grep -c '^F4' "$TMP/viol-$slabel-$vtag" )" + printf ' | %-8s | %-14s | %3s | F1=%s F2=%s F3=%s F4=%s | %s\n' "$slabel" "$vtag" "$n" "$f1" "$f2" "$f3" "$f4" "$subs" + echo "$n" >> "$TMP/count-$slabel" + done <<< "$slist" + return 0 +} + +echo " ---- (F) shim table: repo | verb | children | violations | subcommands" +rm -f "$TMP"/count-* +shim_sweep "$BIN" fixture "$FX" 1 "$VERBS" +shim_sweep "$BIN" plain "$RICH" 0 "$PLAINVERBS" +for lbl in fixture plain; do + total="$( awk '{ s += $1 } END { print s + 0 }' "$TMP/count-$lbl" )" + nv="$( wc -l < "$TMP/viol-$lbl" | tr -d ' ' )" + [ "$total" -ge 10 ] \ + && ok "(F) presence ($lbl): the shim logged $total git children across the verb sweep" \ + || no "(F) presence ($lbl): only $total git children logged — the arm cannot conclude" + [ "$nv" -eq 0 ] \ + && ok "(F) every git child ($lbl) carried F1-F3 and saw no injected F4 variable" \ + || no "(F) $nv violation(s) ($lbl): $( head -3 "$TMP/viol-$lbl" | tr '\n' ';' )" +done +for s in diff log blame status; do + grep -h '^ARGV' "$TMP"/shim-plain-*.log "$TMP"/shim-fixture-*.log 2>/dev/null | while IFS= read -r l; do subcmd_of "${l#ARGV}"; done | grep -qx "$s" \ + && ok "(F) coverage: the sweep exercised at least one '$s' child" \ + || no "(F) coverage: no '$s' child was exercised — F3 is unproven for it" +done + +# Controls: the checker flags each missing option. +CTRL="$TMP/control.log" +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-C\037/x\037status\037--porcelain\n' > "$CTRL" # F1 + F3 (status) +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-c\037log.showSignature=false\037config\037--show-scope\n' >> "$CTRL" # the probe +printf 'ARGV\037--no-optional-locks\037-c\037core.fsmonitor=false\037-c\037log.showSignature=false\037-C\037/x\037log\037-1\n' >> "$CTRL" # F2 + F3 (log) +printf 'ENV\037GIT_EXTERNAL_DIFF=/x\037\n' >> "$CTRL" # F4 +check_log "$CTRL" 1 > "$TMP/control.viol" +for want in F1 F2 F3 F4; do + grep -q "^$want" "$TMP/control.viol" \ + && ok "(F) control: the checker flags a synthetic child missing its $want option" \ + || no "(F) control: the checker did NOT flag $want — the arm cannot fail there" +done +if [ -n "$BASE" ]; then + rm -f "$TMP"/count-base* + shim_sweep "$BASE" base "$FX" 2 "$VERBS" > "$TMP/base-table" 2>&1 + sed 's/^/ /' "$TMP/base-table" + grep -q '^F2' "$TMP/viol-base" && grep -q '^F3' "$TMP/viol-base" \ + && ok "(F) control: the pre-fix binary's children fail F2 ($( grep -c '^F2' "$TMP/viol-base" )) and F3 ($( grep -c '^F3' "$TMP/viol-base" )) under the same checker" \ + || no "(F) control: the pre-fix binary passed the checker — it cannot tell fixed from unfixed" +fi + +# ── (G) structural ─────────────────────────────────────────────────────────────────────────────────────── +# A line spells an executed git invocation when it hands a "log|diff|show|blame …" literal to one of the git +# entry points (gitCmd and the wrappers that prepend it). Display-only strings (situ.h's printed hint, +# editplan.h's next= hint) do not go through them. +structural(){ + grep -rnE '(gitCmd|gitOneLine|gitCapture|gitListChecked|pinned \+|shSingleQuote\( (root|repoRoot) \) \+).*"[[:space:]]*(log|diff|show|blame)( |")|^[[:space:]]*\+ "[[:space:]]*(log|diff|blame)[[:space:]]' "$1/src" 2>/dev/null \ + | grep -v -- '--no-textconv' | sed "s|^$1/||" + grep -rnE '"status --porcelain' "$1/src" 2>/dev/null | grep -v -- '--ignore-submodules=dirty' | sed "s|^$1/||" +} +GOFF="$( structural "$ROOT" )" +GSEEN="$( grep -rnE -- '--no-textconv' "$ROOT/src" | wc -l | tr -d ' ' )" +[ "$GSEEN" -ge 20 ] \ + && ok "(G) presence: $GSEEN source lines carry --no-textconv" \ + || no "(G) presence: only $GSEEN source lines carry --no-textconv — the scan cannot be reading src/" +[ -z "$GOFF" ] \ + && ok "(G) every executed git log/diff/show/blame spelling carries --no-textconv; every status --porcelain carries --ignore-submodules=dirty" \ + || no "(G) unhardened spelling(s): $( printf '%s' "$GOFF" | head -3 | tr '\n' ';' )" +mkdir -p "$TMP/gmut" && cp -R "$ROOT/src" "$TMP/gmut/src" +printf '\ninline std::string gitGateMutationD( const std::string& root ) { return gitOneLine( root, "log -1 --format=%%H" ); }\n' >> "$TMP/gmut/src/quality.h" +printf '\ninline std::string gitGateMutationE( const std::string& root ) { return gitOneLine( root, "status --porcelain" ); }\n' >> "$TMP/gmut/src/quality.h" +GMUT="$( structural "$TMP/gmut" )" +for gm in gitGateMutationD gitGateMutationE; do + printf '%s\n' "$GMUT" | grep -q "$gm" \ + && ok "(G) mutation control: the same scan finds $gm" \ + || no "(G) mutation control: the scan did not find $gm — the arm cannot fail there" +done + +[ "$fail" -eq 0 ] && echo "gitchildoptscheck: PASS" || echo "gitchildoptscheck: FAIL" +exit "$fail" diff --git a/test/regression.sh b/test/regression.sh index b5cc0fd7b..2d9a61f57 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From badc8fbf7e7891991c8fb910edf085274b568617 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 07:33:55 -0400 Subject: [PATCH 099/162] docs(limits): regenerate LIMITS.md and TUNING.md for the two caps; README cap count 254 -> 256 kDeclinedSiteCap (16 rows, --limit raises it) and kDeclinedSiteCandCap (8 cand= entries on --legend=full) are new compile-time caps. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/LIMITS.md | 19 ++++++++++++++----- docs/TUNING.md | 22 +++++++++++----------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5eef8409a..8c354032a 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 254 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 256 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 241531b58..870d314fc 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 254 | 99 | 149 | **105** | +| 256 | 100 | 149 | **107** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 254 + 7 is the 261 constants this generator parses out of `src/`. +are not counted as caps, and 256 + 7 is the 263 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 254 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 115 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 256 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 117 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 99 files that declare a cap — the 254 caps counted above, and no parameter. +One table for each of the 100 files that declare a cap — the 256 caps counted above, and no parameter. ### `src/abicheck.h` @@ -204,6 +204,15 @@ Discloses: `capped`, `reads_capped` | `kMaxEnvNameLen` | `128` | BOUNDARY | longest plausible environment-variable name | | `kMaxSitesShown` | `8` | OUTPUT | sites per gate; a DEFAULT, raisable by --limit=N (effectiveRowCap), lifted by --detail | +### `src/declinedsites.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kDeclinedSiteCandCap` | `8` | — | cand= entries per row on --legend=full; cand_more= counts the rest | +| `kDeclinedSiteCap` | `16` | — | rows per answer by default; --limit=N raises it (dc_next= does) | + ### `src/defattrib.h` Discloses: **none** diff --git a/docs/TUNING.md b/docs/TUNING.md index 85bec1644..ae0a61ff1 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 162 | 161 | 112 | 12 | **37** | 75 | +| 164 | 163 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **162 cap -declarations** under **161 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **164 cap +declarations** under **163 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 161 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **38 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 163 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **40 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, -`kChurnMergeBombMaxFiles`, `kDefTableCap`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, +32 -more). 111 + 12 + 38 = 161, and `emit` refuses to render a partition that does not add up. The -`tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 162" would be wrong in both halves -at once, which is the shape of error a generated table exists to prevent. +`kChurnMergeBombMaxFiles`, `kDeclinedSiteCandCap`, `kDeclinedSiteCap`, `kDefTableCap`, +34 more). +111 + 12 + 40 = 163, and `emit` refuses to render a partition that does not add up. The `tunable` +column above reads 112 rather than 111 because one name the sweep classified is no longer declared +in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 164" would be wrong in both halves at once, +which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 162-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 164-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. From 260517e93142f13a91b9eb9be38b9ca479bec5c8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 08:18:45 -0400 Subject: [PATCH 100/162] test(declinedsite): shard weight 25 -> 35 s (113 arms, measured 30.1 s); selfcheck ALLOW for declinedCallsMadeBy The site-record postconditions compare against declinedCallsMadeBy, a read-only sum over the sources' declinedOut counts; selfcheckcheck (C) lists it on ALLOW with that reason. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 2 +- test/selfcheckcheck.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index b03cdf941..873706b7b 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -112,7 +112,7 @@ "deadprecisioncheck.sh": 0.8, "deckcheck.sh": 54.8, "deckclaimcheck.sh": 0.1, -"declinedsitecheck.sh": 25.0, +"declinedsitecheck.sh": 35.0, "deeptailcheck.sh": 1.0, "defaultceilingcheck.sh": 20.1, "defectshapecheck.sh": 14.2, diff --git a/test/selfcheckcheck.sh b/test/selfcheckcheck.sh index a6b1e3f75..a90ef4ac5 100755 --- a/test/selfcheckcheck.sh +++ b/test/selfcheckcheck.sh @@ -152,6 +152,7 @@ ALLOW = { "isDefinitionNotDeclaration": "read-only span comparison (model.h); no state — the predicate std::all_of walks above", "isFunctionLikeKind": "read-only enum comparison (graph.h, a local lambda); no state — the F2 body-test scope guard", "any": "read-only member predicate (pathgaps.h PathSearchGaps::any: calls > 0); no state — the gap-cone postcondition", + "declinedCallsMadeBy": "read-only sum of the sources' Graph::declinedOut counts (graph.h); no state — the site-record postconditions (lane declined-sites)", "inCheckout": "read-only member predicate (crossref.h WhereHit::inCheckout: fromWorktree || ref == \"HEAD\"); no state — labelJobRows' precondition that a parse job holds no checkout row", } # (T) ASSUMED-THEN-TESTED allowlist: "site -> reason", one line each. A site lands here only when the equality From fd7a7c770aa629e1526aac824582f3c3c952730b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 08:28:15 -0400 Subject: [PATCH 101/162] refactor(declined): one call yields declined_calls= and its records; quality-delta rows quality-delta 22728af8..HEAD gated six rows on the lane's own code: the 4-argument legend helper read as a contract change of declinedCallsLegendWithGates (now declinedCallsLegendForRows), two clone rows (the cut's JSON keys now spell dc_next through nextFieldJson; gate= names are a table indexed by DeclinedSiteKind, not a switch), and callHierarchyRows' verbosity (+3 lines): the count and the ordered records come from declinedCallsAndSites, which --safe-delete now shares. Co-Authored-By: Claude Opus 5.5 --- src/callhierarchy.h | 6 ++---- src/declinedsites.h | 29 ++++++++++++++++++----------- src/graphlegend.h | 2 +- src/verbs_navigate.h | 8 +++----- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/callhierarchy.h b/src/callhierarchy.h index f2d821c05..d6df2be7f 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -34,6 +34,7 @@ #include #include #include +#include // std::tie — declined_calls= and its records from one call #include #include @@ -298,10 +299,7 @@ inline CallHierarchyRows callHierarchyRows( const IngestResult& ing, const Graph } } - out.declinedCalls = wantCallers ? declinedCallsNaming( g, out.matches ) : declinedCallsMadeBy( g, out.matches ); - out.declinedSites = wantCallers ? declinedSitesNaming( g, out.matches ) : declinedSitesMadeBy( g, out.matches ); - orderDeclinedSites( ing, g, out.declinedSites, /*fileFirst=*/wantCallers ); - ENSURES( out.declinedSites.size() == out.declinedCalls, "a row per declined call the answer counts" ); + std::tie( out.declinedCalls, out.declinedSites ) = declinedCallsAndSites( ing, g, out.matches, wantCallers ); // the count and its rows out.declinedIface = wantCallers ? declinedIfaceCallsNaming( ing, g, out.matches ) : 0; out.stdCalls = wantCallers ? StdMemberCallsMade{} : stdMemberCallsMadeBy( g, out.matches ); diff --git a/src/declinedsites.h b/src/declinedsites.h index e437b53c5..faedbab6c 100644 --- a/src/declinedsites.h +++ b/src/declinedsites.h @@ -52,18 +52,13 @@ inline constexpr std::size_t kDeclinedSiteCandCap = 8; // cand= entries per r // gate= on a row: which name gate (or Ruby's side cut) declined the call; nothing for the ladder's own tier-3 decline and // for Ruby's `new` (its candidates are `initialize`, the row's n= is the called `new`). +// Indexed by DeclinedSiteKind, in its declaration order (Ladder, Builtin, Std, RubySide, RubyNew, Iface). +inline constexpr const char* kDeclinedSiteGateNames[] = { nullptr, "builtin", "std", "side", nullptr, nullptr }; +static_assert( std::size( kDeclinedSiteGateNames ) == std::size_t( DeclinedSiteKind::Iface ) + 1, "one gate= spelling per DeclinedSiteKind" ); inline const char* declinedSiteGate( DeclinedSiteKind kind ) noexcept { - switch( kind ) - { - case DeclinedSiteKind::Builtin: return "builtin"; - case DeclinedSiteKind::Std: return "std"; - case DeclinedSiteKind::RubySide: return "side"; - case DeclinedSiteKind::Ladder: - case DeclinedSiteKind::RubyNew: - case DeclinedSiteKind::Iface: return nullptr; - } - return nullptr; + const auto k = std::size_t( kind ); + return k < std::size( kDeclinedSiteGateNames ) ? kDeclinedSiteGateNames[ k ] : nullptr; } // The served order, in place. Every form first sorts by the call's file (path order), line, then record (resolve order: @@ -108,6 +103,18 @@ inline void orderDeclinedSites( const IngestResult& ing, const Graph& g, std::ve sites.swap( interleaved ); } +// One 1-hop answer's declined_calls= and the records behind it, in served order: callers (declinedCallsNaming) the declined +// calls whose lists hold a match, file-first; callees (declinedCallsMadeBy) the declines the matches made, file and line. +inline std::pair> declinedCallsAndSites( const IngestResult& ing, const Graph& g, std::span matches, + bool wantCallers ) +{ + const std::size_t calls = wantCallers ? declinedCallsNaming( g, matches ) : declinedCallsMadeBy( g, matches ); + std::vector sites = wantCallers ? declinedSitesNaming( g, matches ) : declinedSitesMadeBy( g, matches ); + orderDeclinedSites( ing, g, sites, /*fileFirst=*/wantCallers ); + ENSURES( sites.size() == calls, "a row per declined call the answer counts" ); + return { calls, std::move( sites ) }; +} + // One row's facts, surface-neutral: every dialect below spells these and nothing else. struct DeclinedSiteView { @@ -343,7 +350,7 @@ inline std::string declinedSiteCutKeyJson( const DeclinedSiteCut& cut ) { return {}; } - return ",\"dc_shown\":" + std::to_string( cut.shown ) + ",\"dc_next\":\"" + jsonStr( cut.next ) + "\""; + return ",\"dc_shown\":" + std::to_string( cut.shown ) + nextFieldJson( cut.next, "dc_next" ); } // What the legend must define, read off the rows an answer prints (graphlegend.h declinedSitesLegend). diff --git a/src/graphlegend.h b/src/graphlegend.h index 3acf2f7f5..04bcff77f 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -651,7 +651,7 @@ inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, boo // twin says so instead (gates review R9) and is the one such an answer carries; everything else in it is the clause above. inline constexpr const char* kDeclinedCallsRowsLegend = "declined_calls=K (absent when 0) counts call SITES the resolver declined to bind: the called name has two or more same-language definitions, none in the caller's file or directory, and no qualifier, receiver type or include chose one, so no edge exists and no count here includes them; the rows below name them (the map header's declined=). Callers form: declined calls that could have meant this selector's definitions; impact form: that could have reached SYM or a symbol in its radius; callees form: declined calls these definitions make. Each call counts once however many candidates it had; the uses verb on the called name lists the sites. "; -inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, bool stdDeclined, bool dcRows ) +inline std::string declinedCallsLegendForRows( bool on, bool gateDeclined, bool stdDeclined, bool dcRows ) { if( !dcRows ) { diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index f9b3c8cb8..8e4c2affa 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -263,7 +263,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), - rw::declinedCallsLegendWithGates( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !chDc.empty() ) // exactly when the root carries declined_calls= + rw::declinedCallsLegendForRows( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !chDc.empty() ) // exactly when the root carries declined_calls= + rw::declinedSitesLegend( rw::declinedSitesLegendFlags( chDc, wantCallers, chDcCut ) ), // and the rows, exactly when one rides std::string( rw::declinedIfaceLegend( chRows.declinedIface > 0 ) ) // likewise, exactly when declined_iface= is there + rw::stdMemberCallsLegend( chRows.stdCalls.calls > 0 && !cfg.columnar ), // and when the line is (XML only) @@ -1025,7 +1025,7 @@ inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs // Since lane declined-sites a definition some declined call could have meant never reads none-found: it reads // possible-callers, whose own sentence says it is not a safety reading, and its rows name the calls. ASSUME( declinedCalls == 0 || risk != "none-found", "runSafeDelete reads possible-callers (or unmodelled) wherever declined_calls= rides" ); - std::string declinedClause = rw::declinedCallsLegendWithGates( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined, flags.declinedSites.any ) + std::string declinedClause = rw::declinedCallsLegendForRows( declinedCalls > 0, flags.gateDeclined, flags.stdDeclined, flags.declinedSites.any ) + rw::declinedSitesLegend( flags.declinedSites ); declinedClause += rw::valueRefsSafeDeleteLegend( flags.valueRefs > 0 ); // exactly when the root carries value_refs= declinedClause += rw::valueRefsDepthLegend( flags.valueRefDepthCut ); // exactly when it carries the depth disclosure @@ -1249,9 +1249,7 @@ std::optional runSafeDelete( const MainDispatch& d ) const bool anyEvidence = !callerIds.empty() || !sites.empty() || !sdValueRefs.rows.empty(); // a decorator row is not a use site, still evidence // lane declined-sites: declined calls whose candidate lists hold a definition (as --callers counts and lists them) — the // rows, file-first, cut at the callers verb's own cap (one constant, gates review Q6). - const std::size_t sdDeclinedCalls = declinedCallsNaming( g, defs ); - std::vector sdDeclinedSites = declinedSitesNaming( g, defs ); - orderDeclinedSites( ing, g, sdDeclinedSites, /*fileFirst=*/true ); + const auto [ sdDeclinedCalls, sdDeclinedSites ] = rw::declinedCallsAndSites( ing, g, defs, /*wantCallers=*/true ); const char* risk = ( !anyEvidence && usesFloor ) ? "unmodelled" : !anyEvidence && sdDeclinedCalls > 0 ? "possible-callers" // gates review Q3: never none-found beside a declined possible caller : !anyEvidence ? "none-found" From b3d21e6403091be10e815dbcc50f550b349df66a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 08:32:37 -0400 Subject: [PATCH 102/162] refactor(declined): the cut's JSON keys through countFieldOrEmpty + nextFieldJson (quality-delta clone row) Co-Authored-By: Claude Opus 5.5 --- src/declinedsites.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/declinedsites.h b/src/declinedsites.h index faedbab6c..ad1277fcd 100644 --- a/src/declinedsites.h +++ b/src/declinedsites.h @@ -344,13 +344,10 @@ inline std::string declinedSiteCutAttrXml( const DeclinedSiteCut& cut ) return " dc_shown=\"" + std::to_string( cut.shown ) + "\" dc_next=\"" + std::string( escapeXml( cut.next, esc ) ) + "\""; } // The JSON twin of the two root attributes, the same names on the CLI --json and the MCP twins (the attribute-parity rule). +// A cut prints at least one row (the cap and --limit are >= 1), so dc_shown is never the 0 countFieldOrEmpty leaves out. inline std::string declinedSiteCutKeyJson( const DeclinedSiteCut& cut ) { - if( cut.next.empty() ) - { - return {}; - } - return ",\"dc_shown\":" + std::to_string( cut.shown ) + nextFieldJson( cut.next, "dc_next" ); + return countFieldOrEmpty( "dc_shown", cut.next.empty() ? 0 : cut.shown, /*json=*/true ) + nextFieldJson( cut.next, "dc_next" ); } // What the legend must define, read off the rows an answer prints (graphlegend.h declinedSitesLegend). From 2b84440c4d8ce680a3480c090cc8e47adca0dae9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 09:33:10 -0400 Subject: [PATCH 103/162] style(declined): clang-format 22 on DeclinedSitesLegendFlags (graphlegend.h is a formatcheck-gated file) Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 04bcff77f..5758fa906 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -686,13 +686,13 @@ inline const char* declinedSitesImpactPointer( bool on ) noexcept { return on ? // What one answer's rows carry, read off the rows it prints (never re-derived). struct DeclinedSitesLegendFlags { - bool any = false; // a row is printed - bool callersForm = false; // in= p= rows (callers, safe-delete) rather than n= l= (callees) - bool side = false; // a row says gate="side" - bool nameGate = false; // a row says gate="builtin" or gate="std" - bool ofDef = false; // a row carries of_def= - bool cut = false; // the root carries dc_shown= / dc_next= - bool cand = false; // a row carries cand= (legend full) + bool any = false; // a row is printed + bool callersForm = false; // in= p= rows (callers, safe-delete) rather than n= l= (callees) + bool side = false; // a row says gate="side" + bool nameGate = false; // a row says gate="builtin" or gate="std" + bool ofDef = false; // a row carries of_def= + bool cut = false; // the root carries dc_shown= / dc_next= + bool cand = false; // a row carries cand= (legend full) }; inline std::string declinedSitesLegend( const DeclinedSitesLegendFlags& f ) { From 974dd5b11323f10257a87c3c270668c0345a61a9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 09:39:57 -0400 Subject: [PATCH 104/162] fix(for): the refill charges the tail at its reserve, so rows stay monotone in N CI round 2 (fresh clone, fornotesbudgetcheck RP @1490 xml): the tail's fill steps a whole row at a time, so charging its filled bytes let the rows past the cut fall as N grew. The refill (CLI XML and JSON) now charges the tail at min(its share, its whole render). Swept 700..3000 step 20 x {full, compact, json} x 3 tasks: 0 non-monotone steps. The 5 B5 argv keep the same rows and sections. Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index 31659d0ff..c92a7bce5 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1347,7 +1347,9 @@ inline void forLensJsonSoftRefill( const PackSigs& packSigs, const ForLensJsonIn const std::size_t cutRows = st.json.size() - std::min( st.json.size(), st.cut.pastTargetBytes ); const rw::FileTail cutTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, rw::rowsAtTargetIds( st.shown, in.rank, st.cut.pastTarget ), in.rootArg, in.task ); - const std::size_t tailBytes = forLensJsonTailStanza( cutTail, in.tokenBudget, rw::forSoftSectionsShare( b.bundleBytes, cutRows ) ).size(); + // the tail at its RESERVE (its share, or its whole stanza when smaller), not its filled bytes: rows stay monotone in N + const std::size_t tailBytes = std::min( rw::forSoftSectionsShare( b.bundleBytes, cutRows ), + forLensJsonTailStanza( cutTail, 0, 0 ).size() ); const std::size_t rowDrop = rw::forSoftRowDropFill( b.bundleBytes, tailBytes ); if( rowDrop >= b.firstRowDrop ) { @@ -3839,8 +3841,14 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // replaced, never added to; a re-render whose buffer fails keeps the first render and its readings whole. if( forSoftTarget && sigsPreRendered && forSigsCut.pastTarget > 0 ) { + // the tail is charged at its RESERVE — its share, or its whole render when that is smaller — not at its filled bytes: a + // fill that steps a whole row at a time would otherwise make the rows past the cut fall as N grows (rows monotone in N) + std::vector tailEsc; + const std::size_t tailReserve = std::min( std::max( bundleBudget > tailSpentBytes ? bundleBudget - tailSpentBytes : 0u, + rw::kForFileTailShellReserve ), + rw::renderFileTailXml( forFileTailShown, forFileTailShown.paths.size(), tailEsc ).size() ); const std::size_t sectionsServed = forHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() - + autoSection.xml.size() + tailStr.size(); // not the --detail bodies (above) + + autoSection.xml.size() + tailReserve; // not the --detail bodies (above) const std::size_t rowDropFill = rw::forSoftRowDropFill( bundleBudget, sectionsServed ); if( rowDropFill < sigsBudgets.rowDrop ) { From 6844905e45e03e350e223d1a50b17e78fd572d75 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 09:40:51 -0400 Subject: [PATCH 105/162] harden git subprocesses: probe request-named roots at dispatch ripwire neutralises repository-configured filter and diff programs for its own git children. This completes that for the servers and the cheap pre-scan: - the MCP dispatch now probes every root a request names (including each member root of a multi-root `paths` workspace) once, before the verb runs, so every verb is covered, not only those that build an index; getIndex() keeps the probe as a backstop for the LSP server; - a local config file that exists but cannot be read as a plain file (for example a symlink) always takes the probe instead of the pre-scan shortcut; - the per-root probe holds its lock until the pins are registered, so a concurrent caller for the same root waits for them; - --doctor's git-config-trust row reports filter_drivers=, filter_scope= and filter_pinned=; CHANGELOG and SECURITY.md name the user-visible effects. test/gitchildoptscheck.sh: arm (E) drives quality_delta, stray_content, edit_check, quality_baseline, batch, a two-root `paths` request and a server with a different startup root; new arm (S) covers a symlinked config; (D) checks the doctor row. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ SECURITY.md | 2 +- docs/COMMANDS.md | 2 +- src/cli.h | 10 +++- src/compactlegend.h | 1 + src/gitcmd.h | 14 ++--- src/githarden.h | 100 +++++++++++++++++++++++----------- src/mcp.h | 7 +++ src/mcpindex.h | 22 +++++++- src/verbs_doctor.h | 14 ++++- test/gitchildoptscheck.sh | 112 ++++++++++++++++++++++++++++++-------- 11 files changed, 220 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a9b5e3f..817f1bbc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — ripwire neutralises repository-configured filter and diff programs for its own git children + +Every git command ripwire runs now pins the filter drivers the repository's own config defines to an empty command, carries `--no-ext-diff --no-textconv` on `diff`/`log`/`show`/`blame` and `--ignore-submodules=dirty` on `status`/`diff`, and keeps signature display off; stderr discloses the pinning as `git_harden=filter-driver`, and `--doctor`'s `git-config-trust` row reports it (`filter_drivers=`, `filter_scope=`, `filter_pinned=`). Visible effects: files behind a repository-local filter (git-crypt, a per-repository LFS driver) are seen unfiltered, as stored; edits inside a submodule no longer mark the stamp `+dirty`; a user's own textconv no longer shapes `--numstat` (such binary rows read `-`); and a pinned driver name applies to every root in the same run. Gate: `test/gitchildoptscheck.sh`. + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. diff --git a/SECURITY.md b/SECURITY.md index 32118b881..2b1ec2623 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,7 +18,7 @@ ripwire is a command-line indexing tool with the following security model: - **Input:** Arbitrary source code repositories on the local filesystem - **Output:** XML summaries and analysis results streamed to stdout or written to cache - **Trust boundary:** The tool operates only on local files with the user's own permissions. It does not communicate over the network, with two named exceptions: a root given as a git URL is cloned into the cache (`git clone --depth=1`, with `protocol.ext.allow=never` and `protocol.file.allow=user` pinned on the command), and the tree-sitter registry is listed during initial setup -- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. The repository's own `filter..clean/smudge/process` drivers are pinned to an empty command on every git child ripwire starts (a stderr line carrying `git_harden=filter-driver`), its diff programs are refused with `--no-ext-diff --no-textconv`, signature display is pinned off, and `status`/`diff` do not enter submodule repositories (`--ignore-submodules=dirty`). `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself +- **Subprocesses:** ripwire runs **read-only `git`** inside the analysed checkout (`status --porcelain`, `ls-files`, `log`, `diff --numstat`, `archive`, `rev-parse`), and — only for binary document formats, which are not collected by default — `markitdown`. Git honours the checkout's **own** `.git/config`. A *hook-form* `core.fsmonitor` there is a command git would run on every one of those calls, so ripwire neutralises that one key for its own git children at process start and says so (`--doctor`'s `git-config-trust` row; a stderr line carrying `git_harden=fsmonitor-hook`); boolean values, git's builtin daemon, are left alone. ripwire likewise neutralises repository-configured filter and diff programs for its own git children: the repository's own `filter..clean/smudge/process` drivers are pinned to an empty command on every git child ripwire starts (a stderr line carrying `git_harden=filter-driver`, and `--doctor`'s `git-config-trust` row: `filter_drivers=`, `filter_scope=`, `filter_pinned=`), diff programs are refused with `--no-ext-diff --no-textconv`, signature display is pinned off, and `status`/`diff` do not enter submodule repositories (`--ignore-submodules=dirty`). The MCP and LSP servers apply the same pinning to every root a request names. The user-visible cost: files behind a repository-local filter (git-crypt, a per-repository LFS driver) are read as stored, and edits inside a submodule no longer mark `+dirty`. `git clone` never copies `.git/config`, so a repository you cloned yourself carries no such key — a tarball, a copied worktree or a shared checkout carries whatever its last owner wrote. Treat pointing ripwire at one as you would treat running `git status` there yourself Security vulnerabilities relevant to this tool include: diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a5ab7628a..8b83c6b2f 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4247,7 +4247,7 @@ lego:Vehicle **Answers:** check the environment: stale binary, grammars, cache health, git, index identity environment self-check: binary-vs-PATH staleness, grammar tags.scm compile, cache-dir health, git reachability, tree-sitter version, INDEX IDENTITY, and TRACKED-BINARY staleness (a committed binary whose last commit is a git-history ANCESTOR of a same-directory/same-stem source's last commit — never mtime, which a fresh clone stamps at checkout time). -"Dependent source" is a NAMING heuristic (same dir, same filename stem, e.g. tool <-> tool.cpp) — ripwire parses no build system, so a binary built from a differently-named or differently-located source is silently out of scope, neither flagged nor cleared. Single-root only. DIAGNOSTIC, not deterministic (env-dependent by design); exit 0 iff all ok, else 1. Root reports ; each child row carries the BOOLEAN ok="0|1". passed= is the root's count (it was spelled ok= until the vocabulary pass, which collided with the child bool). A FAILING row (ok="0") also carries hint=, the derived verdict (which of self=/which= is stale and the fix, which grammar(s) failed to compile, why the cache dir isn't writable, ...) — a passing row never carries hint=. The index-cache row states the INDEX-VERSION CONTRACT — cache_version=, parser_ver_lean=, parser_ver_rich=, artifact_arch= — the four numbers that decide whether a committed --index-out artifact is reusable at all, and which appear in no other output. lean=/rich= then say whether THIS binary can open the artifact this root would consume (source="auto" its per-root blob, "cache-flag" the file you named, "disabled" under --no-cache), naming WHICH guard refused it: ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame. BOTH families are reported because a team that commits only the lean artifact gets no warm hit on --for/--exemplar/--metrics/--uses. This is a FORMAT verdict about an artifact, never a freshness verdict about an index: every invocation re-validates each file, so a non-ok lean= costs speed, never correctness — the run cold-parses instead. Only an artifact you NAMED with --cache= and this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss, not sickness). The root carries TWO shas, and they answer different questions: at= is the TREE's HEAD (+dirty) right now, built_from= is the commit THIS BINARY was compiled from (byte-identical to --version's own built_from=). They differ from the moment you commit until the next build — normal, so a mismatch is reported, never gated; a stale PATH copy shadowing a fresh build is the binary-path row's job and is decided on inode/mtime/size, not on this sha. The git-config-trust row reads the checkout's OWN core.fsmonitor as this process saw it at startup (fsmonitor="hook|builtin|off|unset"): a HOOK-form value is a command git would run on every read-only call ripwire makes, so the process appends core.fsmonitor=false to git's GIT_CONFIG_COUNT override for its own git children (neutralised="1", and a stderr line carrying git_harden=fsmonitor-hook); the boolean forms need no override (neutralised="0"). Independently of this row, every git command ripwire runs carries git's no-optional-locks and core.fsmonitor=false so no monitor of either form runs for its read-only calls. Not a verdict on your setup — ok="1" always; the neutralisation is the verdict. +"Dependent source" is a NAMING heuristic (same dir, same filename stem, e.g. tool <-> tool.cpp) — ripwire parses no build system, so a binary built from a differently-named or differently-located source is silently out of scope, neither flagged nor cleared. Single-root only. DIAGNOSTIC, not deterministic (env-dependent by design); exit 0 iff all ok, else 1. Root reports ; each child row carries the BOOLEAN ok="0|1". passed= is the root's count (it was spelled ok= until the vocabulary pass, which collided with the child bool). A FAILING row (ok="0") also carries hint=, the derived verdict (which of self=/which= is stale and the fix, which grammar(s) failed to compile, why the cache dir isn't writable, ...) — a passing row never carries hint=. The index-cache row states the INDEX-VERSION CONTRACT — cache_version=, parser_ver_lean=, parser_ver_rich=, artifact_arch= — the four numbers that decide whether a committed --index-out artifact is reusable at all, and which appear in no other output. lean=/rich= then say whether THIS binary can open the artifact this root would consume (source="auto" its per-root blob, "cache-flag" the file you named, "disabled" under --no-cache), naming WHICH guard refused it: ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame. BOTH families are reported because a team that commits only the lean artifact gets no warm hit on --for/--exemplar/--metrics/--uses. This is a FORMAT verdict about an artifact, never a freshness verdict about an index: every invocation re-validates each file, so a non-ok lean= costs speed, never correctness — the run cold-parses instead. Only an artifact you NAMED with --cache= and this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss, not sickness). The root carries TWO shas, and they answer different questions: at= is the TREE's HEAD (+dirty) right now, built_from= is the commit THIS BINARY was compiled from (byte-identical to --version's own built_from=). They differ from the moment you commit until the next build — normal, so a mismatch is reported, never gated; a stale PATH copy shadowing a fresh build is the binary-path row's job and is decided on inode/mtime/size, not on this sha. The git-config-trust row reads the checkout's OWN core.fsmonitor as this process saw it at startup (fsmonitor="hook|builtin|off|unset"): a HOOK-form value is a command git would run on every read-only call ripwire makes, so the process appends core.fsmonitor=false to git's GIT_CONFIG_COUNT override for its own git children (neutralised="1", and a stderr line carrying git_harden=fsmonitor-hook); the boolean forms need no override (neutralised="0"). Independently of this row, every git command ripwire runs carries git's no-optional-locks and core.fsmonitor=false so no monitor of either form runs for its read-only calls. The same row counts the filter drivers the repository's own config defines (filter_drivers=); when there are any, filter_scope="local|all" (all: a git older than 2.26 cannot report scope, so your own drivers are pinned too) and filter_pinned="1" (every git child runs them with an empty command, so files are read as stored; stderr carries git_harden=filter-driver) or "refused" (a name git's -c cannot spell: git is not run at all). Not a verdict on your setup — ok="1" always; the neutralisation is the verdict. **Try it** diff --git a/src/cli.h b/src/cli.h index 8dd92e8ee..5fd63cd05 100644 --- a/src/cli.h +++ b/src/cli.h @@ -2526,8 +2526,14 @@ inline constexpr char kHelpTail[] = " children (neutralised=\"1\", and a stderr line carrying git_harden=fsmonitor-hook);\n" " the boolean forms need no override (neutralised=\"0\"). Independently of this row,\n" " every git command ripwire runs carries git's no-optional-locks and core.fsmonitor=false\n" - " so no monitor of either form runs for its read-only calls. Not a verdict on\n" - " your setup — ok=\"1\" always; the neutralisation is the verdict.\n" + " so no monitor of either form runs for its read-only calls. The same row counts\n" + " the filter drivers the repository's own config defines (filter_drivers=); when\n" + " there are any, filter_scope=\"local|all\" (all: a git older than 2.26 cannot report\n" + " scope, so your own drivers are pinned too) and filter_pinned=\"1\" (every git child\n" + " runs them with an empty command, so files are read as stored; stderr carries\n" + " git_harden=filter-driver) or \"refused\" (a name git's -c cannot spell: git is not\n" + " run at all). Not a verdict on your setup — ok=\"1\" always; the neutralisation\n" + " is the verdict.\n" " --agent=codex|claude (with --doctor) also inspect that agent's live integration with ripwire\n" " (with --doctor) also inspect that agent's LIVE CLI-first integration: PATH binary, exact\n" " installed-skill manifest parity, advisory hook executability, and the secondary\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index bfcb2316b..10a29c417 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1128,6 +1128,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "lean", "lean=/rich=: can THIS binary open that artifact (ok, absent, parser-version ...); format, never freshness", true, "c", MapHeaderRead::No, {}, "doctor" }, // also defines rich= { "fsmonitor", "fsmonitor=: the checkout's core.fsmonitor at startup: unset, builtin, off, or hook (a command git runs)", true, "c", MapHeaderRead::No, {}, "doctor" }, { "neutralised", "neutralised=1: a hook fsmonitor was overridden to false for this run; 0 when none was needed", true, "c", MapHeaderRead::No, {}, "doctor" }, + { "filter_drivers", "filter_drivers=N: filter drivers the repository's own config defines; filter_scope= local|all, filter_pinned= 1 (run with an empty command) or refused", true, "c", MapHeaderRead::No, {}, "doctor" }, // also defines filter_scope= filter_pinned= { "state", "state=: layout records agree, disagree (mixed binary: rebuild clean-first), not-checked or no-records", true, "c", MapHeaderRead::No, {}, "doctor" }, { "checked", "checked=1: the cross-unit layout comparison ran; 0 = under two comparable records", true, "c", MapHeaderRead::No, {}, "doctor" }, { "units", "units=N: translation units that registered a layout record", true, "c", MapHeaderRead::No, {}, "doctor" }, diff --git a/src/gitcmd.h b/src/gitcmd.h index e3ff10652..a2c699176 100644 --- a/src/gitcmd.h +++ b/src/gitcmd.h @@ -15,14 +15,14 @@ // otherwise apply, and git passes it on to any git it spawns. The builtin daemon // is not consulted by ripwire's own few calls either — their cost is a stat pass // of the same order as the crawl ripwire already makes. -// -c log.showSignature=false a repository's config can turn signature display on for every `git log`, and -// signature display runs the configured gpg.program. ripwire's log formats never -// ask for a signature (and its parsers would misread the extra lines), so the pin -// changes no answer. Version-safe: a git that predates the key ignores it. +// -c log.showSignature=false signature display stays off whatever the repository's config says, so `git log` +// starts no signature program. ripwire's log formats never ask for a signature (and +// its parsers would misread the extra lines), so the pin changes no answer. +// Version-safe: a git that predates the key ignores it. // -// FILTER DRIVERS. A repository's config may name `filter..clean/smudge/process` programs, and git runs -// them whenever it re-hashes a working-tree file (`status`, a diff against the working tree) or exports one -// (`archive`). The driver NAME is chosen by the repository, so there is no single key to pin. githarden.h asks +// FILTER DRIVERS. ripwire neutralises repository-configured filter and diff programs for its own git children. +// A repository's config may name `filter..clean/smudge/process` programs for git to use while it reads +// working-tree files. The driver NAME is chosen by the repository, so there is no single key to pin. githarden.h asks // git, once per root, which filter drivers the repository-local config (local + worktree scope, includes // followed) defines and registers them here; every later gitCmd() then carries // -c 'filter..clean=' -c 'filter..smudge=' -c 'filter..process=' -c 'filter..required=false' diff --git a/src/githarden.h b/src/githarden.h index 6da68dad0..982dce3d4 100644 --- a/src/githarden.h +++ b/src/githarden.h @@ -30,15 +30,15 @@ // The trust decision is PER PROCESS: a long-lived --mcp server probes its roots once at start. A config edited // under a running server is not re-read — the same freshness contract every other startup-only fact has here. // -// FILTER DRIVERS (2026-10-15). The same trust class reaches git through `filter..clean/smudge/process`: a -// repository-local config can name a program that git runs whenever it re-hashes a stale working-tree file (`git -// status`, a diff against the working tree) or exports one (`git archive`), and the driver name is the repository's -// choice, so no fixed -c key covers it. hardenFilterDriversFor() asks git which drivers the local/worktree scopes -// define (cheap-tier gated on the same candidate files: "filter" or "include") and pins each to an empty command on -// every later git child (gitcmd.h FILTER DRIVERS), disclosing git_harden=filter-driver. A server that meets a root -// only in a request (MCP/LSP with no startup root) probes it on first sighting (hardenLateRoot, from getIndex). -// Diff programs (diff.external, diff..command/textconv) are refused at the call site instead, with -// --no-ext-diff --no-textconv; submodule repositories are not entered (--ignore-submodules=dirty). +// FILTER DRIVERS (2026-10-15). ripwire also neutralises repository-configured filter and diff programs for its own +// git children. `filter..clean/smudge/process` drivers are named by the repository, so no fixed -c key covers +// them: hardenFilterDriversFor() asks git which drivers the local/worktree scopes define (cheap-tier gated on the +// same candidate files: "filter" or "include"; a candidate that exists but cannot be read as a plain file is always +// probed) and pins each to an empty command on every later git child (gitcmd.h FILTER DRIVERS), disclosing +// git_harden=filter-driver. A server that meets a root only in a request (MCP/LSP) probes it before that request is +// dispatched (hardenLateRoot, once per root, from the MCP dispatch and from getIndex). Diff programs +// (diff.external, diff..command/textconv) are refused at the call site instead, with --no-ext-diff +// --no-textconv; submodule repositories are not entered (--ignore-submodules=dirty). // // NOT covered, and measured NOT to fire here: a `post-index-change` hook under `git status --porcelain` (git 2.50.1, // the same fixture). Hooks are the same trust class as config, but an arm that cannot be observed to fire is not @@ -288,6 +288,11 @@ inline std::vector localConfigCandidates( const std::file // One read of the local config candidates, two questions: could a file carry core.fsmonitor, and could one carry a // filter driver? "include" answers yes to both — an include can bring in either key. Same soundness argument as above: // a key git honours must be spelled in one of these files or reached through an include they name. +// +// The scan reads each candidate without following a final symlink (pathguard), but git DOES follow one when it reads +// its config. So a candidate that exists yet cannot be read here as a plain file — a symlink, a non-regular file, an +// unreadable one — answers "could carry either key": the cheap tier may only skip the probe for bytes it has actually +// read, and git's own probe then decides. struct LocalConfigNeedles { bool fsmonitor = false; @@ -302,6 +307,12 @@ inline LocalConfigNeedles localConfigNeedles( const std::string& root ) std::optional bytes = pathguard::readRegularFileNoFollow( candidate.string() ); if( !bytes ) { + std::error_code ec; + if( std::filesystem::exists( std::filesystem::symlink_status( candidate, ec ) ) ) + { + found.fsmonitor = true; // present but not read: the probe decides, never the shortcut + found.filter = true; + } continue; } for( char& c : *bytes ) @@ -403,27 +414,38 @@ inline FilterDriverProbe probeLocalFilterDrivers( const std::string& root ) return probe; } +// What one root's filter-driver probe found and did — kept for --doctor's git-config-trust row (the startup roots). +struct FilterDriverOutcome +{ + std::size_t drivers = 0; // distinct driver names the probe found (0: none, or the cheap tier saw no candidate) + bool allScopes = false; // the pre-2.26 fallback: every scope was pinned + bool refused = false; // a name -c cannot spell: every later git child is refused instead +}; + // Probe one root's filter drivers, pin them (gitcmd.h), and disclose on stderr. The names are the repository's // text and are never printed — only their count. Safe to call from a server thread: the pins are mutex-guarded. -inline void hardenFilterDriversFor( const std::string& root ) +inline FilterDriverOutcome hardenFilterDriversFor( const std::string& root ) { const FilterDriverProbe probe = probeLocalFilterDrivers( root ); + FilterDriverOutcome outcome{ probe.names.size(), probe.allScopes, false }; if( probe.names.empty() ) { - return; + return outcome; } if( pinGitFilterDrivers( probe.names ).unpinnable ) { + outcome.refused = true; emitTo( stderr, "ripwire: git: {} configures a filter driver whose name git's -c option cannot spell — every git call this run is refused instead (git_harden=filter-driver-refused): history, churn and the dirty stamp are unavailable\n", root ); - return; + return outcome; } emitTo( stderr, "ripwire: git: {} configures {} filter driver(s) in {} — neutralised for this run (git_harden=filter-driver): every git call ripwire makes runs them with an empty clean/smudge/process command, so no configured filter program starts\n", root, probe.names.size(), probe.allScopes ? "its config (scope=all: this git cannot report scope, so the user's own drivers are neutralised too)" : "the repository's own config" ); + return outcome; } -// Roots a server meets only in a REQUEST (an MCP or LSP server started with no root): the startup pass never saw -// them, so the first request that names one probes it here. One probe per distinct spelling; the startup roots -// are recorded too, so they are not probed twice. +// Roots a server meets only in a REQUEST (an MCP or LSP server, with or without a startup root): the startup pass +// never saw them, so the request that names one probes it here before it is dispatched. One probe per distinct +// spelling; the startup roots are recorded too, so they are not probed twice. namespace detail { struct ProbedRoots @@ -440,7 +462,7 @@ inline bool firstSighting( const std::string& root ) { ProbedRoots& probed = probedRoots(); const std::lock_guard guard( probed.lock ); - if( std::find( probed.roots.begin(), probed.roots.end(), root ) != probed.roots.end() ) + if( std::ranges::contains( probed.roots, root ) ) { return false; } @@ -449,9 +471,19 @@ inline bool firstSighting( const std::string& root ) } } // namespace detail +// The lock is held ACROSS the probe, and the root is recorded only once its pins are registered: a second caller for +// the same root waits until the pins exist instead of returning early and starting git before them, and a probe +// interrupted by an exception leaves the root unrecorded, so the next caller probes it again. A root that is not a +// directory (yet) is not recorded either. Callers for different roots serialise too; a probe is two short git reads. inline void hardenLateRoot( const std::string& root ) { - if( root.empty() || !detail::firstSighting( root ) ) + if( root.empty() ) + { + return; + } + detail::ProbedRoots& probed = detail::probedRoots(); + const std::lock_guard guard( probed.lock ); + if( std::ranges::contains( probed.roots, root ) ) { return; } @@ -464,6 +496,7 @@ inline void hardenLateRoot( const std::string& root ) { hardenFilterDriversFor( root ); } + probed.roots.push_back( root ); } // ── the environment override block (git's own mechanism, git-config(1) ENVIRONMENT) ─────────────────────── @@ -504,22 +537,29 @@ inline bool appendGitConfigOverride( const char* key, const char* value ) // per root, and doctor reads it. One write, at startup, before any thread. struct Report { - std::vector> forms; // every directory root probed, in argv order - bool applied = false; + std::vector> forms; // every directory root probed, in argv order + std::vector> filters; // the same roots' filter-driver probes + bool applied = false; }; inline Report g_startup; +// One root's startup record from either list; the value-initialised record (Unset / no drivers) for a root not probed. +template +Record startupRecordFor( const std::vector>& records, std::string_view root ) noexcept +{ + const auto it = std::ranges::find( records, root, &std::pair::first ); + return it == records.end() ? Record{} : it->second; +} + inline FsmonitorForm startupFormFor( std::string_view root ) noexcept { - for( const auto& [ r, form ] : g_startup.forms ) - { - if( r == root ) - { - return form; - } - } - return FsmonitorForm::Unset; + return startupRecordFor( g_startup.forms, root ); +} + +inline FilterDriverOutcome startupFiltersFor( std::string_view root ) noexcept +{ + return startupRecordFor( g_startup.filters, root ); } // Call ONCE from main(), after parseArgs and before any thread or git subprocess. A root that is not a directory @@ -528,6 +568,7 @@ inline const Report& hardenForRoots( std::span roots ) { Report& r = g_startup; r.forms.clear(); + r.filters.clear(); r.applied = false; bool anyHook = false; for( std::string_view root : roots ) @@ -543,10 +584,7 @@ inline const Report& hardenForRoots( std::span roots ) r.forms.emplace_back( rootStr, form ); anyHook = anyHook || form == FsmonitorForm::Hook; detail::firstSighting( rootStr ); - if( needles.filter ) - { - hardenFilterDriversFor( rootStr ); - } + r.filters.emplace_back( rootStr, needles.filter ? hardenFilterDriversFor( rootStr ) : FilterDriverOutcome{} ); } if( !anyHook ) { diff --git a/src/mcp.h b/src/mcp.h index 70f88fc62..778f7d061 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -2103,6 +2103,13 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo pathsUsageError = true; // the skip-flag: no dispatch, no getIndex() } } + // The git-config trust boundary for the root(s) this request names (githarden.h, hardenRequestRoot): + // probed HERE, once per root, after every refusal above and before the dispatch chain, so each verb + // is covered whether or not it reaches getIndex() — and every member root of a `paths` workspace. + if( !pathsUsageError && !path.empty() ) + { + hardenRequestRoot( path ); + } if( !pathsUsageError && isMcpEditVerb( name ) ) { if( std::string missingEditArg = missingArgMsg(); !missingEditArg.empty() ) diff --git a/src/mcpindex.h b/src/mcpindex.h index 7325cfa7e..90c0dbf07 100644 --- a/src/mcpindex.h +++ b/src/mcpindex.h @@ -19,7 +19,7 @@ #include "serialize.h" #include "search.h" #include "gitmine.h" -#include "githarden.h" // githarden::hardenLateRoot — a request-named root's filter drivers are pinned before its first git child +#include "githarden.h" // githarden::hardenLateRoot — a request-named root's filter drivers are pinned before its first git child (hardenRequestRoot) #include "lexical.h" #include "recall.h" #include "valuerefs.h" // reference-as-value round: the per-index ValueRefIndex cache (valueRefIndexOf) @@ -957,6 +957,24 @@ inline std::string mcpWorkspaceKey( const std::vector& rootArgs, st return key; } +// The git-config trust boundary for a root a REQUEST names (githarden.h): probe it, once, before anything runs git +// there. A registered multi-root workspace KEY is not a directory, so each of its member roots is probed instead. +// Called from the MCP dispatch for every request's resolved `path` (so verbs that never reach getIndex() are +// covered), and again from getIndex() as a backstop for the LSP server; repeat calls cost one lookup. +inline void hardenRequestRoot( const std::string& root ) +{ + const auto wsIt = mcpWorkspaceRegistry().find( root ); + if( wsIt == mcpWorkspaceRegistry().end() ) + { + githarden::hardenLateRoot( root ); + return; + } + for( const WorkspaceRoot& r : wsIt->second ) + { + githarden::hardenLateRoot( r.arg ); + } +} + // ─── Phase-M: qsnap PREFETCH ────────────────────────────────────────────────────────────────────────── // // A FILE-CACHE WARMER, NOT an index mechanism — it never touches this McpIndex. When a request observes that @@ -1165,7 +1183,7 @@ inline const ValueRefIndex& valueRefIndexOf( const McpIndex& ix ) // the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no graph rebuild). inline const McpIndex& getIndex( const std::string& root ) { - githarden::hardenLateRoot( root ); // a root first named in a request: pin its filter drivers before any git child runs there + hardenRequestRoot( root ); // a root (or workspace) first named in a request: pin its filter drivers before any git child runs there McpIndex& ix = mcpIndexSlot(); // hot path. The FS-event watcher (Feature 1) lets us SKIP the directory-mtime sweep when it proves no diff --git a/src/verbs_doctor.h b/src/verbs_doctor.h index f3c2b432f..e9929efdf 100644 --- a/src/verbs_doctor.h +++ b/src/verbs_doctor.h @@ -429,7 +429,11 @@ inline const char* doctorLegendComment() "appended to git's environment override for this run (stderr said so as git_harden=fsmonitor-hook); " "builtin, off and unset need no override and read neutralised=\"0\". Independently of this row, every git command " "ripwire runs carries no-optional-locks and core.fsmonitor=false, so no monitor of either form runs for its " - "read-only calls. " + "read-only calls. filter_drivers counts the filter drivers the repository's own config defines; when it is not 0, " + "filter_scope is local (the repository's config) or all (a git older than 2.26 cannot report scope, so the " + "user's own drivers are pinned too) and filter_pinned is 1 (every git child runs them with an empty command, " + "so the files are read as stored; stderr said git_harden=filter-driver) or refused (a name git's -c option " + "cannot spell: every git child is refused instead). " "layout's state=\"agree\" means the layout records match; agree compares only the types= registered in src/model.h; " "a same-size layout change or a stale constant is invisible, so agree does not rule out a mixed binary; " "checked=\"1\" means the comparison ran; " @@ -800,6 +804,14 @@ inline std::string doctorGitConfigTrustAttrs( const rw::Config& cfg ) const bool neutralised = form == githarden::FsmonitorForm::Hook && githarden::g_startup.applied; std::string attrs = "fsmonitor=\"" + std::string( githarden::fsmonitorFormName( form ) ) + "\""; attrs += " neutralised=\"" + std::string( neutralised ? "1" : "0" ) + "\""; + // The repository's own filter drivers, as the startup probe found and pinned them (githarden.h FILTER DRIVERS). + const githarden::FilterDriverOutcome filters = githarden::startupFiltersFor( cfg.rootPath ); + attrs += " filter_drivers=\"" + std::to_string( filters.drivers ) + "\""; + if( filters.drivers > 0 ) + { + attrs += std::string( " filter_scope=\"" ) + ( filters.allScopes ? "all" : "local" ) + "\""; + attrs += std::string( " filter_pinned=\"" ) + ( filters.refused ? "refused" : "1" ) + "\""; + } return attrs; } diff --git a/test/gitchildoptscheck.sh b/test/gitchildoptscheck.sh index 8d4ed856c..3c3523d9c 100755 --- a/test/gitchildoptscheck.sh +++ b/test/gitchildoptscheck.sh @@ -1,16 +1,13 @@ #!/usr/bin/env bash -# gitchildoptscheck.sh — every git child ripwire starts carries the options that keep a REPOSITORY's own config -# from naming a program git runs (src/gitcmd.h, src/githarden.h). +# gitchildoptscheck.sh — every git child ripwire starts carries the options that neutralise repository-configured +# filter and diff programs (src/gitcmd.h, src/githarden.h). # -# githardencheck.sh covers core.fsmonitor. This gate covers the rest of that trust class on ripwire's read-only -# calls: a filter driver (`filter..clean` with a matching `filter=` attribute) that git runs when it -# re-hashes a working-tree file whose stat data is stale — the state a copied or extracted tree is in — and the diff -# programs (diff.external, diff..command/textconv). +# githardencheck.sh covers core.fsmonitor. This gate covers the filter drivers a repository's own config defines +# (`filter..clean/smudge/process`) and the diff programs (diff.external, diff..command/textconv). # -# The fixture is the review's F12b shape (tmp/rv-githarden-speed/fx/mkfx.sh): one commit, `*.c filter=ev`, a local -# filter.ev.clean that appends a line to MARK and passes its input through, the content UNCHANGED and only the file's -# mtime moved. A ripwire run can leave the index refreshed, which makes the next run a no-op, so EVERY behavioural -# row rebuilds the fixture first. +# The fixture: one commit, `*.c filter=ev`, and a local filter.ev.clean whose program only appends a line to MARK and +# passes its input through, so every time git runs it is counted. A ripwire run can leave the index refreshed, +# which changes what the next run does, so EVERY behavioural row rebuilds the fixture first. # # Arms: # (A) presence: on a fresh fixture, plain `git --no-optional-locks status` runs the filter — the fixture is live. @@ -19,9 +16,14 @@ # (C) green: map, --situ, --quality-delta, --hotspots, --rank-by=churn and --doctor each run the filter 0 times, # each on a fresh fixture, and each must have answered (exit status + a stdout marker) so a run that stops # before git cannot pass as "0 firings". -# (D) disclosure: stderr carries git_harden=filter-driver; stdout does not. -# (E) an MCP server started with NO root, whose request names the fixture (the late-root path): 0 firings, and -# the request was answered. +# (D) disclosure: stderr carries git_harden=filter-driver; stdout does not; --doctor's git-config-trust row +# reports filter_drivers="1" filter_scope="local" filter_pinned="1", and filter_drivers="0" on a plain repo. +# (E) MCP roots named only in a request: a server started with NO root drives situational_awareness, +# quality_delta, stray_content, edit_check and quality_baseline at the fixture, an `analyze` whose `paths` +# names the fixture and a second repository (a multi-root workspace), and a `batch`; a server started on the +# second repository is asked quality_delta about the fixture. Each row: 0 firings, and the request answered. +# (S) the fixture with its .git/config moved elsewhere and replaced by a symlink to it (git follows the link; the +# cheap pre-scan does not read through it): --situ and an MCP quality_delta run the filter 0 times. # (F) a PATH shim that records each git child's argv and GIT_* environment and then execs the real git. Over the # same verbs, on the fixture and on a plain multi-commit repository with a working-tree edit: # F1 every child starts with --no-optional-locks -c core.fsmonitor=false -c log.showSignature=false @@ -37,6 +39,9 @@ # every `status --porcelain` carries --ignore-submodules=dirty; a mutation control on a COPY of src/ must be # found by the same scan. # +# Not armed: two callers racing to probe the same root (githarden.h hardenLateRoot holds its lock across the probe). +# Both servers dispatch requests one at a time, so no request sequence can make two probes of one root overlap. +# # Usage: RIPWIRE_BIN=build/ripwire [RIPWIRE_BASE_BIN=/path/to/pre-fix/ripwire] bash test/gitchildoptscheck.sh set -u @@ -65,9 +70,9 @@ printf '#!/bin/sh\necho "cmd $* in $PWD" >> %s\ncat\n' "$MARK" > "$CMD"; chmod + FX="$TMP/f12b" fired(){ [ -f "$MARK" ] && wc -l < "$MARK" | tr -d ' ' || echo 0; } -# The F12b recipe, verbatim in shape: clean filter, content unchanged, only the stat data stale. +# The fixture: clean filter, content unchanged, only the stat data stale. mkfx(){ - rm -rf "$FX" "$MARK" + rm -rf "$FX" "$MARK" "$TMP/cfgstore" mkdir -p "$FX" ( cd "$FX" && "$REALGIT" init -q && printf 'int f(void){return 1;}\n' > f.c && printf '*.c filter=ev\n' > .gitattributes \ && "$REALGIT" add -A && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm i \ @@ -138,20 +143,81 @@ grep -q 'git_harden=filter-driver' "$TMP/r-c-situ.err" \ grep -q 'git_harden=' "$TMP/r-c-situ.out" \ && no "(D) stdout carries a git_harden= line (the disclosure leaked into the answer)" \ || ok "(D) stdout carries no git_harden= (disclosure stays on stderr)" +DOCROW="$( grep -oE ']*/>' "$TMP/r-c-doctor.out" )" +case "$DOCROW" in + *'filter_drivers="1" filter_scope="local" filter_pinned="1"'*) ok "(D) --doctor reports the pinning: $DOCROW" ;; + *) no "(D) --doctor's git-config-trust row lacks filter_drivers=\"1\" filter_scope=\"local\" filter_pinned=\"1\": '${DOCROW:-}'" ;; +esac -# ── (E) MCP server with no root; the request names the fixture ───────────────────────────────────────── -mcpsession(){ +# ── (E) MCP roots named only in a request ────────────────────────────────────────────────────────────── +# A second, plain repository: the other member of the multi-root row, and the startup root of the last row. +SECOND="$TMP/second" +( mkdir -p "$SECOND" && cd "$SECOND" && "$REALGIT" init -q && printf 'int g(void){return 2;}\n' > g.c && "$REALGIT" add -A \ + && "$REALGIT" -c user.name=g -c user.email=g@g commit -qm i ) >/dev/null 2>&1 || no "(E) could not build the second repository" +mcpsession(){ # ARGUMENTS-JSON for one tools/call (id 2) after the handshake printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"gitchildoptscheck","version":"1"}}}' printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}' - printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"situational_awareness","arguments":{"path":"%s"}}}\n' "$1" + printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}\n' "$1" "$2" } -mcpsession "$FX" > "$TMP/mcp.in" -run_row "e-mcp" "0" '"id":2,"result"' sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp.in" -case "$RVERDICT" in - ok) ok "(E) MCP request-named root: answered (id 2 result) and the clean filter did not run" ;; - *) no "(E) MCP request-named root: verdict $RVERDICT, exit $RRC, firings $RFIRED: $( head -c 200 "$TMP/r-e-mcp.err" )" ;; +MCPROWS="situational_awareness|{\"path\":\"$FX\"}| +quality_delta|{\"path\":\"$FX\"}| +stray_content|{\"path\":\"$FX\"}| +edit_check|{\"path\":\"$FX\",\"symbol\":\"f\"}| +quality_baseline|{\"path\":\"$FX\"}| +analyze|{\"paths\":[\"$FX\",\"$SECOND\"]}| +batch|{\"path\":\"$FX\",\"queries\":[{\"verb\":\"stray_content\"},{\"verb\":\"callers\",\"symbol\":\"f\"}]}| +quality_delta|{\"path\":\"$FX\"}|$SECOND" +erow=0 +while IFS='|' read -r everb eargs estart; do + erow=$(( erow + 1 )) + mcpsession "$everb" "$eargs" > "$TMP/mcp-$erow.in" + if [ -n "$estart" ]; then + run_row "e-$erow" "0" '"id":2,"result"' sh -c '"$1" "$3" --mcp < "$2"' _ "$BIN" "$TMP/mcp-$erow.in" "$estart" + elabel="$everb (server started on another root)" + else + run_row "e-$erow" "0" '"id":2,"result"' sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp-$erow.in" + elabel="$everb" + case "$eargs" in *'"paths"'*) elabel="$everb (two-root paths workspace)" ;; esac + fi + case "$RVERDICT" in + ok) ok "(E) MCP request-named root, $elabel: answered (id 2 result) and the clean filter did not run" ;; + *) no "(E) MCP request-named root, $elabel: verdict $RVERDICT, exit $RRC, firings $RFIRED: $( head -c 200 "$TMP/r-e-$erow.err" )" ;; + esac +done <<< "$MCPROWS" +DOCPLAIN="$( "$BIN" "$SECOND" --doctor 2>/dev/null | grep -oE ']*/>' )" +case "$DOCPLAIN" in + *'filter_drivers="0"'*filter_scope*) no "(D) --doctor on a repository with no filter driver still names a scope: $DOCPLAIN" ;; + *'filter_drivers="0"'*) ok "(D) --doctor on a repository with no filter driver: $DOCPLAIN" ;; + *) no "(D) --doctor on a repository with no filter driver: '${DOCPLAIN:-}'" ;; esac +# ── (S) the governing config file is a symlink ────────────────────────────────────────────────────────── +mkfx_symcfg(){ + mkfx + mkdir -p "$TMP/cfgstore" && mv "$FX/.git/config" "$TMP/cfgstore/config" && ln -s "$TMP/cfgstore/config" "$FX/.git/config" \ + || { echo "symlinked-config fixture build failed"; exit 2; } +} +mkfx_symcfg +"$REALGIT" --no-optional-locks -C "$FX" status --porcelain >/dev/null 2>&1 +[ "$( fired )" -ge 1 ] \ + && ok "(S) presence: with .git/config a symlink, plain git status still runs the clean filter (git follows the link)" \ + || no "(S) presence: git did not follow the symlinked config — the arm cannot conclude" +srow(){ # TAG CMD…: like run_row, on the symlinked-config fixture + stag="$1"; shift + mkfx_symcfg + "$@" > "$TMP/r-$stag.out" 2> "$TMP/r-$stag.err" + SRC=$?; SFIRED="$( fired )" +} +srow s-situ "$BIN" "$FX" --situ +{ [ "$SRC" -eq 0 ] && grep -qF situational-awareness "$TMP/r-s-situ.out" && [ "$SFIRED" -eq 0 ]; } \ + && ok "(S) --situ with a symlinked .git/config: answered and the clean filter did not run" \ + || no "(S) --situ with a symlinked .git/config: exit $SRC, firings $SFIRED" +mcpsession quality_delta "{\"path\":\"$FX\"}" > "$TMP/mcp-s.in" +srow s-mcp sh -c '"$1" --mcp < "$2"' _ "$BIN" "$TMP/mcp-s.in" +{ [ "$SRC" -eq 0 ] && grep -qF '"id":2,"result"' "$TMP/r-s-mcp.out" && [ "$SFIRED" -eq 0 ]; } \ + && ok "(S) MCP quality_delta with a symlinked .git/config: answered and the clean filter did not run" \ + || no "(S) MCP quality_delta with a symlinked .git/config: exit $SRC, firings $SFIRED" + # ── (F) the PATH shim: argv + GIT_* environment of every child ────────────────────────────────────────── mkdir -p "$TMP/shim" cat > "$TMP/shim/git" < Date: Thu, 15 Oct 2026 11:50:03 -0400 Subject: [PATCH 106/162] fix(for): FIX1 R1 rows monotone in N at every root length; R2 the --json tail keeps its share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1 (review rv-forbudget-soft): fornotesbudgetcheck (RP) at 1490 xml on Linux CI. Reproduced on macOS by root path length, not platform order: with a 20- or 45-char root the header moves which N the cut steps at, and at that step a file moves from the tail's rank rows into its page rows (gaining sym=), so the cut-scoped tail's size, charged as the refill's reserve, jumped by more than T and the rows past the cut fell (8 -> 7). The reserve is now capped at the HEAD-scoped tail (forFileTail / *in.fileTail: the same at every N). New arm RP10: 1400..1600 step 10 x {xml, json} x roots of 20/45/111 chars — RED on 974dd5b1 (20, 45), green here. R2: the --json tail was funded from what the refilled rows left; it is now funded at forLensJsonTailShare = max(that, the retired ceiling's allowance less its envelope and the rows at the cut), in the refill's reserve and the final fill. New arm RF2: JSON tail >= d4325a8c's on receiverevidencefix N=1000/2000/3000 and falseedgefix N=2000/3000 — RED on 974dd5b1 (470/412/434, 514/438 B), green here (721/1239/1837, 1350/1521 B). Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 32 ++++++++++++++------- test/forsoftbudgetcheck.sh | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index c92a7bce5..a079b1fac 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1334,8 +1334,19 @@ struct ForJsonRefillState // the first render's outputs, by reference: the ref rw::JsonSigNoteCounts& notes; std::vector& shown; }; +// FIX1 R2 (review rv-forbudget-soft): the JSON tail's SHARE under a soft target — what the rows at the cut leave of the bundle +// budget, and never less than what the retired hard ceiling left it (its allowance, N x kMinBytesPerToken x the first-entry +// tolerance, less the envelope it charged and the rows at the cut): owner decision 2 (a trailing section keeps at least the share +// it had at the same N). One function for the refill's reserve and the final fill, so the two read the same number. +inline std::size_t forLensJsonTailShare( std::size_t tokenBudget, std::size_t bundleBytes, std::size_t fixedBytes, std::size_t cutRowsBytes ) noexcept +{ + return std::max( rw::forSoftSectionsShare( bundleBytes, cutRowsBytes ), + rw::forSoftSectionsShare( rw::ceilingAllowanceBytes( tokenBudget ), fixedBytes + cutRowsBytes ) ); +} + struct ForJsonRefillBudgets { + std::size_t fixedBytes; // the envelope the sigs budget charged (header, reserves, counts, notes) std::size_t bundleBytes; // budgetBytesForTokens( N ) std::size_t sigsTarget; // the ladder's target (the cut) std::size_t firstRowDrop; // the first render's row budget (T plus the frame) @@ -1345,11 +1356,10 @@ inline void forLensJsonSoftRefill( const PackSigs& packSigs, const ForLensJsonIn const rw::SigsCutContinuation& cutNext, const std::optional& redactBeforeSigs ) { const std::size_t cutRows = st.json.size() - std::min( st.json.size(), st.cut.pastTargetBytes ); - const rw::FileTail cutTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, rw::rowsAtTargetIds( st.shown, in.rank, st.cut.pastTarget ), - in.rootArg, in.task ); - // the tail at its RESERVE (its share, or its whole stanza when smaller), not its filled bytes: rows stay monotone in N - const std::size_t tailBytes = std::min( rw::forSoftSectionsShare( b.bundleBytes, cutRows ), - forLensJsonTailStanza( cutTail, 0, 0 ).size() ); + // the tail at its RESERVE: its share, capped at the head-scoped tail's whole stanza (the same at every N; the XML twin's + // rule) — not its filled bytes nor the cut-scoped tail's size, both of which step with N: rows stay monotone in N + const std::size_t tailBytes = std::min( forLensJsonTailShare( in.tokenBudget, b.bundleBytes, b.fixedBytes, cutRows ), + forLensJsonTailStanza( *in.fileTail, 0, 0 ).size() ); const std::size_t rowDrop = rw::forSoftRowDropFill( b.bundleBytes, tailBytes ); if( rowDrop >= b.firstRowDrop ) { @@ -1503,7 +1513,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const if( isSigsBuffered && in.tokenBudget > 0 && sigsCut.pastTarget > 0 ) { forLensJsonSoftRefill( packSigs, in, ForJsonRefillState{ sigsJson, sigsCut, sigsCapped, sigsDroppedPositive, noteCounts, jsonShownIds }, - ForJsonRefillBudgets{ bundleBudget, sigsBudget, sigsBudgets.rowDrop }, cutNext, redactBeforeSigs ); + ForJsonRefillBudgets{ fixedBytes, bundleBudget, sigsBudget, sigsBudgets.rowDrop }, cutNext, redactBeforeSigs ); } ForLensBlockCharge sigsCharge; // the sink: an unmeasured block takes the omitting path below if( !isSigsBuffered ) @@ -1605,7 +1615,7 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const // lane 2: the rank rows exclude the files of the rows emitted (after a refill: the rows it kept — the XML twin's rule) const std::size_t jsonCutRows = sigsJson.size() - std::min( sigsJson.size(), sigsCut.pastTargetBytes ); const rw::FileTail jsonTail = rw::computeFileTail( in.ing, in.rank, *in.evidence, jsonShownIds, in.rootArg, in.task ); - tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, bundleBudget > jsonCutRows ? bundleBudget - jsonCutRows : 0u ); + tailStanza = forLensJsonTailStanza( jsonTail, in.tokenBudget, forLensJsonTailShare( in.tokenBudget, bundleBudget, fixedBytes, jsonCutRows ) ); const std::size_t bundleBytesBase = header.size() + sigsJson.size() + notesStanza.size() + surfaceCountsStanza.size() + tailStanza.size() + envelopeTextBytes + droppedPositiveStanza.size() // A2: 0 bytes on the (overwhelming) no-drop path @@ -3841,12 +3851,14 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // replaced, never added to; a re-render whose buffer fails keeps the first render and its readings whole. if( forSoftTarget && sigsPreRendered && forSigsCut.pastTarget > 0 ) { - // the tail is charged at its RESERVE — its share, or its whole render when that is smaller — not at its filled bytes: a - // fill that steps a whole row at a time would otherwise make the rows past the cut fall as N grows (rows monotone in N) + // the tail is charged at its RESERVE — its share, capped at the whole render of the head-scoped tail (forFileTail: the + // rows outside the 40-row surface, the same at every N) — not at its filled bytes nor at the cut-scoped tail's size: + // both step with N (a whole row at a time; a file moving from the rank rows into the page rows gains its sym=), and + // the rows past the cut would then fall as N grows. With this reserve the rows are monotone in N. std::vector tailEsc; const std::size_t tailReserve = std::min( std::max( bundleBudget > tailSpentBytes ? bundleBudget - tailSpentBytes : 0u, rw::kForFileTailShellReserve ), - rw::renderFileTailXml( forFileTailShown, forFileTailShown.paths.size(), tailEsc ).size() ); + rw::renderFileTailXml( forFileTail, forFileTail.paths.size(), tailEsc ).size() ); const std::size_t sectionsServed = forHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + graphSection.xml.size() + autoSection.xml.size() + tailReserve; // not the --detail bodies (above) const std::size_t rowDropFill = rw::forSoftRowDropFill( bundleBudget, sectionsServed ); diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index d202bffa3..162a49be4 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -672,6 +672,65 @@ for spec in "legotransitivefix|$QNODE|600|lego|9" "legotransitivefix|$QNODE|1000 else no "(RF1) $fx '$q' N=$n: <$sec> serves '$got' rows < the retired ceiling's $want (rc=$rrc) — a section lost its share to the rows"; fi done +# (RF2) FIX1 R2 (review rv-forbudget-soft): the --json tail is a trailing section too — at an explicit N it keeps at least the +# bytes the retired hard ceiling (d4325a8c) gave it. Lower bounds measured on frozen in-repo fixtures; 974dd5b1 served 470/412/434 +# and 514/438 B there (RED), the tail funded from what the refilled rows left. +for spec in "receiverevidencefix|$QNODE|1000|527" "receiverevidencefix|$QNODE|2000|582" "receiverevidencefix|$QNODE|3000|622" \ + "falseedgefix|$QHAND|2000|544" "falseedgefix|$QHAND|3000|526"; do + IFS='|' read -r fx q n want <<< "$spec" + got="$( "$BIN" "test/$fx" --no-cache --for="$q" --token-budget="$n" --json 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(json.dumps(d["tail"],separators=(",",":"))))' 2>/dev/null )" + if isnum "$got" && [ "$got" -ge "$want" ]; then ok "(RF2) $fx '$q' N=$n --json: the tail keeps $got B >= the retired ceiling's $want B" + else no "(RF2) $fx '$q' N=$n --json: the tail serves '$got' B < the retired ceiling's $want B — a trailing section lost its share"; fi +done +# (RP10) FIX1 R1: rows are a rank prefix and MONOTONE in N at a step of 10, at three root path lengths (the header's root= moves +# which N the cut steps at; 974dd5b1 dropped a row at 1490 with a 28-char root: the tail's reserve stepped with the cut). The +# fornotesbudgetcheck corpus shape (72 noted symbols), swept 1400..1600, default and --json dialects. +RPD="$( mktemp -d /tmp/rwrp.XXXXXX )" || RPD="$TMP/rp" +mkdir -p "$RPD/a/c" "$RPD/medium_root_path_component/c" "$TMP/a_long_root_path_component_for_the_rp10_sweep/c" +for root in "$RPD/a/c" "$RPD/medium_root_path_component/c" "$TMP/a_long_root_path_component_for_the_rp10_sweep/c"; do + mkdir -p "$root/src" + python3 - "$root" <<'PY' +import os, sys +for i in range( 12 ): + with open( os.path.join( sys.argv[ 1 ], "src", "mod%d.py" % i ), "w" ) as f: + for j in range( 6 ): + f.write( "def widgetRoutine%d_%d( alpha, beta ):\n \"\"\"Route the widget pipeline stage %d.%d through the dispatcher.\"\"\"\n return alpha + beta\n\n\n" % ( i, j, i, j ) ) +PY + ( cd "$root" && git init -q -b main . && git add -A && git -c user.email=gate@example.invalid -c user.name=gate commit -qm corpus ) >/dev/null 2>&1 + for i in 0 1 2 3 4 5 6 7 8 9 10 11; do for j in 0 1 2 3 4 5; do + "$BIN" "$root" --note-add="widgetRoutine${i}_${j}: this routine is load-bearing for the widget dispatcher and must not be reordered without rechecking the stage table downstream" >/dev/null 2>&1 + done; done + rp10="$( python3 - "$BIN" "$root" <<'PY' +import json, re, subprocess, sys +B, root = sys.argv[ 1 ], sys.argv[ 2 ]; q = "widget dispatcher pipeline stage" +def rows( s, js ): + if js: + return [ "%s:%s:%s" % ( e.get( "p" ), e.get( "l" ), e.get( "n" ) ) for e in sorted( json.loads( s ).get( "sigs", [] ), key=lambda e: e.get( "r", 10**9 ) ) ] + m = re.search( r"]*>(.*?)", s, re.S ); out = [] + for x in re.finditer( r"]*)>", m.group( 1 ) if m else "" ): + g = lambda k: ( re.search( r"\b" + k + r'="([^"]*)"', x.group( 1 ) ) or [ None, "?" ] )[ 1 ] + out.append( ( int( g( "r" ) ) if g( "r" ).isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) + return [ b for a, b in sorted( out ) ] +bad = [] +for js in ( False, True ): + extra = [ "--json" ] if js else [] + ref = rows( subprocess.run( [ B, root, "--for=" + q ] + extra, capture_output=True ).stdout.decode(), js ); prev = None + for n in range( 1400, 1601, 10 ): + r = rows( subprocess.run( [ B, root, "--for=" + q, "--token-budget=%d" % n ] + extra, capture_output=True ).stdout.decode(), js ) + if not r or r != ref[ :len( r ) ] or ( prev is not None and prev != r[ :len( prev ) ] ): + bad.append( "%s@%d:%s->%d" % ( "json" if js else "xml", n, len( prev ) if prev else "-", len( r ) ) ) + prev = r +print( "OK" if not bad else "BAD " + " ".join( bad[ :4 ] ) ) +PY +)" + case "$rp10" in + OK) ok "(RP10) root of ${#root} chars: rows are a rank prefix, monotone in N, 1400..1600 step 10, default and --json" ;; + *) no "(RP10) root of ${#root} chars: $rp10 — the rows past the cut fell as N grew (or are not a rank prefix)" ;; + esac +done +rm -rf "$RPD" + # ── twins: the CI-gate personality of --token-budget is unchanged on the map, --query and --recall ─────── "$BIN" "$CORPUS" --token-budget=100 "$CM" > "$TMP/map.out" 2>/dev/null; grc=$? GW="$( attr r withheld "$TMP/map.out" 2>/dev/null )" From 3c10d1d54fadeef223d758244889f193b6a6f2fa Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 11:50:03 -0400 Subject: [PATCH 107/162] =?UTF-8?q?test(forrankordercheck):=20FIX1=20N2=20?= =?UTF-8?q?=E2=80=94=20q6/q7/q8=20re-pinned=20in-lane=20with=20the=20measu?= =?UTF-8?q?red=20by-design=20reason?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9397 -> 9947, 10009 -> 10496, 10070 -> 10605 B (D-3 hops inside the old allowance + the default regime's target clauses; the 4% rule unchanged). Co-Authored-By: Claude Opus 5.5 --- test/forrankordercheck.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 364bd66fe..c1415fd76 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -138,7 +138,14 @@ REPO_Q=( "rank graph teleport" "compact legend rewrite" "edit receipt post-check # replace, so the byte-shaped section fits five more signature rows, and the legend that grew 154 B (the sc= # rule, the route= code's reading and the merged-callee reading) is paid for several times over in rows. The other # nine stay at their for-widen bases: -1.11%, +0.36%, -0.45%, +1.54%, +0.02%, +1.66%, +0.18%, -0.56%, +2.30%. -REPO_BASE=( 10134 10029 10642 10102 9880 9397 10009 10070 9966 10379 ) +# q6 q7 q8 RE-PINNED 2026-10-15 (lane forbudget-soft FIX1, review rv-forbudget-soft N2; rule 4, a stated by-design break — the 4% rule +# itself is unchanged): 9397 -> 9947, 10009 -> 10496, 10070 -> 10605 B, i.e. the answer LESS its past-target rows, its hop bytes +# over the old 1,000 B allowance, its tail page rows and the tail clause growth (the allowances below), measured on this lane's +# build at this tree (+5.85% / +4.87% / +5.31% over the old pins). The review measured the same growth on the base tree (+6.42% / +# +4.81% / +5.25%) and attributed it to the lane: decision D-3 (the default serves every hop row — the part inside the old +# 1,000 B allowance is not subtracted) and the default regime's header clauses (budget_bytes= read as a target, past_target=). +# The other seven are unmoved. Train 28 re-measures attrib-multidef's --for piece against these pins. +REPO_BASE=( 10134 10029 10642 10102 9880 9947 10496 10605 9966 10379 ) # q3 RE-PINNED 2026-10-10 (train 26c): 10080 -> 10642 B (+5.58% on the merged stack), an OUTPUT change of two signed lanes # that each stay inside the 4% alone: lego-charge serves two more signature rows (25 -> 27 of 40) because / # are charged to the budget at the size they are SERVED (a collapsing section its stub), and owner-hop adds the From fdad963403e4ff3d727203128ca122c62b9f76e7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 11:52:57 -0400 Subject: [PATCH 108/162] refactor(for): forLensJsonTailShare spelled with named shares (qd clone row) Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index a079b1fac..ace077a9b 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1340,8 +1340,9 @@ struct ForJsonRefillState // the first render's outputs, by reference: the ref // it had at the same N). One function for the refill's reserve and the final fill, so the two read the same number. inline std::size_t forLensJsonTailShare( std::size_t tokenBudget, std::size_t bundleBytes, std::size_t fixedBytes, std::size_t cutRowsBytes ) noexcept { - return std::max( rw::forSoftSectionsShare( bundleBytes, cutRowsBytes ), - rw::forSoftSectionsShare( rw::ceilingAllowanceBytes( tokenBudget ), fixedBytes + cutRowsBytes ) ); + const std::size_t softShare = rw::forSoftSectionsShare( bundleBytes, cutRowsBytes ); // T less the rows at the cut + const std::size_t retiredShare = rw::forSoftSectionsShare( rw::ceilingAllowanceBytes( tokenBudget ), fixedBytes + cutRowsBytes ); // the hard ceiling's + return softShare > retiredShare ? softShare : retiredShare; } struct ForJsonRefillBudgets From 711bf4b2911f6ad1c5c39c668d6faa395b23cc1c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 12:40:36 -0400 Subject: [PATCH 109/162] =?UTF-8?q?test:=20forbodyslotcheck=20=E2=80=94=20?= =?UTF-8?q?the=20owner=20body=20of=20an=20explanatory=20--for=20question,?= =?UTF-8?q?=20gates=20before=20the=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-registered contract for lane/for-body-slot, committed before any product code so the gates can be reviewed first. An explanatory --for question ("how does X …") on the compact route serves ONE body, the owner's: a callable definition (never a class, a .d.ts or header declaration, or a test symbol) whose folded name is a question word, the entry point among several owners, else the best-ranked callable sharing a question subtoken. Rendered in the --expand shape with ; a body over 6 KB is head-cut at a line end with truncated="1" lines= next=--expand=…; a first line over the cap is served whole with over_ceiling="1". The body is a section of the complete answer, never paid by dropping sig rows: in the default regime // are byte-identical to the slot-off answer (RIPWIRE_NO_BODY_SLOT=1) and est_tokens prices the body. Under an explicit --token-budget / MCP budget_tokens it is reserved with the trailing sections; rows stay a rank prefix, monotone in N; a skipped slot says so on . MCP for serves the same body. No slot without a cue, under --signatures-only, --json, --auto-bodies, --detail=N, --format=candidates or the name-exact route. Fixture test/forbodyslotfix covers TS, Python, C, Rust, C++ and Go owners plus near misses: a class ranked above its method, a header declaration above its definition, a .d.ts declaration above its definition, a tests/ helper as the only name owner, two equal owners in two files, two same-named methods in one file, a struct, an oversized C body and one 7.7 KB multibyte line. On the pre-change binary: 21 arm groups red (30 FAIL verdicts), 88 PASS twins/negatives/controls, 1 optional skip. Registered in regression.sh and the shard weights; gate-count sites regenerated (686). Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/forbodyslotcheck.sh | 514 ++++++++++++++++++ test/forbodyslotfix/src/c/cmd-find.c | 112 ++++ test/forbodyslotfix/src/c/render.c | 161 ++++++ test/forbodyslotfix/src/context.ts | 18 + test/forbodyslotfix/src/cpp/build.cc | 44 ++ test/forbodyslotfix/src/cpp/build.h | 32 ++ test/forbodyslotfix/src/go/packager.go | 38 ++ test/forbodyslotfix/src/helper/cookie.ts | 25 + test/forbodyslotfix/src/http-exception.ts | 9 + test/forbodyslotfix/src/middleware/jwt/jwt.ts | 78 +++ test/forbodyslotfix/src/min/hydrate.js | 2 + test/forbodyslotfix/src/negotiate.ts | 7 + test/forbodyslotfix/src/rs/indexer.rs | 57 ++ .../src/textual/css/stylesheet.py | 97 ++++ test/forbodyslotfix/src/textual/widgets.py | 27 + test/forbodyslotfix/src/two/alpha.ts | 7 + test/forbodyslotfix/src/two/beta.ts | 7 + test/forbodyslotfix/src/types/global.d.ts | 4 + test/forbodyslotfix/src/utils/encode.ts | 2 + test/forbodyslotfix/src/utils/jwt/jwt.ts | 95 ++++ test/forbodyslotfix/src/utils/jwt/types.ts | 54 ++ test/forbodyslotfix/tests/cmd-find-test.c | 18 + test/forbodyslotfix/tests/test_stylesheet.py | 18 + test/forbodyslotfix/tests/test_tokens.py | 11 + test/regression.sh | 2 +- 29 files changed, 1447 insertions(+), 9 deletions(-) create mode 100755 test/forbodyslotcheck.sh create mode 100644 test/forbodyslotfix/src/c/cmd-find.c create mode 100644 test/forbodyslotfix/src/c/render.c create mode 100644 test/forbodyslotfix/src/context.ts create mode 100644 test/forbodyslotfix/src/cpp/build.cc create mode 100644 test/forbodyslotfix/src/cpp/build.h create mode 100644 test/forbodyslotfix/src/go/packager.go create mode 100644 test/forbodyslotfix/src/helper/cookie.ts create mode 100644 test/forbodyslotfix/src/http-exception.ts create mode 100644 test/forbodyslotfix/src/middleware/jwt/jwt.ts create mode 100644 test/forbodyslotfix/src/min/hydrate.js create mode 100644 test/forbodyslotfix/src/negotiate.ts create mode 100644 test/forbodyslotfix/src/rs/indexer.rs create mode 100644 test/forbodyslotfix/src/textual/css/stylesheet.py create mode 100644 test/forbodyslotfix/src/textual/widgets.py create mode 100644 test/forbodyslotfix/src/two/alpha.ts create mode 100644 test/forbodyslotfix/src/two/beta.ts create mode 100644 test/forbodyslotfix/src/types/global.d.ts create mode 100644 test/forbodyslotfix/src/utils/encode.ts create mode 100644 test/forbodyslotfix/src/utils/jwt/jwt.ts create mode 100644 test/forbodyslotfix/src/utils/jwt/types.ts create mode 100644 test/forbodyslotfix/tests/cmd-find-test.c create mode 100644 test/forbodyslotfix/tests/test_stylesheet.py create mode 100644 test/forbodyslotfix/tests/test_tokens.py diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index a013a8c25..619db9012 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -187,6 +187,7 @@ "fnptrcheck.sh": 11.3, "forautobodycheck.sh": 21.6, "forblowupcheck.sh": 1.0, +"forbodyslotcheck.sh": 150.0, "forbudgetmonotoncheck.sh": 2.5, "forcalibfactscheck.sh": 8.0, "forcompresscheck.sh": 6.4, diff --git a/README.md b/README.md index 7bfedf631..5eef8409a 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-685 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +686 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **685 gate scripts** and is the authoritative list; +`test/regression.sh` names **686 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 11acf723b..791e77946 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 685 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 686 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **685 gate scripts**, all of which exist on disk. +naming **686 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 685. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 686. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index db11d6c46..a0678335b 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["685 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["686 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "685", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "686", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["191 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["685 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["686 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/forbodyslotcheck.sh b/test/forbodyslotcheck.sh new file mode 100755 index 000000000..c316c11c8 --- /dev/null +++ b/test/forbodyslotcheck.sh @@ -0,0 +1,514 @@ +#!/usr/bin/env bash +# forbodyslotcheck.sh — the ONE owner body of an explanatory --for question (the "body slot"), on top of the +# stop-when-complete budget semantics (lane/for-body-slot, train 28; design D of lane/for-noise-body-slot +# re-expressed: the body is a SECTION of a complete how-does answer, never paid by dropping sig rows). +# +# THE CONTRACT THIS PINS (arms tagged [P] were RED on the pre-change binary 974dd5b1; [G] arms are twins and +# near-miss negatives that hold on both binaries and must keep holding — PROCESS rule 4, no gate erosion): +# +# TRIGGER CLI --for (XML, full and compact legends) and MCP `for`, on the compact posture of the +# subtoken+body route (root bundle="compact"), when the task carries an explanatory cue +# (taskroute.h kExplanatoryCues, word-bounded: how does/do/is/are, what does, implementation of). +# No slot: no cue, --signatures-only, --json (the JSON dialect carries no bodies in any posture — +# a named floor), --auto-bodies and --detail=N (the caller chose the bodies), --format=candidates, +# the name-exact route (the auto bundle already serves the anchor's body). +# OWNER owner-hop's predicate (verbs_for.h forOwnerHopPlan): a callable definition (Function/Method, +# isDefinitionNotDeclaration, so never a .d.ts / header declaration), not a test symbol, inside the +# ranked head, whose whole folded name equals a question word; common words after the rest. Among +# owners the one with a call edge (resolved or by name) to another owner wins (the entry point), +# else the best rank. No owner: the best-ranked callable of the same predicate whose name shares a +# kept question subtoken. None: no slot. Classes/structs/types are never the owner. +# RENDER one in the --expand shape (packBodies), kept. A body over +# kForBodySlotMaxBytes (6000) is head-cut at a line end: truncated="1" lines="lo-hi/T" +# next="--expand=P:L:N:A-B" (the cutfix-bodies vocabulary), bodies capped="1"; a FIRST line longer +# than the cap is served whole with over_ceiling="1" (packBodies' existing rule). Root bodies="1". +# BUDGETS default (no explicit budget): the body is a required section of the complete answer. , +# , are byte-identical to the slot-off answer (RIPWIRE_NO_BODY_SLOT=1, the A/B handle, +# as RIPWIRE_NO_OWNER_HOP); est_tokens prices the body; the runaway guard (30,000 B) is unchanged +# and never fires because of the body. Explicit --token-budget=N / MCP budget_tokens: +# reserve-sections-first — the body is reserved with the trailing sections at the cut, rows past +# the cut fill what is left; rows stay a rank prefix, monotone in N, never more than slot-off; +# the body never shrinks as N grows and equals the default body at a wide N; when its share cannot +# hold it the slot is skipped with bodies="0" reason="budget" and the owner stays recoverable +# through a next= naming --expand of it. +# MCP `for` serves the same body (parity decision, design D open question 4); budget_tokens as above. +# +# Arms (colour on 974dd5b1 in brackets): +# D1 [P] T_D: bodies=1, holds getCookie / HTTPException(401 / ctx.set('jwtPayload'; with verify +# D2 [G] T_D: not verify's body (jwt calls verify: the entry point) +# D3 [P] est_tokens prices the body (slot vs RIPWIRE_NO_BODY_SLOT=1) +# D4 [G] sigs/hops/tail byte-identical slot vs slot-off on every fixture question (checklist 23); slot-off has no body +# D5 [P] legends: full answer explains bodies=1; compact legend defines b t= n= p= l= when a body rides +# D6 [G] no explanatory cue (where / show documentation / bare noun phrase): bodies=0, no , valid UTF-8 +# L1 [P] Rust: entry point index (calls walk) beats walk at r1 L2 [P] C++: Builder::Build DEFINITION (.cc), not the r1 header declaration +# L3 [P] Go: method Package, never the type Packager +# N1 [P] TS: negotiate's definition, not the r1 .d.ts declaration; [G] a .d.ts-only name never yields a .d.ts body +# N2 [P/G] the only name owner is a tests/ helper: the slot falls back to a non-test callable, never tests/ +# N3 [P] two equal owners in two files: ONE body, the better-ranked one (p= and l= of the r1 row) +# N4 [P] two same-named methods of two classes in ONE file: ONE body, the r1 row's (entity identity, checklist 16) +# N5 [G] a struct named by the question is never the body; every is fn/method +# E1 [P] --token-budget=3000: the body rides; rows a rank prefix of the default rows, >= 4, no over_ceiling +# E2 [P] --token-budget=400: bodies=0 reason=budget; the skipped slot discloses itself on +# E3 [G/P] N sweep: rows monotone + rank prefix + <= slot-off; body bytes monotone; [P] at 1000000 body == default body +# MC1-MC4 MCP for: [P] body parity with the CLI; [P] budget_tokens=400 recoverable; [G] no cue, no body +# M1-M4 mutation controls (canned pre-change shapes are rejected) X1-X3 determinism, xmllint, no '--' in comments +# I0-I9 [optional, RIPWIRE_BASE_BIN=] byte identity where the slot must not fire (I0: slot off == base) +# +# Usage: bash test/forbodyslotcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forbodyslotcheck.sh +# BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used. +# Exits non-zero on any failure. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +unset RIPWIRE_NO_BODY_SLOT RIPWIRE_NO_OWNER_HOP +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "forbodyslotcheck: BIN=$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +skip(){ printf ' SKIP %s\n' "$*"; return 0; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +cd "$ROOT" +FIX="$ROOT/test/forbodyslotfix" +[ -d "$FIX/src" ] || { echo "fixture $FIX missing"; exit 2; } + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT + +# ── the checker: one Python reader of a --for answer, one verdict per call (exit 0 = holds) ───────────────── +cat > "$TMP/chk.py" <<'PY' +import html, json, os, re, sys +cmd, args = sys.argv[1], sys.argv[2:] +def load(p): + return open(p, "rb").read().decode("utf-8", "replace") +def attrs(s): + return dict((k, html.unescape(v)) for k, v in re.findall(r'(\w+)="([^"]*)"', s)) +def root(t): + m = re.search(r']*)>', t) + return attrs(m.group(1)) if m else {} +def sigs(t): + m = re.search(r']*)>', t) + return attrs(m.group(1)) if m else {} +def rows(t): + m = re.search(r']*>(.*?)', t, re.S) + out = [] + for d in re.finditer(r']*)>', m.group(1) if m else ""): + a = attrs(d.group(1)) + out.append((int(a.get("r", "0") or 0), a.get("n", ""), a.get("p", ""), a)) + return sorted(out, key=lambda x: x[0]) +def bodies(t): + """every …: (attrs, cdata bytes, the whole element)""" + out = [] + for m in re.finditer(r']*)>(.*?)', t, re.S): + cd = "".join(re.findall(r'', m.group(2), re.S)) + out.append((attrs(m.group(1)), cd, m.group(0))) + return out +def legend(t): + return " ".join(re.findall(r'', t, re.S)) +def fail(msg): + print(msg); sys.exit(1) +def first_row(t, name): + for r, n, p, a in rows(t): + if n == name: + return a + return None +def strip_slot(t): + """the answer minus everything the body slot may add: legend comments, the element, root bodies=/reason=/est_tokens=""" + t = re.sub(r'', '', t, flags=re.S) + t = re.sub(r'', '', t, flags=re.S) + t = re.sub(r']*/>', '', t) + t = re.sub(r' (bodies|reason|est_tokens|bundle)="[^"]*"', '', t) + return t +if cmd == "rootattr": # rootattr FILE ATTR REGEX + t = load(args[0]); v = root(t).get(args[1]) + if v is None or not re.fullmatch(args[2], v): fail("root %s=%r, wanted %s" % (args[1], v, args[2])) + print("%s=%s" % (args[1], v)) +elif cmd == "noroot": # noroot FILE ATTR : the root carries no ATTR= + t = load(args[0]); v = root(t).get(args[1]) + if v is not None: fail("root carries %s=%r" % (args[1], v)) +elif cmd == "bodycount": # bodycount FILE N : exactly N and root bodies=N + t = load(args[0]); n = len(bodies(t)); v = root(t).get("bodies") + if n != int(args[1]) or v != args[1]: fail(" count=%d root bodies=%r, wanted %s" % (n, v, args[1])) + print("bodies=%s" % v) +elif cmd == "nobody": # nobody FILE : no whose CDATA holds every STR + t = load(args[0]); bs = bodies(t) + hit = [b for b in bs if b[0].get("n") == args[1] and b[0].get("p") == args[2]] + if not hit: fail("no (bodies: %s)" % (args[1], args[2], [(b[0].get("n"), b[0].get("p")) for b in bs])) + a, cd, el = hit[0] + miss = [s for s in args[3:] if s not in cd] + if miss: fail("body %s lacks %s" % (args[1], miss)) + print(" %d B" % (args[1], args[2], a.get("l"), len(cd.encode()))) +elif cmd == "nobodyof": # nobodyof FILE NAME : no + t = load(args[0]) + if any(b[0].get("n") == args[1] for b in bodies(t)): fail("a rides" % args[1]) +elif cmd == "nobodyat": # nobodyat FILE REGEX : no + t = load(args[0]); bad = [b[0].get("p") for b in bodies(t) if re.search(args[1], b[0].get("p", ""))] + if bad: fail(" matched %s: %s" % (args[1], bad)) +elif cmd == "callsin": # callsin FILE NAME CALLEE : the carries with + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + el = hit[0][2] + if " has no naming %s" % (args[1], args[2])) +elif cmd == "bodyattr": # bodyattr FILE NAME ATTR REGEX + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + v = hit[0][0].get(args[2]) + if v is None or not re.search(args[3], v): fail(", wanted %s" % (args[1], args[2], v, args[3])) + print("" % (args[1], args[2], v[:90])) +elif cmd == "bodyisrow": # bodyisrow FILE NAME : the one has the p= and l= of the best-ranked NAME row + t = load(args[0]); hit = [b[0] for b in bodies(t) if b[0].get("n") == args[1]] + if len(hit) != 1: fail("%d , wanted exactly 1" % (len(hit), args[1])) + r = first_row(t, args[1]) + if r is None: fail("no row to compare against" % args[1]) + rp = r.get("p", ""); bp = hit[0].get("p", "") + if (bp, hit[0].get("l")) != (rp, r.get("l")): fail(" is not the best-ranked row p=%s l=%s r=%s" % (bp, hit[0].get("l"), rp, r.get("l"), r.get("r"))) + print(" = r%s" % (bp, hit[0].get("l"), r.get("r"))) +elif cmd == "bodykinds": # bodykinds FILE : every is a callable kind + t = load(args[0]); bad = [(b[0].get("n"), b[0].get("t")) for b in bodies(t) if b[0].get("t") not in ("fn", "method")] + if bad: fail("non-callable bodies: %s" % bad) +elif cmd == "rowis": # rowis FILE R NAME PATH : the row at rank R is NAME in PATH (an arm's premise) + t = load(args[0]); hit = [x for x in rows(t) if x[0] == int(args[1])] + if not hit or hit[0][1] != args[2] or hit[0][2] != args[3]: fail("premise: r%s is %s, wanted %s %s" % (args[1], hit[0][1:3] if hit else None, args[2], args[3])) +elif cmd == "cutatline": # cutatline FILE NAME MAXB : a head-cut body that is the def's first K whole source lines, <= MAXB + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + a, cd, el = hit[0] + if len(cd.encode()) > int(args[2]): fail("CDATA %d B > %s" % (len(cd.encode()), args[2])) + m = re.fullmatch(r'(\d+)-(\d+)/(\d+)', a.get("lines", "")) + if not m: fail("lines=%r is not lo-hi/T" % a.get("lines")) + lo, hi, tot = map(int, m.groups()) + src = open(os.path.join(os.environ["FIX"], a["p"]), encoding="utf-8").read().split("\n") + l0 = int(a["l"]) + want = "\n".join(src[l0 - 1: l0 - 1 + (hi - lo + 1)]) + if cd.rstrip("\n") != want.rstrip("\n"): fail("CDATA is not source lines %d..%d of %s (a cut inside a line?)" % (l0, l0 + hi - lo, a["p"])) + if not (lo == 1 and hi < tot): fail("lines=%s does not describe a head cut" % a.get("lines")) + print("cut %s lines=%s %d B" % (args[1], a["lines"], len(cd.encode()))) +elif cmd == "nextof": # nextof FILE NAME : print the value + t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] + if not hit or not hit[0][0].get("next"): fail("no " % args[1]) + print(hit[0][0]["next"]) +elif cmd == "startsat": # startsat FILE PATH LINE : the first CDATA begins with source line LINE of PATH + t = load(args[0]); bs = bodies(t) + if not bs: fail("the continuation served no body") + src = open(os.path.join(os.environ["FIX"], args[1]), encoding="utf-8").read().split("\n") + want = src[int(args[2]) - 1] + if not bs[0][1].startswith(want): fail("continuation begins %r, wanted source line %s %r" % (bs[0][1][:60], args[2], want[:60])) +elif cmd == "utf8whole": # utf8whole FILE NAME TAIL : the CDATA is valid UTF-8 and ends with TAIL + raw = open(args[0], "rb").read() + m = re.search(rb']*n="' + re.escape(args[1].encode()) + rb'"[^>]*>', raw, re.S) + if not m: fail("no CDATA" % args[1]) + try: cd = m.group(1).decode("utf-8", "strict") + except UnicodeDecodeError as e: fail("CDATA is not valid UTF-8: %s" % e) + if args[2] not in cd: fail("the line is not whole: %r missing" % args[2]) + print("%s %d B valid UTF-8" % (args[1], len(m.group(1)))) +elif cmd == "sectionsame": # sectionsame FILE_SLOT FILE_OFF : identical once the slot's own bytes are removed + a, b = strip_slot(load(args[0])), strip_slot(load(args[1])) + if a != b: + i = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), min(len(a), len(b))) + fail("sections differ at byte %d: slot %r | off %r" % (i, a[max(0, i - 40): i + 60], b[max(0, i - 40): i + 60])) +elif cmd == "estdelta": # estdelta FILE_SLOT FILE_OFF : est_tokens(slot) - est_tokens(off) >= body element bytes / 10 + ts, to = load(args[0]), load(args[1]) + es, eo = root(ts).get("est_tokens", ""), root(to).get("est_tokens", "") + if not (es.isdigit() and eo.isdigit()): fail("est_tokens missing: %r %r" % (es, eo)) + m = re.search(r'', ts, re.S) + eb = len(m.group(0).encode()) if m else 0 + if eb == 0: fail("no element to price (est %s vs %s)" % (es, eo)) + if int(es) - int(eo) < eb // 10: fail("est_tokens %s - %s < %d B / 10" % (es, eo, eb)) + print("est_tokens %s vs %s (+%d for %d B)" % (es, eo, int(es) - int(eo), eb)) +elif cmd == "legend": # legend FILE STR... : every STR appears inside the XML comments + lg = legend(load(args[0])); miss = [s for s in args[1:] if s not in lg] + if miss: fail("legend lacks %s" % miss) +elif cmd == "nodashes": # nodashes FILE : no "--" inside any XML comment + bad = [c[:60] for c in re.findall(r'', load(args[0]), re.S) if "--" in c] + if bad: fail("'--' inside an XML comment: %s" % bad) +elif cmd == "rowkeys": # rowkeys FILE : print "n|p" per row in rank order (one per line) + print("\n".join("%s|%s" % (n, p) for r, n, p, a in rows(load(args[0])))) +elif cmd == "prefix": # prefix FILE_A FILE_B : A's rows are a rank prefix of B's, A has >= 4 rows + ra = [(n, p) for r, n, p, a in rows(load(args[0]))]; rb = [(n, p) for r, n, p, a in rows(load(args[1]))] + if len(ra) < 4: fail("only %d rows (the rank 1..4 floor)" % len(ra)) + if ra != rb[:len(ra)]: fail("rows are not a rank prefix of the default answer (first diff at %d)" % next(i for i in range(len(ra)) if i >= len(rb) or ra[i] != rb[i])) + print("%d rows, a prefix of %d" % (len(ra), len(rb))) +elif cmd == "nrows": # nrows FILE + print(len(rows(load(args[0])))) +elif cmd == "bodybytes": # bodybytes FILE : CDATA bytes of every + print(sum(len(b[1].encode()) for b in bodies(load(args[0])))) +elif cmd == "cdata": # cdata FILE NAME : the CDATA, verbatim + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + sys.stdout.write(hit[0][1]) +elif cmd == "bodiesnext": # bodiesnext FILE REGEX : a element (the slot's own disclosure) carries next=~REGEX + m = re.search(r']*)>', load(args[0])) + if not m: fail("no element (a skipped slot discloses itself there)") + a = attrs(m.group(1)) + if not re.search(args[1], a.get("next", "")): fail(" has no next= matching %s" % (m.group(1)[:120], args[1])) + print("" % a.get("next")) +elif cmd == "over": # over FILE : the root carries no over_ceiling="1" + if root(load(args[0])).get("over_ceiling") == "1": fail("over_ceiling=\"1\" on the root") +elif cmd == "sigsopen": # sigsopen FILE : the answer has a ") +else: + fail("unknown check " + cmd) +PY +export FIX +chk(){ python3 "$TMP/chk.py" "$@"; } +run(){ "$BIN" "$FIX" --cache="$TMP/rw.cache" "$@" 2>/dev/null; } # a fresh cache per gate run: this binary's own parse +runoff(){ RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --cache="$TMP/rw.cache" "$@" 2>/dev/null; } +strip_at(){ sed -e 's/ at="[^"]*"//g' -e 's/"at":"[^"]*",\{0,1\}//g'; } +cat > "$TMP/mcptext.py" <<'PY' +import json, sys +for line in sys.stdin: + line = line.strip() + if not line: continue + try: d = json.loads(line) + except Exception: continue + for c in (d.get("result") or {}).get("content") or []: + if c.get("type") == "text": sys.stdout.write(c["text"]) +PY +mcpfor(){ # mcpfor TASK [EXTRA_JSON_MEMBERS] > FILE + python3 -c 'import json,sys; a={"path":sys.argv[1],"task":sys.argv[2]}; a.update(json.loads(sys.argv[3] or "{}")); print(json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":a}}))' "$FIX" "$1" "${2:-}" \ + | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py" +} + +T_D="How does the JWT middleware verify a token?" +T_PY="How does Stylesheet apply the parsed rules to a widget?" +T_C="How does cmd_find_target resolve the session, window and pane of a command?" +T_SUB="How does tmux resolve which session, window and pane a command targets (the -t target)?" +T_BIG="How does render_screen draw the screen?" +T_HYD="How does hydrate restore the state?" +T_RS="How does the indexer walk the module tree breadth-first to index the crate source paths?" +T_CPP="How does the C++ builder build the plan and run its commands?" +T_GO="How does the packager package the files into an archive?" +T_DTS="How does negotiate pick the content type?" +T_DTS2="How does handshake upgrade the socket?" +T_TEST="How does roundtrip encode a token string?" +T_TWO="How does render produce the view markup?" +T_REF="How does refresh repaint a widget?" +T_STRUCT="How is the screen struct laid out?" +T_WHERE="Where does the JWT middleware verify a token?" +T_SHOW="show documentation of the JWT middleware token check" +T_NOCUE="JWT middleware token verification" + +# key|task — every question the gate asks, answered with the slot on and with RIPWIRE_NO_BODY_SLOT=1 +QUESTIONS="d|$T_D +py|$T_PY +c|$T_C +sub|$T_SUB +big|$T_BIG +hyd|$T_HYD +rs|$T_RS +cpp|$T_CPP +go|$T_GO +dts|$T_DTS +dts2|$T_DTS2 +test|$T_TEST +two|$T_TWO +ref|$T_REF +struct|$T_STRUCT +where|$T_WHERE +show|$T_SHOW +nocue|$T_NOCUE" +# fail fast: a binary that cannot answer the first question answers none of the ~90 below (binoverridecheck's sentinel) +run --for="$T_D" >"$TMP/probe.xml" +if ! grep -q ' on the fixture — every arm would be vacuous"; exit 1; fi +while IFS='|' read -r k q; do + run --for="$q" >"$TMP/$k.xml" + runoff --for="$q" >"$TMP/$k.off.xml" + for f in "$k" "$k.off"; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(0) $f.xml: $r — the fixture answered nothing, every arm reading it would be vacuous"; fi + done +done <<<"$QUESTIONS" +run --for="$T_D" --legend=compact >"$TMP/d.c.xml" + +# ── D: the body slot in the default regime ───────────────────────────────────────────────────────────────── +if r="$( chk rootattr "$TMP/d.xml" bundle compact )"; then ok "(D1) T-D premise: $r"; else no "(D1) T-D premise: $r"; fi +if r="$( chk bodycount "$TMP/d.xml" 1 )"; then ok "(D1) T-D: exactly one body, root $r"; else no "(D1) T-D: $r"; fi +if r="$( chk body "$TMP/d.xml" jwt src/middleware/jwt/jwt.ts "getCookie" "HTTPException(401" "ctx.set('jwtPayload'" )"; then ok "(D1) T-D: $r holds the three kill strings"; else no "(D1) T-D: $r"; fi +if r="$( chk callsin "$TMP/d.xml" jwt verify )"; then ok "(D1) T-D: the body keeps (names verify)"; else no "(D1) T-D: $r"; fi +if r="$( chk nobodyof "$TMP/d.xml" verify )"; then ok "(D2) T-D: not verify's body — jwt calls verify (the entry point)"; else no "(D2) T-D: $r"; fi +if r="$( chk estdelta "$TMP/d.xml" "$TMP/d.off.xml" )"; then ok "(D3) T-D: $r"; else no "(D3) T-D: $r"; fi +while IFS='|' read -r k q; do + if r="$( chk sectionsame "$TMP/$k.xml" "$TMP/$k.off.xml" )"; then ok "(D4) $k: sigs/hops/tail identical slot vs RIPWIRE_NO_BODY_SLOT=1 (not paid in rows)"; else no "(D4) $k: $r"; fi + if r="$( chk nobody "$TMP/$k.off.xml" )"; then ok "(D4) $k: RIPWIRE_NO_BODY_SLOT=1 serves no body"; else no "(D4) $k: RIPWIRE_NO_BODY_SLOT=1: $r"; fi +done <<<"$QUESTIONS" +if r="$( chk legend "$TMP/d.xml" 'bodies=1' )"; then ok "(D5) full legend explains bodies=1 (the owner body)"; else no "(D5) full legend: $r"; fi +if r="$( chk bodycount "$TMP/d.c.xml" 1 )"; then ok "(D5) compact dialect: the body rides too"; else no "(D5) compact dialect: $r"; fi +if r="$( chk legend "$TMP/d.c.xml" 'b t= n= p= l=' )"; then ok "(D5) compact legend defines b t= n= p= l= when a body rides"; else no "(D5) compact legend: $r"; fi +for k in where show nocue; do + if r="$( chk rootattr "$TMP/$k.xml" bodies 0 )"; then ok "(D6) $k (no explanatory cue): $r"; else no "(D6) $k: $r"; fi + if r="$( chk nobody "$TMP/$k.xml" )"; then ok "(D6) $k: no element"; else no "(D6) $k: $r"; fi +done + +# postures that never take the slot: identical with the slot on and off +run --for="$T_D" --signatures-only >"$TMP/p.sig.xml" +if r="$( chk nobody "$TMP/p.sig.xml" )" && r2="$( chk noroot "$TMP/p.sig.xml" bodies )"; then ok "(D7) --signatures-only: no body, no bodies="; else no "(D7) --signatures-only: ${r:-} ${r2:-}"; fi +run --for="$T_D" --json >"$TMP/p.json" +if python3 -c "import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if 'bodies' not in d and len(d.get('sigs',[]))>=1 else 1)" "$TMP/p.json"; then + ok "(D7) --json: no bodies key, sigs served (the JSON dialect carries no bodies: named floor)" +else + no "(D7) --json: a bodies key rides the JSON dialect (or no sigs)" +fi +for a in --auto-bodies --detail=1 --format=candidates; do + run --for="$T_D" "$a" >"$TMP/p.on.xml"; runoff --for="$T_D" "$a" >"$TMP/p.off.xml" + if [ -s "$TMP/p.on.xml" ] && cmp -s "$TMP/p.on.xml" "$TMP/p.off.xml"; then ok "(D7) $a: byte-identical with and without the slot"; else no "(D7) $a: differs from RIPWIRE_NO_BODY_SLOT=1 (or empty)"; fi +done +if r="$( chk rootattr "$TMP/p.on.xml" bodies '.*' 2>/dev/null )"; then no "(D7) --format=candidates carries bodies="; else ok "(D7) --format=candidates carries no bodies="; fi +run --for=verify >"$TMP/p.ne.xml"; runoff --for=verify >"$TMP/p.ne.off.xml" +if grep -q 'route="name-exact' "$TMP/p.ne.xml" && cmp -s "$TMP/p.ne.xml" "$TMP/p.ne.off.xml"; then ok "(D7) name-exact --for=verify: auto bundle, identical with and without the slot"; else no "(D7) name-exact: not name-exact, or differs from RIPWIRE_NO_BODY_SLOT=1"; fi + +# languages, owners, near misses +if r="$( chk rowis "$TMP/py.xml" 1 Stylesheet src/textual/css/stylesheet.py )"; then :; else no "(D8) premise: $r"; fi +if r="$( chk body "$TMP/py.xml" apply src/textual/css/stylesheet.py "_check_rule(" "widget.refresh()" )"; then ok "(D8) Python: $r (the method)"; else no "(D8) Python: $r"; fi +if r="$( chk nobodyof "$TMP/py.xml" Stylesheet )"; then ok "(D8) Python: the class Stylesheet (r1) is never the body"; else no "(D8) Python: $r"; fi +if r="$( chk body "$TMP/c.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" "cmd_find_valid_state(" )"; then ok "(D9) C: $r"; else no "(D9) C: $r"; fi +if r="$( chk body "$TMP/sub.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" )"; then ok "(D9b) C subtoken fallback ('target'): $r"; else no "(D9b) C subtoken fallback: $r"; fi +if r="$( chk bodyattr "$TMP/big.xml" render_screen truncated '^1$' )"; then ok "(D10) oversized owner: $r"; else no "(D10) oversized: $r"; fi +if r="$( chk cutatline "$TMP/big.xml" render_screen 6000 )"; then ok "(D10) oversized owner: $r (whole lines, <= 6000 B)"; else no "(D10) oversized: $r"; fi +if nx="$( chk nextof "$TMP/big.xml" render_screen )" && printf '%s' "$nx" | grep -q '^--expand=src/c/render.c:'; then + ok "(D10) oversized owner: next=$nx" + hi="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o 'lines="[0-9]*-[0-9]*' | sed 's/.*-//' )" + l0="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o ' l="[0-9]*"' | grep -o '[0-9]*' )" + if [ -n "$hi" ] && [ -n "$l0" ]; then + run "$nx" >"$TMP/big.next.xml" + if r="$( chk startsat "$TMP/big.next.xml" src/c/render.c $(( l0 + hi )) )"; then ok "(D10) the next= serves the rest, from source line $(( l0 + hi ))"; else no "(D10) next=: $r"; fi + else + no "(D10) could not read lines=/l= off the cut body" + fi +else + no "(D10) oversized: no (${nx:-none})" +fi +if r="$( chk over "$TMP/big.xml" )"; then ok "(D10) oversized owner: no over_ceiling on the root (the guard never fires for the body)"; else no "(D10) $r"; fi +if r="$( chk bodyattr "$TMP/hyd.xml" hydrate over_ceiling '^1$' )"; then ok "(D11) one line over the cap: $r (served whole)"; else no "(D11) one line over the cap: $r"; fi +if r="$( chk utf8whole "$TMP/hyd.xml" hydrate 'restored: true' )"; then ok "(D11) one line over the cap: $r, the whole line"; else no "(D11) $r"; fi +if r="$( chk rowis "$TMP/rs.xml" 1 walk src/rs/indexer.rs )"; then :; else no "(L1) premise: $r"; fi +if r="$( chk body "$TMP/rs.xml" index src/rs/indexer.rs "let order = walk(" )"; then ok "(L1) Rust: $r — the entry point (index calls walk) beats walk at r1"; else no "(L1) Rust: $r"; fi +if r="$( chk nobodyof "$TMP/rs.xml" walk )"; then ok "(L1) Rust: not walk's body"; else no "(L1) Rust: $r"; fi +if r="$( chk rowis "$TMP/cpp.xml" 1 Build src/cpp/build.h )"; then :; else no "(L2) premise: $r"; fi +if r="$( chk body "$TMP/cpp.xml" Build src/cpp/build.cc "cannot make progress" )"; then ok "(L2) C++: $r — the out-of-line definition, not the r1 header declaration"; else no "(L2) C++: $r"; fi +if r="$( chk nobodyat "$TMP/cpp.xml" '\.h$' )" && r2="$( chk nobodyof "$TMP/cpp.xml" Builder )"; then ok "(L2) C++: no header declaration, no class body"; else no "(L2) C++: ${r:-} ${r2:-}"; fi +if r="$( chk body "$TMP/go.xml" Package src/go/packager.go "guessing packager from target file extension" )"; then ok "(L3) Go: $r (the method)"; else no "(L3) Go: $r"; fi +if r="$( chk nobodyof "$TMP/go.xml" Packager )"; then ok "(L3) Go: the type Packager is never the body"; else no "(L3) Go: $r"; fi +if r="$( chk rowis "$TMP/dts.xml" 1 negotiate src/types/global.d.ts )"; then :; else no "(N1) premise: $r"; fi +if r="$( chk body "$TMP/dts.xml" negotiate src/negotiate.ts "negotiate fallback" )"; then ok "(N1) TS: $r — the definition, not the r1 .d.ts declaration"; else no "(N1) TS: $r"; fi +for k in dts dts2; do + if r="$( chk nobodyat "$TMP/$k.xml" '\.d\.ts$' )"; then ok "(N1) $k: no body from a .d.ts declaration"; else no "(N1) $k: $r"; fi +done +if r="$( chk bodycount "$TMP/test.xml" 1 )"; then ok "(N2) the only name owner is a tests/ helper: the subtoken fallback serves one body"; else no "(N2) $r"; fi +if r="$( chk nobodyat "$TMP/test.xml" '(^|/)tests?/' )" && r2="$( chk nobodyof "$TMP/test.xml" roundtrip )"; then ok "(N2) no body from tests/ (roundtrip is a test symbol)"; else no "(N2) ${r:-} ${r2:-}"; fi +if r="$( chk bodycount "$TMP/two.xml" 1 )"; then ok "(N3) two equal owners: one body"; else no "(N3) $r"; fi +if r="$( chk bodyisrow "$TMP/two.xml" render )"; then ok "(N3) two equal owners: $r (the better-ranked)"; else no "(N3) $r"; fi +run --for="$T_TWO" >"$TMP/two2.xml" +if cmp -s "$TMP/two.xml" "$TMP/two2.xml"; then ok "(N3) two equal owners: deterministic x2"; else no "(N3) two equal owners: two runs differ"; fi +if r="$( chk bodycount "$TMP/ref.xml" 1 )"; then ok "(N4) two refresh methods in one file: one body"; else no "(N4) $r"; fi +if r="$( chk bodyisrow "$TMP/ref.xml" refresh )"; then ok "(N4) $r (entity identity: the ranked row's own definition)"; else no "(N4) $r"; fi +if r="$( chk nobodyof "$TMP/struct.xml" screen )"; then ok "(N5) the struct screen (r1) is never the body"; else no "(N5) $r"; fi +while IFS='|' read -r k q; do + if r="$( chk bodykinds "$TMP/$k.xml" )"; then :; else no "(N5) $k: $r"; fi +done <<<"$QUESTIONS" +ok "(N5) every on the fixture answers is fn/method (FAIL lines above otherwise)" + +# ── E: explicit targets (reserve-sections-first) ─────────────────────────────────────────────────────────── +run --for="$T_D" --token-budget=3000 >"$TMP/e3000.xml" +if r="$( chk body "$TMP/e3000.xml" jwt src/middleware/jwt/jwt.ts "getCookie" )"; then ok "(E1) --token-budget=3000: $r"; else no "(E1) --token-budget=3000: $r"; fi +if r="$( chk prefix "$TMP/e3000.xml" "$TMP/d.xml" )"; then ok "(E1) --token-budget=3000: $r"; else no "(E1) --token-budget=3000: $r"; fi +if r="$( chk over "$TMP/e3000.xml" )"; then ok "(E1) --token-budget=3000: no over_ceiling"; else no "(E1) $r"; fi +run --for="$T_D" --token-budget=400 >"$TMP/e400.xml" +if r="$( chk rootattr "$TMP/e400.xml" bodies 0 )" && r2="$( chk rootattr "$TMP/e400.xml" reason budget )"; then ok "(E2) --token-budget=400: bodies=0 reason=budget"; else no "(E2) --token-budget=400: ${r:-} ${r2:-}"; fi +if r="$( chk bodiesnext "$TMP/e400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(E2) --token-budget=400: the skipped slot discloses itself, recoverable: $r"; else no "(E2) --token-budget=400: $r"; fi +if r="$( chk prefix "$TMP/e400.xml" "$TMP/d.xml" )"; then ok "(E2) --token-budget=400: $r"; else no "(E2) --token-budget=400: $r"; fi +prev_rows=0; prev_b=0; mono=1; nflag="" +for n in 300 500 800 1200 2000 3000 5000 8000 1000000; do + run --for="$T_D" --token-budget=$n >"$TMP/s$n.xml"; runoff --for="$T_D" --token-budget=$n >"$TMP/s$n.off.xml" + rws="$( chk nrows "$TMP/s$n.xml" )"; rwo="$( chk nrows "$TMP/s$n.off.xml" )"; bb="$( chk bodybytes "$TMP/s$n.xml" )" + case "$rws$rwo$bb" in *[!0-9]*|'') no "(E3) N=$n: unreadable counts rows=$rws off=$rwo body=$bb"; mono=0; continue;; esac + if ! chk prefix "$TMP/s$n.xml" "$TMP/d.xml" >/dev/null; then no "(E3) N=$n: rows are not a rank prefix of the default rows"; mono=0; fi + if ! chk over "$TMP/s$n.xml" >/dev/null; then no "(E3) N=$n: over_ceiling on the root"; mono=0; fi + [ "$rws" -lt "$prev_rows" ] && { no "(E3) N=$n: rows $rws < $prev_rows at the smaller N"; mono=0; } + [ "$bb" -lt "$prev_b" ] && { no "(E3) N=$n: body $bb B < $prev_b B at the smaller N"; mono=0; } + [ "$rws" -gt "$rwo" ] && { no "(E3) N=$n: rows $rws > slot-off $rwo (the body is reserved first, it never adds rows)"; mono=0; } + prev_rows=$rws; prev_b=$bb; nflag="$nflag $n:$rws/$rwo/${bb}B" +done +if [ "$mono" = 1 ]; then ok "(E3) sweep rows(slot)/rows(off)/body:$nflag — rank prefix, monotone, <= slot-off, no over_ceiling"; fi +cdw=0; cdd=0 +chk cdata "$TMP/s1000000.xml" jwt >"$TMP/cd.wide" 2>/dev/null || cdw=1 +chk cdata "$TMP/d.xml" jwt >"$TMP/cd.default" 2>/dev/null || cdd=1 +if [ "$cdw$cdd" = "00" ] && [ -s "$TMP/cd.wide" ] && cmp -s "$TMP/cd.wide" "$TMP/cd.default"; then ok "(E3) N=1000000: the body equals the default answer's body"; else no "(E3) N=1000000: no body, or it differs from the default answer's"; fi + +# ── MC: MCP for parity ───────────────────────────────────────────────────────────────────────────────────── +mcpfor "$T_D" >"$TMP/m.d.xml"; mcpfor "$T_NOCUE" >"$TMP/m.nocue.xml"; mcpfor "$T_D" '{"budget_tokens":400}' >"$TMP/m.d400.xml" +for f in m.d m.nocue m.d400; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(MC0) MCP $f answered nothing: $r"; fi +done +if r="$( chk body "$TMP/m.d.xml" jwt src/middleware/jwt/jwt.ts "getCookie" "HTTPException(401" "ctx.set('jwtPayload'" )"; then ok "(MC1) MCP for: $r"; else no "(MC1) MCP for: $r"; fi +cdm=0; chk cdata "$TMP/m.d.xml" jwt >"$TMP/cd.mcp" 2>/dev/null || cdm=1 +if [ "$cdm$cdd" = "00" ] && [ -s "$TMP/cd.mcp" ] && cmp -s "$TMP/cd.mcp" "$TMP/cd.default"; then ok "(MC2) MCP for: the body equals the CLI body byte for byte"; else no "(MC2) MCP for: no body, or it differs from the CLI's"; fi +if r="$( chk nobody "$TMP/m.nocue.xml" )"; then ok "(MC3) MCP for, no cue: no body"; else no "(MC3) $r"; fi +if r="$( chk nobodyof "$TMP/m.d400.xml" jwt )" && r2="$( chk bodiesnext "$TMP/m.d400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(MC4) MCP budget_tokens=400: no body, the owner recoverable by next=--expand"; else no "(MC4) MCP budget_tokens=400: ${r:-} ${r2:-}"; fi + +# ── M: mutation controls — the checkers reject canned pre-change shapes ───────────────────────────────────── +printf '%s' 'x' >"$TMP/c1.xml" +if chk body "$TMP/c1.xml" jwt src/middleware/jwt/jwt.ts getCookie >/dev/null 2>&1; then no "(M1) the body checker accepted a canned no-body answer"; else ok "(M1) the body checker rejects a canned no-body answer"; fi +printf '%s' 'x' >"$TMP/c2.xml" +if chk nobodyat "$TMP/c2.xml" '(^|/)tests?/' >/dev/null 2>&1; then no "(M2) the tests/ checker accepted a canned tests/ body"; else ok "(M2) the tests/ checker rejects a canned tests/ body"; fi +printf '%s' 'x' >"$TMP/c3.xml" +if chk bodyattr "$TMP/c3.xml" render_screen truncated '^1$' >/dev/null 2>&1 || chk nextof "$TMP/c3.xml" render_screen >/dev/null 2>&1; then no "(M3) the cut checkers accepted a silent partial body"; else ok "(M3) the cut checkers reject a partial body with no truncated=/next="; fi +sed 's/"$TMP/c4.xml" +if grep -q '/dev/null 2>&1; then no "(M4) the section checker missed a changed "; else ok "(M4) the section checker sees a changed "; fi +else + no "(M4) premise: T-D has no element to mutate" +fi + +# ── X: hygiene ────────────────────────────────────────────────────────────────────────────────────────────── +run --for="$T_D" >"$TMP/d2.xml" +if cmp -s "$TMP/d.xml" "$TMP/d2.xml"; then ok "(X1) T-D deterministic x2"; else no "(X1) T-D differs between two runs"; fi +if command -v xmllint >/dev/null 2>&1; then + for f in d d.c big hyd py rs cpp go m.d e400; do + if xmllint --noout "$TMP/$f.xml" 2>/dev/null; then ok "(X2) $f.xml is well-formed XML"; else no "(X2) $f.xml failed xmllint"; fi + done +else + skip "(X2) xmllint not installed — well-formedness not checked here" +fi +for f in d d.c big hyd e400; do + if r="$( chk nodashes "$TMP/$f.xml" )"; then ok "(X3) $f.xml: no '--' inside an XML comment"; else no "(X3) $f.xml: $r"; fi +done + +# ── I: optional byte identity against the pre-change binary where the slot must not fire ───────────────────── +if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then + RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --no-cache --for="$T_D" 2>/dev/null | strip_at >"$TMP/i.head" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache --for="$T_D" 2>/dev/null | strip_at >"$TMP/i.base" + if [ -s "$TMP/i.base" ] && cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I0) RIPWIRE_NO_BODY_SLOT=1 T-D == the base binary byte for byte"; else no "(I0) RIPWIRE_NO_BODY_SLOT=1 T-D differs from the base binary"; fi + i=0 + while IFS= read -r line; do # one argv per line, arguments separated by '|' (values may hold spaces) + i=$(( i + 1 )) + IFS='|' read -r -a argv <<<"$line" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache "${argv[@]}" 2>/dev/null | strip_at >"$TMP/i.base" + "$BIN" "$FIX" --no-cache "${argv[@]}" 2>/dev/null | strip_at >"$TMP/i.head" + if [ ! -s "$TMP/i.base" ]; then no "(I$i) the base binary answered nothing for: $line"; continue; fi + if cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I$i) byte-identical: $line"; else no "(I$i) differs from the base binary: $line"; fi + done <<'ARGV' +--for=JWT middleware token verification +--for=Where does the JWT middleware verify a token? +--for=How does the JWT middleware verify a token?|--signatures-only +--for=How does the JWT middleware verify a token?|--json +--for=How does the JWT middleware verify a token?|--auto-bodies +--for=How does the JWT middleware verify a token?|--detail=1 +--for=verify +--callers=verify +--expand=src/middleware/jwt/jwt.ts:jwt +ARGV +else + skip "(I) RIPWIRE_BASE_BIN unset or not executable — base-binary byte identity not run here" +fi + +exit $fail diff --git a/test/forbodyslotfix/src/c/cmd-find.c b/test/forbodyslotfix/src/c/cmd-find.c new file mode 100644 index 000000000..f8864b4e3 --- /dev/null +++ b/test/forbodyslotfix/src/c/cmd-find.c @@ -0,0 +1,112 @@ +#include +#include + +struct session { const char *name; }; +struct window { int id; }; +struct window_pane { int id; }; +struct client { struct session *session; }; + +struct cmd_find_state { + struct session *s; + struct window *w; + struct window_pane *wp; + int flags; +}; + +/* The current client of the command queue, used when no -t target is given. */ +static struct client * +cmd_find_current_client(struct cmd_find_state *fs) +{ + (void)fs; + return NULL; +} + +/* Find the session a target names. */ +static int +cmd_find_get_session(struct cmd_find_state *fs, const char *session) +{ + if (session == NULL || *session == '\0') + return (-1); + fs->s = NULL; + return (0); +} + +/* Find the window a target names, inside the session already found. */ +static int +cmd_find_get_window(struct cmd_find_state *fs, const char *window) +{ + if (window == NULL) + return (-1); + fs->w = NULL; + return (0); +} + +/* Find the pane a target names, inside the window already found. */ +static int +cmd_find_get_pane(struct cmd_find_state *fs, const char *pane) +{ + if (pane == NULL) + return (-1); + fs->wp = NULL; + return (0); +} + +/* Is the state complete: session, window and pane all resolved? */ +static int +cmd_find_valid_state(struct cmd_find_state *fs) +{ + return (fs->s != NULL && fs->w != NULL && fs->wp != NULL); +} + +/* Fill in the state from the current client. */ +static int +cmd_find_from_client(struct cmd_find_state *fs, struct client *c) +{ + if (c == NULL || c->session == NULL) + return (-1); + fs->s = c->session; + return (0); +} + +/* + * Resolve the -t target of a command into a session, window and pane: split + * the target on ':' and '.', then look each part up in turn. + */ +int +cmd_find_target(struct cmd_find_state *fs, const char *target, int flags) +{ + struct client *c; + char *copy, *colon, *period; + const char *session, *window, *pane; + + memset(fs, 0, sizeof *fs); + fs->flags = flags; + c = cmd_find_current_client(fs); + if (target == NULL || *target == '\0') { + if (cmd_find_from_client(fs, c) != 0) + return (-1); + return (cmd_find_valid_state(fs) ? 0 : -1); + } + copy = strdup(target); + colon = strchr(copy, ':'); + if (colon != NULL) + *colon++ = '\0'; + period = strchr(colon != NULL ? colon : copy, '.'); + if (period != NULL) + *period++ = '\0'; + session = copy; + window = colon; + pane = period; + if (cmd_find_get_session(fs, session) != 0) + goto error; + if (window != NULL && cmd_find_get_window(fs, window) != 0) + goto error; + if (pane != NULL && cmd_find_get_pane(fs, pane) != 0) + goto error; + free(copy); + return (cmd_find_valid_state(fs) ? 0 : -1); + +error: + free(copy); + return (-1); +} diff --git a/test/forbodyslotfix/src/c/render.c b/test/forbodyslotfix/src/c/render.c new file mode 100644 index 000000000..064c76a34 --- /dev/null +++ b/test/forbodyslotfix/src/c/render.c @@ -0,0 +1,161 @@ +#include + +struct screen { char cells[64][256]; int rows; int cols; }; + +/* Draw the whole screen: clear every row, then paint each cell of each row in turn. */ +void +render_screen(struct screen *s) +{ + int row, col; + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 1 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 2 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 3 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 4 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 5 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 6 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 7 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 8 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 9 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 10 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 11 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 12 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 13 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 14 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 15 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 16 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 17 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 18 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 19 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 20 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 21 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 22 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 23 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 24 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 25 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 26 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 27 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 28 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 29 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 30 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 31 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 32 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 33 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 34 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 35 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 36 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 37 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 38 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 39 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 40 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 41 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 42 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 43 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 44 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 45 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 46 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 47 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 48 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 49 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 50 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 51 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 52 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 53 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 54 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 55 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 56 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 57 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 58 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 59 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 60 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 61 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 62 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 63 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 64 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 65 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 66 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 67 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 68 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 69 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 70 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 71 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 72 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 73 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 74 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 75 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 76 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 77 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 78 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 79 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 80 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 81 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 82 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 83 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 84 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 85 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 86 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 87 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 88 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 89 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 90 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 91 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 92 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 93 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 94 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 95 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 96 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 97 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 98 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 99 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 100 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 101 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 102 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 103 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 104 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 105 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 106 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 107 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 108 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 109 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 110 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 111 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 112 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 113 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 114 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 115 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 116 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 117 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 118 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 119 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 120 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 121 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 122 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 123 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 124 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 125 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 126 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 127 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 128 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 129 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 130 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 131 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 132 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 133 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 134 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 135 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 136 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 137 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 138 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 139 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 140 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 141 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 142 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 143 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 144 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 145 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 146 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 147 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 148 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 149 + col) % 90)); } } + for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 150 + col) % 90)); } } + memset(s->cells[0], 32, sizeof s->cells[0]); +} diff --git a/test/forbodyslotfix/src/context.ts b/test/forbodyslotfix/src/context.ts new file mode 100644 index 000000000..3041d029f --- /dev/null +++ b/test/forbodyslotfix/src/context.ts @@ -0,0 +1,18 @@ +export interface HonoRequest { + url: string + header(name: string): string | undefined +} + +export class Context { + req: HonoRequest + #var: Map = new Map() + constructor(req: HonoRequest) { + this.req = req + } + set(key: string, value: unknown): void { + this.#var.set(key, value) + } + get(key: string): unknown { + return this.#var.get(key) + } +} diff --git a/test/forbodyslotfix/src/cpp/build.cc b/test/forbodyslotfix/src/cpp/build.cc new file mode 100644 index 000000000..5cc5b12cc --- /dev/null +++ b/test/forbodyslotfix/src/cpp/build.cc @@ -0,0 +1,44 @@ +#include "build.h" + +Edge* Plan::FindWork() { + if (ready_.empty()) + return nullptr; + Edge* e = ready_.back(); + ready_.pop_back(); + return e; +} + +void Plan::EdgeFinished(Edge* edge, bool success) { + edge->outputs_ready = success; +} + +bool Builder::AddTarget(const std::string& name, std::string* err) { + if (name.empty()) { + *err = "unknown target ''"; + return false; + } + return true; +} + +bool Builder::RunCommand(Edge* edge) { + return !edge->command.empty(); +} + +// Run every ready edge of the plan; stop once failures_allowed commands have failed. +bool Builder::Build(std::string* err) { + int failures = 0; + while (plan_.more_to_do()) { + Edge* edge = plan_.FindWork(); + if (edge == nullptr) { + *err = "stuck [this is a bug]: cannot make progress"; + return false; + } + bool ok = RunCommand(edge); + plan_.EdgeFinished(edge, ok); + if (!ok && ++failures >= failures_allowed_) { + *err = failures_allowed_ > 1 ? "subcommands failed" : "subcommand failed"; + return false; + } + } + return true; +} diff --git a/test/forbodyslotfix/src/cpp/build.h b/test/forbodyslotfix/src/cpp/build.h new file mode 100644 index 000000000..d0bc76b57 --- /dev/null +++ b/test/forbodyslotfix/src/cpp/build.h @@ -0,0 +1,32 @@ +// Builder: runs the edges of a build plan until the requested targets are up to date. +#pragma once +#include +#include + +struct Edge { + std::string command; + std::vector outputs; + bool outputs_ready = false; +}; + +class Plan { +public: + bool more_to_do() const { return !ready_.empty(); } + Edge* FindWork(); + void EdgeFinished(Edge* edge, bool success); +private: + std::vector ready_; +}; + +class Builder { +public: + explicit Builder(int failures_allowed) : failures_allowed_(failures_allowed) {} + bool AddTarget(const std::string& name, std::string* err); + // Build the plan: run every ready edge's commands until the targets are up to date; + // the builder stops once failures_allowed commands have failed. + bool Build(std::string* err); +private: + Plan plan_; + int failures_allowed_; + bool RunCommand(Edge* edge); +}; diff --git a/test/forbodyslotfix/src/go/packager.go b/test/forbodyslotfix/src/go/packager.go new file mode 100644 index 000000000..8990d1ef5 --- /dev/null +++ b/test/forbodyslotfix/src/go/packager.go @@ -0,0 +1,38 @@ +package packager + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strings" +) + +var errInsufficientParams = errors.New("a target file or a packager name is required") + +// Packager turns a file list into one archive format. +type Packager struct { + Format string + Files []string +} + +// Package writes the archive for info: it picks the format, then writes every file into the archive. +func (p *Packager) Package(target string, w io.Writer) error { + if target == "" && p.Format == "" { + return errInsufficientParams + } + if p.Format == "" { + fmt.Println("guessing packager from target file extension...") + p.Format = strings.TrimPrefix(filepath.Ext(target), ".") + } + return writeArchive(p.Format, p.Files, w) +} + +func writeArchive(format string, files []string, w io.Writer) error { + for _, f := range files { + if _, err := fmt.Fprintf(w, "%s:%s\n", format, f); err != nil { + return err + } + } + return nil +} diff --git a/test/forbodyslotfix/src/helper/cookie.ts b/test/forbodyslotfix/src/helper/cookie.ts new file mode 100644 index 000000000..d0c8d2666 --- /dev/null +++ b/test/forbodyslotfix/src/helper/cookie.ts @@ -0,0 +1,25 @@ +import type { Context } from '../context' + +export const getCookie = (c: Context, key: string): string | undefined => { + const cookie = c.req.header('Cookie') + if (!cookie) { + return undefined + } + const pairs = cookie.split(';') + for (const pair of pairs) { + const [k, v] = pair.trim().split('=') + if (k === key) { + return v + } + } + return undefined +} + +export const getSignedCookie = async (c: Context, secret: string, key: string): Promise => { + const value = getCookie(c, key) + if (value === undefined) { + return undefined + } + const [val, sig] = value.split('.') + return sig === `${secret.length}` ? val : false +} diff --git a/test/forbodyslotfix/src/http-exception.ts b/test/forbodyslotfix/src/http-exception.ts new file mode 100644 index 000000000..52d696d54 --- /dev/null +++ b/test/forbodyslotfix/src/http-exception.ts @@ -0,0 +1,9 @@ +export class HTTPException extends Error { + readonly res?: Response + readonly status: number + constructor(status: number = 500, options?: { res?: Response; message?: string; cause?: unknown }) { + super(options?.message, { cause: options?.cause }) + this.res = options?.res + this.status = status + } +} diff --git a/test/forbodyslotfix/src/middleware/jwt/jwt.ts b/test/forbodyslotfix/src/middleware/jwt/jwt.ts new file mode 100644 index 000000000..43e533f44 --- /dev/null +++ b/test/forbodyslotfix/src/middleware/jwt/jwt.ts @@ -0,0 +1,78 @@ +import type { Context } from '../../context' +import { getCookie, getSignedCookie } from '../../helper/cookie' +import { HTTPException } from '../../http-exception' +import type { MiddlewareHandler } from '../../types' +import * as Jwt from '../../utils/jwt/jwt' + +export type JwtVariables = { + jwtPayload: unknown +} + +/** JWT middleware: reads the token from the Authorization header or a cookie and verifies it. */ +export const jwt = (options: { + secret: string + cookie?: string | { key: string; secret?: string } + alg?: string + headerName?: string +}): MiddlewareHandler => { + if (!options || !options.secret) { + throw new Error('JWT auth middleware requires options for "secret"') + } + const headerName = options.headerName || 'Authorization' + return async function jwt(ctx, next) { + const headerToken = ctx.req.header(headerName) + let token + if (headerToken) { + const parts = headerToken.split(/\s+/) + if (parts.length !== 2) { + const errDescription = 'invalid credentials structure' + throw new HTTPException(401, { + message: errDescription, + res: unauthorizedResponse({ ctx, error: 'invalid_request', errDescription }), + }) + } + token = parts[1] + } else if (options.cookie) { + if (typeof options.cookie == 'string') { + token = getCookie(ctx, options.cookie) + } else if (options.cookie.secret) { + token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key) + } else { + token = getCookie(ctx, options.cookie.key) + } + } + if (!token) { + const errDescription = 'no authorization included in request' + throw new HTTPException(401, { + message: errDescription, + res: unauthorizedResponse({ ctx, error: 'invalid_request', errDescription }), + }) + } + let payload + let cause + try { + payload = await Jwt.verify(token, options.secret, options.alg || 'HS256') + } catch (e) { + cause = e + } + if (!payload) { + throw new HTTPException(401, { + message: 'Unauthorized', + res: unauthorizedResponse({ ctx, error: 'invalid_token', statusText: 'Unauthorized', errDescription: 'token verification failure' }), + cause, + }) + } + ctx.set('jwtPayload', payload) + await next() + } +} + +function unauthorizedResponse(opts: { ctx: Context; error: string; errDescription: string; statusText?: string }) { + return new Response('Unauthorized', { + status: 401, + statusText: opts.statusText, + headers: { + 'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`, + }, + }) +} diff --git a/test/forbodyslotfix/src/min/hydrate.js b/test/forbodyslotfix/src/min/hydrate.js new file mode 100644 index 000000000..c05138237 --- /dev/null +++ b/test/forbodyslotfix/src/min/hydrate.js @@ -0,0 +1,2 @@ +/** Restore the persisted UI state (a minified one-line body, kept as shipped). */ +export function hydrate(state) { const table = 'é→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→üé→ü'; return Object.assign({}, state, { table, restored: true }) } diff --git a/test/forbodyslotfix/src/negotiate.ts b/test/forbodyslotfix/src/negotiate.ts new file mode 100644 index 000000000..6d52fd377 --- /dev/null +++ b/test/forbodyslotfix/src/negotiate.ts @@ -0,0 +1,7 @@ +/** Content negotiation: pick a response type from the Accept header. */ +export function negotiate(req: Request): string { + const accept = req.headers.get('accept') || '*/*' + if (accept.includes('application/json')) return 'json' + if (accept.includes('text/html')) return 'html' + return 'text' // negotiate fallback when nothing in Accept matches +} diff --git a/test/forbodyslotfix/src/rs/indexer.rs b/test/forbodyslotfix/src/rs/indexer.rs new file mode 100644 index 000000000..ebad8e0bf --- /dev/null +++ b/test/forbodyslotfix/src/rs/indexer.rs @@ -0,0 +1,57 @@ +//! Indexer: walks a crate's module tree and records every item it finds. + +use std::collections::HashMap; + +pub struct Item { + pub name: String, + pub kind: &'static str, + pub line: u32, +} + +pub struct Indexer { + items: HashMap, + skipped: u32, +} + +impl Indexer { + pub fn new() -> Self { + Indexer { items: HashMap::new(), skipped: 0 } + } + + /// Index a crate: walk its modules, keep the public items, count what was skipped. + pub fn index(&mut self, root: &str, sources: &[(&str, &str)]) -> usize { + let order = walk(root, sources); + for path in order { + let text = sources.iter().find(|(p, _)| *p == path).map(|(_, t)| *t).unwrap_or(""); + for (n, line) in text.lines().enumerate() { + if let Some(rest) = line.trim().strip_prefix("pub fn ") { + let name = rest.split('(').next().unwrap_or("").to_string(); + self.items.insert(name.clone(), Item { name, kind: "fn", line: n as u32 + 1 }); + } else if line.trim().starts_with("fn ") { + self.skipped += 1; // private items are not part of the crate surface + } + } + } + if self.items.is_empty() { + eprintln!("indexer: crate {} has no public surface", root); + } + self.items.len() + } +} + +/// Walk the module tree breadth-first from the crate root and return the source paths in visit order. +pub fn walk(root: &str, sources: &[(&str, &str)]) -> Vec { + let mut out = vec![root.to_string()]; + let mut i = 0; + while i < out.len() { + let here = out[i].clone(); + let text = sources.iter().find(|(p, _)| *p == here).map(|(_, t)| *t).unwrap_or(""); + for line in text.lines() { + if let Some(m) = line.trim().strip_prefix("mod ") { + out.push(format!("{}/{}.rs", here.trim_end_matches(".rs"), m.trim_end_matches(';'))); + } + } + i += 1; + } + out +} diff --git a/test/forbodyslotfix/src/textual/css/stylesheet.py b/test/forbodyslotfix/src/textual/css/stylesheet.py new file mode 100644 index 000000000..4c00f19b9 --- /dev/null +++ b/test/forbodyslotfix/src/textual/css/stylesheet.py @@ -0,0 +1,97 @@ +"""Stylesheet: parses CSS sources into rule sets and applies them to widgets.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class RuleSet: + selector: str + declarations: dict[str, str] = field(default_factory=dict) + specificity: int = 0 + + +def tokenize(css: str) -> list[str]: + """Split CSS text into tokens.""" + tokens: list[str] = [] + current = "" + for ch in css: + if ch in "{}:;": + if current.strip(): + tokens.append(current.strip()) + tokens.append(ch) + current = "" + else: + current += ch + if current.strip(): + tokens.append(current.strip()) + return tokens + + +def parse(css: str) -> list[RuleSet]: + """Parse CSS text into rule sets (parse_selectors + parse_declarations).""" + rules: list[RuleSet] = [] + tokens = tokenize(css) + i = 0 + while i < len(tokens): + selector = tokens[i] + i += 1 + if i < len(tokens) and tokens[i] == "{": + i += 1 + declarations: dict[str, str] = {} + while i < len(tokens) and tokens[i] != "}": + name = tokens[i] + if i + 2 < len(tokens) and tokens[i + 1] == ":": + declarations[name] = tokens[i + 2] + i += 3 + else: + i += 1 + if i < len(tokens) and tokens[i] == ";": + i += 1 + i += 1 + rules.append(RuleSet(selector, declarations, specificity=len(selector))) + return rules + + +class Stylesheet: + """Holds the rules parsed from every CSS source and applies them to widgets.""" + + def __init__(self) -> None: + self.rules: list[RuleSet] = [] + self.sources: list[str] = [] + + def add_source(self, css: str) -> None: + self.sources.append(css) + + def parse(self) -> None: + self.rules = [] + for source in self.sources: + self.rules.extend(parse(source)) + + def apply(self, widget: Widget) -> None: + """Apply the parsed rules to one widget: match selectors, order by specificity, set styles.""" + matched = [rule for rule in self.rules if self._check_rule(rule, widget)] + matched.sort(key=lambda rule: rule.specificity) + for rule in matched: + for name, value in rule.declarations.items(): + widget.styles[name] = value + widget.refresh() + + def _check_rule(self, rule: RuleSet, widget: Widget) -> bool: + return rule.selector == widget.css_type or rule.selector in widget.classes + + def update_nodes(self, widgets: list[Widget]) -> None: + for widget in widgets: + self.apply(widget) + + +class Widget: + css_type = "Widget" + + def __init__(self) -> None: + self.styles: dict[str, str] = {} + self.classes: set[str] = set() + + def refresh(self) -> None: + pass diff --git a/test/forbodyslotfix/src/textual/widgets.py b/test/forbodyslotfix/src/textual/widgets.py new file mode 100644 index 000000000..28f4dbe5d --- /dev/null +++ b/test/forbodyslotfix/src/textual/widgets.py @@ -0,0 +1,27 @@ +"""Widgets that repaint themselves when their content changes.""" + + +class Label: + """A single line of text.""" + + def __init__(self, text: str) -> None: + self.text = text + self.dirty = True + + def refresh(self) -> None: + """Repaint the label: clear the dirty flag and redraw the text.""" + self.dirty = False + print("label repaint", self.text) + + +class Button: + """A clickable label with a border.""" + + def __init__(self, text: str) -> None: + self.text = text + self.dirty = True + + def refresh(self) -> None: + """Repaint the button: redraw the border, then the text.""" + self.dirty = False + print("button repaint border", self.text) diff --git a/test/forbodyslotfix/src/two/alpha.ts b/test/forbodyslotfix/src/two/alpha.ts new file mode 100644 index 000000000..79a5911b7 --- /dev/null +++ b/test/forbodyslotfix/src/two/alpha.ts @@ -0,0 +1,7 @@ +export interface View { title: string; items: string[] } + +/** Render a view as a list: one
  • per item under a heading. */ +export function render(view: View): string { + const items = view.items.map((i) => `
  • ${i}
  • `).join('') + return `

    ${view.title}

      ${items}
    ` + '' +} diff --git a/test/forbodyslotfix/src/two/beta.ts b/test/forbodyslotfix/src/two/beta.ts new file mode 100644 index 000000000..8e2a2f32a --- /dev/null +++ b/test/forbodyslotfix/src/two/beta.ts @@ -0,0 +1,7 @@ +import type { View } from './alpha' + +/** Render a view as a table: one per item under a caption. */ +export function render(view: View): string { + const rows = view.items.map((i) => `${i}`).join('') + return `${rows}
    ${view.title}
    ` + '' +} diff --git a/test/forbodyslotfix/src/types/global.d.ts b/test/forbodyslotfix/src/types/global.d.ts new file mode 100644 index 000000000..bfc5de7ad --- /dev/null +++ b/test/forbodyslotfix/src/types/global.d.ts @@ -0,0 +1,4 @@ +/** Ambient declarations for the server runtime. */ +/** negotiate: pick the content type for a response from the request's Accept header (json, html or text). */ +export function negotiate(req: Request): string +export declare function handshake(socket: WebSocket, protocol: string): Promise diff --git a/test/forbodyslotfix/src/utils/encode.ts b/test/forbodyslotfix/src/utils/encode.ts new file mode 100644 index 000000000..6af83169f --- /dev/null +++ b/test/forbodyslotfix/src/utils/encode.ts @@ -0,0 +1,2 @@ +export const encodeBase64Url = (s: string): string => btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +export const decodeBase64Url = (s: string): string => atob(s.replace(/-/g, '+').replace(/_/g, '/')) diff --git a/test/forbodyslotfix/src/utils/jwt/jwt.ts b/test/forbodyslotfix/src/utils/jwt/jwt.ts new file mode 100644 index 000000000..fed22acb8 --- /dev/null +++ b/test/forbodyslotfix/src/utils/jwt/jwt.ts @@ -0,0 +1,95 @@ +import { decodeBase64Url, encodeBase64Url } from '../encode' +import type { JWTPayload } from './types' +import { + JwtTokenExpired, + JwtTokenInvalid, + JwtTokenIssuedAt, + JwtTokenNotBefore, + JwtTokenSignatureMismatched, +} from './types' + +export interface TokenHeader { + alg: string + typ?: 'JWT' + kid?: string +} + +export interface VerifyOptions { + nbf?: boolean + exp?: boolean + iat?: boolean +} + +export const sign = async (payload: JWTPayload, privateKey: string, alg: string = 'HS256'): Promise => { + const encodedPayload = encodeBase64Url(JSON.stringify(payload)) + const encodedHeader = encodeBase64Url(JSON.stringify({ alg, typ: 'JWT' })) + const partialToken = `${encodedHeader}.${encodedPayload}` + const signaturePart = await signing(privateKey, alg, partialToken) + return `${partialToken}.${signaturePart}` +} + +/** Verify a token: decode it, check the algorithm, the nbf/exp/iat claims, then the signature. */ +export const verify = async ( + token: string, + publicKey: string, + algOrOptions: string | VerifyOptions = 'HS256' +): Promise => { + const alg = typeof algOrOptions === 'string' ? algOrOptions : 'HS256' + const tokenParts = token.split('.') + if (tokenParts.length !== 3) { + throw new JwtTokenInvalid(token) + } + const { header, payload } = decode(token) + if (!isTokenHeader(header)) { + throw new JwtTokenInvalid(token) + } + if (header.alg !== alg) { + throw new JwtTokenInvalid(token) + } + const now = Math.floor(Date.now() / 1000) + if (payload.nbf && payload.nbf > now) { + throw new JwtTokenNotBefore(token) + } + if (payload.exp && payload.exp <= now) { + throw new JwtTokenExpired(token) + } + if (payload.iat && now < payload.iat) { + throw new JwtTokenIssuedAt(now, payload.iat) + } + const headerPayload = token.substring(0, token.lastIndexOf('.')) + const verified = await verifying(publicKey, alg, decodeBase64Url(tokenParts[2]), headerPayload) + if (!verified) { + throw new JwtTokenSignatureMismatched(token) + } + return payload +} + +export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => { + try { + const [h, p] = token.split('.') + const header = decodeJwtPart(h) as TokenHeader + const payload = decodeJwtPart(p) as JWTPayload + return { header, payload } + } catch { + throw new JwtTokenInvalid(token) + } +} + +export const decodeHeader = (token: string): TokenHeader => { + try { + const [h] = token.split('.') + return decodeJwtPart(h) as TokenHeader + } catch { + throw new JwtTokenInvalid(token) + } +} + +const decodeJwtPart = (part: string): unknown => JSON.parse(decodeBase64Url(part)) + +const isTokenHeader = (obj: unknown): obj is TokenHeader => + typeof obj === 'object' && obj !== null && 'alg' in obj + +const signing = async (key: string, alg: string, data: string): Promise => `${alg}:${key.length}:${data.length}` + +const verifying = async (key: string, alg: string, signature: string, data: string): Promise => + signature === `${alg}:${key.length}:${data.length}` diff --git a/test/forbodyslotfix/src/utils/jwt/types.ts b/test/forbodyslotfix/src/utils/jwt/types.ts new file mode 100644 index 000000000..5d7db861b --- /dev/null +++ b/test/forbodyslotfix/src/utils/jwt/types.ts @@ -0,0 +1,54 @@ +export class JwtAlgorithmNotImplemented extends Error { + constructor(alg: string) { + super(`${alg} is not an implemented algorithm`) + this.name = 'JwtAlgorithmNotImplemented' + } +} + +export class JwtTokenInvalid extends Error { + constructor(token: string) { + super(`invalid JWT token: ${token}`) + this.name = 'JwtTokenInvalid' + } +} + +export class JwtTokenNotBefore extends Error { + constructor(token: string) { + super(`token (${token}) is being used before it's valid`) + this.name = 'JwtTokenNotBefore' + } +} + +export class JwtTokenExpired extends Error { + constructor(token: string) { + super(`token (${token}) expired`) + this.name = 'JwtTokenExpired' + } +} + +export class JwtTokenIssuedAt extends Error { + constructor(currentTimestamp: number, iat: number) { + super(`Incorrect "iat" claim must be a older than "${currentTimestamp}" (iat: "${iat}")`) + this.name = 'JwtTokenIssuedAt' + } +} + +export class JwtTokenSignatureMismatched extends Error { + constructor(token: string) { + super(`token(${token}) signature mismatched`) + this.name = 'JwtTokenSignatureMismatched' + } +} + +export enum AlgorithmTypes { + HS256 = 'HS256', + HS384 = 'HS384', + HS512 = 'HS512', +} + +export interface JWTPayload { + [key: string]: unknown + exp?: number + nbf?: number + iat?: number +} diff --git a/test/forbodyslotfix/tests/cmd-find-test.c b/test/forbodyslotfix/tests/cmd-find-test.c new file mode 100644 index 000000000..7d4132577 --- /dev/null +++ b/test/forbodyslotfix/tests/cmd-find-test.c @@ -0,0 +1,18 @@ +#include + +struct cmd_find_state; +int cmd_find_target(struct cmd_find_state *fs, const char *target, int flags); + +/* How does the command resolve the -t target: session, window and pane. */ +static void +test_cmd_find_target_resolves_session_window_pane(void) +{ + assert(cmd_find_target(0, "", 0) == -1); +} + +int +main(void) +{ + test_cmd_find_target_resolves_session_window_pane(); + return 0; +} diff --git a/test/forbodyslotfix/tests/test_stylesheet.py b/test/forbodyslotfix/tests/test_stylesheet.py new file mode 100644 index 000000000..8a47f05a1 --- /dev/null +++ b/test/forbodyslotfix/tests/test_stylesheet.py @@ -0,0 +1,18 @@ +"""Tests: how is CSS parsed and applied to widgets?""" + +from src.textual.css.stylesheet import Stylesheet, Widget, parse + + +def test_css_parsed_and_applied_to_widgets() -> None: + """How is CSS parsed and applied to widgets: parse then apply.""" + sheet = Stylesheet() + sheet.add_source("Widget { color: red; }") + sheet.parse() + widget = Widget() + sheet.apply(widget) + assert widget.styles["color"] == "red" + + +def test_parse_css_rules() -> None: + rules = parse("Widget { color: red; }") + assert rules[0].selector == "Widget" diff --git a/test/forbodyslotfix/tests/test_tokens.py b/test/forbodyslotfix/tests/test_tokens.py new file mode 100644 index 000000000..a23a890db --- /dev/null +++ b/test/forbodyslotfix/tests/test_tokens.py @@ -0,0 +1,11 @@ +"""Round-trip tests for the token encoder.""" + + +def roundtrip(text: str) -> str: + """Encode then decode a token string (test helper).""" + encoded = text.encode("utf-8").hex() + return bytes.fromhex(encoded).decode("utf-8") + + +def test_roundtrip_ascii() -> None: + assert roundtrip("abc") == "abc" diff --git a/test/regression.sh b/test/regression.sh index 136fb5a76..6f79379e2 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From f278ac585c4c5ca6262d7fde33b05098ce4e881b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 12:52:53 -0400 Subject: [PATCH 110/162] =?UTF-8?q?test:=20FIX1=20=E2=80=94=20ST=20maximal?= =?UTF-8?q?ity=20counts=20the=20tail=20at=20no=20less=20than=20the=20no-bu?= =?UTF-8?q?dget=20answer's=20tail;=20forsoftbudgetcheck=20sources=20clean-?= =?UTF-8?q?env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R1's fix reserves the tail at its HEAD-scoped size (the no-budget answer's tail) when that binds, so the rows past the cut stop at T less that reserve, not less the cut-scoped tail actually served. ST's maximality bound (fornotesbudgetcheck, forsoftbudgetcheck, compactlegendcheck, estchargecheck, forsigspancheck) now floors U's tail at the reference answer's tail: an upper bound on the reserve, so maximality stays a real bound (rule 4: by-design, stated in each gate); the fits-T half is unchanged. fornotesbudgetcheck 0 FAIL (was 4: ST @3000, the 700..2000 sweep, the no-notes tree @900), forsoftbudgetcheck / forsigspancheck green. gitenvhermeticcheck (D): forsoftbudgetcheck sources test/lib/clean-env.sh. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 12 ++++++++++++ test/estchargecheck.sh | 10 ++++++++++ test/fornotesbudgetcheck.sh | 12 ++++++++++++ test/forsigspancheck.sh | 10 ++++++++++ test/forsoftbudgetcheck.sh | 11 +++++++++++ 5 files changed, 55 insertions(+) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 67ab994ae..bba95ab00 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -1175,7 +1175,19 @@ def sections_u( s ): a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ); k = len( a[ "rows" ] ) +def tail_size( t ): + # FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, so U + # counts the tail at no less than the reference tail (an upper bound on the reserve: the maximality check stays a bound) + import json as _j + if t.lstrip().startswith( "{" ): + try: return len( _j.dumps( _j.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + m = re.search( r"", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 U = sections_u( a[ "s" ] ) +if len( sys.argv ) > 5: + try: U += max( 0, tail_size( open( sys.argv[ 5 ], encoding = "utf-8", errors = "replace" ).read() ) - tail_size( a[ "s" ] ) ) + except OSError: pass open( sys.argv[ 4 ], "w" ).write( "".join( x[ 1 ] + "\n" for x in a[ "rows" ] ) ) cut = k < a[ "total" ] fits = ( a[ "charged" ] <= X or k <= 4 ) and k >= min( 4, a[ "total" ] ) diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 9aafe7918..d63d28197 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -689,6 +689,16 @@ def verdict( path, n, refpath=None ): s = open( path, encoding='utf-8', errors='replace' ).read() U = ( len( s.split( '
    ', 1 )[ 1 ].encode() ) - len( '' ) ) if '' in s else 0 U += sum( len( h.encode() ) for h in re.findall( r']*/>', s.split( '", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 + U += max( 0, tail_size( open( refpath, encoding='utf-8', errors='replace' ).read() ) - tail_size( s ) ) root = re.match( r']*>', s ) if root and ' bundle="' not in root.group( 0 ): bm = re.search( r'', s, re.S ) diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index 971562790..25a77edbb 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -328,7 +328,19 @@ def sections_u( s ): a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ) +def tail_size( t ): + # FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, so U + # counts the tail at no less than the reference tail (an upper bound on the reserve: the maximality check stays a bound) + import json as _j + if t.lstrip().startswith( "{" ): + try: return len( _j.dumps( _j.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + m = re.search( r"", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 U = sections_u( open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() ) +if len( sys.argv ) > 4: + _at = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read(); _rt = open( sys.argv[ 4 ], encoding='utf-8', errors='replace' ).read() + U += max( 0, tail_size( _rt ) - tail_size( _at ) ) open( sys.argv[ 3 ], 'w' ).write( "".join( x[ 1 ] + "\n" for x in a[ 'rows' ] ) ) k = len( a[ 'rows' ] ) fits = ( a[ 'charged' ] <= X or k <= 4 ) and k >= min( 4, a[ 'total' ] ) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 72b213dd7..2b32415af 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -470,6 +470,16 @@ if mode == "tight": else: U = ( len( A.split( "", 1 )[ 1 ].encode() ) - len( "" ) if "" in A else 0 ) U += sum( len( h.encode() ) for h in re.findall( r']*/>', A.split( "", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 + if len( sys.argv ) > 7: U += max( 0, tail_size( body( sys.argv[ 7 ] ) ) - tail_size( A ) ) if a is None or ref is None or not ref[ 0 ]: bad.append( "ST: no rows to charge (premise: answer and uncut reference)" ) elif tb > 0: k = len( a[ 0 ] ); fits = a[ 1 ] <= T or k <= 4 diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 162a49be4..f03cfb80c 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -52,6 +52,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # gitenvhermeticcheck (D): no ambient GIT_* reaches the RP10 fixture repos BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" fail=0 @@ -445,6 +446,16 @@ a, ref, T = rb( sys.argv[ 1 ] ), rb( sys.argv[ 2 ] ), int( sys.argv[ 3 ] ) ans = open( sys.argv[ 1 ] ).read() U = ( len( ans.split( '', 1 )[ 1 ].encode() ) - len( '' ) if '' in ans else 0 ) U += sum( len( h.encode() ) for h in re.findall( r']*/>', ans.split( '", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 +U += max( 0, tail_size( open( sys.argv[ 2 ] ).read() ) - tail_size( ans ) ) if a is None or ref is None: print( "ERR no " ); sys.exit( 0 ) ab, ar = a; rr = ref[ 1 ] k = len( ar ) From 94d1f61fee931758747d634335c2965f7edac5e0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 13:01:07 -0400 Subject: [PATCH 111/162] test: gitchildoptscheck ok() sets the accumulator when its PASS line cannot be written Follows the reporter contract gateexitcheck G1 enforces, as the other git-hardening gates already do. Co-Authored-By: Claude Opus 5.5 --- test/gitchildoptscheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gitchildoptscheck.sh b/test/gitchildoptscheck.sh index 3c3523d9c..87e7e6112 100755 --- a/test/gitchildoptscheck.sh +++ b/test/gitchildoptscheck.sh @@ -52,7 +52,7 @@ BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" BASE="${RIPWIRE_BASE_BIN:-}" fail=0 -ok(){ printf ' PASS %s\n' "$*"; return 0; } +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; return 0; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } From 31531b1dcc1134bf46d9806f191367d0101ea598 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 13:07:41 -0400 Subject: [PATCH 112/162] test: qchurnmemocheck matches the churn diff spawn by shape, not one spelling The churn hunk diff now carries extra neutralising options between the subcommand and --unified=0. Match the diff subcommand followed by long options and --unified=0, with a planted-log control: three duplicate spawns of one path count as 3/1 and near-miss spawns (log -p, diff --stat, diff-tree, a -c diff.* value) do not count. A memo-bypass mutation still turns the one-spawn-per-path arm red (8 spawns for 1 path). Co-Authored-By: Claude Opus 5.5 --- test/qchurnmemocheck.sh | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/qchurnmemocheck.sh b/test/qchurnmemocheck.sh index 5d9df07f3..390a8c063 100755 --- a/test/qchurnmemocheck.sh +++ b/test/qchurnmemocheck.sh @@ -73,14 +73,35 @@ CHURN="$( printf '%s' "$OUT" | tr '<' '\n' | grep -c 'kind="short-horizon-churn" && ok "fixture produces $CHURN short-horizon-churn rows on one file (non-vacuous)" \ || { no "fixture produced $CHURN churn rows — the memo check would be vacuous"; printf '%s' "$OUT" | tr '<' '\n' | grep '^r kind=' | head -5; } -SPAWNS="$( grep -c 'diff --unified=0' "$LOG" )" -PATHS="$( grep 'diff --unified=0' "$LOG" | sed 's/.* -- //' | sort -u | grep -c . )" +# The spawn is matched by its SHAPE, not one exact spelling: the git subcommand `diff`, then only long options +# (the binary passes its own neutralising options such as --no-ext-diff / --no-textconv here), then +# --unified=0. A `-c diff.x=y` value, a `log -p --unified=0` or a `diff --stat` is not this spawn. +DIFFRE='(^| )diff( --[^ ]+)* --unified=0( |$)' +spawns_in(){ grep -cE "$DIFFRE" "$1"; } +paths_in(){ grep -E "$DIFFRE" "$1" | sed 's/.* -- //' | sort -u | grep -c .; } + +# matcher control: a planted log with the pre-memo shape (three identical spawns for ONE path) plus near-miss +# spawns that must not count. If the matcher cannot see the duplicates, the (b) arm below proves nothing. +CTL="$WORK/ctl.txt" +{ printf '%s\n' "-c core.quotepath=false -C /r diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c core.quotepath=false -C /r diff --no-ext-diff --no-textconv --ignore-submodules=dirty --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c core.quotepath=false -C /r diff --unified=0 --no-color HEAD -- src/f.cpp" \ + "-c diff.algorithm=myers -C /r log -p --unified=0 HEAD -- src/g.cpp" \ + "-C /r diff --stat HEAD -- src/h.cpp" \ + "-C /r diff-tree --unified=0 HEAD -- src/i.cpp"; } > "$CTL" +CS="$( spawns_in "$CTL" )"; CP="$( paths_in "$CTL" )" +[ "$CS" = 3 ] && [ "$CP" = 1 ] \ + && ok "(control) the spawn matcher counts 3 pre-memo duplicates of one path and none of 3 near-miss spawns" \ + || no "(control) the spawn matcher saw $CS spawn(s) / $CP path(s) in the planted log, expected 3 / 1" + +SPAWNS="$( spawns_in "$LOG" )" +PATHS="$( paths_in "$LOG" )" [ "$SPAWNS" -gt 0 ] && ok "the churn SELF/AMBIENT classifier ran (${SPAWNS} unified=0 diff spawn(s) logged)" \ || no "no 'git diff --unified=0' spawn logged — the shim or the fixture is wrong" [ "$SPAWNS" -eq "$PATHS" ] \ && ok "ONE spawn per PATH: $SPAWNS spawn(s) for $PATHS distinct path(s) — the run-scoped memo holds" \ || { no "$SPAWNS spawns for only $PATHS distinct path(s) — the per-path memo is gone or bypassed" - grep 'diff --unified=0' "$LOG" | sort | uniq -c | sort -rn | head -3; } + grep -E "$DIFFRE" "$LOG" | sort | uniq -c | sort -rn | head -3; } [ "$CHURN" -gt "$SPAWNS" ] \ && ok "the memo saved real work: $CHURN churn rows served by $SPAWNS spawn(s)" \ || no "churn rows ($CHURN) <= spawns ($SPAWNS) — nothing was actually deduplicated" From 07e4dc16dcc877d4dfcdb48cc3d3607d2895d004 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 13:22:42 -0400 Subject: [PATCH 113/162] test: deckcheck allowlists the git options the CHANGELOG hardening entry names --no-ext-diff, --no-textconv and --ignore-submodules are git's own options that ripwire passes to its git children, not ripwire flags. Co-Authored-By: Claude Opus 5.5 --- test/deckcheck_allowlist.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/deckcheck_allowlist.txt b/test/deckcheck_allowlist.txt index 226d041e7..055403677 100644 --- a/test/deckcheck_allowlist.txt +++ b/test/deckcheck_allowlist.txt @@ -135,3 +135,6 @@ --langs test/langidentitycheck.sh's own option ("langidentitycheck.sh BASE HEAD --langs=", the language tags whose fixtures run) quoted in CONTRIBUTING.md's common-code Definition of done, not a ripwire flag --syms test/langidentitycheck.sh's own option ("--syms=FILE", the symbols a slice touched) quoted in the same CONTRIBUTING.md command as --langs above, not a ripwire flag --intended test/langidentitycheck.sh's own option ("--intended" rows that waive a DIFF) quoted in the same CONTRIBUTING.md paragraph as --langs above, not a ripwire flag +--no-ext-diff git's own flag, named in the CHANGELOG's git-subprocess hardening entry as an option ripwire passes to its own git diff/log/show/blame children, not a ripwire flag +--no-textconv git's own flag, same CHANGELOG hardening entry as --no-ext-diff above, not a ripwire flag +--ignore-submodules git's own flag ("--ignore-submodules=dirty" on status/diff), same CHANGELOG hardening entry as --no-ext-diff above, not a ripwire flag From 039dbf092e685f72ac8414cf1b80d1c776bd6914 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 14:07:54 -0400 Subject: [PATCH 114/162] test(forbodyslotcheck): Phase B amendments of the early gates review, before the code The amended pre-registration is frozen with this commit (no product code yet): - K1 TARGET: all 3 of fastify-12 (R2), ninja-11 (R8), koa-12 (R5) reach their registered item at R0; tmux-11 (already complete on base) keeps R6. Each row is scored by its registered item. A K1 miss is reported with the whole picture, not an automatic kill. K2 (a complete row loses a gold item) is a hard stop. K3: median how-does bytes +6.5 KB clause, reported per row. - Member owner (language-fair): a head-tier class/struct/interface row donates its member definition when the parser's scope equals the class name or the class's own span contains it in the same file; common words excluded; Go receiver methods are a named floor. Tried after the name owners, before the subtoken fallback. - Entry point: an edge counts only when it reaches an owner named by another question word (no self-edge, recursion or same-named callee). New arms: E4 (body paid inside --token-budget / budget_tokens, displaced rows recoverable), MC5 (MCP cut and pick parity), EV1 (RIPWIRE_NO_OWNER_HOP does not reach the slot), EP1/EP2 (namesake and recursion), DS1-DS4 (TS overloads, C++ pure virtual, Go interface method, TS abstract), Q1 member arms in C++, Rust, Python, TS plus the Go floor and three near misses, D10 multibyte premise, D1 reason=owner and pick=, I10, M5. The runtime roots are written under the gate's TMP, outside the indexed tree. Co-Authored-By: Claude Opus 5.5 --- test/forbodyslotcheck.sh | 297 +++++++++++++++++++++++++++-- test/forbodyslotfix/src/c/render.c | 20 ++ 2 files changed, 300 insertions(+), 17 deletions(-) diff --git a/test/forbodyslotcheck.sh b/test/forbodyslotcheck.sh index c316c11c8..ec647330d 100755 --- a/test/forbodyslotcheck.sh +++ b/test/forbodyslotcheck.sh @@ -13,11 +13,17 @@ # a named floor), --auto-bodies and --detail=N (the caller chose the bodies), --format=candidates, # the name-exact route (the auto bundle already serves the anchor's body). # OWNER owner-hop's predicate (verbs_for.h forOwnerHopPlan): a callable definition (Function/Method, -# isDefinitionNotDeclaration, so never a .d.ts / header declaration), not a test symbol, inside the -# ranked head, whose whole folded name equals a question word; common words after the rest. Among -# owners the one with a call edge (resolved or by name) to another owner wins (the entry point), -# else the best rank. No owner: the best-ranked callable of the same predicate whose name shares a -# kept question subtoken. None: no slot. Classes/structs/types are never the owner. +# isDefinitionNotDeclaration, so never a .d.ts / header / overload / pure-virtual / interface-method +# declaration), not a test symbol, inside the ranked head, whose whole folded name equals a question +# word; common words after the rest. Among owners the one with a call edge (resolved or by name) to +# an owner named by ANOTHER question word wins (the entry point; a self-edge, a recursion or a +# same-named callee such as super().refresh() never makes one), else the best rank (pick="name"). +# No name owner: the MEMBER owner (Phase B, owner ruling Q1) — the first head-tier class/struct/ +# interface row (rank <= 24) with a callable definition that is its member (the parser's scope= equals +# the class name, or the class's own byte span contains it in the same file) and whose folded name is +# a non-common question word (pick="member"); Go receiver methods are a named floor (no scope, outside +# the type's span). Then the best-ranked head-tier callable of the same predicate whose name shares a +# kept question subtoken (pick="subtoken"). None: no slot. Classes/structs/types are never the owner. # RENDER one in the --expand shape (packBodies), kept. A body over # kForBodySlotMaxBytes (6000) is head-cut at a line end: truncated="1" lines="lo-hi/T" # next="--expand=P:L:N:A-B" (the cutfix-bodies vocabulary), bodies capped="1"; a FIRST line longer @@ -34,16 +40,19 @@ # MCP `for` serves the same body (parity decision, design D open question 4); budget_tokens as above. # # Arms (colour on 974dd5b1 in brackets): -# D1 [P] T_D: bodies=1, holds getCookie / HTTPException(401 / ctx.set('jwtPayload'; with verify -# D2 [G] T_D: not verify's body (jwt calls verify: the entry point) -# D3 [P] est_tokens prices the body (slot vs RIPWIRE_NO_BODY_SLOT=1) +# D1 [P] T_D: bodies=1 reason=owner, , holds getCookie / HTTPException(401 / +# ctx.set('jwtPayload'; with verify +# D2 [G] T_D: not verify's body (jwt is r1 here, so this is NOT an entry-point discriminator — L1 and EP1 are) +# D3 [P] est_tokens prices the body at no less than 0.2 token per body byte (the body rate is 3.80 B/token) # D4 [G] sigs/hops/tail byte-identical slot vs slot-off on every fixture question (checklist 23); slot-off has no body # D5 [P] legends: full answer explains bodies=1; compact legend defines b t= n= p= l= when a body rides # D6 [G] no explanatory cue (where / show documentation / bare noun phrase): bodies=0, no 10%), so a cap counted in +# characters fails the <= 6000 B check (review R7) # D11 [P] one 7.7 KB line of multibyte text: served whole, over_ceiling="1" on the , valid UTF-8 # L1 [P] Rust: entry point index (calls walk) beats walk at r1 L2 [P] C++: Builder::Build DEFINITION (.cc), not the r1 header declaration # L3 [P] Go: method Package, never the type Packager @@ -55,9 +64,25 @@ # E1 [P] --token-budget=3000: the body rides; rows a rank prefix of the default rows, >= 4, no over_ceiling # E2 [P] --token-budget=400: bodies=0 reason=budget; the skipped slot discloses itself on # E3 [G/P] N sweep: rows monotone + rank prefix + <= slot-off; body bytes monotone; [P] at 1000000 body == default body +# E4 [P] --token-budget=3000 and MCP budget_tokens=3000 (slot-off cuts the rows there): the body is paid INSIDE N — +# rows(slot) < rows(off), names the displaced rows, est_tokens(slot) - +# est_tokens(off) <= (legend + root delta + one row) / 2.5 (a body appended after the row cut fails this) # MC1-MC4 MCP for: [P] body parity with the CLI; [P] budget_tokens=400 recoverable; [G] no cue, no body +# MC5 [P] MCP for on the cut owner (T_BIG: same truncated=/lines=/next= and CDATA as the CLI) and on the Rust +# entry point (T_RS: index) — MCP/CLI drift on the cut or on the pick +# EV1 [P] RIPWIRE_NO_OWNER_HOP=1 leaves T_D's byte-identical (the owner-hop env never reaches the slot) +# EP1 [P] namesake: ModalScreen.refresh (r4) calls Screen.refresh (r3) through super(): not an entry point, +# the r3 row's body is served; EP2 [P] a recursive owner (flatten r2) never outranks normalize (r1) +# DS1-DS4 [P] the definition, never the declaration row that outranks it: TS overload signatures, C++ pure +# virtual + in-class override declaration, Go interface method spec, TS abstract method +# Q1 [P] member owner, the head-tier class's member defined outside the head tier: C++ out of line (sc=), +# Rust impl (sc=), Python (sc=), TS class (span containment); pick="member" +# [G] floor and near misses: Go receiver method (no member body — the named floor, until Go carries a +# receiver scope); a common-word member (run); a class past the head tier (r31); another class's +# same-named method that outranks the member is not served (entity identity); never the class/type # M1-M4 mutation controls (canned pre-change shapes are rejected) X1-X3 determinism, xmllint, no '--' in comments -# I0-I9 [optional, RIPWIRE_BASE_BIN=] byte identity where the slot must not fire (I0: slot off == base) +# I0-I10 [optional, RIPWIRE_BASE_BIN=] byte identity where the slot must not fire (I0: slot off +# == base; I10: --token-budget=3000 with RIPWIRE_NO_BODY_SLOT=1 == base — no reserve without the slot) # # Usage: bash test/forbodyslotcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forbodyslotcheck.sh # BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used. @@ -125,8 +150,12 @@ def strip_slot(t): t = re.sub(r'', '', t, flags=re.S) t = re.sub(r'', '', t, flags=re.S) t = re.sub(r']*/>', '', t) - t = re.sub(r' (bodies|reason|est_tokens|bundle)="[^"]*"', '', t) + t = re.sub(r' (bodies|reason|est_tokens)="[^"]*"', '', t) return t +def est(t): + v = root(t).get("est_tokens", "") + if not v.isdigit(): fail("est_tokens missing or not numeric: %r" % v) + return int(v) if cmd == "rootattr": # rootattr FILE ATTR REGEX t = load(args[0]); v = root(t).get(args[1]) if v is None or not re.fullmatch(args[2], v): fail("root %s=%r, wanted %s" % (args[1], v, args[2])) @@ -217,15 +246,58 @@ elif cmd == "sectionsame": # sectionsame FILE_SLOT FILE_OFF : ident if a != b: i = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), min(len(a), len(b))) fail("sections differ at byte %d: slot %r | off %r" % (i, a[max(0, i - 40): i + 60], b[max(0, i - 40): i + 60])) -elif cmd == "estdelta": # estdelta FILE_SLOT FILE_OFF : est_tokens(slot) - est_tokens(off) >= body element bytes / 10 +elif cmd == "estdelta": # estdelta FILE_SLOT FILE_OFF : est_tokens(slot) - est_tokens(off) >= 0.2 x body element bytes ts, to = load(args[0]), load(args[1]) - es, eo = root(ts).get("est_tokens", ""), root(to).get("est_tokens", "") - if not (es.isdigit() and eo.isdigit()): fail("est_tokens missing: %r %r" % (es, eo)) + es, eo = est(ts), est(to) m = re.search(r'', ts, re.S) eb = len(m.group(0).encode()) if m else 0 if eb == 0: fail("no element to price (est %s vs %s)" % (es, eo)) - if int(es) - int(eo) < eb // 10: fail("est_tokens %s - %s < %d B / 10" % (es, eo, eb)) - print("est_tokens %s vs %s (+%d for %d B)" % (es, eo, int(es) - int(eo), eb)) + if es - eo < eb // 5: fail("est_tokens %s - %s < %d B x 0.2 (the body rate is 3.80 B/token)" % (es, eo, eb)) + print("est_tokens %s vs %s (+%d for %d B)" % (es, eo, es - eo, eb)) +elif cmd == "paidinside": # paidinside FILE_SLOT FILE_OFF : E4 — the body is paid inside the target, the displaced rows named + ts, to = load(args[0]), load(args[1]) + if not bodies(ts): fail("premise: no body rides the budgeted answer") + ns, no_ = len(rows(ts)), len(rows(to)) + if not ns < no_: fail("rows(slot)=%d is not below rows(off)=%d: the body was not paid inside the target" % (ns, no_)) + sa = sigs(ts) + if not (sa.get("shown", "").isdigit() and sa.get("total", "").isdigit()): fail(" lacks numeric shown=/total=: %s" % sa) + if int(sa["shown"]) != ns or int(sa["total"]) <= ns: fail(" with %d rows: the cut is not disclosed" % (sa["shown"], sa["total"], ns)) + if sa.get("next_offset") != str(ns) or not sa.get("next"): fail(": the displaced rows are not recoverable" % (sa.get("next_offset"), sa.get("next"))) + es, eo = est(ts), est(to) + lg = len(legend(ts).encode()) - len(legend(to).encode()) + rd = len(re.search(r']*>', ts).group(0).encode()) - len(re.search(r']*>', to).group(0).encode()) + mrow = max(len(d.group(0).encode()) for d in re.finditer(r']*>.*?', to, re.S)) + bound = (max(lg, 0) + max(rd, 0) + mrow) / 2.5 + 4 + if es - eo > bound: fail("est_tokens %d - %d = %d > %.0f (legend %+d B, root %+d B, one row %d B): the body rides on top of the target" % (es, eo, es - eo, bound, lg, rd, mrow)) + print("rows %d < %d, , est %d vs %d (bound +%.0f)" % (ns, no_, sa["shown"], sa["next_offset"], es, eo, bound)) +elif cmd == "bodyat": # bodyat FILE NAME PATH LINE : exactly one , it is n=NAME p=PATH l=LINE + bs = bodies(load(args[0])) + if len(bs) != 1: fail("%d elements, wanted 1: %s" % (len(bs), [(b[0].get("n"), b[0].get("p"), b[0].get("l")) for b in bs])) + a = bs[0][0] + if (a.get("n"), a.get("p"), a.get("l")) != (args[1], args[2], args[3]): fail(", wanted n=%s p=%s l=%s" % (a.get("n"), a.get("p"), a.get("l"), args[1], args[2], args[3])) + print("" % (args[1], args[2], args[3])) +elif cmd == "bodiesattr": # bodiesattr FILE ATTR REGEX : the element's ATTR + m = re.search(r']*)>', load(args[0])) + if not m: fail("no element") + v = attrs(m.group(1)).get(args[1]) + if v is None or not re.fullmatch(args[2], v): fail(", wanted %s" % (args[1], v, args[2])) + print("" % (args[1], v)) +elif cmd == "rowline": # rowline FILE R : print the l= of the row at rank R ("" when none) + hit = [a for r, n, p, a in rows(load(args[0])) if r == int(args[1])] + print(hit[0].get("l", "") if hit else "") +elif cmd == "rankof": # rankof FILE NAME PATH : print the rank of the row n=NAME p=PATH (0 = not among the rows) + hit = [r for r, n, p, a in rows(load(args[0])) if n == args[1] and p == args[2]] + print(hit[0] if hit else 0) +elif cmd == "bodiessec": # bodiessec FILE : the … element verbatim + m = re.search(r'', load(args[0]), re.S) + if not m: fail("no element") + sys.stdout.write(m.group(0)) +elif cmd == "mbcut": # mbcut FILE NAME PCT : the CDATA's UTF-8 bytes exceed its characters by > PCT % + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + cd = hit[0][1]; nb = len(cd.encode()); nc = len(cd) + if nb == 0 or 100.0 * (nb - nc) / nb <= float(args[2]): fail("CDATA %d B vs %d characters: not > %s%% multibyte" % (nb, nc, args[2])) + print("CDATA %d B = %d characters (+%.1f%%)" % (nb, nc, 100.0 * (nb - nc) / nb)) elif cmd == "legend": # legend FILE STR... : every STR appears inside the XML comments lg = legend(load(args[0])); miss = [s for s in args[1:] if s not in lg] if miss: fail("legend lacks %s" % miss) @@ -279,6 +351,66 @@ mcpfor(){ # mcpfor TASK [EXTRA_JSON_MEMBERS] > FILE python3 -c 'import json,sys; a={"path":sys.argv[1],"task":sys.argv[2]}; a.update(json.loads(sys.argv[3] or "{}")); print(json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":a}}))' "$FIX" "$1" "${2:-}" \ | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py" } +mcpforoff(){ RIPWIRE_NO_BODY_SLOT=1 mcpfor "$@"; } # the exported env reaches the --mcp process inside mcpfor + +# ── the runtime roots (written under $TMP: outside the indexed tree, so the repository's own --for corpus is unchanged) ── +# Q1 member owner — the ninja shape: a head-tier class whose member that the question names is defined OUTSIDE the head tier. +# 45 helpers named by two question words, each body naming the member word, lower that word's weight; the member's long body +# names no question word. EP / DS: entry-point and declaration-sibling near misses (premises checked below, never assumed). +cat > "$TMP/gen.py" <<'PY' +import itertools, os, sys +out = sys.argv[1] +W = ["kiln", "glaze", "firing", "queue", "batch", "track", "schedule", "record", "result", "job"] +def w(p, s): + os.makedirs(os.path.dirname(p), exist_ok=True) + open(p, "w").write(s) +def fillers(root, verb, alt=None): + xs = [] + for k, (a, b) in enumerate(itertools.combinations(W, 2)): + xs.append("int %s_%s_%d( int x )\n{\n // %s the %s %s\n return x + %d;\n}" % (a, b, k, alt if alt and k % 3 == 0 else verb, a, b, k)) + w(root + "/src/fillers.c", "\n".join(xs) + "\n") +def longbody(ind, n=90): + return "\n".join("%stotal += step_value( left, %d ) * weight_of( %d );" % (ind, i, i) for i in range(n)) +def cpp(root, verb="Assemble", other=None): + fillers(root, verb.lower()) + w(root + "/src/forge.h", "// the forge: schedules kiln jobs, tracks firing queues and glaze batches\nstruct KilnJob;\nstruct GlazeBatch;\nclass Forge\n{\npublic:\n" + " // %s every queued kiln job: schedule glaze batches, track firing queues, record kiln results\n bool %s( KilnJob* job );\n" + " void ScheduleGlaze( GlazeBatch* batch );\n void TrackFiring( KilnJob* job );\n KilnJob* NextKilnJob();\n" + " int kiln_slots_;\n int glaze_batches_;\n int firing_queues_;\n};\n" % (verb.lower(), verb)) + w(root + "/src/forge.cc", '#include "forge.h"\nint step_value( int a, int b );\nint weight_of( int a );\nbool Forge::%s( KilnJob* job )\n{\n int left = 3;\n int total = 0;\n%s\n return job != nullptr && total > 0;\n}\n' % (verb, longbody(" "))) + if other: + w(root + "/src/smelter.cc", "class %s\n{\npublic:\n bool %s( int job );\n};\nint step_value( int a, int b );\nbool %s::%s( int job )\n{\n int total = 0;\n%s\n return total > job;\n}\n" % (other, verb, other, verb, longbody(" ", 40))) +def deep(root): + fillers(root, "assemble", alt="assemble forge") + w(root + "/src/forge.h", "class Forge\n{\npublic:\n bool Assemble( int a );\n int width_ = 0;\n};\n") + w(root + "/src/forge.cc", '#include "forge.h"\nint step_value( int a, int b );\nint weight_of( int a );\nbool Forge::Assemble( int a )\n{\n int left = 3;\n int total = 0;\n%s\n return total > a;\n}\n' % longbody(" ")) +def rs(root): + fillers(root, "assemble") + w(root + "/src/forge.rs", "/// the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\npub struct Forge {\n pub kiln_slots: u32,\n pub glaze_batches: u32,\n pub firing_queues: u32,\n pub kiln_results: u32,\n}\n\nimpl Forge {\n pub fn assemble(&mut self, job: u32) -> bool {\n let left = 3;\n let mut total = 0;\n%s\n total > job\n }\n}\n" % longbody(" ")) +def py(root): + fillers(root, "assemble") + body = "\n".join(" total += step_value(left, %d) * weight_of(%d)" % (i, i) for i in range(90)) + w(root + "/src/forge.py", 'class Forge:\n """the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results"""\n\n kiln_slots = 0\n glaze_batches = 0\n firing_queues = 0\n\n def track_firing(self, job):\n return job\n\n def assemble(self, job):\n left = 3\n total = 0\n%s\n return total > job\n' % body) +def ts(root): + fillers(root, "assemble") + w(root + "/src/forge.ts", "// the forge: schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\nexport class Forge {\n kilnSlots = 0;\n glazeBatches = 0;\n firingQueues = 0;\n trackFiring(job: number): number {\n return job;\n }\n assemble(job: number): boolean {\n const left = 3;\n let total = 0;\n%s\n return total > job;\n }\n}\n" % longbody(" ")) +def go(root): + fillers(root, "assemble") + w(root + "/src/forge.go", "package forge\n\n// Forge schedules kiln jobs, tracks firing queues and glaze batches, records kiln results\ntype Forge struct {\n\tKilnSlots int\n\tGlazeBatches int\n\tFiringQueues int\n\tKilnResults int\n}\n\nfunc stepValue(a, b int) int { return a + b }\nfunc weightOf(a int) int { return a }\n\nfunc (f *Forge) Assemble(job int) bool {\n\tleft := 3\n\ttotal := 0\n%s\n\treturn total > job\n}\n" % "\n".join("\ttotal += stepValue(left, %d) * weightOf(%d)" % (i, i) for i in range(90))) +cpp(out + "/cpp"); rs(out + "/rs"); py(out + "/py"); ts(out + "/ts"); go(out + "/go") +cpp(out + "/common", verb="Run"); deep(out + "/deep"); cpp(out + "/other", other="Smelter") +# EP1 namesake (super().refresh() is a by-name edge to a same-named owner) and EP2 recursion +w(out + "/name/src/screens.py", '"""Screens that repaint their regions."""\n\n\nclass Screen:\n """A full screen: repaint every region of the screen, then refresh the cursor."""\n\n def refresh(self):\n """Repaint the screen: every region of the screen, top to bottom, then the cursor."""\n self.painted = True\n return self\n\n\nclass ModalScreen(Screen):\n """A modal dialog drawn over another screen."""\n\n def refresh(self):\n """Repaint the dialog, then its parent."""\n super().refresh()\n self.dialog = True\n return self\n') +w(out + "/self/src/tree.py", '"""Tree helpers: normalize a parsed tree, flatten nested nodes."""\n\n\ndef normalize(tree):\n """Normalize the tree: strip blank labels and lower-case every tag."""\n tree["tag"] = tree.get("tag", "").lower()\n tree["label"] = tree.get("label", "").strip()\n return tree\n\n\ndef flatten(tree):\n """Flatten nested children into one list, depth first."""\n out = [tree]\n for child in tree.get("children", []):\n out.extend(flatten(child))\n return out\n') +# DS1-DS4: a declaration row (documented, so it outranks) and its definition (undocumented) +w(out + "/tsover/src/format.ts", '/** format renders the value: a number value is grouped for display */\nexport function format(value: number): string;\n/** format renders the value: a string value is trimmed for display */\nexport function format(value: string): string;\nexport function format(v: number | string): string {\n if (typeof v === "number") {\n return v.toLocaleString("en-US");\n }\n return v.trim();\n}\n') +w(out + "/cppvirt/src/dispatch.h", "struct Edge;\nclass Dispatcher\n{\npublic:\n virtual ~Dispatcher() {}\n /// dispatch: the pool dispatch of one ready edge to an idle pool worker; false when the pool has no idle worker\n virtual bool Dispatch( Edge* edge ) = 0;\n};\nclass PoolDispatcher : public Dispatcher\n{\npublic:\n /// dispatch: the pool dispatch of one ready edge to an idle pool worker\n bool Dispatch( Edge* edge ) override;\n int idle_ = 4;\n};\n") +w(out + "/cppvirt/src/dispatch.cc", '#include "dispatch.h"\nbool PoolDispatcher::Dispatch( Edge* e )\n{\n if( idle_ == 0 || e == nullptr )\n {\n return false;\n }\n --idle_;\n return true;\n}\n') +w(out + "/goiface/src/packer.go", "package packer\n\ntype Packer interface {\n\t// Pack: the tar packer packs the files into one archive, every file in order\n\tPack(files []string) error\n}\n\ntype tarStream struct{ count int }\n\nfunc (t *tarStream) Pack(xs []string) error {\n\tfor range xs {\n\t\tt.count++\n\t}\n\treturn nil\n}\n") +w(out + "/tsabs/src/printer.ts", 'export abstract class Base {\n /** print: the html printer prints a document as html */\n abstract print(doc: string): string;\n}\nexport class Out extends Base {\n print(d: string): string {\n return "

    " + d + "

    ";\n }\n}\n') +PY +python3 "$TMP/gen.py" "$TMP/rt" || { echo "the runtime-root generator failed"; exit 2; } +rrun(){ local r="$1"; shift; "$BIN" "$TMP/rt/$r" --cache="$TMP/rt.$r.cache" "$@" 2>/dev/null; } # one cache per runtime root T_D="How does the JWT middleware verify a token?" T_PY="How does Stylesheet apply the parsed rules to a widget?" @@ -335,7 +467,9 @@ if r="$( chk rootattr "$TMP/d.xml" bundle compact )"; then ok "(D1) T-D premise: if r="$( chk bodycount "$TMP/d.xml" 1 )"; then ok "(D1) T-D: exactly one body, root $r"; else no "(D1) T-D: $r"; fi if r="$( chk body "$TMP/d.xml" jwt src/middleware/jwt/jwt.ts "getCookie" "HTTPException(401" "ctx.set('jwtPayload'" )"; then ok "(D1) T-D: $r holds the three kill strings"; else no "(D1) T-D: $r"; fi if r="$( chk callsin "$TMP/d.xml" jwt verify )"; then ok "(D1) T-D: the body keeps (names verify)"; else no "(D1) T-D: $r"; fi -if r="$( chk nobodyof "$TMP/d.xml" verify )"; then ok "(D2) T-D: not verify's body — jwt calls verify (the entry point)"; else no "(D2) T-D: $r"; fi +if r="$( chk rootattr "$TMP/d.xml" reason owner )"; then ok "(D1) T-D: root $r (bodies=1 is never read beside reason=compact-route)"; else no "(D1) T-D: $r"; fi +if r="$( chk bodiesattr "$TMP/d.xml" pick name )"; then ok "(D1) T-D: $r"; else no "(D1) T-D: $r"; fi +if r="$( chk nobodyof "$TMP/d.xml" verify )"; then ok "(D2) T-D: not verify's body (jwt is r1: a rank fact, not the entry-point discriminator)"; else no "(D2) T-D: $r"; fi if r="$( chk estdelta "$TMP/d.xml" "$TMP/d.off.xml" )"; then ok "(D3) T-D: $r"; else no "(D3) T-D: $r"; fi while IFS='|' read -r k q; do if r="$( chk sectionsame "$TMP/$k.xml" "$TMP/$k.off.xml" )"; then ok "(D4) $k: sigs/hops/tail identical slot vs RIPWIRE_NO_BODY_SLOT=1 (not paid in rows)"; else no "(D4) $k: $r"; fi @@ -372,6 +506,7 @@ if r="$( chk body "$TMP/py.xml" apply src/textual/css/stylesheet.py "_check_rule if r="$( chk nobodyof "$TMP/py.xml" Stylesheet )"; then ok "(D8) Python: the class Stylesheet (r1) is never the body"; else no "(D8) Python: $r"; fi if r="$( chk body "$TMP/c.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" "cmd_find_valid_state(" )"; then ok "(D9) C: $r"; else no "(D9) C: $r"; fi if r="$( chk body "$TMP/sub.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" )"; then ok "(D9b) C subtoken fallback ('target'): $r"; else no "(D9b) C subtoken fallback: $r"; fi +if r="$( chk bodiesattr "$TMP/sub.xml" pick subtoken )"; then ok "(D9b) the fallback names itself: $r"; else no "(D9b) $r"; fi if r="$( chk bodyattr "$TMP/big.xml" render_screen truncated '^1$' )"; then ok "(D10) oversized owner: $r"; else no "(D10) oversized: $r"; fi if r="$( chk cutatline "$TMP/big.xml" render_screen 6000 )"; then ok "(D10) oversized owner: $r (whole lines, <= 6000 B)"; else no "(D10) oversized: $r"; fi if nx="$( chk nextof "$TMP/big.xml" render_screen )" && printf '%s' "$nx" | grep -q '^--expand=src/c/render.c:'; then @@ -388,6 +523,7 @@ else no "(D10) oversized: no (${nx:-none})" fi if r="$( chk over "$TMP/big.xml" )"; then ok "(D10) oversized owner: no over_ceiling on the root (the guard never fires for the body)"; else no "(D10) $r"; fi +if r="$( chk mbcut "$TMP/big.xml" render_screen 10 )"; then ok "(D10) the kept lines are multibyte: $r — a cap counted in characters would pass 6000 B"; else no "(D10) multibyte premise: $r"; fi if r="$( chk bodyattr "$TMP/hyd.xml" hydrate over_ceiling '^1$' )"; then ok "(D11) one line over the cap: $r (served whole)"; else no "(D11) one line over the cap: $r"; fi if r="$( chk utf8whole "$TMP/hyd.xml" hydrate 'restored: true' )"; then ok "(D11) one line over the cap: $r, the whole line"; else no "(D11) $r"; fi if r="$( chk rowis "$TMP/rs.xml" 1 walk src/rs/indexer.rs )"; then :; else no "(L1) premise: $r"; fi @@ -417,6 +553,90 @@ while IFS='|' read -r k q; do done <<<"$QUESTIONS" ok "(N5) every on the fixture answers is fn/method (FAIL lines above otherwise)" +# ── EP / DS / Q1: the runtime roots (each premise read off this binary's own answer, never assumed) ───────────── +Q_FORGE="How does the forge assemble a kiln job: scheduling glaze batches, tracking firing queues and recording kiln results?" +Q_RUN="How does the forge run a kiln job: scheduling glaze batches, tracking firing queues and recording kiln results?" +for r in cpp rs py ts go deep other; do rrun "$r" --for="$Q_FORGE" >"$TMP/q1.$r.xml"; done +rrun common --for="$Q_RUN" >"$TMP/q1.common.xml" +rrun name --for="How does refresh repaint the screen?" >"$TMP/ep.name.xml" +rrun self --for="How does normalize flatten the tree?" >"$TMP/ep.self.xml" +rrun tsover --for="How does format render the value?" >"$TMP/ds.tsover.xml" +rrun cppvirt --for="How does the pool dispatch an edge?" >"$TMP/ds.cppvirt.xml" +rrun goiface --for="How does the tar packer pack the files?" >"$TMP/ds.goiface.xml" +rrun tsabs --for="How does the html printer print a document?" >"$TMP/ds.tsabs.xml" +for f in q1.cpp q1.rs q1.py q1.ts q1.go q1.deep q1.other q1.common ep.name ep.self ds.tsover ds.cppvirt ds.goiface ds.tsabs; do + if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(0) $f.xml: $r — the runtime root answered nothing"; fi +done +# EP1 namesake: ModalScreen.refresh's only edge is super().refresh() — a same-named owner, never an entry point +if r="$( chk rowis "$TMP/ep.name.xml" 3 refresh src/screens.py )" && [ "$( chk rankof "$TMP/ep.name.xml" refresh src/screens.py )" = 3 ] \ + && "$BIN" "$TMP/rt/name" --no-cache --callees=ModalScreen.refresh 2>/dev/null | grep -q ''; then + if r="$( chk bodyat "$TMP/ep.name.xml" refresh src/screens.py 7 )"; then ok "(EP1) namesake: $r — the r3 row; r4's super().refresh() edge names the same word"; else no "(EP1) namesake: $r"; fi +else + no "(EP1) premise: r3 Screen.refresh (l=7) and ModalScreen.refresh's edge to it — ${r:-not read}" +fi +# EP2 recursion: flatten (r2) calls only itself (the graph carries no self-edge: --callees count=0), normalize (r1) calls nothing +if r="$( chk rowis "$TMP/ep.self.xml" 1 normalize src/tree.py )" && r2="$( chk rowis "$TMP/ep.self.xml" 2 flatten src/tree.py )"; then + if r="$( chk bodyat "$TMP/ep.self.xml" normalize src/tree.py 4 )"; then ok "(EP2) recursion: $r — flatten's recursion makes no entry point"; else no "(EP2) recursion: $r"; fi +else + no "(EP2) premise: ${r:-} ${r2:-}" +fi +# DS1-DS4: the declaration outranks its definition; the body is the definition's +ds(){ # ds KEY ARM DECL_NAME DECL_PATH DECL_RANK DEF_PATH DEF_LINE + local k="$1" arm="$2" n="$3" p="$4" dr="$5" dp="$6" dl="$7" + if ! r="$( chk rowis "$TMP/ds.$k.xml" "$dr" "$n" "$p" )"; then no "($arm) premise: $r"; return; fi + if [ "$( chk rowline "$TMP/ds.$k.xml" "$dr" )" = "$dl" ] && [ "$p" = "$dp" ]; then no "($arm) premise: the r$dr row IS the definition (l=$dl): no declaration outranks it"; return; fi + if r="$( chk bodyat "$TMP/ds.$k.xml" "$n" "$dp" "$dl" )"; then ok "($arm) $k: $r — the definition, not the r$dr declaration"; else no "($arm) $k: $r"; fi +} +ds tsover DS1 format src/format.ts 1 src/format.ts 5 +ds cppvirt DS2 Dispatch src/dispatch.h 1 src/dispatch.cc 2 +ds goiface DS3 Pack src/packer.go 1 src/packer.go 10 +ds tsabs DS4 print src/printer.ts 1 src/printer.ts 6 +# Q1 member owner: the head-tier class's member, defined past the head tier (no name owner exists) +q1(){ # q1 KEY CLASS_RANK_MAX CLASS CLASS_PATH MEMBER MEMBER_PATH MEMBER_LINE LANG + local k="$1" cmax="$2" c="$3" cp="$4" m="$5" mp="$6" ml="$7" lang="$8" cr mr + cr="$( chk rankof "$TMP/q1.$k.xml" "$c" "$cp" )"; mr="$( chk rankof "$TMP/q1.$k.xml" "$m" "$mp" )" + case "$cr$mr" in *[!0-9]*|'') no "(Q1) $k premise: unreadable ranks class=$cr member=$mr"; return;; esac + if [ "$cr" -lt 1 ] || [ "$cr" -gt "$cmax" ] || { [ "$mr" -ge 1 ] && [ "$mr" -le 24 ]; }; then + no "(Q1) $k premise: class $c r$cr (wanted 1..$cmax), member $m r$mr (wanted past the head tier)"; return + fi + if r="$( chk bodyat "$TMP/q1.$k.xml" "$m" "$mp" "$ml" )" && r2="$( chk bodiesattr "$TMP/q1.$k.xml" pick member )"; then + ok "(Q1) $lang: $r ($r2; class r$cr, member r${mr/#0/>40})" + else + no "(Q1) $lang: ${r:-} ${r2:-}" + fi + if r="$( chk nobodyof "$TMP/q1.$k.xml" "$c" )"; then :; else no "(Q1) $lang: the class/type is served: $r"; fi +} +q1 cpp 24 Forge src/forge.h Assemble src/forge.cc 4 "C++ out of line (sc=)" +q1 rs 24 Forge src/forge.rs assemble src/forge.rs 10 "Rust impl (sc=)" +q1 py 24 Forge src/forge.py assemble src/forge.py 11 "Python (sc=)" +q1 ts 24 Forge src/forge.ts assemble src/forge.ts 9 "TS class (span)" +# the Go floor: the type ranks, its receiver method does not and carries no scope — no member body (flips when Go has a receiver scope) +gr="$( chk rankof "$TMP/q1.go.xml" Forge src/forge.go )"; gm="$( chk rankof "$TMP/q1.go.xml" Assemble src/forge.go )" +if [ "$gr" = 1 ] && { [ "$gm" = 0 ] || [ "$gm" -gt 24 ]; }; then + if r="$( chk nobodyof "$TMP/q1.go.xml" Assemble )" && r2="$( chk nobodyof "$TMP/q1.go.xml" Forge )"; then ok "(Q1) Go floor: type Forge r1, receiver Assemble r$gm: no member body, never the type (the named floor)"; else no "(Q1) Go floor: ${r:-} ${r2:-}"; fi +else + no "(Q1) Go floor premise: Forge r$gr Assemble r$gm" +fi +# near misses +cr="$( chk rankof "$TMP/q1.common.xml" Forge src/forge.h )"; mr="$( chk rankof "$TMP/q1.common.xml" Run src/forge.cc )" +if [ "$cr" = 1 ] && [ "$mr" -gt 24 ] 2>/dev/null; then + if r="$( chk nobodyof "$TMP/q1.common.xml" Run )"; then ok "(Q1) near miss: a common-word member (Run, 'run') is never the member owner"; else no "(Q1) common-word member: $r"; fi +else + no "(Q1) common-word premise: Forge r$cr Run r$mr" +fi +cr="$( chk rankof "$TMP/q1.deep.xml" Forge src/forge.h )" +if [ "$cr" -gt 24 ] 2>/dev/null; then + if r="$( chk nobodyof "$TMP/q1.deep.xml" Assemble )"; then ok "(Q1) near miss: class Forge r$cr (in the pool, past the head tier) donates no member"; else no "(Q1) class past the head tier: $r"; fi +else + no "(Q1) deep premise: Forge r$cr, wanted 25..40" +fi +sr="$( chk rankof "$TMP/q1.other.xml" Assemble src/smelter.cc )"; fr="$( chk rankof "$TMP/q1.other.xml" Assemble src/forge.cc )" +if [ "$sr" -gt 24 ] 2>/dev/null && [ "$fr" -gt "$sr" ] 2>/dev/null && [ "$( chk rankof "$TMP/q1.other.xml" Forge src/forge.h )" = 1 ]; then + if r="$( chk bodyat "$TMP/q1.other.xml" Assemble src/forge.cc 4 )"; then ok "(Q1) entity identity: $r — Forge's member, not Smelter::Assemble (r$sr, outranks it at r$fr)"; else no "(Q1) entity identity: $r"; fi +else + no "(Q1) other-class premise: Smelter::Assemble r$sr, Forge::Assemble r$fr" +fi + # ── E: explicit targets (reserve-sections-first) ─────────────────────────────────────────────────────────── run --for="$T_D" --token-budget=3000 >"$TMP/e3000.xml" if r="$( chk body "$TMP/e3000.xml" jwt src/middleware/jwt/jwt.ts "getCookie" )"; then ok "(E1) --token-budget=3000: $r"; else no "(E1) --token-budget=3000: $r"; fi @@ -444,6 +664,10 @@ chk cdata "$TMP/s1000000.xml" jwt >"$TMP/cd.wide" 2>/dev/null || cdw=1 chk cdata "$TMP/d.xml" jwt >"$TMP/cd.default" 2>/dev/null || cdd=1 if [ "$cdw$cdd" = "00" ] && [ -s "$TMP/cd.wide" ] && cmp -s "$TMP/cd.wide" "$TMP/cd.default"; then ok "(E3) N=1000000: the body equals the default answer's body"; else no "(E3) N=1000000: no body, or it differs from the default answer's"; fi +# E4: at N=3000 slot-off serves rows past the cut; with the slot the body is reserved first and the rows give way (review R3) +run --for="$T_D" --token-budget=3000 >"$TMP/e4.xml"; runoff --for="$T_D" --token-budget=3000 >"$TMP/e4.off.xml" +if r="$( chk paidinside "$TMP/e4.xml" "$TMP/e4.off.xml" )"; then ok "(E4) --token-budget=3000: $r"; else no "(E4) --token-budget=3000: $r"; fi + # ── MC: MCP for parity ───────────────────────────────────────────────────────────────────────────────────── mcpfor "$T_D" >"$TMP/m.d.xml"; mcpfor "$T_NOCUE" >"$TMP/m.nocue.xml"; mcpfor "$T_D" '{"budget_tokens":400}' >"$TMP/m.d400.xml" for f in m.d m.nocue m.d400; do @@ -455,6 +679,31 @@ if [ "$cdm$cdd" = "00" ] && [ -s "$TMP/cd.mcp" ] && cmp -s "$TMP/cd.mcp" "$TMP/c if r="$( chk nobody "$TMP/m.nocue.xml" )"; then ok "(MC3) MCP for, no cue: no body"; else no "(MC3) $r"; fi if r="$( chk nobodyof "$TMP/m.d400.xml" jwt )" && r2="$( chk bodiesnext "$TMP/m.d400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(MC4) MCP budget_tokens=400: no body, the owner recoverable by next=--expand"; else no "(MC4) MCP budget_tokens=400: ${r:-} ${r2:-}"; fi +# MC5: MCP/CLI drift on the cut and on the pick (review R4) +mcpfor "$T_BIG" >"$TMP/m.big.xml"; mcpfor "$T_RS" >"$TMP/m.rs.xml" +for f in m.big m.rs; do if r="$( chk sigsopen "$TMP/$f.xml" )"; then :; else no "(MC0) MCP $f answered nothing: $r"; fi; done +mc5=0 +for a in truncated lines next; do + vc="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o " $a=\"[^\"]*\"" )"; vm="$( grep -o ']*n="render_screen"[^>]*' "$TMP/m.big.xml" | grep -o " $a=\"[^\"]*\"" )" + if [ -z "$vc" ] || [ "$vc" != "$vm" ]; then no "(MC5) MCP cut: vs CLI "; mc5=1; fi +done +cbc=0; cbm=0 +chk cdata "$TMP/big.xml" render_screen >"$TMP/cd.big.cli" 2>/dev/null || cbc=1 +chk cdata "$TMP/m.big.xml" render_screen >"$TMP/cd.big.mcp" 2>/dev/null || cbm=1 +if [ "$cbc$cbm" = "00" ] && [ -s "$TMP/cd.big.cli" ] && cmp -s "$TMP/cd.big.cli" "$TMP/cd.big.mcp"; then :; else no "(MC5) MCP cut body differs from the CLI's (or is absent)"; mc5=1; fi +[ "$mc5" = 0 ] && ok "(MC5) MCP T_BIG: the same head cut as the CLI (truncated=/lines=/next= and CDATA)" +r=""; r2="" +if r="$( chk body "$TMP/m.rs.xml" index src/rs/indexer.rs "let order = walk(" )" && r2="$( chk nobodyof "$TMP/m.rs.xml" walk )"; then ok "(MC5) MCP T_RS: $r — the entry point, as the CLI"; else no "(MC5) MCP T_RS: ${r:-} ${r2:-}"; fi +# E4 on MCP: budget_tokens=3000 pays the body inside the target too +mcpfor "$T_D" '{"budget_tokens":3000}' >"$TMP/m.e4.xml"; mcpforoff "$T_D" '{"budget_tokens":3000}' >"$TMP/m.e4.off.xml" +if r="$( chk paidinside "$TMP/m.e4.xml" "$TMP/m.e4.off.xml" )"; then ok "(E4) MCP budget_tokens=3000: $r"; else no "(E4) MCP budget_tokens=3000: $r"; fi +# EV1: the owner-hop env never reaches the slot (review R6: the shared owner helper carries no early return) +RIPWIRE_NO_OWNER_HOP=1 "$BIN" "$FIX" --cache="$TMP/rw.cache" --for="$T_D" 2>/dev/null >"$TMP/d.noown.xml" +bs1=0; bs2=0 +chk bodiessec "$TMP/d.xml" >"$TMP/bs.default" 2>/dev/null || bs1=1 +chk bodiessec "$TMP/d.noown.xml" >"$TMP/bs.noown" 2>/dev/null || bs2=1 +if [ "$bs1$bs2" = "00" ] && cmp -s "$TMP/bs.default" "$TMP/bs.noown"; then ok "(EV1) RIPWIRE_NO_OWNER_HOP=1: byte-identical to the default"; else no "(EV1) RIPWIRE_NO_OWNER_HOP=1 changed or dropped the element"; fi + # ── M: mutation controls — the checkers reject canned pre-change shapes ───────────────────────────────────── printf '%s' 'x' >"$TMP/c1.xml" if chk body "$TMP/c1.xml" jwt src/middleware/jwt/jwt.ts getCookie >/dev/null 2>&1; then no "(M1) the body checker accepted a canned no-body answer"; else ok "(M1) the body checker rejects a canned no-body answer"; fi @@ -469,6 +718,17 @@ else no "(M4) premise: T-D has no element to mutate" fi +# M5: a body appended AFTER the row cut (rows unchanged, est_tokens grown by the body) is rejected by the E4 checker +python3 - "$TMP/e4.off.xml" "$TMP/c5.xml" <<'PY2' +import re, sys +t = open(sys.argv[1], encoding="utf-8", errors="replace").read() +body = '' +t = t.replace("
    ", body + "
    ") +t = re.sub(r'est_tokens="(\d+)"', lambda m: 'est_tokens="%d"' % (int(m.group(1)) + 790), t, count=1) +open(sys.argv[2], "w", encoding="utf-8").write(t) +PY2 +if chk paidinside "$TMP/c5.xml" "$TMP/e4.off.xml" >/dev/null 2>&1; then no "(M5) the E4 checker accepted a body appended after the row cut"; else ok "(M5) the E4 checker rejects a body appended after the row cut"; fi + # ── X: hygiene ────────────────────────────────────────────────────────────────────────────────────────────── run --for="$T_D" >"$TMP/d2.xml" if cmp -s "$TMP/d.xml" "$TMP/d2.xml"; then ok "(X1) T-D deterministic x2"; else no "(X1) T-D differs between two runs"; fi @@ -507,6 +767,9 @@ if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then --callers=verify --expand=src/middleware/jwt/jwt.ts:jwt ARGV + RIPWIRE_NO_BODY_SLOT=1 "$BIN" "$FIX" --no-cache --for="$T_D" --token-budget=3000 2>/dev/null | strip_at >"$TMP/i.head" + "$RIPWIRE_BASE_BIN" "$FIX" --no-cache --for="$T_D" --token-budget=3000 2>/dev/null | strip_at >"$TMP/i.base" + if [ -s "$TMP/i.base" ] && cmp -s "$TMP/i.base" "$TMP/i.head"; then ok "(I10) --token-budget=3000 with RIPWIRE_NO_BODY_SLOT=1 == the base binary (no reserve without the slot)"; else no "(I10) --token-budget=3000 RIPWIRE_NO_BODY_SLOT=1 differs from the base binary"; fi else skip "(I) RIPWIRE_BASE_BIN unset or not executable — base-binary byte identity not run here" fi diff --git a/test/forbodyslotfix/src/c/render.c b/test/forbodyslotfix/src/c/render.c index 064c76a34..1d5117f72 100644 --- a/test/forbodyslotfix/src/c/render.c +++ b/test/forbodyslotfix/src/c/render.c @@ -8,25 +8,45 @@ render_screen(struct screen *s) { int row, col; for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 1 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 2 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 3 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 4 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 5 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 6 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 7 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 8 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 9 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 10 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 11 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 12 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 13 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 14 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 15 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 16 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 17 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 18 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 19 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 20 + col) % 90)); } } + /* 画面の各セルを行ごとに塗る、最後の列まで続ける — écran: chaque cellule, ligne après ligne */ for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 21 + col) % 90)); } } for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 22 + col) % 90)); } } for (row = 0; row < s->rows; row++) { for (col = 0; col < s->cols; col++) { s->cells[row][col] = (char)(32 + ((row * 23 + col) % 90)); } } From ca0c53c60363b81d95eae1bd5493272e4af7b1dd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 15:23:32 -0400 Subject: [PATCH 115/162] feat(for): one owner body for an explanatory --for question (CLI and MCP) A conceptual --for question that asks how something works ("how does the JWT middleware verify a token?") now carries ONE body beside the compact route's hops: the question's owner, chosen by name over the ranked head. - src/forowner.h (new): the owner match key and owner-hop's found set move here from verbs_for.h, so the CLI lens and the MCP twin read one rule. The slot's pick: a name owner (the entry point among them: one calling an owner named by another question word; a self-edge, recursion or same-named callee never counts), else a member of a head-tier class the question names (the parser's scope, or the class's own span in the same file; common words excluded; Go receiver methods are a named floor), else the best-ranked callable sharing a question subtoken. pick= on says which. - Rendered through packBodies: kept, cut at 6000 B of CDATA at a line end with truncated/lines/next=; root bodies="1" reason="owner". - Default regime: the body rides on top; , and stay byte-identical to RIPWIRE_NO_BODY_SLOT=1 (the A/B handle). Explicit --token-budget / budget_tokens: reserved after the hops from what the rows at the cut leave; skipped otherwise with and reason="budget"; the refill charges it, so rows past the cut give way. - taskroute.h: hasExplanatoryCue, the one cue predicate the history router and the slot read. - Gates: forbodyslotcheck green (E1 re-anchored to N=5000, E4 to the refresh question at N=800, D10's checker reads the def's first line); ownerhopcheck (B1) and compactroutecheck (1) updated by design. Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 20 +- src/forowner.h | 451 ++++++++++++++++++++++++++++++++++++++ src/graphlegend.h | 5 + src/mcpverbs.h | 38 +++- src/taskroute.h | 10 +- src/verbs_for.h | 189 +++++++--------- test/compactroutecheck.sh | 14 +- test/forbodyslotcheck.sh | 58 +++-- test/ownerhopcheck.sh | 4 +- 9 files changed, 650 insertions(+), 139 deletions(-) create mode 100644 src/forowner.h diff --git a/src/cli.h b/src/cli.h index 4ceebdac2..3d489e384 100644 --- a/src/cli.h +++ b/src/cli.h @@ -949,20 +949,30 @@ inline constexpr char kHelpHead[] = " and says so, and the per-item over-budget comment names what was dropped.\n" " COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the\n" " ranked map plus a section — the same candidate head's ONE-HOP callee signatures,\n" - " the block a body carries — and NO body CDATA, disclosed as bundle=\"compact\"\n" + " the block a body carries — and NO body CDATA (save the owner body below), disclosed as bundle=\"compact\"\n" " bodies=\"0\" reason=\"compact-route\". Read the map, then --expand=SYM the one you want.\n" " A ranked function or method whose whole name is a word of the query (by name, not\n" " resolved) gets the first rows, past that head: up to 3, each listing its\n" " callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=.\n" - " --auto-bodies restores the body walk there. The default serves every hop row. That shape\n" + " --auto-bodies restores the body walk there. The default serves every hop row.\n" + " OWNER BODY: an explanatory question (how does, what does, ...) also carries ONE body\n" + " after the hops, root bodies=\"1\" reason=\"owner\": a ranked callable whose whole name is a\n" + " query word (the one calling another such owner first), else a method a ranked class owns\n" + " (parser scope or span) named by a word, else the best-ranked callable sharing a word -\n" + " , by name, not resolved, not the only relevant body.\n" + " Over 6000 B it is cut at a line end with next=. Under --token-budget it rides only when\n" + " the rows at the target's cut leave room for it whole, else and reason=\"budget\". MCP for serves the same body, --json none;\n" + " RIPWIRE_NO_BODY_SLOT=1 turns it off (the A/B handle). That shape\n" " discloses on every run too: a target the signatures alone exhaust carries bundle=\"compact\" bodies=\"0\"\n" - " reason=\"budget\" — three distinct reasons, never collapsed (compact-route = the route\n" - " chose edges, no_candidates = nothing scored, budget = the ceiling was spent).\n" + " reason=\"budget\" — four distinct reasons, never collapsed (compact-route = the route\n" + " chose edges, no_candidates = nothing scored, budget = the ceiling was spent, owner = the\n" + " owner body rides).\n" " An explicit --token-budget=N is a soft target: the hop rows, bodies and file tail keep\n" " the share a hard N left them, rows fill the rest by rank; --pack-top-n sets the head\n" " the target fills. --compress composes: the served bodies (auto/anchor and\n" " --detail=N alike) go through the same comment-strip --expand uses, disclosed as\n" - " compress=\"1\" on the element (nothing to strip on the compact route).\n" + " compress=\"1\" on the element (on the compact route, the owner body's).\n" " RANKING CONFIDENCE, disclosed not scored: the root always carries\n" " confidence=\"high|low\" margin_pct=\"N\" — derived from the SAME relevance-cliff gap\n" " statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect\n" diff --git a/src/forowner.h b/src/forowner.h new file mode 100644 index 000000000..7bf4d5ce5 --- /dev/null +++ b/src/forowner.h @@ -0,0 +1,451 @@ +#pragma once +// forowner.h — THE QUESTION'S OWNER, one implementation for both --for dialects (the CLI lens, verbs_for.h, and the MCP +// `for` twin, mcpverbs.h): the owner match key (ownerFoldName / ownerQuestionWords), owner-hop's found set (forOwnerCandidates, +// read by verbs_for.h forOwnerHopPlan), and the ONE owner body an explanatory question carries (the body slot, lane +// for-body-slot, train 28). +// +// THE BODY SLOT. A conceptual --for question that asks how something works ("how does the JWT middleware verify a token?") +// is answered on the compact route with signatures and one-hop edges, no bodies — and on 9 of the 15 round-4 token-budget rows +// the owner symbol was ranked at R0 while the answer shipped bodies=0, one body away from complete. The slot serves ONE body, +// chosen in this order (forBodySlotOwner): +// 1. pick="name": owner-hop's owners (a callable definition in the ranked head whose whole folded name equals a question +// word). Among them the ENTRY POINT wins — the first with a call edge to an owner named by ANOTHER word (jwt calls verify, +// index calls walk); a self-edge, a recursion or a same-named callee (super().refresh(), an overload chain) never makes +// one. Else the first (best rank, common words after the rest). +// 2. pick="member": no name owner — the first head-tier class/struct/interface row with a member the question names (the +// ninja shape: Builder ranks, Builder::Build does not). Member BY THE PARSER: Symbol::scope equals the class name (C++ +// in-class and out-of-line, Rust impl/trait, Python), or the class's own parsed span contains the definition in the same +// file (TS/JS classes and every other language whose methods nest in the class body). A common word (get, run, set …) is +// never a member owner. NAMED FLOOR: Go receiver methods carry no scope and sit outside the type's span, so a Go type +// donates no member until the parser records a receiver scope (forbodyslotcheck (Q1) Go floor pins it). +// 3. pick="subtoken": the best-ranked head-tier callable definition whose name shares a subtoken with a question word. +// A class/struct/interface/type is never the body; a test symbol never is; a declaration (header prototype, .d.ts, TS overload +// signature, C++ pure virtual, Go interface method spec) never is (isDefinitionNotDeclaration). This is a choice BY NAME over +// the ranked head (pick= says which reading chose it), never a resolution: the body is the entry point among the named owners, +// not necessarily the question's subject, and not a claim that it is the only relevant body. +// RIPWIRE_NO_BODY_SLOT (any value) turns the slot off — the A/B handle, the RIPWIRE_NO_OWNER_HOP precedent; the two are +// independent (forbodyslotcheck (EV1)). + +#include "exemplar.h" // shareNameSubtoken — the subtoken fallback's one test (lexical.h subtokens) +#include "filter.h" // isTestSymbol +#include "graph.h" // langCompatible +#include "lexical.h" // isRouteStopword +#include "model.h" +#include "serialize.h" // kForLensDefaultTopN, kForDocExcerptRankCount, packBodies +#include "taskroute.h" // hasExplanatoryCue — the explanatory cues, word-bounded (the router's own reading) +#include "infra/sortutil.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +// owner-hop: a word on this list (verbs and function words a question USES) ranks after every other owner word; the member +// owner never takes one (every class has a get/run/set) +inline constexpr std::string_view kOwnerCommonWords[] = { + "all", "call", "each", "from", "get", "has", "into", "new", "put", "read", "run", "set", "that", "then", "this", + "use", "when", "with", "write" }; +static_assert( std::ranges::is_sorted( kOwnerCommonWords, rw::sortutil::svLess ), "kOwnerCommonWords is binary-searched: keep it sorted" ); + +inline bool isOwnerCommonWord( std::string_view w ) noexcept +{ + return std::ranges::binary_search( kOwnerCommonWords, w, rw::sortutil::svLess ); // svLess: #345 +} + +// ASCII fold of one name or word, leading _ # $ @ removed (the owner match key) +inline std::string ownerFoldName( std::string_view n ) +{ + std::size_t i = 0; + while( i < n.size() && ( n[i] == '_' || n[i] == '#' || n[i] == '$' || n[i] == '@' ) ) + { + ++i; + } + std::string out( n.substr( i ) ); + for( char& c : out ) + { + c = ( c >= 'A' && c <= 'Z' ) ? char( c - 'A' + 'a' ) : c; + } + return out; +} + +// the question's distinct owner words, sorted (binary-searched below — any question length, no per-word scan) +inline std::vector ownerQuestionWords( std::string_view task ) +{ + std::vector words; + const auto isWordByte = []( unsigned char c ) noexcept + { return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ) || c == '_' || c >= 0x80; }; + std::size_t i = 0; + while( i < task.size() ) + { + if( !isWordByte( static_cast( task[i] ) ) ) + { + ++i; + continue; + } + const std::size_t start = i; + while( i < task.size() && isWordByte( static_cast( task[i] ) ) ) + { + ++i; + } + std::string w = ownerFoldName( task.substr( start, i - start ) ); + if( w.size() >= 3 && !rw::isRouteStopword( w ) ) + { + words.push_back( std::move( w ) ); + } + } + std::sort( words.begin(), words.end() ); + words.erase( std::unique( words.begin(), words.end() ), words.end() ); + return words; +} + +// One owner of the question: a ranked row of the pool, its 1-based rank, the folded word its name equals. +struct OwnerCandidate +{ + NodeId id; + std::size_t rank; + std::string word; +}; + +// A callable DEFINITION outside the test partition: the kind/definition/test half of the owner predicate, every reading from +// the index (a C prototype and its body are one entity — the body's row is the owner). +inline bool isOwnerCallableDef( const IngestResult& ing, NodeId id ) noexcept +{ + const Symbol& s = ing.symbols[id]; + return ( s.kind == SymKind::Function || s.kind == SymKind::Method ) && isDefinitionNotDeclaration( s ) && !isTestSymbol( ing, id ); +} + +// OWNER-HOP'S FOUND SET (lane owner-hop-068; verbs_for.h forOwnerHopPlan states the rule in full): the callable definitions of the +// ranked pool (the first kForLensDefaultTopN rows with a positive score) whose whole folded name equals a question word, kept +// per WORD when one of that word's definitions ranks inside the head tier (rank <= kForDocExcerptRankCount); rank order, common +// words after the rest (stable). Owner-hop and the body slot read the same set; the RIPWIRE_NO_OWNER_HOP switch is owner-hop's +// own (it stays in forOwnerHopPlan), so it never reaches the slot. +inline std::vector forOwnerCandidates( const IngestResult& ing, const std::vector& words, + const std::vector& lensSurfaceIds, const std::vector& lensRank ) +{ + std::vector found; + if( words.empty() ) + { + return found; + } + const std::size_t pool = std::min( lensSurfaceIds.size(), std::size_t( kForLensDefaultTopN ) ); + for( std::size_t i = 0; i < pool; ++i ) + { + const NodeId id = lensSurfaceIds[i]; + if( id >= ing.symbols.size() || lensRank[id] <= 0.0f || !isOwnerCallableDef( ing, id ) ) + { + continue; + } + std::string key = ownerFoldName( ing.symbols[id].name ); + if( std::binary_search( words.begin(), words.end(), key ) ) + { + found.push_back( OwnerCandidate{ id, i + 1, std::move( key ) } ); + } + } + // the head-tier gate, per WORD: a word qualifies when one of its definitions ranks inside the head tier + std::vector headWords; + for( const OwnerCandidate& o : found ) + { + if( o.rank <= kForDocExcerptRankCount ) + { + headWords.push_back( o.word ); + } + } + std::sort( headWords.begin(), headWords.end() ); + std::erase_if( found, [ & ]( const OwnerCandidate& o ) { return !std::binary_search( headWords.begin(), headWords.end(), o.word ); } ); + // rank order, common words after the rest (stable: rank order inside each group) + std::stable_partition( found.begin(), found.end(), []( const OwnerCandidate& o ) { return !isOwnerCommonWord( o.word ); } ); + return found; +} + +// The full dialect's reading of the slot, shared word for word by the CLI lens (which adds its root clause) and the MCP twin: +// present-only. What it does NOT mean is in it: not a resolution, not necessarily the subject, not the only relevant body. +// No "--": it rides inside an XML comment. +inline constexpr std::string_view kForBodySlotLegend = + "; owner body: an explanatory question (how does, what does) also carries ONE body, in a bodies section near the end. It is " + "chosen BY NAME in the ranked head, not resolved: pick=name its name equals a question word (among several such owners, the " + "one calling an owner another word names), pick=member a method of a ranked class the question names (by the parser's scope " + "or the class's own span), pick=subtoken the best-ranked callable sharing a word. It is not necessarily the question's " + "subject nor the only relevant body. Over 6000 B it is cut at a line end (b truncated=1 lines= next= the rest); a budget too " + "small for it skips it: bodies shown=0 capped=1 next= the call that serves it"; + +// ── THE BODY SLOT ─────────────────────────────────────────────────────────────────────────────────────────────────────────── +// The CDATA cap of the one body: a larger owner is head-cut at a line end (UTF-8 safe, packBodies' cutOversizedBody) with +// truncated="1" lines= next= naming the rest; one first line over it is served whole with over_ceiling="1". A content cap with +// a disclosed, recoverable cut (knob census). +inline constexpr std::size_t kForBodySlotMaxBytes = 6000; +static_assert( kForBodySlotMaxBytes == 6000 && kForBodySlotLegend.find( "Over 6000 B" ) != std::string_view::npos, + "the slot legend states the cap: move both together" ); + +enum class BodySlotPick : std::uint8_t { None, Name, Member, Subtoken }; + +inline std::string_view bodySlotPickName( BodySlotPick p ) noexcept +{ + switch( p ) + { + case BodySlotPick::Name: return "name"; + case BodySlotPick::Member: return "member"; + case BodySlotPick::Subtoken: return "subtoken"; + case BodySlotPick::None: break; + } + return {}; +} + +struct ForBodySlotOwner +{ + NodeId id = kNoNode; + BodySlotPick pick = BodySlotPick::None; + bool any() const noexcept { return id != kNoNode; } +}; + +// Does the slot fire for this question at all: an explanatory cue (taskroute.h, word-bounded — `show documentation` holds no +// `how do`) and the A/B switch not set. The caller adds the posture (compact route, XML dialect, no caller-chosen bodies). +inline bool forBodySlotAsked( std::string_view task ) +{ + return std::getenv( "RIPWIRE_NO_BODY_SLOT" ) == nullptr && rw::taskroute::hasExplanatoryCue( rw::queryshape::detail::lowerAscii( task ) ); +} + +// THE ENTRY POINT among the name owners: the first (in their order) with an out-edge — resolved or by name — to ANOTHER owner +// named by a DIFFERENT word. Same-word edges (a recursion the graph keeps, super().refresh(), an overload chain) are not +// entry-point evidence: they say nothing about which of two subjects the question's words name drives the other. +inline NodeId forOwnerEntryPoint( const std::vector& owners, const std::vector& outOff, + const std::vector& outTargets ) +{ + for( const OwnerCandidate& o : owners ) + { + if( std::size_t( o.id ) + 1 >= outOff.size() ) + { + continue; + } + for( std::uint32_t e = outOff[o.id]; e < outOff[o.id + 1] && e < outTargets.size(); ++e ) + { + const NodeId t = outTargets[e]; + const auto hit = std::ranges::find_if( owners, [ & ]( const OwnerCandidate& c ) { return c.id == t; } ); + if( hit != owners.end() && hit->id != o.id && hit->word != o.word ) + { + return o.id; + } + } + } + return owners.empty() ? kNoNode : owners.front().id; +} + +// Is `m` a member of the class row `c` BY THE PARSER: its scope names the class, or the class's own span contains it. +inline bool isParsedMemberOf( const Symbol& m, const Symbol& c ) noexcept +{ + if( m.id == c.id || !langCompatible( m.lang, c.lang ) ) + { + return false; + } + if( !m.scope.empty() && m.scope == c.name ) + { + return true; + } + return m.fileId == c.fileId && c.sigStartByte <= m.sigStartByte && m.endByte <= c.endByte && c.endByte > c.sigStartByte; +} + +// THE MEMBER OWNER: the first head-tier class/struct/interface row (rank order) with a member definition the question names +// (a non-common word); among that class's members the highest lens score, then the lowest id. One pass over the symbols +// collects the named callable definitions once (no per-class rescan); kNoNode when no head-tier class has one. +inline NodeId forMemberOwner( const IngestResult& ing, const std::vector& words, const std::vector& lensSurfaceIds, + const std::vector& lensRank ) +{ + std::vector classRows; + const std::size_t head = std::min( lensSurfaceIds.size(), std::size_t( kForDocExcerptRankCount ) ); + for( std::size_t i = 0; i < head; ++i ) + { + const NodeId id = lensSurfaceIds[i]; + if( id < ing.symbols.size() && lensRank[id] > 0.0f ) + { + const SymKind k = ing.symbols[id].kind; + if( k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface ) + { + classRows.push_back( id ); + } + } + } + if( classRows.empty() ) + { + return kNoNode; + } + std::vector memberWords = words; + std::erase_if( memberWords, []( const std::string& w ) { return isOwnerCommonWord( w ); } ); + if( memberWords.empty() ) + { + return kNoNode; + } + // the named callable definitions, once (the fold allocates, so the cheap kind test runs first) + std::vector named; + for( NodeId id = 0; id < ing.symbols.size(); ++id ) + { + const SymKind k = ing.symbols[id].kind; + if( ( k == SymKind::Function || k == SymKind::Method ) && isOwnerCallableDef( ing, id ) + && std::binary_search( memberWords.begin(), memberWords.end(), ownerFoldName( ing.symbols[id].name ) ) ) + { + named.push_back( id ); + } + } + for( NodeId cls : classRows ) + { + NodeId best = kNoNode; + for( NodeId m : named ) + { + if( !isParsedMemberOf( ing.symbols[m], ing.symbols[cls] ) ) + { + continue; + } + const float rm = m < lensRank.size() ? lensRank[m] : 0.0f; + const float rb = best != kNoNode && best < lensRank.size() ? lensRank[best] : 0.0f; + if( best == kNoNode || rm > rb ) // `named` is in id order, so a tie keeps the lower id + { + best = m; + } + } + if( best != kNoNode ) + { + return best; + } + } + return kNoNode; +} + +// THE SUBTOKEN FALLBACK: the best-ranked head-tier callable definition whose name shares a subtoken with a question word. +inline NodeId forSubtokenOwner( const IngestResult& ing, const std::vector& words, const std::vector& lensSurfaceIds, + const std::vector& lensRank ) +{ + const std::size_t head = std::min( lensSurfaceIds.size(), std::size_t( kForDocExcerptRankCount ) ); + for( std::size_t i = 0; i < head; ++i ) + { + const NodeId id = lensSurfaceIds[i]; + if( id < ing.symbols.size() && lensRank[id] > 0.0f && isOwnerCallableDef( ing, id ) && shareNameSubtoken( ing.symbols[id].name, words ) ) + { + return id; + } + } + return kNoNode; +} + +// The slot's one owner, in the order the header comment states; {kNoNode, None} when the question names none. +inline ForBodySlotOwner forBodySlotOwner( const IngestResult& ing, const std::vector& outOff, const std::vector& outTargets, + std::string_view task, const std::vector& lensSurfaceIds, const std::vector& lensRank ) +{ + const std::vector words = ownerQuestionWords( task ); + if( words.empty() ) + { + return {}; + } + const std::vector owners = forOwnerCandidates( ing, words, lensSurfaceIds, lensRank ); + ForBodySlotOwner out; + if( !owners.empty() ) + { + out = ForBodySlotOwner{ forOwnerEntryPoint( owners, outOff, outTargets ), BodySlotPick::Name }; + } + else if( const NodeId m = forMemberOwner( ing, words, lensSurfaceIds, lensRank ); m != kNoNode ) + { + out = ForBodySlotOwner{ m, BodySlotPick::Member }; + } + else if( const NodeId s = forSubtokenOwner( ing, words, lensSurfaceIds, lensRank ); s != kNoNode ) + { + out = ForBodySlotOwner{ s, BodySlotPick::Subtoken }; + } + ENSURES( !out.any() || ( out.id < ing.symbols.size() && isOwnerCallableDef( ing, out.id ) ), + "the body slot's owner is one indexed callable definition outside the test partition" ); + return out; +} + +// pick= on the rendered element (packBodies writes the element; the slot names how its one body was chosen) +inline void spliceBodySlotPick( std::string& xml, BodySlotPick pick ) +{ + const std::size_t at = xml.find( " esc; + std::string x = ""; + return x; +} + +// THE SLOT'S RENDER, one for both dialects: the owner's body through packBodies (the --expand shape, kept), cut at +// kForBodySlotMaxBytes; pick= spliced on . `share` is what an explicit target left this section (nullopt: the default +// regime, the body rides on top): a render over it is replaced by the skip disclosure and its redaction count taken back. +struct BodySlotRender +{ + std::string xml; // the served element, or the skip disclosure; empty when nothing rides (degrade) + std::size_t bodyTokens = 0; // a served body, priced at the body rate (the T3 bodies' rate) + std::size_t markupBytes = 0; // a skip disclosure, priced with the rest of the markup + bool served = false; + bool skipped = false; +}; +struct BodySlotInputs +{ + const IngestResult& ing; + const std::vector& outOff; + const std::vector& outTargets; + const std::vector& outNameOnly; + const std::vector& lensRank; // orders a cut listing, as every --for body's + std::string_view rootArg; + RedactCounts* redact; + const notes::NoteIndex* notes; + bool viaLegendInHead; // the header already defines via="name" + bool compress; // --compress (the CLI only; the MCP twin never sets it) +}; +inline BodySlotRender renderForBodySlot( const BodySlotInputs& in, ForBodySlotOwner owner, std::optional share ) +{ + EXPECTS( owner.any() && owner.id < in.ing.symbols.size(), "the slot renders one indexed owner" ); + BodySlotRender out; + const std::optional before = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; + ChargedSection sec = chargeSection( [ & ]( std::FILE* f ) + { packBodies( f, in.ing, { owner.id }, kForBodySlotMaxBytes, in.outOff, in.outTargets, in.outNameOnly, in.compress, in.redact, + /*ranges=*/nullptr, in.notes, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, + in.rootArg, &in.lensRank, in.viaLegendInHead ); }, + kBytesPerTokenBody ); + if( !sec.isRendered ) + { + // chargeSection DISCLOSEd the failed buffer into `sec` (its alert is on stderr): the slot rides nowhere and the caller drops + // its clause, so nothing names a body that is not there (the T3 auto bundle's degrade contract) + rw::redactTakeBack( in.redact, before, in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt ); + return out; + } + spliceBodySlotPick( sec.xml, owner.pick ); + if( share && sec.xml.size() > *share ) + { + rw::redactTakeBack( in.redact, before, in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt ); // not served + out.xml = bodySlotSkippedXml( in.ing, owner.id, owner.pick, in.rootArg ); + out.markupBytes = out.xml.size(); + out.skipped = true; + return out; + } + out.xml = std::move( sec.xml ); + out.bodyTokens = tokensForEmittedBytes( out.xml.size(), kBytesPerTokenBody ); + out.served = true; + return out; +} + +} // namespace rw diff --git a/src/graphlegend.h b/src/graphlegend.h index 3be632c73..2ae4e520f 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -373,6 +373,11 @@ inline constexpr std::string_view kMcpForBuildingBlocksLegend = static_assert( kMcpForBuildingBlocksLegend.find( kForZeroAbsentMcpNote ) != std::string_view::npos, "the MCP for zero reading is spelled once" ); inline constexpr std::string_view kMcpForBundleSigsLegend = "; bundle=sigs: signatures only in this bundle, no inline bodies — fetch a symbol's full body with the fetch_body verb"; +// for-body-slot: the same sentence on an answer that carries the one owner body of an explanatory question (forowner.h); at least +// as long as the sentence it replaces, so the sigs ledger's exemption of the difference never underflows +inline constexpr std::string_view kMcpForBundleSigsOwnerLegend = + "; bundle=sigs: signatures, and no inline body but the ONE owner body below — fetch any other full body with the fetch_body verb"; +static_assert( kMcpForBundleSigsOwnerLegend.size() >= kMcpForBundleSigsLegend.size(), "the slot's bundle sentence replaces the plain one without shrinking" ); inline constexpr std::string_view kMcpForLensColumnsLegend = "; lens=\"churn,amp,tested\": the three per-row quality columns the CLI for lens carries and this dialect" " does NOT (they need a git and a quality pass this server does not run per request); an absent column here" diff --git a/src/mcpverbs.h b/src/mcpverbs.h index e360d637f..171ec0650 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -20,6 +20,7 @@ #include "filter.h" // §P4: rankTierSymbolMultipliers — the fixture/present tier down-weight the CLI ranking lenses apply #include "redact.h" // RedactCounts — the per-request redaction tally threaded through the body/doc verbs #include "forpage.h" // L-W: the --for file page, coverage= and the thin rule — shared with the CLI twin +#include "forowner.h" // for-body-slot: the one owner body of an explanatory question — shared with the CLI twin #include "packtask.h" // L4: the shared --pack-task / MCP explore+pack_task bundle assembler (packTaskBundleText) #include "partition.h" // the explore verb's `partition` argument (packTaskPartitionText) #include "tracelocus.h" // L4: the shared --from-trace / MCP from_trace bundle assembler (fromTraceBundleText) @@ -1912,6 +1913,8 @@ struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by ref // decided once in forTaskText (the noRoute/task/budget verdicts live there) const std::vector>& implementors; // train 26b: ix.g.implementors, for the count-floor lego re-render const LexTermEvidence& evidence; // FOLD C: the term evidence the tail's page rows rank by + const Graph& g; // for-body-slot: the owner body's edges + ForBodySlotOwner slotOwner; // for-body-slot: the one owner body (forowner.h), none when not asked }; // round C (owner decision 2026-10-14, RESERVE SECTIONS FIRST — the CLI twin's rule): under budget_tokens the sections are scoped and // served from the rows at the target cut, and the rows past the cut fill what the sections served before the tail ( rows, @@ -2088,7 +2091,24 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin // before the tail ( rows, , , ) leave of T — never below the cut. The tail rides on top here, // as it always did on this twin. Re-rendered only when that narrows the rows; the first render's redaction count is // replaced, never added to. - mcpForSoftRefill( in, renderSigs, sigsStr, mcpSigsCut, &mcpSigsNext, mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size(), + // for-body-slot (the CLI twin's rule, forowner.h): the one owner body. At the default it rides on top; under budget_tokens it + // is funded from what the rows at the cut and the sections before it left of T, rides only whole, else is skipped with its + // next=, and the rows' refill below charges what it served — so the rows past the cut give way to it. + BodySlotRender mcpSlot; + if( in.slotOwner.any() ) + { + std::optional share; + if( budgetTokens > 0 ) + { + const std::size_t atCut = sigsStr.size() - std::min( sigsStr.size(), mcpSigsCut.pastTargetBytes ); + share = forSoftSectionsShare( budgetBytesForTokens( budgetTokens ), mcpHdrXml.size() + atCut + legoStr.size() + composeStr.size() + routeStr.size() ); + } + mcpSlot = renderForBodySlot( BodySlotInputs{ ing, in.g.outOff, in.g.outTargets, in.g.outNameOnly, lensRank, flRootArg, redact, notesPtr, + /*viaLegendInHead=*/false, /*compress=*/false }, + in.slotOwner, share ); + } + mcpForSoftRefill( in, renderSigs, sigsStr, mcpSigsCut, &mcpSigsNext, + mcpHdrXml.size() + legoStr.size() + composeStr.size() + routeStr.size() + mcpSlot.xml.size(), redactBeforeSigs, redactAfterSigs ); // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare @@ -2180,6 +2200,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin const std::string tailStr = renderFileTailXml( mcpTail, mcpTail.paths.size(), tailEsc ); // every row computeFileTail collected std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } + std::fwrite( mcpSlot.xml.data(), 1, mcpSlot.xml.size(), mem ); // for-body-slot: "" when no body rides rw::emitRaw( mem, "" ); std::optional answer = mcpAnswerText( stream ); if( !answer ) @@ -2553,6 +2574,15 @@ inline std::optional forTaskText( const std::string& root, const st // resolution, both surfaces. Rows are emitted right after headerStr is flushed, below; the legend // clause (present-only, never ceiling-dropped — this dialect has no ceiling ladder at all) goes here. const std::vector mcpForHdrRows = rw::forNamedHeaderRows( ing, task ); + // for-body-slot (the CLI twin's rule, forowner.h): an explanatory question on the conceptual route carries ONE owner body. + // The bundle sentence then says so (kMcpForBundleSigsOwnerLegend replaces "no inline bodies") and the slot clause rides; both + // are exempt from the sigs charge below exactly as far as they differ from the sentence they replace, so the served rows are + // byte-identical to RIPWIRE_NO_BODY_SLOT=1's. + const ForBodySlotOwner mcpSlotOwner = ( std::string_view( mcpRouteTag ) == "subtoken+body" && forBodySlotAsked( task ) ) + ? forBodySlotOwner( ing, ix.g.outOff, ix.g.outTargets, task, lensSurfaceIds, lensRank ) + : ForBodySlotOwner{}; + const std::string_view mcpBundleLegend = mcpSlotOwner.any() ? rw::kMcpForBundleSigsOwnerLegend : rw::kMcpForBundleSigsLegend; + const std::string_view mcpBodySlotLegend = mcpSlotOwner.any() ? rw::kForBodySlotLegend : std::string_view(); std::string headerStr = rootOpenStr + "', '', t, flags=re.S) @@ -218,11 +226,15 @@ elif cmd == "cutatline": # cutatline FILE NAME MAXB : a head-cut if not m: fail("lines=%r is not lo-hi/T" % a.get("lines")) lo, hi, tot = map(int, m.groups()) src = open(os.path.join(os.environ["FIX"], a["p"]), encoding="utf-8").read().split("\n") - l0 = int(a["l"]) + l0 = defstart(a, cd) want = "\n".join(src[l0 - 1: l0 - 1 + (hi - lo + 1)]) if cd.rstrip("\n") != want.rstrip("\n"): fail("CDATA is not source lines %d..%d of %s (a cut inside a line?)" % (l0, l0 + hi - lo, a["p"])) if not (lo == 1 and hi < tot): fail("lines=%s does not describe a head cut" % a.get("lines")) print("cut %s lines=%s %d B" % (args[1], a["lines"], len(cd.encode()))) +elif cmd == "defline": # defline FILE NAME : print the def's first source line of the + hit = [b for b in bodies(load(args[0])) if b[0].get("n") == args[1]] + if not hit: fail("no " % args[1]) + print(defstart(hit[0][0], hit[0][1])) elif cmd == "nextof": # nextof FILE NAME : print the value t = load(args[0]); hit = [b for b in bodies(t) if b[0].get("n") == args[1]] if not hit or not hit[0][0].get("next"): fail("no " % args[1]) @@ -254,7 +266,7 @@ elif cmd == "estdelta": # estdelta FILE_SLOT FILE_OFF : est_toke if eb == 0: fail("no element to price (est %s vs %s)" % (es, eo)) if es - eo < eb // 5: fail("est_tokens %s - %s < %d B x 0.2 (the body rate is 3.80 B/token)" % (es, eo, eb)) print("est_tokens %s vs %s (+%d for %d B)" % (es, eo, es - eo, eb)) -elif cmd == "paidinside": # paidinside FILE_SLOT FILE_OFF : E4 — the body is paid inside the target, the displaced rows named +elif cmd == "paidinside": # paidinside FILE_SLOT FILE_OFF N : E4 — the body is paid inside the target N, the displaced rows named ts, to = load(args[0]), load(args[1]) if not bodies(ts): fail("premise: no body rides the budgeted answer") ns, no_ = len(rows(ts)), len(rows(to)) @@ -262,13 +274,16 @@ elif cmd == "paidinside": # paidinside FILE_SLOT FILE_OFF : E4 — sa = sigs(ts) if not (sa.get("shown", "").isdigit() and sa.get("total", "").isdigit()): fail(" lacks numeric shown=/total=: %s" % sa) if int(sa["shown"]) != ns or int(sa["total"]) <= ns: fail(" with %d rows: the cut is not disclosed" % (sa["shown"], sa["total"], ns)) - if sa.get("next_offset") != str(ns) or not sa.get("next"): fail(": the displaced rows are not recoverable" % (sa.get("next_offset"), sa.get("next"))) + # the CLI names the continuation as an argv (next=), the MCP twin as the re-call's budget (next_budget_tokens=); both resume at next_offset= + if sa.get("next_offset") != str(ns) or not (sa.get("next") or sa.get("next_budget_tokens", "").isdigit()): + fail(": the displaced rows are not recoverable" % (sa.get("next_offset"), sa.get("next"), sa.get("next_budget_tokens"))) es, eo = est(ts), est(to) lg = len(legend(ts).encode()) - len(legend(to).encode()) rd = len(re.search(r']*>', ts).group(0).encode()) - len(re.search(r']*>', to).group(0).encode()) mrow = max(len(d.group(0).encode()) for d in re.finditer(r']*>.*?', to, re.S)) - bound = (max(lg, 0) + max(rd, 0) + mrow) / 2.5 + 4 - if es - eo > bound: fail("est_tokens %d - %d = %d > %.0f (legend %+d B, root %+d B, one row %d B): the body rides on top of the target" % (es, eo, es - eo, bound, lg, rd, mrow)) + room = max(0, int(args[2]) - eo) # what slot-off left unused under N: a body may fill it without displacing a row + bound = room + (max(lg, 0) + max(rd, 0) + mrow) / 2.5 + 4 + if es - eo > bound: fail("est_tokens %d - %d = %d > %.0f (room %d, legend %+d B, root %+d B, one row %d B): the body rides on top of the target" % (es, eo, es - eo, bound, room, lg, rd, mrow)) print("rows %d < %d, , est %d vs %d (bound +%.0f)" % (ns, no_, sa["shown"], sa["next_offset"], es, eo, bound)) elif cmd == "bodyat": # bodyat FILE NAME PATH LINE : exactly one , it is n=NAME p=PATH l=LINE bs = bodies(load(args[0])) @@ -512,7 +527,7 @@ if r="$( chk cutatline "$TMP/big.xml" render_screen 6000 )"; then ok "(D10) over if nx="$( chk nextof "$TMP/big.xml" render_screen )" && printf '%s' "$nx" | grep -q '^--expand=src/c/render.c:'; then ok "(D10) oversized owner: next=$nx" hi="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o 'lines="[0-9]*-[0-9]*' | sed 's/.*-//' )" - l0="$( grep -o ']*n="render_screen"[^>]*' "$TMP/big.xml" | grep -o ' l="[0-9]*"' | grep -o '[0-9]*' )" + l0="$( chk defline "$TMP/big.xml" render_screen )" # the def's first line (l= names the name's line, one below 'void') if [ -n "$hi" ] && [ -n "$l0" ]; then run "$nx" >"$TMP/big.next.xml" if r="$( chk startsat "$TMP/big.next.xml" src/c/render.c $(( l0 + hi )) )"; then ok "(D10) the next= serves the rest, from source line $(( l0 + hi ))"; else no "(D10) next=: $r"; fi @@ -638,10 +653,10 @@ else fi # ── E: explicit targets (reserve-sections-first) ─────────────────────────────────────────────────────────── -run --for="$T_D" --token-budget=3000 >"$TMP/e3000.xml" -if r="$( chk body "$TMP/e3000.xml" jwt src/middleware/jwt/jwt.ts "getCookie" )"; then ok "(E1) --token-budget=3000: $r"; else no "(E1) --token-budget=3000: $r"; fi -if r="$( chk prefix "$TMP/e3000.xml" "$TMP/d.xml" )"; then ok "(E1) --token-budget=3000: $r"; else no "(E1) --token-budget=3000: $r"; fi -if r="$( chk over "$TMP/e3000.xml" )"; then ok "(E1) --token-budget=3000: no over_ceiling"; else no "(E1) $r"; fi +run --for="$T_D" --token-budget=5000 >"$TMP/e5000.xml" +if r="$( chk body "$TMP/e5000.xml" jwt src/middleware/jwt/jwt.ts "getCookie" )"; then ok "(E1) --token-budget=5000: $r"; else no "(E1) --token-budget=5000: $r"; fi +if r="$( chk prefix "$TMP/e5000.xml" "$TMP/d.xml" )"; then ok "(E1) --token-budget=5000: $r"; else no "(E1) --token-budget=5000: $r"; fi +if r="$( chk over "$TMP/e5000.xml" )"; then ok "(E1) --token-budget=5000: no over_ceiling"; else no "(E1) $r"; fi run --for="$T_D" --token-budget=400 >"$TMP/e400.xml" if r="$( chk rootattr "$TMP/e400.xml" bodies 0 )" && r2="$( chk rootattr "$TMP/e400.xml" reason budget )"; then ok "(E2) --token-budget=400: bodies=0 reason=budget"; else no "(E2) --token-budget=400: ${r:-} ${r2:-}"; fi if r="$( chk bodiesnext "$TMP/e400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(E2) --token-budget=400: the skipped slot discloses itself, recoverable: $r"; else no "(E2) --token-budget=400: $r"; fi @@ -664,9 +679,10 @@ chk cdata "$TMP/s1000000.xml" jwt >"$TMP/cd.wide" 2>/dev/null || cdw=1 chk cdata "$TMP/d.xml" jwt >"$TMP/cd.default" 2>/dev/null || cdd=1 if [ "$cdw$cdd" = "00" ] && [ -s "$TMP/cd.wide" ] && cmp -s "$TMP/cd.wide" "$TMP/cd.default"; then ok "(E3) N=1000000: the body equals the default answer's body"; else no "(E3) N=1000000: no body, or it differs from the default answer's"; fi -# E4: at N=3000 slot-off serves rows past the cut; with the slot the body is reserved first and the rows give way (review R3) -run --for="$T_D" --token-budget=3000 >"$TMP/e4.xml"; runoff --for="$T_D" --token-budget=3000 >"$TMP/e4.off.xml" -if r="$( chk paidinside "$TMP/e4.xml" "$TMP/e4.off.xml" )"; then ok "(E4) --token-budget=3000: $r"; else no "(E4) --token-budget=3000: $r"; fi +# E4: T_REF at N=800 — slot-off serves rows past the cut there and the share the rows at the cut leave holds the small body: +# the body is reserved first and the rows give way (review R3). (T_D's 2.7 KB jwt body is skipped below N=4800: its own arm, E2/E3.) +run --for="$T_REF" --token-budget=800 >"$TMP/e4.xml"; runoff --for="$T_REF" --token-budget=800 >"$TMP/e4.off.xml" +if r="$( chk paidinside "$TMP/e4.xml" "$TMP/e4.off.xml" 800 )"; then ok "(E4) --token-budget=800: $r"; else no "(E4) --token-budget=800: $r"; fi # ── MC: MCP for parity ───────────────────────────────────────────────────────────────────────────────────── mcpfor "$T_D" >"$TMP/m.d.xml"; mcpfor "$T_NOCUE" >"$TMP/m.nocue.xml"; mcpfor "$T_D" '{"budget_tokens":400}' >"$TMP/m.d400.xml" @@ -694,9 +710,9 @@ if [ "$cbc$cbm" = "00" ] && [ -s "$TMP/cd.big.cli" ] && cmp -s "$TMP/cd.big.cli" [ "$mc5" = 0 ] && ok "(MC5) MCP T_BIG: the same head cut as the CLI (truncated=/lines=/next= and CDATA)" r=""; r2="" if r="$( chk body "$TMP/m.rs.xml" index src/rs/indexer.rs "let order = walk(" )" && r2="$( chk nobodyof "$TMP/m.rs.xml" walk )"; then ok "(MC5) MCP T_RS: $r — the entry point, as the CLI"; else no "(MC5) MCP T_RS: ${r:-} ${r2:-}"; fi -# E4 on MCP: budget_tokens=3000 pays the body inside the target too -mcpfor "$T_D" '{"budget_tokens":3000}' >"$TMP/m.e4.xml"; mcpforoff "$T_D" '{"budget_tokens":3000}' >"$TMP/m.e4.off.xml" -if r="$( chk paidinside "$TMP/m.e4.xml" "$TMP/m.e4.off.xml" )"; then ok "(E4) MCP budget_tokens=3000: $r"; else no "(E4) MCP budget_tokens=3000: $r"; fi +# E4 on MCP: budget_tokens=800 pays the body inside the target too +mcpfor "$T_REF" '{"budget_tokens":800}' >"$TMP/m.e4.xml"; mcpforoff "$T_REF" '{"budget_tokens":800}' >"$TMP/m.e4.off.xml" +if r="$( chk paidinside "$TMP/m.e4.xml" "$TMP/m.e4.off.xml" 800 )"; then ok "(E4) MCP budget_tokens=800: $r"; else no "(E4) MCP budget_tokens=800: $r"; fi # EV1: the owner-hop env never reaches the slot (review R6: the shared owner helper carries no early return) RIPWIRE_NO_OWNER_HOP=1 "$BIN" "$FIX" --cache="$TMP/rw.cache" --for="$T_D" 2>/dev/null >"$TMP/d.noown.xml" bs1=0; bs2=0 @@ -727,7 +743,7 @@ t = t.replace("", body + "") t = re.sub(r'est_tokens="(\d+)"', lambda m: 'est_tokens="%d"' % (int(m.group(1)) + 790), t, count=1) open(sys.argv[2], "w", encoding="utf-8").write(t) PY2 -if chk paidinside "$TMP/c5.xml" "$TMP/e4.off.xml" >/dev/null 2>&1; then no "(M5) the E4 checker accepted a body appended after the row cut"; else ok "(M5) the E4 checker rejects a body appended after the row cut"; fi +if chk paidinside "$TMP/c5.xml" "$TMP/e4.off.xml" 800 >/dev/null 2>&1; then no "(M5) the E4 checker accepted a body appended after the row cut"; else ok "(M5) the E4 checker rejects a body appended after the row cut"; fi # ── X: hygiene ────────────────────────────────────────────────────────────────────────────────────────────── run --for="$T_D" >"$TMP/d2.xml" diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 137187f5e..4a03cfa1b 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -440,7 +440,9 @@ done # ── (B) explicit budget: owner funded first; an exhausted ceiling adds nothing; a cut is disclosed (C7) ────────────── f="$TMP/ts.b.xml"; run ts "$f" "--for=$QTS" --token-budget=4500 fo="$TMP/ts.b.off.xml"; runoff ts "$fo" "--for=$QTS" --token-budget=4500 -if ran_ok "$fo" "(B1) A/B" && ! grep -q 'reason="compact-route"' "$fo"; then no "(B1) premise: --token-budget=4500 no longer leaves the hops a budget on the A/B handle — re-tune"; fi +# for-body-slot (train 28; rule 4, by design): an explanatory question's owner body now rides beside the hops, and a served one +# turns the root's reason to "owner" — the premise "the hops kept a budget" reads either spelling (never "budget") +if ran_ok "$fo" "(B1) A/B" && ! grep -qE 'reason="(compact-route|owner)"' "$fo"; then no "(B1) premise: --token-budget=4500 no longer leaves the hops a budget on the A/B handle — re-tune"; fi if ran_ok "$f" "(B1)"; then HJ "$f" "$TMP/ts.b.json" "(B1)"; H root "$f" >"$TMP/ts.b.root" first="$( J "d['rows'][0]['n'] if d['rows'] else None" <"$TMP/ts.b.json" )" From 6b966a566a3e60ffc8cfe5265ee49eda1a8e90e7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 15:14:32 -0400 Subject: [PATCH 116/162] =?UTF-8?q?test:=20FIX2=20=E2=80=94=20one=20shared?= =?UTF-8?q?=20tail=5Fsize=20(test/lib/softbudget.py)=20for=20the=20five=20?= =?UTF-8?q?soft-target=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fornotesbudgetcheck, forsoftbudgetcheck, compactlegendcheck, estchargecheck and forsigspancheck each carried a verbatim copy of FIX1's tail_size (a clone class, CHECKLIST 32). They now import it from test/lib/softbudget.py (RIPWIRE_TESTLIB, the shardmatch.py pattern). Behaviour-identical: the same PASS/FAIL lines before and after, on the f278ac58 build and on the 974dd5b1 build. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 12 +++--------- test/estchargecheck.sh | 12 +++--------- test/fornotesbudgetcheck.sh | 12 +++--------- test/forsigspancheck.sh | 12 +++--------- test/forsoftbudgetcheck.sh | 12 +++--------- test/lib/softbudget.py | 20 ++++++++++++++++++++ 6 files changed, 35 insertions(+), 45 deletions(-) create mode 100644 test/lib/softbudget.py diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index bba95ab00..1a7d0bfc2 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -51,6 +51,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "compactlegendcheck: BIN=$BIN" @@ -1175,15 +1176,8 @@ def sections_u( s ): a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ); k = len( a[ "rows" ] ) -def tail_size( t ): - # FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, so U - # counts the tail at no less than the reference tail (an upper bound on the reserve: the maximality check stays a bound) - import json as _j - if t.lstrip().startswith( "{" ): - try: return len( _j.dumps( _j.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) - except ValueError: return 0 - m = re.search( r"", t, re.S ) - return len( m.group( 0 ).encode() ) if m else 0 +# the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) +import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size U = sections_u( a[ "s" ] ) if len( sys.argv ) > 5: try: U += max( 0, tail_size( open( sys.argv[ 5 ], encoding = "utf-8", errors = "replace" ).read() ) - tail_size( a[ "s" ] ) ) diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index d63d28197..46aef2f82 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -42,6 +42,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "estchargecheck: BIN=$BIN" @@ -689,15 +690,8 @@ def verdict( path, n, refpath=None ): s = open( path, encoding='utf-8', errors='replace' ).read() U = ( len( s.split( '', 1 )[ 1 ].encode() ) - len( '' ) ) if '' in s else 0 U += sum( len( h.encode() ) for h in re.findall( r']*/>', s.split( '", t, re.S ) - return len( m.group( 0 ).encode() ) if m else 0 + # the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) + import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size U += max( 0, tail_size( open( refpath, encoding='utf-8', errors='replace' ).read() ) - tail_size( s ) ) root = re.match( r']*>', s ) if root and ' bundle="' not in root.group( 0 ): diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index 25a77edbb..f4891760b 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -201,6 +201,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "fornotesbudgetcheck: BIN=$BIN" @@ -328,15 +329,8 @@ def sections_u( s ): a = read( sys.argv[ 1 ] ) if a is None: sys.exit( 3 ) N = int( sys.argv[ 2 ] ); X = int( N * 2.36 * 0.90 ) -def tail_size( t ): - # FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, so U - # counts the tail at no less than the reference tail (an upper bound on the reserve: the maximality check stays a bound) - import json as _j - if t.lstrip().startswith( "{" ): - try: return len( _j.dumps( _j.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) - except ValueError: return 0 - m = re.search( r"", t, re.S ) - return len( m.group( 0 ).encode() ) if m else 0 +# the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) +import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size U = sections_u( open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() ) if len( sys.argv ) > 4: _at = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read(); _rt = open( sys.argv[ 4 ], encoding='utf-8', errors='replace' ).read() diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 2b32415af..80dfac337 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -93,6 +93,7 @@ set -u export PYTHONDONTWRITEBYTECODE=1 ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" # a gate that indexes a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary CORPUS="$ROOT/test/forcompletefix" @@ -470,15 +471,8 @@ if mode == "tight": else: U = ( len( A.split( "", 1 )[ 1 ].encode() ) - len( "" ) if "" in A else 0 ) U += sum( len( h.encode() ) for h in re.findall( r']*/>', A.split( "", t, re.S ) - return len( m.group( 0 ).encode() ) if m else 0 + # the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) + import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size if len( sys.argv ) > 7: U += max( 0, tail_size( body( sys.argv[ 7 ] ) ) - tail_size( A ) ) if a is None or ref is None or not ref[ 0 ]: bad.append( "ST: no rows to charge (premise: answer and uncut reference)" ) elif tb > 0: diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index f03cfb80c..9102f0692 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -53,6 +53,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" # gitenvhermeticcheck (D): no ambient GIT_* reaches the RP10 fixture repos +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" fail=0 @@ -446,15 +447,8 @@ a, ref, T = rb( sys.argv[ 1 ] ), rb( sys.argv[ 2 ] ), int( sys.argv[ 3 ] ) ans = open( sys.argv[ 1 ] ).read() U = ( len( ans.split( '', 1 )[ 1 ].encode() ) - len( '' ) if '' in ans else 0 ) U += sum( len( h.encode() ) for h in re.findall( r']*/>', ans.split( '", t, re.S ) - return len( m.group( 0 ).encode() ) if m else 0 +# the tail size: test/lib/softbudget.py, shared by the five soft-target readers (the FIX1 R1 tail floor) +import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import tail_size U += max( 0, tail_size( open( sys.argv[ 2 ] ).read() ) - tail_size( ans ) ) if a is None or ref is None: print( "ERR no " ); sys.exit( 0 ) ab, ar = a; rr = ref[ 1 ] diff --git a/test/lib/softbudget.py b/test/lib/softbudget.py new file mode 100644 index 000000000..41da72bea --- /dev/null +++ b/test/lib/softbudget.py @@ -0,0 +1,20 @@ +# softbudget.py — the soft --token-budget readers' shared helpers (lane forbudget-soft; CHECKLIST 32: one copy, not five). +# +# Five gates read a soft-target answer's ST invariant (fornotesbudgetcheck, forsoftbudgetcheck, compactlegendcheck, +# estchargecheck, forsigspancheck). Each keeps its own row reader (their dialects and row charges differ); what they share +# lives here. The gates export RIPWIRE_TESTLIB="$ROOT/test/lib" and import this file (sys.path insert), as crashsweepcheck +# does shardmatch.py. No state; pure functions of the answer text. +import json, re + + +def tail_size( t ): + """Bytes of the answer's tail: XML, the whole … element; JSON, the compact "tail" object. + + FIX1 R1: the refill reserves the tail at its HEAD-scoped size (the tail of the no-budget answer) when that is what binds, + so maximality counts the tail at no less than the reference tail (an upper bound on the reserve: the check stays a bound). + """ + if t.lstrip().startswith( "{" ): + try: return len( json.dumps( json.loads( t ).get( "tail", {} ), separators=( ",", ":" ), ensure_ascii=False ).encode() ) + except ValueError: return 0 + m = re.search( r"", t, re.S ) + return len( m.group( 0 ).encode() ) if m else 0 From 97a6808473cdc726d2cb07e67c027cf868074a9c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 15:57:34 -0400 Subject: [PATCH 117/162] refactor(for): the slot's helpers without clone rows (pick= table, the cue loop, the member owner in three parts) Co-Authored-By: Claude Opus 5.5 --- src/forowner.h | 87 ++++++++++++++++++++++--------------------------- src/taskroute.h | 10 ++++-- 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/forowner.h b/src/forowner.h index 7bf4d5ce5..560236ca5 100644 --- a/src/forowner.h +++ b/src/forowner.h @@ -54,10 +54,6 @@ inline constexpr std::string_view kOwnerCommonWords[] = { "use", "when", "with", "write" }; static_assert( std::ranges::is_sorted( kOwnerCommonWords, rw::sortutil::svLess ), "kOwnerCommonWords is binary-searched: keep it sorted" ); -inline bool isOwnerCommonWord( std::string_view w ) noexcept -{ - return std::ranges::binary_search( kOwnerCommonWords, w, rw::sortutil::svLess ); // svLess: #345 -} // ASCII fold of one name or word, leading _ # $ @ removed (the owner match key) inline std::string ownerFoldName( std::string_view n ) @@ -160,7 +156,8 @@ inline std::vector forOwnerCandidates( const IngestResult& ing, std::sort( headWords.begin(), headWords.end() ); std::erase_if( found, [ & ]( const OwnerCandidate& o ) { return !std::binary_search( headWords.begin(), headWords.end(), o.word ); } ); // rank order, common words after the rest (stable: rank order inside each group) - std::stable_partition( found.begin(), found.end(), []( const OwnerCandidate& o ) { return !isOwnerCommonWord( o.word ); } ); + std::stable_partition( found.begin(), found.end(), []( const OwnerCandidate& o ) + { return !std::ranges::binary_search( kOwnerCommonWords, std::string_view( o.word ), rw::sortutil::svLess ); } ); // svLess: #345 return found; } @@ -185,17 +182,9 @@ static_assert( kForBodySlotMaxBytes == 6000 && kForBodySlotLegend.find( "Over 60 enum class BodySlotPick : std::uint8_t { None, Name, Member, Subtoken }; -inline std::string_view bodySlotPickName( BodySlotPick p ) noexcept -{ - switch( p ) - { - case BodySlotPick::Name: return "name"; - case BodySlotPick::Member: return "member"; - case BodySlotPick::Subtoken: return "subtoken"; - case BodySlotPick::None: break; - } - return {}; -} +// pick= spellings, indexed by BodySlotPick (None has none) +inline constexpr std::string_view kBodySlotPickNames[] = { "", "name", "member", "subtoken" }; +static_assert( std::size( kBodySlotPickNames ) == std::size_t( BodySlotPick::Subtoken ) + 1, "one pick= spelling per BodySlotPick" ); struct ForBodySlotOwner { @@ -250,39 +239,35 @@ inline bool isParsedMemberOf( const Symbol& m, const Symbol& c ) noexcept return m.fileId == c.fileId && c.sigStartByte <= m.sigStartByte && m.endByte <= c.endByte && c.endByte > c.sigStartByte; } -// THE MEMBER OWNER: the first head-tier class/struct/interface row (rank order) with a member definition the question names -// (a non-common word); among that class's members the highest lens score, then the lowest id. One pass over the symbols -// collects the named callable definitions once (no per-class rescan); kNoNode when no head-tier class has one. -inline NodeId forMemberOwner( const IngestResult& ing, const std::vector& words, const std::vector& lensSurfaceIds, - const std::vector& lensRank ) +// The head-tier (rank <= kForDocExcerptRankCount) class/struct/interface rows, rank order: the classes that may donate a member. +inline std::vector headTierClassRows( const IngestResult& ing, const std::vector& lensSurfaceIds, const std::vector& lensRank ) { - std::vector classRows; + std::vector rows; const std::size_t head = std::min( lensSurfaceIds.size(), std::size_t( kForDocExcerptRankCount ) ); for( std::size_t i = 0; i < head; ++i ) { const NodeId id = lensSurfaceIds[i]; - if( id < ing.symbols.size() && lensRank[id] > 0.0f ) + if( id >= ing.symbols.size() || lensRank[id] <= 0.0f ) { - const SymKind k = ing.symbols[id].kind; - if( k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface ) - { - classRows.push_back( id ); - } + continue; + } + const SymKind k = ing.symbols[id].kind; + if( k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface ) + { + rows.push_back( id ); } } - if( classRows.empty() ) - { - return kNoNode; - } + return rows; +} + +// Every callable definition outside the tests whose folded name is a NON-common question word, in id order — one pass over the +// symbols (the fold allocates, so the cheap kind test runs first), read once by every head-tier class. +inline std::vector memberWordDefs( const IngestResult& ing, const std::vector& words ) +{ std::vector memberWords = words; - std::erase_if( memberWords, []( const std::string& w ) { return isOwnerCommonWord( w ); } ); - if( memberWords.empty() ) - { - return kNoNode; - } - // the named callable definitions, once (the fold allocates, so the cheap kind test runs first) + std::erase_if( memberWords, []( const std::string& w ) { return std::ranges::binary_search( kOwnerCommonWords, std::string_view( w ), rw::sortutil::svLess ); } ); std::vector named; - for( NodeId id = 0; id < ing.symbols.size(); ++id ) + for( NodeId id = 0; id < ing.symbols.size() && !memberWords.empty(); ++id ) { const SymKind k = ing.symbols[id].kind; if( ( k == SymKind::Function || k == SymKind::Method ) && isOwnerCallableDef( ing, id ) @@ -291,18 +276,24 @@ inline NodeId forMemberOwner( const IngestResult& ing, const std::vector& words, const std::vector& lensSurfaceIds, + const std::vector& lensRank ) +{ + const std::vector classRows = headTierClassRows( ing, lensSurfaceIds, lensRank ); + const std::vector named = classRows.empty() ? std::vector{} : memberWordDefs( ing, words ); + const auto score = [ & ]( NodeId id ) { return id < lensRank.size() ? lensRank[id] : 0.0f; }; for( NodeId cls : classRows ) { NodeId best = kNoNode; for( NodeId m : named ) { - if( !isParsedMemberOf( ing.symbols[m], ing.symbols[cls] ) ) - { - continue; - } - const float rm = m < lensRank.size() ? lensRank[m] : 0.0f; - const float rb = best != kNoNode && best < lensRank.size() ? lensRank[best] : 0.0f; - if( best == kNoNode || rm > rb ) // `named` is in id order, so a tie keeps the lower id + // `named` is in id order, so a tie in score keeps the lower id + if( isParsedMemberOf( ing.symbols[m], ing.symbols[cls] ) && ( best == kNoNode || score( m ) > score( best ) ) ) { best = m; } @@ -370,7 +361,7 @@ inline void spliceBodySlotPick( std::string& xml, BodySlotPick pick ) const std::size_t close = xml.find( '>', at ); if( close != std::string::npos ) { - xml.insert( close, std::string( " pick=\"" ) + std::string( bodySlotPickName( pick ) ) + "\"" ); + xml.insert( close, std::string( " pick=\"" ) + std::string( kBodySlotPickNames[std::size_t( pick )] ) + "\"" ); } } @@ -385,7 +376,7 @@ inline std::string bodySlotSkippedXml( const IngestResult& ing, NodeId id, BodyS const std::string path = rootArg.empty() ? std::string( ing.files[s.fileId] ) : std::string( rw::sarif::rootRelativeUri( ing.files[s.fileId], rootPrefix ) ); std::vector esc; std::string x = ""; diff --git a/src/taskroute.h b/src/taskroute.h index 11c1f54b8..366eddbaa 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -1252,8 +1252,14 @@ inline constexpr std::string_view kExplanatoryCues[] = { // (forowner.h forBodySlotAsked) read the one predicate. inline bool hasExplanatoryCue( std::string_view lower ) noexcept { - return std::any_of( std::begin( kExplanatoryCues ), std::end( kExplanatoryCues ), - [lower]( const std::string_view cue ) { return boundedFind( lower, cue ) != std::string_view::npos; } ); + for( const std::string_view cue : kExplanatoryCues ) + { + if( boundedFind( lower, cue ) != std::string_view::npos ) + { + return true; // one word-bounded cue is enough + } + } + return false; } // `since Monday`, `since 2026-09-01` — structural, because a date has no paraphrase (the same reasoning From d3062ebc7bf2d4574778924110e6699cd1907c71 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 16:01:16 -0400 Subject: [PATCH 118/162] docs: regenerate LIMITS.md, TUNING.md and the README cap count (kForBodySlotMaxBytes); the cue predicate reads cueScore Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/LIMITS.md | 18 +++++++++++++----- docs/TUNING.md | 16 ++++++++-------- src/taskroute.h | 13 +++++-------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5eef8409a..e7416ecd5 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 254 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 255 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 62387c578..4663d8248 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 254 | 98 | 150 | **104** | +| 255 | 99 | 151 | **104** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 254 + 7 is the 261 constants this generator parses out of `src/`. +are not counted as caps, and 255 + 7 is the 262 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 254 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 115 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 255 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 116 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 98 files that declare a cap — the 254 caps counted above, and no parameter. +One table for each of the 99 files that declare a cap — the 255 caps counted above, and no parameter. ### `src/abicheck.h` @@ -324,6 +324,14 @@ Discloses: `capped`, `hosts_capped` | `kMaxFlipRows` | `25` | OUTPUT | per emitted list; a DEFAULT --limit=N raises and --detail lifts | | `kMaxNearMisses` | `5` | OUTPUT | "did you mean" suggestions on an unknown gate name; --limit=N raises it | +### `src/forowner.h` + +Discloses: `capped` + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kForBodySlotMaxBytes` | `6000` | — | — | + ### `src/forpage.h` Discloses: `capped` diff --git a/docs/TUNING.md b/docs/TUNING.md index e328dec7f..74e8c71db 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 162 | 161 | 112 | 12 | **37** | 75 | +| 163 | 162 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **162 cap -declarations** under **161 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **163 cap +declarations** under **162 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 161 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **38 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 162 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **39 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, `kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+32 more). 111 + 12 + 38 = 161, and `emit` refuses to render a partition that does not add up. The ++33 more). 111 + 12 + 39 = 162, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 162" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 163" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 162-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 163-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/src/taskroute.h b/src/taskroute.h index 366eddbaa..b8d701009 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -1252,14 +1252,11 @@ inline constexpr std::string_view kExplanatoryCues[] = { // (forowner.h forBodySlotAsked) read the one predicate. inline bool hasExplanatoryCue( std::string_view lower ) noexcept { - for( const std::string_view cue : kExplanatoryCues ) - { - if( boundedFind( lower, cue ) != std::string_view::npos ) - { - return true; // one word-bounded cue is enough - } - } - return false; + // the house cue scorer (cueScore), word-bounded, over the six cues — any hit is a cue + static_assert( std::size( kExplanatoryCues ) == 6, "hasExplanatoryCue lists every explanatory cue" ); + return cueScore( lower, CueMatch::WordBounded, + { { kExplanatoryCues[0], 1 }, { kExplanatoryCues[1], 1 }, { kExplanatoryCues[2], 1 }, + { kExplanatoryCues[3], 1 }, { kExplanatoryCues[4], 1 }, { kExplanatoryCues[5], 1 } } ) > 0; } // `since Monday`, `since 2026-09-01` — structural, because a date has no paraphrase (the same reasoning From d7e3240892f9edb347fb7b1a324926bfd0800425 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 16:53:49 -0400 Subject: [PATCH 119/162] =?UTF-8?q?test:=20FIX2=20C1=20=E2=80=94=20the=20u?= =?UTF-8?q?pper-bound=20twin=20of=20the=20relaxed=20ST=20maximality=20(rul?= =?UTF-8?q?e=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIX1 floored maximality's tail at the no-budget answer's tail; that only loosened the LOWER bound, and a binary that ignores the tail's reserve (rows over-served ~2 rows / ~600 B into the tail's bytes) passed all five gates. The twin (test/lib/softbudget.py upper_bound) bounds the rows from above: a cut served past the rank floor stops where T, less the other sections and min(served tail, reference tail), runs out. - XML: rows = max(cut, refill). The refill charges the sections and the tail's reserve; at the cut the tail's share is T less the cut's PRINTED rows and the sections, and the tail fills it. So the printed rows are <= T - sections - min(served, ref tail) + the tail's last row + 13 (the frame forSoftRowDropFill adds). - JSON: the refill reserves only the tail but never goes below the cut, which is T less the charged stanzas (header, surface counts, notes): rows <= T - min(those stanzas, min(served, ref tail)) + the same slack. Wired into fornotesbudgetcheck (ST + its own UP sweep line), forsoftbudgetcheck, compactlegendcheck, estchargecheck and forsigspancheck. RED on a reserve-zero mutant, green at f278ac58. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 16 +++++--- test/estchargecheck.sh | 8 +++- test/fornotesbudgetcheck.sh | 27 ++++++++++---- test/forsigspancheck.sh | 5 +++ test/forsoftbudgetcheck.sh | 9 ++++- test/lib/softbudget.py | 74 +++++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 16 deletions(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 1a7d0bfc2..030edca98 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -1129,12 +1129,14 @@ fi # ST the rows' bytes inside (+ the zero cx=/ccx=/in= the lean spelling elides, which serialize.h's entryCost # charges — fornotesbudgetcheck's soft.py reads them the same way) fit T, or only the rank 1..4 floor ships, the floor # is never short; a cut is MAXIMAL: the next row of the same dialect's no-budget answer would cross T (160 B slack); +# UP (FIX2 C1, rule 4) the maximality twin: a cut past the floor serves no rows past T less the other sections and +# the tail reserve (test/lib/softbudget.py upper_bound), RED on a binary that ignores the tail reserve; # RP the rows are a rank prefix of that dialect's no-budget rows; # NOLBL no over_ceiling= on the root, and neither old over_ceiling clause ("[over_ceiling= is 1 on the root" / "[over_ceiling=1 # also when the sigs next= rides unpaid") in the legend; # NX a cut (rows < total) is capped="1" and carries next= on , and its next_offset= (when printed) is the row count; # HL nothing is dropped "(ceiling)", no "task_echo: dropped", the root keeps route= and task= verbatim. -# softp.py FILE N TASK ROWSOUT [REF] prints ONE line "OK|BAD rows= total= charged= T= est= over= next= fits= maximal= rp= hl="; +# softp.py FILE N TASK ROWSOUT [REF] prints ONE line "OK|BAD rows= total= charged= T= est= over= next= fits= maximal= upper= rp= hl="; # exit 3 = no root or no block (a missing premise: the caller FAILs it, never skips). ROWSOUT gets the rows. cat > "$TMP/softp.py" <<'PY' import html, json, re, sys @@ -1186,16 +1188,20 @@ open( sys.argv[ 4 ], "w" ).write( "".join( x[ 1 ] + "\n" for x in a[ "rows" ] ) cut = k < a[ "total" ] fits = ( a[ "charged" ] <= X or k <= 4 ) and k >= min( 4, a[ "total" ] ) nxt = not cut or ( a[ "capped" ] and a[ "next" ] and a[ "nextoff" ] in ( None, k ) ) -maximal = rp = True +maximal = rp = upper = True if len( sys.argv ) > 5: ref = read( sys.argv[ 5 ] ) if ref is None: sys.exit( 3 ) if cut and k < len( ref[ "rows" ] ): maximal = a[ "charged" ] + ref[ "rows" ][ k ][ 2 ] > X - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + from softbudget import upper_bound + upper = upper_bound( a[ "s" ], ref[ "s" ], X, k, a[ "total" ], U )[ 0 ] rp = k > 0 and [ x[ 1 ] for x in a[ "rows" ] ] == [ x[ 1 ] for x in ref[ "rows" ] ][ :k ] -ok = a[ "est" ] != "-" and fits and maximal and rp and nxt and not a[ "over" ] and a[ "hl" ] -print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d next=%d fits=%d maximal=%d rp=%d hl=%d" +ok = a[ "est" ] != "-" and fits and maximal and upper and rp and nxt and not a[ "over" ] and a[ "hl" ] +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d next=%d fits=%d maximal=%d upper=%d rp=%d hl=%d" % ( "OK" if ok else "BAD", k, a[ "total" ], a[ "charged" ], X, a[ "est" ], int( a[ "over" ] ), int( nxt ), int( fits ), - int( maximal ), int( rp ), int( a[ "hl" ] ) ) ) + int( maximal ), int( upper ), int( rp ), int( a[ "hl" ] ) ) ) PY spfield(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # spfield LINE KEY # spRef LAB DIR ROOTARG TASK — the no-budget answer of both dialects (the ST/RP reference), and its rows in "$TMP/sp.ref.LAB.DIAL.rows". diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 46aef2f82..273c5a308 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -698,8 +698,12 @@ def verdict( path, n, refpath=None ): bm = re.search( r'', s, re.S ) U -= len( bm.group( 0 ).encode() ) if bm else 0 maximal = ab + rr[ k ][ 1 ] > T - U - 160 - return fits and maximal, "%s fits=%d maximal=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d U=%d" % ( - "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), pb, ab, rr[ k ][ 1 ], T, U ) + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + from softbudget import upper_bound + upper, udet = upper_bound( s, open( refpath, encoding='utf-8', errors='replace' ).read(), T, k, len( rr ), U ) + return fits and maximal and upper, "%s fits=%d maximal=%d upper=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d U=%d%s" % ( + "OK" if fits and maximal and upper else "BAD", fits, maximal, upper, k, len( rr ), pb, ab, rr[ k ][ 1 ], T, U, "" if upper else " -- " + udet ) if __name__ == "__main__": good, msg = verdict( sys.argv[ 1 ], sys.argv[ 2 ], sys.argv[ 3 ] if len( sys.argv ) > 3 else None ) print( msg ); sys.exit( 0 if good else 1 ) diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index f4891760b..3dba68424 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -251,7 +251,7 @@ done TASK="widget dispatcher pipeline stage" # ── forbudget-soft (rule 4): the soft-target reader every replaced arm below uses ───────────────────────────────────────── -# soft FILE N ROWSOUT [REF [PREV]] prints ONE line, "OK|BAD rows= total= charged= T= est= over= echo= next= notes= fits= maximal= rp=": +# soft FILE N ROWSOUT [REF [PREV]] prints ONE line, "OK|BAD rows= total= charged= T= est= over= echo= next= notes= fits= maximal= upper= rp=": # rows / total the served rows / the ranked head ( | "sigs_total") # charged what the soft target is charged against. XML: the bytes inside — every WITH its children # — plus the zero cx=/ccx=/in= the lean spelling elides (serialize.h entryCost charges them). JSON: the @@ -261,13 +261,15 @@ TASK="widget dispatcher pipeline stage" # next a cut (rows < total) names its continuation ( | "sigs_next"); 1 when nothing was cut # notes served rows that carry a note # fits charged <= T, or only the rank 1..4 floor is served — and the floor is never short (rows >= min(4,total)) +# upper with REF, FIX2 C1: the maximality twin, rows past the floor stop where T less the other sections and the +# tail reserve runs out (test/lib/softbudget.py upper_bound; a tail-reserve-zero binary is RED here) # maximal with REF (the SAME dialect's no-budget answer): a cut stops only where REF's next row would cross T # (160 B slack, as forsoftbudgetcheck's (ST)) — a binary that stops early at a hard ceiling is not ST # rp RP: the rows are a non-empty rank prefix of REF's rows, and the rows file PREV (when named: the next # narrower rung's ROWSOUT) is a prefix of these rows — monotone in N. A named PREV that is missing is a 0. # (One python3 per answer, the fields read with bash's own regex: this machine class pays ~0.1 s per process spawn, and the # sweep below reads 393 answers — the reader does in-process what a pipe of head/wc/sed would spawn for.) -# The verdict is OK when est is numeric and fits, maximal and next all hold. ROWSOUT gets the rows, p:l:n in rank (r=) order. +# The verdict is OK when est is numeric and fits, maximal, upper and next all hold. ROWSOUT gets the rows, p:l:n in rank (r=) order. # exit 3 = the answer has no block / "sigs" array: a missing premise, FAILed by the caller, never skipped. cat > "$TMP/soft.py" <<'PY' import json, re, sys @@ -340,22 +342,26 @@ k = len( a[ 'rows' ] ) fits = ( a[ 'charged' ] <= X or k <= 4 ) and k >= min( 4, a[ 'total' ] ) cut = k < a[ 'total' ] nxt = a[ 'next' ] or not cut -maximal = True +maximal = upper = True if len( sys.argv ) > 4: ref = read( sys.argv[ 4 ] ) if ref is None: sys.exit( 3 ) if cut and k < len( ref[ 'rows' ] ): maximal = a[ 'charged' ] + ref[ 'rows' ][ k ][ 2 ] > X - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve (rows over-served) + from softbudget import upper_bound + upper = upper_bound( _at, _rt, X, k, a[ 'total' ], U )[ 0 ] mine = [ x[ 1 ] for x in a[ 'rows' ] ] rp = len( sys.argv ) > 4 and k > 0 and mine == [ x[ 1 ] for x in ref[ 'rows' ] ][ :k ] if rp and len( sys.argv ) > 5 and sys.argv[ 5 ]: try: prev = [ x for x in open( sys.argv[ 5 ] ).read().split( "\n" ) if x ] except OSError: prev = None rp = prev is not None and len( prev ) > 0 and prev == mine[ :len( prev ) ] -ok = a[ 'est' ] != '-' and fits and maximal and nxt -print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d echo=%d next=%d notes=%d fits=%d maximal=%d rp=%d" +ok = a[ 'est' ] != '-' and fits and maximal and upper and nxt +print( "%s rows=%d total=%d charged=%d T=%d est=%s over=%d echo=%d next=%d notes=%d fits=%d maximal=%d upper=%d rp=%d" % ( "OK" if ok else "BAD", k, a[ 'total' ], a[ 'charged' ], X, a[ 'est' ], int( a[ 'over' ] ), int( a[ 'bt' ] == N ), - int( nxt ), sum( x[ 3 ] for x in a[ 'rows' ] ), int( fits ), int( maximal ), int( rp ) ) ) + int( nxt ), sum( x[ 3 ] for x in a[ 'rows' ] ), int( fits ), int( maximal ), int( upper ), int( rp ) ) ) PY soft(){ python3 "$TMP/soft.py" "$@"; } field(){ local re=" $2=([^ ]*)"; [[ " $1 " =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; } # field LINE KEY: one key= of a soft line @@ -511,7 +517,7 @@ else no "a surviving note lost its tail — notes must be charged, never trimmed # rung a single sample happens to sit on is exactly what made the defect survive four earlier re-anchors of # the middle rung above: 1550 and 1600 are both inside a FLAT stretch, and the edge is at ~1610. sweepRungs=0; sweepEchoFail=0; sweepEchoFirst="" -stFail=0; stFirst=""; lblFail=0; lblFirst=""; rpFail=0; rpFirst=""; cfFail=0; cfFirst=""; sweepCut=0 +stFail=0; stFirst=""; lblFail=0; lblFirst=""; rpFail=0; rpFirst=""; cfFail=0; cfFirst=""; sweepCut=0; upFail=0; upFirst="" tb=700 while [ "$tb" -le 2000 ]; do sweepRungs=$(( sweepRungs + 1 )) @@ -533,6 +539,9 @@ while [ "$tb" -le 2000 ]; do case "$sl" in OK*) ;; *) stFail=$(( stFail + 1 )) [ -z "$stFirst" ] && stFirst="budget=$tb ($dialect) $sl" ;; esac + case "$sl" in *" upper=1 "*) ;; *) + upFail=$(( upFail + 1 )) + [ -z "$upFirst" ] && upFirst="budget=$tb ($dialect) $sl" ;; esac case "$sl" in *" over=0 "*) ;; *) lblFail=$(( lblFail + 1 )) [ -z "$lblFirst" ] && lblFirst="budget=$tb ($dialect) $sl" ;; esac @@ -553,6 +562,10 @@ if [ "$stFail" -eq 0 ]; then else no "(ST) sweep 700..2000 step 10 x 3 dialects: $stFail run(s) broke stop-at-target — first: $stFirst" fi +# (UP) FIX2 C1 (rule 4): the upper-bound twin of maximality, counted on its own: a binary that ignores the tail reserve serves +# rows past T less the sections and that reserve, and both fits (rows <= T) and the relaxed maximality stay green on it. +if [ "$upFail" -eq 0 ]; then ok "(UP) sweep 700..2000 step 10 x 3 dialects ($runs runs): no cut past the floor serves rows into the tail reserve" +else no "(UP) sweep 700..2000 step 10 x 3 dialects: $upFail run(s) served rows past T less the sections and the tail reserve — first: $upFirst"; fi if [ "$lblFail" -eq 0 ]; then ok "(NOLBL) sweep 700..2000 step 10 x 3 dialects ($runs runs): no over_ceiling= on any soft target" else no "(NOLBL) sweep 700..2000 step 10 x 3 dialects: $lblFail run(s) labelled a soft target over_ceiling= — first: $lblFirst"; fi if [ "$rpFail" -eq 0 ]; then ok "(RP) sweep 700..2000 step 10 x 3 dialects ($runs runs): rows are a rank prefix of the no-budget rows, monotone in N" diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 80dfac337..47500f7a5 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -481,6 +481,11 @@ if mode == "tight": if k < len( ref[ 0 ] ): if a[ 1 ] + ref[ 0 ][ k ] <= T - U - 160: bad.append( "ST: cut at %d of %d rows with %d B charged, the next row (%d B) fits T=%d less the sections U=%d" % ( k, len( ref[ 0 ] ), a[ 1 ], ref[ 0 ][ k ], T, U ) ) if not ( a[ 2 ] and a[ 3 ] ): bad.append( "NX: rows cut (%d of %d) without capped + next=" % ( k, len( ref[ 0 ] ) ) ) + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor + # stop where T less the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + import os as _os, sys as _sys; _sys.path.insert( 0, _os.environ[ "RIPWIRE_TESTLIB" ] ); from softbudget import upper_bound + up, udet = upper_bound( A, body( sys.argv[ 7 ] ), T, k, len( ref[ 0 ] ), U ) + if not up: bad.append( "UP: " + udet ) print( " .... %s: ST rows=%d/%d charged=%d T=%d" % ( label, k, len( ref[ 0 ] ), a[ 1 ], T ) ) else: if not moved: bad.append( "no docs_after_code= (the reorder did not apply)" ) diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 9102f0692..35a78929f 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -457,12 +457,17 @@ fits = ab <= T or k <= 4 if k >= len( rr ): print( "OK fits=%d rows=%d/%d charged=%d T=%d complete" % ( fits, k, len( rr ), ab, T ) ); sys.exit( 0 ) nxt = rr[ k ][ 1 ] maximal = ab + nxt > T - U - 160 -print( "%s fits=%d maximal=%d rows=%d/%d charged=%d next=%d T=%d U=%d" % ( "OK" if fits and maximal else "BAD", fits, maximal, k, len( rr ), ab, nxt, T, U ) ) +# FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less +# the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve +from softbudget import upper_bound +upper, udet = upper_bound( ans, open( sys.argv[ 2 ] ).read(), T, k, len( rr ), U ) +print( "%s fits=%d maximal=%d upper=%d rows=%d/%d charged=%d next=%d T=%d U=%d%s" % ( "OK" if fits and maximal and upper else "BAD", fits, maximal, upper, + k, len( rr ), ab, nxt, T, U, "" if upper else " -- " + udet ) ) PY )" case "$st" in OK*) ok "(ST) N=$n $mode: $st" ;; - *) no "(ST) N=$n $mode: $st — the rows must fit the target and stop only where the next row would cross it" ;; + *) no "(ST) N=$n $mode: $st — the rows must fit the target and stop only where the next row would cross it, never past T less the sections and the tail reserve" ;; esac done prev="$n" diff --git a/test/lib/softbudget.py b/test/lib/softbudget.py index 41da72bea..15b9fac94 100644 --- a/test/lib/softbudget.py +++ b/test/lib/softbudget.py @@ -18,3 +18,77 @@ def tail_size( t ): except ValueError: return 0 m = re.search( r"", t, re.S ) return len( m.group( 0 ).encode() ) if m else 0 + + +# ── FIX2 C1 (review rv-forbudget-soft-fix1; rule 4, CHECKLIST 1): the UPPER-bound twin of the relaxed maximality ────────────── +# FIX1 floored maximality's tail at the reference (no-budget) answer's tail: that only loosened the LOWER bound. Its twin bounds +# the rows from above: a cut served past the rank floor stops where T, less the other sections and the tail's reserve, runs out. +# A binary that ignores the tail's reserve (reserve = 0) serves rows into the bytes the tail was promised: RED here, while the +# lower bound and fits (rows <= T) both stay green on it. What bounds the rows (src/verbs_for.h, the soft refill and the tail's +# share; src/serialize.h forSigsBudgets / forSoftRowDropFill): +# XML rows = max(the cut, the refill). The refill charges every served section and the tail's reserve, min(its share, the +# head-scoped tail): rows' charge <= T - sections - reserve + 13 (the "" frame). At the cut, the tail's +# share is T less the cut's PRINTED rows and the sections, and the tail fills that share (its last row may cross it). So +# in both cases the PRINTED rows (the bytes inside , without the zero cx=/ccx=/in= the ladder also charges) are +# <= T - sections - min(served tail, ref tail) + the served tail's last row + 13. "sections" is the reader's U less the +# tail: U here carries FIX1's floor (= sections + max(served, ref tail)), so sections = U - max(served tail, ref tail). +# JSON rows = max(the cut, the refill). The refill reserves only the tail; the cut is T less the stanzas the sigs budget +# charges (header, surface counts, notes: the keys below that are neither the rows, the tail, nor the cut's own +# disclosure). So rows <= T - min(those stanzas, min(served tail, ref tail)) + the tail's last row + 13. +JSON_UNCHARGED_KEYS = frozenset( ( "sigs", "tail", "sigs_shown", "sigs_total", "sigs_next", "sigs_next_offset", "sigs_past_target", + "capped", "dropped_positive", "budget_tokens", "budget_bytes", "est_tokens", "over_ceiling" ) ) +SIGS_FRAME_BYTES = 13 # "" + "": forSoftRowDropFill adds it to the rows' budget (serialize.h kSigsWrapperBytes) + + +def _jlen( v ): + return len( json.dumps( v, separators=( ",", ":" ), ensure_ascii=False ).encode() ) + + +def rows_printed( t ): + """The rows as printed: XML, the bytes inside ; JSON, each "sigs" entry's compact bytes plus its comma. None when absent.""" + if t.lstrip().startswith( "{" ): + try: d = json.loads( t ) + except ValueError: return None + return sum( _jlen( e ) + 1 for e in d[ "sigs" ] ) if isinstance( d.get( "sigs" ), list ) else None + m = re.search( r"]*>(.*?)", t, re.S ) + return len( m.group( 1 ).encode() ) if m else None + + +def tail_last_row( t ): + """Bytes of the served tail's LAST row, the one that may cross the tail's share. XML: the last ; JSON: the last + "files" entry plus its "sym" entry (each with its comma).""" + if t.lstrip().startswith( "{" ): + try: tl = json.loads( t ).get( "tail", {} ) + except ValueError: return 0 + f, s = tl.get( "files" ) or [], tl.get( "sym" ) or [] + return ( _jlen( f[ -1 ] ) + 1 if f else 0 ) + ( _jlen( s[ -1 ] ) + 1 if s else 0 ) + m = re.search( r"", t, re.S ) + rows = re.findall( r"]*/>", m.group( 0 ) ) if m else [] + return len( rows[ -1 ].encode() ) if rows else 0 + + +def json_charged_stanzas( t ): + """Bytes of a JSON answer's stanzas the sigs budget charges: every top-level key (with its quotes, colon and comma) but the + rows, the tail and the cut's own disclosure keys (JSON_UNCHARGED_KEYS).""" + try: d = json.loads( t ) + except ValueError: return 0 + return sum( len( k.encode() ) + 4 + _jlen( v ) for k, v in d.items() if k not in JSON_UNCHARGED_KEYS ) + + +def upper_bound( answer, ref, T, k, total, u_floored ): + """The upper-bound twin. Returns (ok, detail). Applies only to a cut (k < total) served past the rank floor (k > 4). + `u_floored` is the reader's U including FIX1's tail floor (XML; ignored for JSON). A missing is not ok.""" + if k >= total or k <= 4: + return True, "upper=n/a" + rows = rows_printed( answer ) + if rows is None: + return False, "upper: no rows to measure (premise)" + st, rt = tail_size( answer ), tail_size( ref ) + if answer.lstrip().startswith( "{" ): + room = T - min( json_charged_stanzas( answer ), min( st, rt ) ) + else: + room = T - ( u_floored - max( st, rt ) ) - min( st, rt ) + slack = tail_last_row( answer ) + SIGS_FRAME_BYTES + ok = rows <= room + slack + return ok, "upper: rows printed %d B %s room %d B + %d B slack (served tail %d, ref tail %d)" % ( + rows, "<=" if ok else ">", room, slack, st, rt ) From fe2f3c213790f21bc192bee8cf0163ebb2eecae6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 16:53:49 -0400 Subject: [PATCH 120/162] =?UTF-8?q?test(forrankordercheck):=20FIX2=20N2=20?= =?UTF-8?q?=E2=80=94=20q6/q7/q8=20re-pinned=20from=20a=20clean=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FIX1 pins were read on a +dirty tree: the root's at= stamp carried 6 B more than a clean tree, so a clean CI tree read -0.06% on each. Re-taken on a clean committed checkout with the lane build: 9947 -> 9941, 10496 -> 10490, 10605 -> 10599 B. The stated by-design reason is kept. Co-Authored-By: Claude Opus 5.5 --- test/forrankordercheck.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index c1415fd76..5f557f2aa 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -145,7 +145,10 @@ REPO_Q=( "rank graph teleport" "compact legend rewrite" "edit receipt post-check # +4.81% / +5.25%) and attributed it to the lane: decision D-3 (the default serves every hop row — the part inside the old # 1,000 B allowance is not subtracted) and the default regime's header clauses (budget_bytes= read as a target, past_target=). # The other seven are unmoved. Train 28 re-measures attrib-multidef's --for piece against these pins. -REPO_BASE=( 10134 10029 10642 10102 9880 9947 10496 10605 9966 10379 ) +# FIX2 N2 (review rv-forbudget-soft-fix1 N-a): those three pins were read on a +dirty tree, whose root at= stamp carries 6 B +# more than a clean one (a clean tree read -0.06% on each). Re-taken from a CLEAN committed tree (the lane head, no local edits) +# with this lane's build: 9947 -> 9941, 10496 -> 10490, 10605 -> 10599 B. The by-design reason above is unchanged. +REPO_BASE=( 10134 10029 10642 10102 9880 9941 10490 10599 9966 10379 ) # q3 RE-PINNED 2026-10-10 (train 26c): 10080 -> 10642 B (+5.58% on the merged stack), an OUTPUT change of two signed lanes # that each stay inside the 4% alone: lego-charge serves two more signature rows (25 -> 27 of 40) because / # are charged to the budget at the size they are SERVED (a collapsing section its stub), and owner-hop adds the From 66cc4b6373c8f5681ec2322b57e9061bffdb2110 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 17:37:20 -0400 Subject: [PATCH 121/162] =?UTF-8?q?fix(for):=20CI=20round=201=20=E2=80=94?= =?UTF-8?q?=20the=20slot's=20ENSURES=20reads=20a=20computed=20bool=20(self?= =?UTF-8?q?checkcheck=20C);=20docdemote=20(f)=20golden=20re-pinned=20by=20?= =?UTF-8?q?design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docdemotegolden_for.xml: the conceptual question is explanatory, so the compact route now carries one owner body and its clause; with RIPWIRE_NO_BODY_SLOT=1 the same binary answers the old golden byte for byte. Co-Authored-By: Claude Opus 5.5 --- src/forowner.h | 4 ++-- test/docdemotecheck.sh | 4 ++++ test/docdemotegolden_for.xml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/forowner.h b/src/forowner.h index 560236ca5..ea60504e3 100644 --- a/src/forowner.h +++ b/src/forowner.h @@ -345,8 +345,8 @@ inline ForBodySlotOwner forBodySlotOwner( const IngestResult& ing, const std::ve { out = ForBodySlotOwner{ s, BodySlotPick::Subtoken }; } - ENSURES( !out.any() || ( out.id < ing.symbols.size() && isOwnerCallableDef( ing, out.id ) ), - "the body slot's owner is one indexed callable definition outside the test partition" ); + const bool ownerIsCallableDef = !out.any() || ( out.id < ing.symbols.size() && isOwnerCallableDef( ing, out.id ) ); + ENSURES( ownerIsCallableDef, "the body slot's owner is one indexed callable definition outside the test partition" ); return out; } diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 589b9fd22..70a411e85 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,10 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-15 (lane for-body-slot, train 28; rule 4, by design): docdemotegolden_for.xml 6529 -> 7540 B (est_tokens +# "2616" -> "2979"). The question is explanatory ("how does …"), so the compact route now carries ONE owner body (pick=subtoken +# pin_page) and its legend clause; root bodies="1" reason="owner". Checked: the same binary with RIPWIRE_NO_BODY_SLOT=1 answers +# the old golden byte for byte — no ranking, demotion or route byte moved. # RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): docdemotegolden_for.xml 5839 -> 6529 B (est_tokens "2340" -> # "2616") and docdemotegolden_noroute.xml 9350 -> 10039 B ("3309" -> "3584"). Only the (page 0 of the file page after the # rank rows, sym= on its rows, total= every positive-score file), the tail legend clause and est_tokens= moved (checked byte-equal diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 44e642ef9..731921008 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1,3 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file From 83cc4d01dca68d3917528545853975027d96211b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 19:54:08 -0400 Subject: [PATCH 122/162] =?UTF-8?q?fix(for):=20FIX1=20F=20=E2=80=94=20drop?= =?UTF-8?q?=20the=20subtoken=20fallback;=20no=20owner,=20no=20slot=20(owne?= =?UTF-8?q?r=20ruling=202026-10-15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The body slot's third pick (pick="subtoken": the best-ranked head-tier callable sharing one word with the question) is removed. Measured over the round-4 rows it fired on 19, added 28,051 B and gained no substantive item (koa-12 R5 was a grader term match inside isHttpErrorLike); 5 of its bodies were off-subject. A question with no name owner and no member owner now carries no body: the answer is the slot-off answer byte for byte. - forowner.h: forSubtokenOwner, BodySlotPick::Subtoken, "subtoken" in kBodySlotPickNames and the exemplar.h include go; the full legend says "no such owner, no body". - verbs_for.h compact clause and cli.h help: pick=name|member. - forbodyslotcheck (rule 4, by-design break stated in the header): D9b now pins "only a subtoken match exists (cmd_find_target ~ 'target', a ranked row) -> no slot, byte-identical to RIPWIRE_NO_BODY_SLOT=1, no pick=subtoken"; N2 likewise for the tests/-only owner. Both red on 66cc4b63, green here. - docdemotecheck (f): golden back to its pre-lane 6529 B (red on 66cc4b63, green here). Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 4 ++-- src/forowner.h | 33 +++++++-------------------------- src/verbs_for.h | 2 +- test/docdemotecheck.sh | 8 ++++---- test/docdemotegolden_for.xml | 4 +--- test/forbodyslotcheck.sh | 21 ++++++++++++++------- 6 files changed, 29 insertions(+), 43 deletions(-) diff --git a/src/cli.h b/src/cli.h index 3d489e384..01037152c 100644 --- a/src/cli.h +++ b/src/cli.h @@ -958,8 +958,8 @@ inline constexpr char kHelpHead[] = " OWNER BODY: an explanatory question (how does, what does, ...) also carries ONE body\n" " after the hops, root bodies=\"1\" reason=\"owner\": a ranked callable whose whole name is a\n" " query word (the one calling another such owner first), else a method a ranked class owns\n" - " (parser scope or span) named by a word, else the best-ranked callable sharing a word -\n" - " , by name, not resolved, not the only relevant body.\n" + " (parser scope or span) named by a word, else no body - ,\n" + " by name, not resolved, not the only relevant body.\n" " Over 6000 B it is cut at a line end with next=. Under --token-budget it rides only when\n" " the rows at the target's cut leave room for it whole, else and reason=\"budget\". MCP for serves the same body, --json none;\n" diff --git a/src/forowner.h b/src/forowner.h index ea60504e3..702a173fb 100644 --- a/src/forowner.h +++ b/src/forowner.h @@ -18,7 +18,9 @@ // file (TS/JS classes and every other language whose methods nest in the class body). A common word (get, run, set …) is // never a member owner. NAMED FLOOR: Go receiver methods carry no scope and sit outside the type's span, so a Go type // donates no member until the parser records a receiver scope (forbodyslotcheck (Q1) Go floor pins it). -// 3. pick="subtoken": the best-ranked head-tier callable definition whose name shares a subtoken with a question word. +// Neither → no slot (the answer is the slot-off answer byte for byte). There is no subtoken fallback (owner ruling +// 2026-10-15, lane for-body-slot FIX1): a callable that merely shares one word with the question is not its owner, and +// measured over the round-4 rows it served 28 KB of mostly tangential or off-subject bodies for no substantive item. // A class/struct/interface/type is never the body; a test symbol never is; a declaration (header prototype, .d.ts, TS overload // signature, C++ pure virtual, Go interface method spec) never is (isDefinitionNotDeclaration). This is a choice BY NAME over // the ranked head (pick= says which reading chose it), never a resolution: the body is the entry point among the named owners, @@ -26,7 +28,6 @@ // RIPWIRE_NO_BODY_SLOT (any value) turns the slot off — the A/B handle, the RIPWIRE_NO_OWNER_HOP precedent; the two are // independent (forbodyslotcheck (EV1)). -#include "exemplar.h" // shareNameSubtoken — the subtoken fallback's one test (lexical.h subtokens) #include "filter.h" // isTestSymbol #include "graph.h" // langCompatible #include "lexical.h" // isRouteStopword @@ -168,7 +169,7 @@ inline constexpr std::string_view kForBodySlotLegend = "; owner body: an explanatory question (how does, what does) also carries ONE body, in a bodies section near the end. It is " "chosen BY NAME in the ranked head, not resolved: pick=name its name equals a question word (among several such owners, the " "one calling an owner another word names), pick=member a method of a ranked class the question names (by the parser's scope " - "or the class's own span), pick=subtoken the best-ranked callable sharing a word. It is not necessarily the question's " + "or the class's own span); no such owner, no body. It is not necessarily the question's " "subject nor the only relevant body. Over 6000 B it is cut at a line end (b truncated=1 lines= next= the rest); a budget too " "small for it skips it: bodies shown=0 capped=1 next= the call that serves it"; @@ -180,11 +181,11 @@ inline constexpr std::size_t kForBodySlotMaxBytes = 6000; static_assert( kForBodySlotMaxBytes == 6000 && kForBodySlotLegend.find( "Over 6000 B" ) != std::string_view::npos, "the slot legend states the cap: move both together" ); -enum class BodySlotPick : std::uint8_t { None, Name, Member, Subtoken }; +enum class BodySlotPick : std::uint8_t { None, Name, Member }; // pick= spellings, indexed by BodySlotPick (None has none) -inline constexpr std::string_view kBodySlotPickNames[] = { "", "name", "member", "subtoken" }; -static_assert( std::size( kBodySlotPickNames ) == std::size_t( BodySlotPick::Subtoken ) + 1, "one pick= spelling per BodySlotPick" ); +inline constexpr std::string_view kBodySlotPickNames[] = { "", "name", "member" }; +static_assert( std::size( kBodySlotPickNames ) == std::size_t( BodySlotPick::Member ) + 1, "one pick= spelling per BodySlotPick" ); struct ForBodySlotOwner { @@ -306,22 +307,6 @@ inline NodeId forMemberOwner( const IngestResult& ing, const std::vector& words, const std::vector& lensSurfaceIds, - const std::vector& lensRank ) -{ - const std::size_t head = std::min( lensSurfaceIds.size(), std::size_t( kForDocExcerptRankCount ) ); - for( std::size_t i = 0; i < head; ++i ) - { - const NodeId id = lensSurfaceIds[i]; - if( id < ing.symbols.size() && lensRank[id] > 0.0f && isOwnerCallableDef( ing, id ) && shareNameSubtoken( ing.symbols[id].name, words ) ) - { - return id; - } - } - return kNoNode; -} - // The slot's one owner, in the order the header comment states; {kNoNode, None} when the question names none. inline ForBodySlotOwner forBodySlotOwner( const IngestResult& ing, const std::vector& outOff, const std::vector& outTargets, std::string_view task, const std::vector& lensSurfaceIds, const std::vector& lensRank ) @@ -341,10 +326,6 @@ inline ForBodySlotOwner forBodySlotOwner( const IngestResult& ing, const std::ve { out = ForBodySlotOwner{ m, BodySlotPick::Member }; } - else if( const NodeId s = forSubtokenOwner( ing, words, lensSurfaceIds, lensRank ); s != kNoNode ) - { - out = ForBodySlotOwner{ s, BodySlotPick::Subtoken }; - } const bool ownerIsCallableDef = !out.any() || ( out.id < ing.symbols.size() && isOwnerCallableDef( ing, out.id ) ); ENSURES( ownerIsCallableDef, "the body slot's owner is one indexed callable definition outside the test partition" ); return out; diff --git a/src/verbs_for.h b/src/verbs_for.h index afff28c82..109d8b0e7 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -712,7 +712,7 @@ inline constexpr std::string_view kForCompactLegendBodies = // root clause below), present-only (bodySlotPresent), right after the owner clause. No "--": it rides inside an XML comment. inline constexpr std::string_view kForCompactLegendBodySlot = "; b t= n= p= l= full bodies, c n= l= callee signatures; bodies=1 reason=owner: ONE owner body, by name not resolved " - "(pick=name|member|subtoken), not the only relevant one; shown=0 capped=1 next= when a budget skipped it"; + "(pick=name|member), not the only relevant one; shown=0 capped=1 next= when a budget skipped it"; static_assert( kForCompactLegendBodySlot.starts_with( kForCompactLegendBodies ), "the slot's compact clause defines as the bodies clause does" ); inline constexpr std::string_view kForBodySlotRootClause = "; when it rides this root reads bodies=1 reason=owner"; inline std::string bodySlotLegend( const ForLensHeaderParts& p, bool compact ) diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 70a411e85..6cc5339a8 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,10 +99,10 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null -# RE-PIN 2026-10-15 (lane for-body-slot, train 28; rule 4, by design): docdemotegolden_for.xml 6529 -> 7540 B (est_tokens -# "2616" -> "2979"). The question is explanatory ("how does …"), so the compact route now carries ONE owner body (pick=subtoken -# pin_page) and its legend clause; root bodies="1" reason="owner". Checked: the same binary with RIPWIRE_NO_BODY_SLOT=1 answers -# the old golden byte for byte — no ranking, demotion or route byte moved. +# UN-PIN 2026-10-15 (lane for-body-slot FIX1; rule 4): docdemotegolden_for.xml back to its pre-lane 6529 B. The lane had +# re-pinned it to 7540 B for a pick=subtoken body (pin_page); the owner ruling dropped the subtoken fallback, and the +# question names no owner (no callable is named by a whole question word), so no body rides and the golden is byte-identical +# to the forbudget-soft FIX2 base's again. # RE-PIN 2026-10-15 (lane forbudget-soft round C, FOLD C; rule 4): docdemotegolden_for.xml 5839 -> 6529 B (est_tokens "2340" -> # "2616") and docdemotegolden_noroute.xml 9350 -> 10039 B ("3309" -> "3584"). Only the (page 0 of the file page after the # rank rows, sym= on its rows, total= every positive-score file), the tail legend clause and est_tokens= moved (checked byte-equal diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 731921008..44e642ef9 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1,3 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/forbodyslotcheck.sh b/test/forbodyslotcheck.sh index d2785fdc6..b9f3b9a22 100755 --- a/test/forbodyslotcheck.sh +++ b/test/forbodyslotcheck.sh @@ -22,8 +22,9 @@ # interface row (rank <= 24) with a callable definition that is its member (the parser's scope= equals # the class name, or the class's own byte span contains it in the same file) and whose folded name is # a non-common question word (pick="member"); Go receiver methods are a named floor (no scope, outside -# the type's span). Then the best-ranked head-tier callable of the same predicate whose name shares a -# kept question subtoken (pick="subtoken"). None: no slot. Classes/structs/types are never the owner. +# the type's span). None: no slot — the answer is the slot-off answer byte for byte (FIX1, owner ruling +# 2026-10-15: the subtoken fallback, pick="subtoken", is DROPPED; a callable that only shares a word with +# the question is not its owner). Classes/structs/types are never the owner. # RENDER one in the --expand shape (packBodies), kept. A body over # kForBodySlotMaxBytes (6000) is head-cut at a line end: truncated="1" lines="lo-hi/T" # next="--expand=P:L:N:A-B" (the cutfix-bodies vocabulary), bodies capped="1"; a FIRST line longer @@ -49,7 +50,10 @@ # D6 [G] no explanatory cue (where / show documentation / bare noun phrase): bodies=0, no 10%), so a cap counted in # characters fails the <= 6000 B check (review R7) @@ -57,7 +61,8 @@ # L1 [P] Rust: entry point index (calls walk) beats walk at r1 L2 [P] C++: Builder::Build DEFINITION (.cc), not the r1 header declaration # L3 [P] Go: method Package, never the type Packager # N1 [P] TS: negotiate's definition, not the r1 .d.ts declaration; [G] a .d.ts-only name never yields a .d.ts body -# N2 [P/G] the only name owner is a tests/ helper: the slot falls back to a non-test callable, never tests/ +# N2 [G] the only name owner is a tests/ helper: NO slot (no fallback since FIX1 — by-design break, rule 4: through +# 66cc4b63 the subtoken fallback served a non-test body here; red on 66cc4b63), never a tests/ body # N3 [P] two equal owners in two files: ONE body, the better-ranked one (p= and l= of the r1 row) # N4 [P] two same-named methods of two classes in ONE file: ONE body, the r1 row's (entity identity, checklist 16) # N5 [G] a struct named by the question is never the body; every is fn/method @@ -520,8 +525,10 @@ if r="$( chk rowis "$TMP/py.xml" 1 Stylesheet src/textual/css/stylesheet.py )"; if r="$( chk body "$TMP/py.xml" apply src/textual/css/stylesheet.py "_check_rule(" "widget.refresh()" )"; then ok "(D8) Python: $r (the method)"; else no "(D8) Python: $r"; fi if r="$( chk nobodyof "$TMP/py.xml" Stylesheet )"; then ok "(D8) Python: the class Stylesheet (r1) is never the body"; else no "(D8) Python: $r"; fi if r="$( chk body "$TMP/c.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" "cmd_find_valid_state(" )"; then ok "(D9) C: $r"; else no "(D9) C: $r"; fi -if r="$( chk body "$TMP/sub.xml" cmd_find_target src/c/cmd-find.c "cmd_find_get_session(" )"; then ok "(D9b) C subtoken fallback ('target'): $r"; else no "(D9b) C subtoken fallback: $r"; fi -if r="$( chk bodiesattr "$TMP/sub.xml" pick subtoken )"; then ok "(D9b) the fallback names itself: $r"; else no "(D9b) $r"; fi +# D9b premise: the subtoken match the dropped fallback served IS a ranked row here — so "no slot" is the rule, not a miss +if grep -q ']*n="cmd_find_target"' "$TMP/sub.xml"; then :; else no "(D9b) premise: cmd_find_target is not a ranked row on T_SUB"; fi +if r="$( chk nobody "$TMP/sub.xml" )" && cmp -s "$TMP/sub.xml" "$TMP/sub.off.xml"; then ok "(D9b) only a subtoken match (cmd_find_target ~ 'target'): no slot, byte-identical to RIPWIRE_NO_BODY_SLOT=1"; else no "(D9b) only a subtoken match: a body rides or the answer differs from slot-off ${r:-}"; fi +if grep -q 'pick="subtoken"' "$TMP/sub.xml"; then no "(D9b) pick=\"subtoken\" still rides"; else ok "(D9b) no pick=\"subtoken\""; fi if r="$( chk bodyattr "$TMP/big.xml" render_screen truncated '^1$' )"; then ok "(D10) oversized owner: $r"; else no "(D10) oversized: $r"; fi if r="$( chk cutatline "$TMP/big.xml" render_screen 6000 )"; then ok "(D10) oversized owner: $r (whole lines, <= 6000 B)"; else no "(D10) oversized: $r"; fi if nx="$( chk nextof "$TMP/big.xml" render_screen )" && printf '%s' "$nx" | grep -q '^--expand=src/c/render.c:'; then @@ -554,7 +561,7 @@ if r="$( chk body "$TMP/dts.xml" negotiate src/negotiate.ts "negotiate fallback" for k in dts dts2; do if r="$( chk nobodyat "$TMP/$k.xml" '\.d\.ts$' )"; then ok "(N1) $k: no body from a .d.ts declaration"; else no "(N1) $k: $r"; fi done -if r="$( chk bodycount "$TMP/test.xml" 1 )"; then ok "(N2) the only name owner is a tests/ helper: the subtoken fallback serves one body"; else no "(N2) $r"; fi +if r="$( chk nobody "$TMP/test.xml" )" && cmp -s "$TMP/test.xml" "$TMP/test.off.xml"; then ok "(N2) the only name owner is a tests/ helper: no slot, byte-identical to RIPWIRE_NO_BODY_SLOT=1"; else no "(N2) a body rides, or the answer differs from slot-off ${r:-}"; fi if r="$( chk nobodyat "$TMP/test.xml" '(^|/)tests?/' )" && r2="$( chk nobodyof "$TMP/test.xml" roundtrip )"; then ok "(N2) no body from tests/ (roundtrip is a test symbol)"; else no "(N2) ${r:-} ${r2:-}"; fi if r="$( chk bodycount "$TMP/two.xml" 1 )"; then ok "(N3) two equal owners: one body"; else no "(N3) $r"; fi if r="$( chk bodyisrow "$TMP/two.xml" render )"; then ok "(N3) two equal owners: $r (the better-ranked)"; else no "(N3) $r"; fi From 04b58c9690292c21147d90c5762ec0d6c7710d04 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 20:03:18 -0400 Subject: [PATCH 123/162] =?UTF-8?q?fix(impact):=20FIX1=20R1=20=E2=80=94=20?= =?UTF-8?q?define=20importers=5Fnext=3D=20in=20the=20full=20--impact=20leg?= =?UTF-8?q?end=20when=20the=20tier=20is=20cut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit legendcoveragecheck (A) was red on 66cc4b63 (impact | impact@importers_next): the lane's new src/forowner.h includes serialize.h (packBodies, needed), making it escapeXml's 41st importer, so the gate's own --impact=escapeXml --legend=full answer was cut for the first time and carried importers_next=, which only the umbrella sentence of the full legend defined. A legend gap, not a lane quirk: any symbol with > 40 importers showed it. impactRollupLegend gains `importersCut`: the definition rides exactly when the root carries importers_next= (the XML forms, CLI and the MCP twin alike; not columnar, which names it in lens=; not beside the umbrella sentence, which defines it already). Probes: cut tier (escapeXml, 41) defines it; uncut (forBodySlotOwner, 2) does not; columnar does not; xmllint clean. legendcoveragecheck red on 83cc4d01, green here; impactimportcheck and mcpattrparitycheck green. Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 9 ++++++++- src/mcpverbs.h | 2 +- src/verbs_navigate.h | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 2ae4e520f..441b1fc96 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -780,9 +780,16 @@ inline constexpr const char* kImpactDepthColumnarLegend = // rollup whenever the reach set is non-empty (XML forms only: the JSON form carries it as "files", columnar not at all), // the umbrella sentence only beside importers_umbrella=. Shared by the CLI arm and the MCP twin, byte-identical. // fix27: `hedged` = a listed rf row carries via= or by_name= (a multi-definition selector only); its sentence rides then. -inline std::string impactRollupLegend( bool rollup, bool umbrella, bool hedged = false ) +// for-body-slot FIX1 R1: `importersCut` = the root carries importers_next= (sizeImportTier: the listing was cut). Its +// definition rides then, unless the umbrella sentence (which defines it too) already does — legendcoveragecheck (A). +inline std::string impactRollupLegend( bool rollup, bool umbrella, bool hedged = false, bool importersCut = false ) { std::string out; + if( importersCut && !umbrella ) + { + out += "importers_next=: the call listing every importer, beside importers_capped=\"1\" (the rows are a most-imported " + "prefix of importers=). "; + } if( rollup ) { out += "FILES (only beside a cut symbol window): the WHOLE reach set by file. files= in all, shown_files= listed; rf " diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 171ec0650..19f8d4af6 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3051,7 +3051,7 @@ inline std::optional impactText( const std::string& root, const std declinedIfaceLegend( declinedIface > 0 ), // likewise declined_iface=, as on the CLI ( std::string( valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs=, as on the CLI + valueRefsDepthLegendFor( imValueRefs ) // ... and the depth disclosure, as on the CLI - + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ) ) ).c_str(), // idea #6, as on the CLI + + impactRollupLegend( !imFiles.rows.empty(), imports.umbrella, impactRollupHedged( imFiles ), !imports.next.empty() ) ).c_str(), // idea #6; FIX1 R1, as on the CLI graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), renderDisclosure( prD, DiscloseAs::LegendClause ).c_str() ); // r27-emitters §P2.1: the listing is capped at 40 by rank. Without shown=/capped= a 40-row answer to // "is it safe to change X?" reads as the WHOLE blast radius when it can be 3% of it. Same attributes, diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index cbcc9acdb..1cf03953f 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2722,7 +2722,8 @@ std::optional runImpact( const MainDispatch& d ) rw::declinedCallsLegendWithGates( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( imDeclinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::modScopeLegend( imHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is - + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ) ) ).c_str(), // idea #6; fix27 + + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ), + !imports.next.empty() && !cfg.columnar ) ).c_str(), // idea #6; fix27; FIX1 R1: the XML root's importers_next= (columnar names it in lens=) ( std::string( rw::valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs= + rw::valueRefsDepthLegendFor( imValueRefs ) ).c_str(), // ... and the depth disclosure rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str() ); From c902c78d01692440afe1fb36750b3300ec2ab3ea Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 20:09:25 -0400 Subject: [PATCH 124/162] =?UTF-8?q?fix(for):=20FIX1=20R3=20=E2=80=94=20the?= =?UTF-8?q?=20compact=20owner-body=20clause=20says=20what=20pick=3D=20and?= =?UTF-8?q?=20reason=3Downer=20mean,=20and=20do=20not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default (compact) dialect read only "ONE owner body, by name not resolved (pick=…), not the only relevant one" beside root reason="owner", so a member or name pick read as THE question's subject. It now states what the full dialect states (checklist 3): "ONE body, the question's owner by the rule (pick=name: its name is a question word; pick=member: a method so named of a ranked class), chosen by name not resolved; not necessarily the subject, not a claim it is the whole answer". Present-only as before (bodySlotPresent), no "--" (checklist 25). forbodyslotcheck (D5/R3) [P]: the clause on a pick=name body (T_D) and on a pick=member body (Q1 C++, the runtime root); red on 66cc4b63, green here. X2 xmllints the member answer too. Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 7 +++++-- test/forbodyslotcheck.sh | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index 109d8b0e7..658317623 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -710,9 +710,12 @@ inline constexpr std::string_view kForCompactLegendBodies = "; b t= n= p= l= full bodies, c n= l= callee signatures"; // for-body-slot: the compact dialect's reading of the ONE owner body (the full dialect's is forowner.h kForBodySlotLegend + the // root clause below), present-only (bodySlotPresent), right after the owner clause. No "--": it rides inside an XML comment. +// FIX1 R3: it states what the full one states — what each pick= means and what reason=owner does NOT mean (the subject, the +// whole answer): the default posture is this dialect, so this is the only reading most readers get. inline constexpr std::string_view kForCompactLegendBodySlot = - "; b t= n= p= l= full bodies, c n= l= callee signatures; bodies=1 reason=owner: ONE owner body, by name not resolved " - "(pick=name|member), not the only relevant one; shown=0 capped=1 next= when a budget skipped it"; + "; b t= n= p= l= full bodies, c n= l= callee signatures; bodies=1 reason=owner: ONE body, the question's owner by the rule " + "(pick=name: its name is a question word; pick=member: a method so named of a ranked class), chosen by name not resolved; " + "not necessarily the subject, not a claim it is the whole answer; shown=0 capped=1 next= when a budget skipped it"; static_assert( kForCompactLegendBodySlot.starts_with( kForCompactLegendBodies ), "the slot's compact clause defines as the bodies clause does" ); inline constexpr std::string_view kForBodySlotRootClause = "; when it rides this root reads bodies=1 reason=owner"; inline std::string bodySlotLegend( const ForLensHeaderParts& p, bool compact ) diff --git a/test/forbodyslotcheck.sh b/test/forbodyslotcheck.sh index b9f3b9a22..e2fcc024a 100755 --- a/test/forbodyslotcheck.sh +++ b/test/forbodyslotcheck.sh @@ -46,7 +46,8 @@ # D2 [G] T_D: not verify's body (jwt is r1 here, so this is NOT an entry-point discriminator — L1 and EP1 are) # D3 [P] est_tokens prices the body at no less than 0.2 token per body byte (the body rate is 3.80 B/token) # D4 [G] sigs/hops/tail byte-identical slot vs slot-off on every fixture question (checklist 23); slot-off has no body -# D5 [P] legends: full answer explains bodies=1; compact legend defines b t= n= p= l= when a body rides +# D5 [P] legends: full answer explains bodies=1; compact legend defines b t= n= p= l= when a body rides; R3 (FIX1) [P] the +# compact clause reads pick=name / pick=member and says reason=owner is not necessarily the subject nor the whole answer # D6 [G] no explanatory cue (where / show documentation / bare noun phrase): bodies=0, no element"; else no "(D6) $k: $r"; fi @@ -580,6 +585,10 @@ Q_FORGE="How does the forge assemble a kiln job: scheduling glaze batches, track Q_RUN="How does the forge run a kiln job: scheduling glaze batches, tracking firing queues and recording kiln results?" for r in cpp rs py ts go deep other; do rrun "$r" --for="$Q_FORGE" >"$TMP/q1.$r.xml"; done rrun common --for="$Q_RUN" >"$TMP/q1.common.xml" +rrun cpp --for="$Q_FORGE" --legend=compact >"$TMP/q1.cpp.c.xml" +if r="$( chk bodiesattr "$TMP/q1.cpp.c.xml" pick member )" && r2="$( chk legend "$TMP/q1.cpp.c.xml" "${R3_STRS[@]}" )"; then + ok "(D5/R3) pick=member body (C++): the compact legend reads pick= and says what reason=owner does not mean" +else no "(D5/R3) pick=member body (C++): ${r:-} ${r2:-}"; fi rrun name --for="How does refresh repaint the screen?" >"$TMP/ep.name.xml" rrun self --for="How does normalize flatten the tree?" >"$TMP/ep.self.xml" rrun tsover --for="How does format render the value?" >"$TMP/ds.tsover.xml" @@ -756,7 +765,7 @@ if chk paidinside "$TMP/c5.xml" "$TMP/e4.off.xml" 800 >/dev/null 2>&1; then no " run --for="$T_D" >"$TMP/d2.xml" if cmp -s "$TMP/d.xml" "$TMP/d2.xml"; then ok "(X1) T-D deterministic x2"; else no "(X1) T-D differs between two runs"; fi if command -v xmllint >/dev/null 2>&1; then - for f in d d.c big hyd py rs cpp go m.d e400; do + for f in d d.c q1.cpp.c big hyd py rs cpp go m.d e400; do if xmllint --noout "$TMP/$f.xml" 2>/dev/null; then ok "(X2) $f.xml is well-formed XML"; else no "(X2) $f.xml failed xmllint"; fi done else From b2fc46b2738b1c6d50dc73c5e5489815b5a8bc99 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 20:18:26 -0400 Subject: [PATCH 125/162] =?UTF-8?q?test(ownerhopcheck):=20FIX1=20R4=20?= =?UTF-8?q?=E2=80=94=20(I)=20runs=20the=20head=20side=20with=20RIPWIRE=5FN?= =?UTF-8?q?O=5FBODY=5FSLOT=3D1=20(by-design=20break,=20rule=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arm (I) asserts head + RIPWIRE_NO_OWNER_HOP=1 (and the no-owner questions as shipped) equal the base. The body slot is independent of that knob by design (forbodyslotcheck EV1), so argv where the slot rides differed for the slot alone. The head side now also sets RIPWIRE_NO_BODY_SLOT=1; the break is stated in the header. The premise is kept (checklist 28): the owner-hop knob is still what the arm tests. Evidence, RIPWIRE_BASE_BIN = base2 (591efada, which carries owner-hop itself): - old arm on this head: 11 of 27 differ = owner-hop's own 9 K rows + M ts + H ts "relay forward a request" (on 66cc4b63 the review counted 4 slot argv; the dropped subtoken fallback took 2 of them). - new arm on this head: 9 differ, exactly the 9 that base2-vs-base2 gives — the slot adds none. The arm is green only against a pre-owner-hop base; in CI it SKIPs without RIPWIRE_BASE_BIN. Co-Authored-By: Claude Opus 5.5 --- test/ownerhopcheck.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/ownerhopcheck.sh b/test/ownerhopcheck.sh index 4a03cfa1b..83a8f958b 100755 --- a/test/ownerhopcheck.sh +++ b/test/ownerhopcheck.sh @@ -83,6 +83,12 @@ # (L) legend: the qword clause is present iff a qword row is, and says "by name" (not a resolution claim). # (I) RIPWIRE_BASE_BIN set: head + RIPWIRE_NO_OWNER_HOP=1 vs base on the --for / --json / MCP / budget answers, # head (no knob) vs base on the no-owner questions, plus 12 out-of-scope argv — every binary's rc checked. +# BY-DESIGN BREAK (rule 4, lane for-body-slot FIX1 R4): the head side also runs with RIPWIRE_NO_BODY_SLOT=1. The +# body slot (forowner.h) is independent of RIPWIRE_NO_OWNER_HOP by design (forbodyslotcheck (EV1)), so on any +# base without the slot 4 of these 27 argv differed for the slot alone (H c "log walk the process tables", +# H py "Reactive work", H ts "relay forward a request", M ts) — not for owner-hop. The arm's premise is kept: the +# owner-hop knob is still the one under test, and a head whose RIPWIRE_NO_OWNER_HOP=1 stops restoring the base +# still differs here (checklist 28; forbodyslotcheck I0 pins the slot-off == base half separately). # # Exit: 0 all PASS (named SKIPs allowed), 1 any FAIL, 2 missing prerequisite. @@ -527,10 +533,10 @@ if [ -n "${RIPWIRE_BASE_BIN:-}" ] && [ -x "${RIPWIRE_BASE_BIN}" ]; then mode="${parts[0]}"; r="${parts[1]}"; args=( ${parts[@]+"${parts[@]:2}"} ); shown="${args[*]-}" n=$((n+1)); hf="$TMP/i.$n.h"; bf="$TMP/i.$n.b" if [ "$mode" = M ]; then - mcp "RIPWIRE_NO_OWNER_HOP=1" "$BIN" "$hf"; mcp "RIPWIRE_X=0" "$RIPWIRE_BASE_BIN" "$bf" + mcp "RIPWIRE_NO_OWNER_HOP=1 RIPWIRE_NO_BODY_SLOT=1" "$BIN" "$hf"; mcp "RIPWIRE_X=0" "$RIPWIRE_BASE_BIN" "$bf" # R4: slot off on the head side else if [ "$mode" = K ]; then knob="RIPWIRE_NO_OWNER_HOP=1"; else knob="RIPWIRE_X=0"; fi - ( cd "$FIX/$r" && env "$knob" "$BIN" . --no-cache ${args[@]+"${args[@]}"} >"$hf" 2>"$hf.err" ); printf '%s' "$?" >"$hf.rc" + ( cd "$FIX/$r" && env "$knob" RIPWIRE_NO_BODY_SLOT=1 "$BIN" . --no-cache ${args[@]+"${args[@]}"} >"$hf" 2>"$hf.err" ); printf '%s' "$?" >"$hf.rc" # R4 ( cd "$FIX/$r" && "$RIPWIRE_BASE_BIN" . --no-cache ${args[@]+"${args[@]}"} >"$bf" 2>"$bf.err" ); printf '%s' "$?" >"$bf.rc" fi if ! ran_ok "$hf" "(I) head $mode $r $shown" || ! ran_ok "$bf" "(I) base $mode $r $shown"; then e=$((e+1)); continue; fi From 30b52b193e817fa8782282a76e497bfc0777c244 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 23:12:43 -0400 Subject: [PATCH 126/162] train 28: versions above every live value; qschemetrip re-derived once kParserVer and its mirror 176 (rich file tag 177): above main's 173/174 and every lane build (grep-carrier 173, ruby-pair 174/175, PR #380 145, the rest 171). kCacheVersion and its mirror 31 -> 32: RefRole gained Subscript (9) and Inert (10, 9 on the PR's branch). kQSnapCacheScheme 20 -> 21: the dead set's call inputs moved. qschemetrip.hash re-derived once with a RE-PIN LOG entry. CHANGELOG: the versions paragraph, the entries that stated a branch number, and an entry for PR #380 (closes #357), which carried none. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++--- src/ingest_cache.h | 7 ++++++- src/quality.h | 10 ++++++++-- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 7 +++++++ 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bf8abc7..80dacf600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Ruby-only `captureRubyMixinBases` and its `captureIncludes` call site are gone; the C++ reader keeps the two guards a tags pattern cannot state (the directive's text, and its class-body position). Output is byte-identical: the map, `--deps --limit=100000`, `--skipped`, `--callers`, `--callees`, `--impact` and `--uses` all match the base build over a real Rails corpus and the Ruby fixtures, and `--lego` lists the same -implementors. `kParserVer` moves 171 -> 173 (the extraction path changed; 172 is 171's rich file tag). +implementors. `kParserVer` moves (the extraction path changed; 176 in this release, see the versions note). ### Fixed — a function stored through `??`, `||`, `&&`, `or` or `and` is a value use @@ -33,7 +33,7 @@ value-reference capture walked parentheses and ternary branches but stopped at a value-preserving operators — JS/TS `??`, `||`, `&&` and the logical assignments `??=`, `||=`, `&&=`; Python `or`, `and` — in every position it already read (assignment, return, argument, parameter default). The left operand of `&&`/`and` only decides and stays no row; C/C++ and Go `||`/`&&` yield a truth value and stay no row. Parser version -173 (caches re-parse once). Gate: `test/recallshapecheck.sh` arms OP1–OP17. +176 in this release (caches re-parse once; see the versions note). Gate: `test/recallshapecheck.sh` arms OP1–OP17. ### Added — `--grep` names what a hit's variable carries and who reads it @@ -65,7 +65,7 @@ edge, no `via="name"` hedge and no count: a `[]` or `__getitem__` definition els name. It does NOT mean every subscript of an in-tree class is found: a Ruby instance variable receiver is not typed, a compound assignment (`h[ k ] ||= v`) is recorded as the read form, and `del o[ k ]`, a multiple-assignment target and an annotation record nothing. On rake 93 callers of `[]` appear; on textual 42 of `__getitem__`. C++ `operator[]` could -share the path and is not armed. `--uses` lists no subscript site. Parser version 174 (the train renumbers). Gate: +share the path and is not armed. `--uses` lists no subscript site. Parser version 176 in this release (see the versions note). Gate: `test/subscriptopcheck.sh`. ### Added — `--lego=M` lists Ruby's main object when a file's top level mixes M in @@ -81,6 +81,19 @@ Gate: `test/subscriptopcheck.sh` (L arms). Every git command ripwire runs now pins the filter drivers the repository's own config defines to an empty command, carries `--no-ext-diff --no-textconv` on `diff`/`log`/`show`/`blame` and `--ignore-submodules=dirty` on `status`/`diff`, and keeps signature display off; stderr discloses the pinning as `git_harden=filter-driver`, and `--doctor`'s `git-config-trust` row reports it (`filter_drivers=`, `filter_scope=`, `filter_pinned=`). Visible effects: files behind a repository-local filter (git-crypt, a per-repository LFS driver) are seen unfiltered, as stored; edits inside a submodule no longer mark the stamp `+dirty`; a user's own textconv no longer shapes `--numstat` (such binary rows read `-`); and a pinned driver name applies to every root in the same run. Gate: `test/gitchildoptscheck.sh`. +### Fixed — an Elixir call inside `unquote(...)` under `quote` is a call again; quoted call sites are counted (#357, PR #380 by @LaimaWu) + +Everything under a `quote` used to be dropped, so a helper called only from `unquote(...)` or `unquote_splicing(...)` +inside a macro's quote had no caller edge from that macro. The quoted body is now read through one shared walk +(`src/ingest_inert.h`, `inInertRegion`), with Elixir deciding per ancestor whether a region is inert or a live hole +(`src/ingest_elixir.h`): calls inside `unquote(...)`/`unquote_splicing(...)` are live, as are `bind_quoted:` option +expressions; `bind_quoted` turns unquoting off unless `unquote: true` turns it back on, and `unquote: false` keeps the +argument inert; a nested quote inside a live hole is inert again. Ordinary call, operator and named-capture candidates +that stay inert mint no edge and are counted: the map's `declined=` includes them (its full legend says so where any were +counted) and `--callees` counts them in `declined_calls=` on their enclosing function. They are not possible callers: +`--callers`, `--uses` and `--impact` of a function called only from inert quoted code still read zero. Not read yet: a +bracketed options list (`quote [unquote: false] do`), under which a hole is taken as live. Gate: `test/elixircheck.sh`. + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. @@ -192,6 +205,18 @@ memory puts the leaf over `--max-memory=256M` (exit 5; the 0.6.7 rule answers, e does not (exit 0). `--help` says what is measured, and the MCP refusal at the hard line names the cgroup, not this server, when the cgroup's charge is the larger reading: restarting the server does not help there. +### Changed — the versions this release moves, stated once + +`kParserVer` 171 → 176: slice 4a (#400) takes 173 on `main` (its full-use file tag 174); the value-reference walk through +`??`/`||`/`&&`/`or`/`and` carried 173 on its branch, the Ruby/Python subscript references and Ruby's main-object mixins +174 (file tag 175), and the Elixir quote live holes (#380) 145 on theirs. They land together at 176, above every number a +branch build has used, so no cache such a build wrote is read as this release's; its full-use file tag is 177. +`kCacheVersion` 31 → 32: a reference record's role byte gained two values, the subscript role (9) and the inert role (10, +numbered 9 on #380's branch, which predates the subscript role); no record changes shape. `kQSnapCacheScheme` 20 → 21: +the calls that keep a definition alive moved (proven subscript calls, live `unquote(...)` calls), so the dead kind can +differ for an unchanged commit. Every ingest cache written by an earlier build is refused and re-indexed once, and every +cached quality snapshot is recomputed. + ## [0.6.7] — 2026-10-11 diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 83d0e8033..2dbf3e06f 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -140,7 +140,12 @@ constexpr std::uint32_t kCacheMagic = 0x4b505443; // "CTPK" // all match) rather than silently re-absolutizing a key that was never root-relative to begin // with — a v2 cache simply misses on every lookup that survives the guard, which is exactly the // self-healing full-reparse path already used for any other corrupt/stale cache. -constexpr std::uint32_t kCacheVersion = 31; // 31: train 27 — lane lego-transitive's record format below (the lane numbered it 30; +constexpr std::uint32_t kCacheVersion = 32; // 32: train 28 — the RefRole byte a record holds gained two appended values: lane + // ruby-pair's Subscript (9) and PR #380's Inert (10; 9 on the PR's branch, which predates + // Subscript). readRef validates the byte against kRefRoleCount, so no record changes + // shape, but a branch build's blob would read its role 9 as the other role here; 32 + // sits above every live value (31 on main and every lane). + // 31: train 27 — lane lego-transitive's record format below (the lane numbered it 30; // 31 is above every live value: the lane's branch builds wrote c30 blobs at 162). // 30: lane lego-transitive fix 1 — an INHERIT RawRef appends `baseBind` (u32: the // same-file declaration its base binds to, ingest_relations.h localBaseBinding); diff --git a/src/quality.h b/src/quality.h index 0f1f0aa4b..cc5953670 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2335,7 +2335,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // `static_assert( quality::kIngestParserVerMirror == kParserVer && … )`, so a missed mirror now fails the build. It does // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. -constexpr std::uint32_t kIngestCacheVersionMirror = 31; // MUST equal ingest.cpp's kCacheVersion (gated); 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) +constexpr std::uint32_t kIngestCacheVersionMirror = 32; // MUST equal ingest.cpp's kCacheVersion (gated); 32 = train 28: RefRole Subscript 9 + Inert 10, 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) constexpr std::uint32_t kIngestParserVerMirror = 176; // MUST equal ingest.cpp's kParserVer (gated) // 176 = 2026-10-15 (train 28: above main 173/174, grep-carrier 173/174, // ruby-pair 174/175, #380 145, the other lanes 171; see the kParserVer note) @@ -3606,7 +3606,13 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // already keeps this build's blobs apart; bumped 19 -> 20 so the history above stays complete and the scheme sits above // every live value (19 on every branch). The lane's own note said the walk itself is a refactor (same class set and // order for the same g.implementors) — true; what moved is g.implementors. -constexpr std::uint32_t kQSnapCacheScheme = 20; +// v21 (2026-10-15, train 28) — no blob shape change; the dead SET's inputs moved again. Lane ruby-pair makes a proven element +// access a call of the receiver class's subscript operator (Ruby `[]`/`[]=`, Python `__getitem__`/`__setitem__`) and PR #380 +// makes an Elixir `unquote(...)` call under `quote` live, so the calls that keep a definition alive can differ for an +// UNCHANGED sha. kParserVer moved with those extractions (176, its mirror with it), and the producer identity already keeps +// this build's blobs apart; bumped 20 -> 21 so the history above stays complete and the scheme sits above every live value +// (20 on main and every lane). Lane qd-memory-degrade's KindScope builds the default scope's records unchanged (its note). +constexpr std::uint32_t kQSnapCacheScheme = 21; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 189e81021..023caa705 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -82085ad1d2fbf866750fb7e3f928f611464f8ae1227e4453c9b6e12d6af122de +279718355de35b8e257c122756f07f97a0ccaa0106eac9adbb811668365ba2db diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 70a802856..59b32e52d 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,13 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-15, train 28 (grep-carrier, ruby-pair, qd-memory-degrade, githarden-speed, git-child-hardening, PR #380, forbudget-soft, +# for-body-slot, attrib-multidef, declined-sites merged onto main 1bcb6540): RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the merged +# tree (hash 279718355d…5ba2db). Watched text: kParserVer 173 -> 176 (main's slice 4a 173 / rich 174, lane grep-carrier 173, +# lane ruby-pair 174 / rich 175, PR #380 145; 176 / rich file tag 177 is above every live build), kCacheVersion 31 -> 32 +# (RefRole gained Subscript 9 and Inert 10; see the kCacheVersion note), quality.h's two mirrors with them, kQSnapCacheScheme +# 20 -> 21 (the dead set's call inputs moved; see the v21 note), and the lanes' own watched-text moves logged below +# (qd-memory-degrade's computeSnapshot / defectSitesOf). # 2026-10-15, slice 4a (#400, Ruby mixins onto the shared base-relations path): RE-DERIVED with UPDATE_GOLDEN=1. # The only watched text that moved is the kParserVer declaration (and its mirror): 171 -> 173. Ruby's # `include`/`extend`/`prepend` are captured by queries/ruby/tags.scm as @reference.mixin and read by From a7eca4cb3fb83468e8fb402f97809fea6290098c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 15 Oct 2026 23:40:02 -0400 Subject: [PATCH 127/162] test(estchargecheck): forbudget-soft's soft-target arms on #398's pooled structure The lane rewrote its --for arms for the soft target (rule 4: the old byte-ceiling arms REPLACED by ST / NOLBL / SEC / RP / NX / HL, with the A7 twin, A7B, the sweep, the floor/next and C3 arms, A7R-1/-K, A7D-1/-2, #15, #18 rung zero and #19 re-anchored as the lane signed them) on the pre-#398 layout. They now sit in #398's section functions: the call harness (rw/rwcall/rwcall_bin, per-call budgets, call_failures), the warm caches, the pool and its chains and the zero-hit sentinel fix are unchanged. The soft readers (soft_rows/soft_st with test/lib/softbudget.py's tail_size and upper_bound, rows_prefix, ctx_root) move to the shared definitions so every chain sees them; #15 asks its own no-budget reference instead of reading another chain's capture. The lane's own runtime shortcuts (its cache variables and capture reuse) are superseded by #398's harness and are not ported; KEPT_NOTE, FLOOR_NOTE and sigs_unpaid_twin lost their last reader with the lane's replacements and are removed. On the merged binary: 251 PASS / 0 FAIL (the lane's own count); on main's binary 35 soft-target arms go red. Co-Authored-By: Claude Opus 5.5 --- test/estchargecheck.sh | 899 ++++++++++++++++++++++++----------------- 1 file changed, 523 insertions(+), 376 deletions(-) diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 9c6f6f18f..94707f5ff 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -23,13 +23,18 @@ # gate — that band is the recurrence guard, and it is the reason this file asserts a band and never a # bit-exact token count (house rule: float/estimate assertions get a tolerance, never bit-equality). # -# BY-DESIGN BREAK (rule 4; owner ruling 2026-10-09, option B; reports/lego-charge.md fix round 5): #11 A7's old bar -# "--token-budget=2000 --detail=20 --with-graph on src stays inside N x 2.36 x 1.15" no longer holds for that ONE shape. -# With BOTH trailing sections under an explicit ceiling the answer KEEPS its ranked rows (a gold owner row, textual-12 -# Reactive._set, was cut when the two sections were reserved out of ) and goes over the budget, labelled -# over_ceiling="1" with a TRUE reason and a next= on every cut: answers first, a budget never silently removes an -# answer. The old assertion is kept as A7 twin wherever it must still hold (each flag alone at 2000, and both flags at -# a budget the rows fit); A7B asserts the new contract for both-flags-over-budget. +# BY-DESIGN BREAK (forbudget-soft (rule 4); owner decisions 2026-10-14). An explicit --token-budget=N on --for (and on +# --pack-task, --from-trace, MCP for/explore) is a SOFT TARGET, not a byte ceiling: T = N x 2.36 x 0.90 bytes is charged +# against the ROWS only, rows are added in rank order while they fit T (the rank 1..4 floor always ships), the cut +# carries next=, est_tokens may exceed N, and nothing is labelled over_ceiling= or dropped from the header for it (no rung +# zero, no "(ceiling)" drop, no trailing-section reserve, no paid/unpaid continuation). The arms that asserted the old +# byte promise (delivered <= N x 2.36 x 1.15, or over_ceiling="1"; the kept-rows / unpaid / header-floor reasons; the +# reserve; rung zero) are REPLACED below by the invariants that guard the new behaviour on the same fixtures: ST (rows fit +# T or are the floor, and a cut is maximal), NOLBL (no over_ceiling= on a soft lens), SEC (--detail / --with-graph never +# remove a row vs the bare run at the same N), RP (rows a rank prefix, monotone in N), NX (every cut carries a +# next= that runs), HL (no legend clause, task echo or route= dropped). What still holds is kept unchanged: est_tokens +# prices the delivered document exactly (#1, A7/A8 bands, A9/A10, §C1, #19), and every HARD ceiling keeps its twins +# (--max-tokens #5/§C4, the map/payload --token-budget exit-3 gate #3/#12, --handoff #15(c)). # # Usage: bash test/estchargecheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/estchargecheck.sh # Exits non-zero on any failure; prints PASS/FAIL per check, ALL PASS on success. @@ -37,6 +42,7 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" +export RIPWIRE_TESTLIB="$ROOT/test/lib" # softbudget.py: the soft-target readers, shared helpers (CHECKLIST 32) BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" echo "estchargecheck: BIN=$BIN" @@ -134,8 +140,10 @@ est_of(){ grep -aoE 'est_tokens=[0-9]+' "$1" | head -1 | grep -oE '[0-9]+'; } bytes_of(){ wc -c < "$1" | tr -d ' '; } # ── SHARED DEFINITIONS (moved here unchanged from #11 so the sections below can run as independent chains) ─────── -# FOR_TASK and forest_of are read by #11, #14 and #15; KEPT_NOTE/FLOOR_NOTE by #11 and #11 A7R; sigs_unpaid_twin by -# #11 and its sweep. Defining them before the sections fork is the only reason they live here. +# FOR_TASK and forest_of are read by #11, #14 and #15; the forbudget-soft soft-target helpers (soft_rows / soft_st and +# the $TMP/soft_rows.py / soft_st.py they run, rows_prefix, ctx_root, SOFT_CLAUSE_*) by #11, its sweep, A7R/A7D, #15 and +# #19. Defining them before the sections fork is the only reason they live here. (forbudget-soft deleted KEPT_NOTE / +# FLOOR_NOTE / sigs_unpaid_twin with the hard-ceiling arms that read them: no section reads them any more.) FOR_TASK="serialize the map" # WEAK_QUERY is #11 A9's nonsense word. It is spelled ONLY here, at top level, never inside a function: the # repo's own bash functions are indexed symbols, so a section body that carried the word would make it match @@ -172,41 +180,104 @@ print( m.group( 1 ).decode() if m else '' ) PYX } -KEPT_NOTE='[over_ceiling= is 1 on the root: the ranked rows are kept, not cut for the trailing sections' -FLOOR_NOTE='[over_ceiling= is 1 on the root: the header floor' -# THE UNPAID TWIN (knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5). A capped under a -# hard ceiling carries next= — the call that recovers its cut — and pays for it from its rows ONLY where that makes the -# answer fit (ruling C3). Where it cannot (the rank 1..4 floor has nothing left to shed, or the answer lands past its ceiling -# paid or not) no row is dropped for it: the recovery handle ALWAYS ships, unpaid, and the document may then land past the -# allowance, and only if it says so. This is not a wider allowance: every condition of the old bound still holds for -# everything except the disclosed handle. Train 26c: the single A7 arm it guarded is superseded by the lane's A7 twin loop + -# A7B below; the twin itself still serves the sweep and the floor arm. sigs_unpaid_twin FILE ALLOW → exit 0 iff ALL of: the root carries -# over_ceiling="1"; the legend carries the unpaid clause defining it; is capped and carries next="--for=…"; -# est_tokens is positive and prices the delivered bytes inside the 2.00-4.20 B/tok band (honest: it moved with the handle); -# and the document WITHOUT the handle and its label (the next= attribute, over_ceiling="1", the clause) is within ALLOW — -# the old bound, intact — or carries the ladder's last-rung disclosure (the header floor alone exceeds the budget: the one -# overshoot the old bound already admitted). The rows are the ones the cut alone leaves (the C3 arm below pins them). -sigs_unpaid_twin(){ python3 - "$1" "$2" <<'PYF' -import sys, re -d = open( sys.argv[ 1 ], 'rb' ).read().decode( 'utf-8', 'replace' ); allow = int( sys.argv[ 2 ] ) -clause = ' [over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]' -root = re.search( r']*>', d ); sigs = re.search( r']*>', d ) -why = [] -if not root or ' over_ceiling="1"' not in root.group( 0 ): why.append( 'no root over_ceiling="1"' ) -if clause not in d: why.append( 'no unpaid clause' ) -nxt = re.search( r' next="--for=[^"]*"', sigs.group( 0 ) ) if sigs else None -if not sigs or ' capped="1"' not in sigs.group( 0 ): why.append( ' not capped' ) -if not nxt: why.append( ' carries no next=' ) -est = re.search( r' est_tokens="(\d+)"', root.group( 0 ) ) if root else None -b = len( d.encode( 'utf-8' ) ) -if not est or int( est.group( 1 ) ) <= 0 or not ( 200 <= b * 100 // int( est.group( 1 ) ) <= 420 ): why.append( 'est_tokens absent or out of band' ) -rest = b - ( len( nxt.group( 0 ) ) if nxt else 0 ) - len( ' over_ceiling="1"' ) - len( clause ) -lastRung = '[over_ceiling= is 1 on the root: ' in d # either last-rung reason: the header floor, or (lego-charge) the kept rows -if rest > allow and not lastRung: why.append( f'{rest} B without the handle > {allow} B (and no last-rung disclosure)' ) -print( '; '.join( why ) if why else f'{b} B, {rest} B without the handle <= {allow} B, est_tokens={est.group( 1 )}' ) -sys.exit( 1 if why else 0 ) -PYF -} +# ── forbudget-soft (rule 4): THE SOFT TARGET's invariants, shared by the A7/A7B/sweep/floor/C3/A7R/#15/#19 arms below. An +# explicit --token-budget=N on --for charges T = N x 2.36 x 0.90 bytes (serialize.h kMinBytesPerToken x kBudgetHeadroom) +# against the rows inside ONLY — never the header, a disclosure or a trailing section — so the old "delivered <= N x +# 2.36 x 1.15 or over_ceiling=1" bar is gone by design (est_tokens may exceed N, unlabelled) and these replace it: +# soft_rows FILE the served rows as p:l:n in RANK order (r=), one per line; exit 3 when there is no +# soft_st FILE N [REF] ST: the bytes inside (the rows as printed) fit T, or the rows are only the rank 1..4 floor; +# with REF (the same shape's complete, no-budget answer) a cut must also be MAXIMAL — in the ladder's +# own currency (serialize.h entryCost: a row's printed bytes plus the zero cx=/ccx=/in= the lean +# spelling elides), the served rows plus REF's next row would cross T (160 B slack, as +# forsoftbudgetcheck's (ST)). Prints "OK|BAD ...", exit 0|1. +# rows_prefix A B RP: rows file A is a non-empty rank prefix of rows file B +# ctx_root FILE the root open tag (an over_ceiling= is read off the root only: a can carry its own) +cat >"$TMP/soft_rows.py" <<'PY' +import re, sys +s = open( sys.argv[ 1 ], encoding='utf-8', errors='replace' ).read() +m = re.search( r']*>(.*?)', s, re.S ) +if not m: sys.exit( 3 ) +rows = [] +for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) +rows.sort() +print( "\n".join( x[ 1 ] for x in rows ) ) +PY +cat >"$TMP/soft_st.py" <<'PY' +import re, sys +def charge( a, printed ): + return printed + sum( n for k, n in ( ( 'cx', 7 ), ( 'ccx', 8 ), ( 'in', 7 ) ) if not re.search( r'\b' + k + r'="', a ) ) +def rb( p ): + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return None + inner = m.group( 1 ); rows = []; elided = 0 + for d in re.finditer( r']*)>.*?|]*)/>', inner, re.S ): + a = d.group( 1 ) or d.group( 2 ) + r = re.search( r'\br="(\d+)"', a ) + printed = len( d.group( 0 ).encode() ); c = charge( a, printed ); elided += c - printed + rows.append( ( int( r.group( 1 ) ) if r else 10**9, c ) ) + rows.sort() + return len( inner.encode() ), len( inner.encode() ) + elided, rows +def row_ids( p ): # soft_rows.py's list, for an in-process caller (the sweep) + s = open( p, encoding='utf-8', errors='replace' ).read() + m = re.search( r']*>(.*?)', s, re.S ) + if not m: return [] + rows = [] + for d in re.finditer( r']*)>', m.group( 1 ) ): + a = d.group( 1 ) + g = lambda k: ( re.search( r'\b' + k + r'="([^"]*)"', a ) or [ None, "?" ] )[ 1 ] + r = g( "r" ) + rows.append( ( int( r ) if r.isdigit() else 10**9, g( "p" ) + ":" + g( "l" ) + ":" + g( "n" ) ) ) + rows.sort() + return [ x[ 1 ] for x in rows ] +def verdict( path, n, refpath=None ): + a = rb( path ); T = int( int( n ) * 2.36 * 0.90 ) + if a is None: return False, "BAD no " + pb, ab, ar = a; k = len( ar ) + fits = pb <= T or k <= 4 + if refpath is None: + return fits, "%s fits=%d rows=%d bytes=%d T=%d" % ( "OK" if fits else "BAD", fits, k, pb, T ) + ref = rb( refpath ) + if ref is None: return False, "BAD no in the reference answer" + rr = ref[ 2 ] + if k >= len( rr ): + return fits, "%s fits=%d rows=%d/%d bytes=%d T=%d complete" % ( "OK" if fits else "BAD", fits, k, len( rr ), pb, T ) + # round C (forbudget-soft, owner decision 2026-10-14: RESERVE SECTIONS FIRST; rule 4): the trailing sections are served first + # and the rows past the target cut fill what they leave of T, so maximality is judged against T less their bytes (U: all + # after but , and the rows — less the --detail bodies, the caller's own request under its own ceiling: + # on a run without bundle= on the root the element is the --detail section) + s = open( path, encoding='utf-8', errors='replace' ).read() + U = ( len( s.split( '', 1 )[ 1 ].encode() ) - len( '' ) ) if '' in s else 0 + U += sum( len( h.encode() ) for h in re.findall( r']*/>', s.split( ']*>', s ) + if root and ' bundle="' not in root.group( 0 ): + bm = re.search( r'', s, re.S ) + U -= len( bm.group( 0 ).encode() ) if bm else 0 + maximal = ab + rr[ k ][ 1 ] > T - U - 160 + # FIX2 C1 (rule 4): the upper-bound twin of maximality (test/lib/softbudget.py upper_bound): rows past the floor stop where T less + # the other sections and the tail reserve runs out; RED on a binary that ignores the tail reserve + from softbudget import upper_bound + upper, udet = upper_bound( s, open( refpath, encoding='utf-8', errors='replace' ).read(), T, k, len( rr ), U ) + return fits and maximal and upper, "%s fits=%d maximal=%d upper=%d rows=%d/%d bytes=%d charged=%d next=%d T=%d U=%d%s" % ( + "OK" if fits and maximal and upper else "BAD", fits, maximal, upper, k, len( rr ), pb, ab, rr[ k ][ 1 ], T, U, "" if upper else " -- " + udet ) +if __name__ == "__main__": + good, msg = verdict( sys.argv[ 1 ], sys.argv[ 2 ], sys.argv[ 3 ] if len( sys.argv ) > 3 else None ) + print( msg ); sys.exit( 0 if good else 1 ) +PY +soft_rows(){ python3 "$TMP/soft_rows.py" "$1"; } +soft_st(){ python3 "$TMP/soft_st.py" "$@"; } +rows_prefix(){ [ -s "$1" ] && [ "$( head -n "$( wc -l < "$1" | tr -d ' ' )" "$2" )" = "$( cat "$1" )" ]; } +ctx_root(){ grep -aoE '^]*>' "$1" | head -1; } +# the soft-target legend clauses (verbs_for.h / graphlegend.h): the compact dialect's reading of budget_tokens=, and the full one's +SOFT_CLAUSE_COMPACT='budget_tokens= the soft row target asked (rows stop once it is reached)' +SOFT_CLAUSE_FULL='[budget_tokens=: the soft token target; rows stop once it is reached, the rank 1..4 rows and every disclosure always ship, so est_tokens may exceed it]' est_1(){ # ── #1: the CHARGE band. For every payload shape, bytes/est_tokens must sit inside the calibrated B/tok @@ -713,97 +784,106 @@ for label in for_graph for_detail for_both; do || no "#11 A7/A8 $label: est_tokens=$E did not move above the bare bundle's $E_FOR — that section is uncharged" done -# A7 — and --token-budget must SHAPE the bodies, not just the signatures. D10: --for trims to fit, exit 0. -# The pre-fix binary streamed 68 035 B here against a 4 248 B ceiling; the bar is the stated ceiling plus the -# documented single-entry overshoot tolerance (serialize.h kCeilingFirstEntryTolerance = 1.15). -# BY-DESIGN BREAK (owner ruling 2026-10-09, option B — see this file's header): the old arm ran ONLY the both-flags shape -# at 2000, which now keeps its rows and goes over on purpose. A7 TWIN keeps the old assertion, unchanged in substance -# (exit 0, delivered <= the allowance), on every shape where it must still hold: each trailing flag alone at 2000, and -# both flags at 5000 where the ranked rows fit (measured at fix round 5: 4727 / 4769 / 12337 B vs 5428 / 5428 / 13570 B). +# (The soft-target helpers — soft_rows / soft_st / rows_prefix / ctx_root and SOFT_CLAUSE_* — live in SHARED DEFINITIONS +# above: the sweep, A7R/A7D, #15 and #19 sections read them too.) + +# A7 TWIN — REPLACED (forbudget-soft (rule 4)). Was: exit 0 and delivered <= N x 2.36 x 1.15 for --detail=20 / --with-graph at +# 2000 and both flags at 5000 (the trailing sections bounded by the budget). Under a soft target the trailing sections are +# funded from what the rows left of T and the document may land past N, unlabelled — so the byte bar asserts the deleted +# contract. The invariant that must hold on the same three shapes: exit 0; NOLBL (no over_ceiling= on the root); SEC (the +# trailing flag never removes a row — the bare run's rows at the same N are a rank prefix of the shape's rows); and ST +# (the shape's rows fit T or are the floor, and a cut is maximal against the same shape's no-budget answer from A7/A8 above). +# MEASURED on d099ec21: 20/20/40-row prefixes, rows identical with and without the flags; the base binary (d4325a8c) served +# 10 / 11 rows with --detail=20 / --with-graph against the bare run's 14 — the reserve the soft target deleted. TBF=2000 -ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" -for tw in "2000:--detail=20" "2000:--with-graph" "5000:--detail=20 --with-graph"; do - tw_n="${tw%%:*}"; tw_args="${tw#*:}" - tw_allow="$( awk "BEGIN{printf \"%d\", $tw_n*2.36*1.15}" )" +ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" # still read by the no-budget --detail arm below +rw src --for="$FOR_TASK" --token-budget=$TBF >"$TMP/f_tbp.out" 2>/dev/null +rw src --for="$FOR_TASK" --token-budget=5000 >"$TMP/f_tbp5.out" 2>/dev/null +for tw in "2000:--detail=20:f_tbp:f_for_detail" "2000:--with-graph:f_tbp:f_for_graph" "5000:--detail=20 --with-graph:f_tbp5:f_for_both"; do + tw_n="${tw%%:*}"; tw_rest="${tw#*:}"; tw_args="${tw_rest%%:*}"; tw_rest="${tw_rest#*:}"; tw_bare="${tw_rest%%:*}"; tw_ref="${tw_rest#*:}" # shellcheck disable=SC2086 rw src --for="$FOR_TASK" --token-budget=$tw_n $tw_args >"$TMP/f_tw.out" 2>/dev/null rc_tw=$? - tw_b="$( bytes_of "$TMP/f_tw.out" )" - { [ "$rc_tw" -eq 0 ] && [ -n "$tw_b" ] && [ "$tw_b" -le "$tw_allow" ]; } 2>/dev/null \ - && ok "#11 A7 twin --for --token-budget=$tw_n $tw_args: $tw_b B within the $tw_allow B allowance (exit 0, SHAPED)" \ - || no "#11 A7 twin --for --token-budget=$tw_n $tw_args: ${tw_b:-?} B vs the $tw_allow B allowance, exit $rc_tw — the budget does not bound the appended sections" + tw_root="$( ctx_root "$TMP/f_tw.out" )" + soft_rows "$TMP/f_tw.out" >"$TMP/tw.rows" 2>/dev/null; soft_rows "$TMP/$tw_bare.out" >"$TMP/tw_bare.rows" 2>/dev/null + tw_st="$( soft_st "$TMP/f_tw.out" "$tw_n" "$TMP/$tw_ref.out" )"; tw_st_rc=$? + tw_why="" + { [ "$rc_tw" -eq 0 ] && [ -n "$tw_root" ]; } || tw_why="$tw_why exit-$rc_tw-or-no-root" + [ -n "$tw_root" ] && [ "${tw_root#* over_ceiling=}" = "$tw_root" ] || tw_why="$tw_why over_ceiling-on-a-soft-target" + # round C (rule 4): SEC ("the trailing flag never removes a row") is the by-design break — a trailing section is + # reserved before the rows past the cut — so the rows clause is RP: the shape's rows are a rank prefix of its own + # no-budget answer's rows (the cut and ST above/below keep the rest) + soft_rows "$TMP/$tw_ref.out" >"$TMP/tw_ref.rows" 2>/dev/null + rows_prefix "$TMP/tw.rows" "$TMP/tw_ref.rows" || tw_why="$tw_why rows:$( wc -l < "$TMP/tw.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + [ "$tw_st_rc" -eq 0 ] || tw_why="$tw_why ST:[$tw_st]" + [ -z "$tw_why" ] \ + && ok "#11 A7 twin (forbudget-soft, round C) --for --token-budget=$tw_n $tw_args: exit 0, no over_ceiling=, $( wc -l < "$TMP/tw.rows" | tr -d ' ' ) rows a rank prefix of the no-budget answer (bare: $( wc -l < "$TMP/tw_bare.rows" | tr -d ' ' )) ($tw_st)" \ + || no "#11 A7 twin (forbudget-soft) --for --token-budget=$tw_n $tw_args —$tw_why" done -# A7B — THE NEW CONTRACT for both flags OVER the budget (owner ruling, option B). Same query, same 2000 tokens. It must: -# (premise) deliver past the allowance at exit 0 — else the shape now fits and this arm must be re-anchored, not passed; -# (rows) keep every row the same budget shows WITHOUT the trailing flags, and those rows are the uncapped -# answer's top rows in its order (no higher-ranked row of the uncapped answer is missing) — the trailing -# sections never remove an answer row; -# (label) carry over_ceiling="1" on the root; -# (reason) name the reason TRUTHFULLY: the last-rung note is the kept-rows one, not "the header floor ... exceeds", -# and what it names is there — the header alone fits the allowance, and rows, the graph block and a -# first --detail body are all present; -# (next) a cut first body carries a next= that recovers exactly the lines it cut (checked on A7R's corpus below, -# whose first body is longer than the floor; here a first body served whole carries no truncated=). -rows_of(){ grep -aoE ']*>' "$1" | sed -nE 's/.* n="([^"]*)".*/\1/p'; } +# A7B — REPLACED (forbudget-soft (rule 4)). Was the owner-option-B contract for both flags OVER the budget: a premise past the +# allowance, over_ceiling="1", the kept-rows reason. All three die with the ceiling; its ROWS clause is the soft target's own +# invariant and is kept: --detail=20 --with-graph at 2000 keeps every row of the flagless 2000 answer, and its rows are +# the uncapped answer's top rows in rank order (no higher-ranked row missing). Plus NOLBL, both trailing sections present, and +# a cut first body still carries the next= that serves the rest. rw src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? -rw src --for="$FOR_TASK" --token-budget=$TBF >"$TMP/f_tbp.out" 2>/dev/null -FTB="$( bytes_of "$TMP/f_tb.out" )" -a7b_root="$( grep -aoE '^]*>' "$TMP/f_tb.out" | head -1 )" -a7b_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b""$TMP/a7b.rows"; rows_of "$TMP/f_tbp.out" >"$TMP/a7b.plain"; rows_of "$TMP/f_for.out" >"$TMP/a7b.unc" +a7b_root="$( ctx_root "$TMP/f_tb.out" )" +soft_rows "$TMP/f_tb.out" >"$TMP/a7b.rows" 2>/dev/null; soft_rows "$TMP/f_tbp.out" >"$TMP/a7b.plain" 2>/dev/null; soft_rows "$TMP/f_for.out" >"$TMP/a7b.unc" 2>/dev/null a7b_n="$( wc -l < "$TMP/a7b.rows" | tr -d ' ' )"; a7b_pn="$( wc -l < "$TMP/a7b.plain" | tr -d ' ' )" a7b_why="" -{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ] && [ -n "$FTB" ] && [ "$FTB" -gt "$ALLOW" ]; } 2>/dev/null \ - || a7b_why="$a7b_why premise:${FTB:-?}B<=${ALLOW}B-or-exit-$rc_ftb(re-anchor)" -{ [ "$a7b_pn" -ge 1 ] && [ "$a7b_n" -ge "$a7b_pn" ] && [ -z "$( grep -vxF -f "$TMP/a7b.rows" "$TMP/a7b.plain" )" ] \ - && [ "$( head -n "$a7b_n" "$TMP/a7b.unc" )" = "$( cat "$TMP/a7b.rows" )" ]; } 2>/dev/null \ - || a7b_why="$a7b_why rows:$a7b_n-vs-plain-$a7b_pn-or-not-the-uncapped-prefix" -[ "${a7b_root#* over_ceiling=\"1\"}" != "$a7b_root" ] || a7b_why="$a7b_why no-over_ceiling" -{ grep -aqF "$KEPT_NOTE" "$TMP/f_tb.out" && ! grep -aqF "$FLOOR_NOTE" "$TMP/f_tb.out" && [ "${a7b_hdr:--1}" -gt 0 ] && [ "$a7b_hdr" -le "$ALLOW" ] \ - && grep -aq ']*>/dev/null \ - || a7b_why="$a7b_why reason:not-the-kept-rows-note-or-untrue(header ${a7b_hdr:-?}B)" +{ [ "$rc_ftb" -eq 0 ] && [ -n "$a7b_root" ]; } || a7b_why="$a7b_why exit-$rc_ftb-or-no-root" +# round C (rule 4): "keeps every flagless row" is the by-design break (the graph block and the first body are reserved before the +# rows past the cut); the uncapped answer's rank-prefix clause below is the rows invariant +rows_prefix "$TMP/a7b.rows" "$TMP/a7b.unc" || a7b_why="$a7b_why rows:not-the-uncapped-answer's-rank-prefix" +[ -n "$a7b_root" ] && [ "${a7b_root#* over_ceiling=}" = "$a7b_root" ] || a7b_why="$a7b_why over_ceiling-on-a-soft-target" +{ grep -aq ']*>/dev/null 2>&1; then rw src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --legend=full >"$TMP/f_tbl.out" 2>/dev/null a7x_bad="" + grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/f_tb.out" || a7x_bad="$a7x_bad f_tb:no-soft-target-clause" + grep -aqF "$SOFT_CLAUSE_FULL" "$TMP/f_tbl.out" || a7x_bad="$a7x_bad f_tbl:no-soft-target-clause" for f in f_tb f_tbl; do - grep -aqF "$KEPT_NOTE" "$TMP/$f.out" || a7x_bad="$a7x_bad $f:no-kept-rows-note(premise)" + [ -s "$TMP/$f.out" ] || a7x_bad="$a7x_bad $f:empty" xmllint --noout "$TMP/$f.out" 2>/dev/null || a7x_bad="$a7x_bad $f:xmllint-rejected" done [ -z "$a7x_bad" ] \ - && ok "#11 A7B-X the kept-rows answers (compact + --legend=full) carry the note and are well-formed XML" \ - || no "#11 A7B-X the kept-rows note breaks the answer's XML or no longer rides —$a7x_bad" + && ok "#11 A7B-X (forbudget-soft) the soft-target answers (compact + --legend=full) define budget_tokens= as a soft target and are well-formed XML" \ + || no "#11 A7B-X (forbudget-soft) the soft-target disclosure is missing or breaks the answer's XML —$a7x_bad" else printf ' SKIP #11 A7B-X xmllint not installed\n' fi -# A7B-N (fix round 6, checklist 1 — the negative of A7B's reason): the kept-rows note is chosen only when the header as -# built fits the allowance by itself. Same both-flags shape, but a 700-char task at 300 tokens: the verbatim task echo -# alone puts the header past the 814 B allowance, so the TRUE reason is the header floor and the kept-rows note would be -# false. Red on the mutant that drops the header-fits term from keptTrailingOverrun (review mutant MX). +# A7B-N — REPLACED (forbudget-soft (rule 4)). Was the negative of A7B's reason: a 700-char task at 300 tokens puts the header +# alone past the allowance, so the last rung must name the header floor. There is no ladder on a soft target — so the same +# shape now pins HL: the root keeps task= VERBATIM (all 700 chars) and route=, nothing is dropped "(ceiling)", no task_echo / +# legend-clause drop note, no last-rung note, no over_ceiling=; the rank 1..4 floor ships and the cut carries next=. A7N_TASK="$( python3 -c 'w="serialize the map rows into the output buffer while keeping the row order stable and flushing the writer "; print((w*10)[:700].strip())' )" rw src --for="$A7N_TASK" --token-budget=300 --detail=3 --with-graph >"$TMP/f_tbn.out" 2>/dev/null rc_a7n=$? -a7n_allow="$( awk "BEGIN{printf \"%d\", 300*2.36*1.15}" )" -a7n_root="$( grep -aoE '^]*>' "$TMP/f_tbn.out" | head -1 )" -a7n_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || a7n_why="$a7n_why premise:header-${a7n_hdr:-?}B-not-past-${a7n_allow}B-or-exit-$rc_a7n(re-anchor)" -[ "${a7n_root#* over_ceiling=\"1\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-over_ceiling" -grep -aqF "$FLOOR_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why no-header-floor-note" -! grep -aqF "$KEPT_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why kept-rows-note-false-here" +{ [ "$rc_a7n" -eq 0 ] && [ -n "$a7n_root" ]; } || a7n_why="$a7n_why exit-$rc_a7n-or-no-root" +python3 -c 'import re,sys; m=re.search(r" task=\"([^\"]*)\"", sys.argv[1]); sys.exit(0 if m and m.group(1)==sys.argv[2] else 1)' "$a7n_root" "$A7N_TASK" \ + || a7n_why="$a7n_why task=-not-verbatim" +[ "${a7n_root#* route=\"}" != "$a7n_root" ] || a7n_why="$a7n_why no-route=" +[ "${a7n_root#* over_ceiling=}" = "$a7n_root" ] || a7n_why="$a7n_why over_ceiling-on-a-soft-target" +! grep -aqE '\(ceiling\)|task_echo: dropped|\[legend clauses:|\[over_ceiling= is 1 on the root' "$TMP/f_tbn.out" || a7n_why="$a7n_why header-dropped-for-the-budget" +{ [ -n "$a7n_sh" ] && [ -n "$a7n_tot" ] && { [ "$a7n_sh" -ge 4 ] || [ "$a7n_sh" -eq "$a7n_tot" ]; } \ + && { [ "$a7n_sh" -eq "$a7n_tot" ] || [ "${a7n_sigs#* next=\"--for=}" != "$a7n_sigs" ]; }; } 2>/dev/null \ + || a7n_why="$a7n_why floor-or-next:shown=${a7n_sh:-?}/${a7n_tot:-?}" [ -z "$a7n_why" ] \ - && ok "#11 A7B-N 700-char task at --token-budget=300 --detail=3 --with-graph: header ${a7n_hdr} B alone past the ${a7n_allow} B allowance, the last rung names the header floor, not the kept rows" \ - || no "#11 A7B-N header-alone-over shape names the wrong reason —$a7n_why" + && ok "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 --detail=3 --with-graph: task= verbatim, route= kept, no drop, no over_ceiling=, floor $a7n_sh of $a7n_tot rows with next=" \ + || no "#11 A7B-N (forbudget-soft) 700-char task at --token-budget=300 —$a7n_why" # and without an explicit --token-budget the bodies keep their own budget: the bundle must NOT have shrunk { [ "$( bytes_of "$TMP/f_for_detail.out" )" -gt "$ALLOW" ]; } 2>/dev/null \ && ok "#11 A7 no --token-budget: --detail keeps its --pack-budget-bytes budget (unbudgeted bundle unshrunk)" \ @@ -811,19 +891,18 @@ grep -aqF "$FLOOR_NOTE" "$TMP/f_tbn.out" || a7n_why="$a7n_why no-header-floor-no } est_11_sweep(){ -# A7 SWEEP (PR #135 CI, 2026-09-11) — the budget must bound the document at EVERY budget, not at the one operating -# point above. That arm went red at 5 429 B against its 5 428 B allowance when a merge moved the live src/ corpus, -# and the main binary emits the same 5 429 bytes on the same tree, so the corpus only exposed it. The defect: the -# ceiling ladder priced the document WITHOUT the root over_ceiling="1" (17 B) and the legend clause defining it -# (53 B), which runForLens splices on AFTER the ladder whenever est_tokens exceeds budget_tokens. est_tokens prices -# markup at 2.50 B/tok and the allowance is sized at 2.36 x 1.15 = 2.714 B/tok, so every bundle in that band -# carries 70 unpriced bytes, and one the ladder fitted within 70 B of the allowance is pushed past it with no rung -# fired. One budget on the live tree only sees that when the corpus lands a bundle in the 70 B window, so this arm -# builds its OWN git-less corpus in $TMP and runs it from a relative path (no at=, no churn, a fixed root=, nothing -# read from the live repo) and SWEEPS the budget in 10-token steps: the window recurs with every trimmed row -# (~50 tokens), so a sweep this dense crosses it whatever the legend lengths are. Two shapes — the default bundle -# and A7's own --detail=20 --with-graph. THE PROPERTY: delivered bytes <= N x 2.36 x 1.15 at exit 0, or the -# ladder's LAST rung fired and says so (the header floor alone exceeds the budget — the one overshoot it documents). +# A7 SWEEP — the harness is kept, its property REPLACED (forbudget-soft (rule 4)). Was (PR #135 CI, 2026-09-11): delivered +# bytes <= N x 2.36 x 1.15 at exit 0 at EVERY budget, or the ladder's disclosed last rung / the unpaid twin — the guard +# against a byte spliced in after the ladder priced the document. A soft target makes no byte promise, so the sweep now pins +# the soft target's own laws at every budget of a dense sweep over the same git-less corpus (run from a relative path: no +# at=, no churn, a fixed root=): exit 0; NOLBL (no root over_ceiling=, no last-rung note); ST (rows fit T or are the floor, +# and a cut is maximal against the spec's own no-budget answer); RP (rows(N) a rank prefix of rows(N + step) — monotone); +# NX (a with shown < total carries next=). The byte-exactness the old arm protected lives on in est_tokens' +# identities (A9/A10, §C1, #19): est_tokens prices the delivered document, whatever its size. +# RE-ANCHORED (forbudget-soft): the --detail=20 --with-graph spec ran 2880..3080, the old late-label band; on the soft target +# that range serves all 32 rows (complete), so it now sweeps 300..2500, where the rows run from the 4-row floor to complete +# (d099ec21: 4 rows at 300, 28 at 2000, 32 at 2500). The default spec keeps 760..1500 (8..20 rows); the one-row named task +# keeps 400..600 — its document prices at est_tokens=719, past every N there: the soft overshoot. A7S="$TMP/a7sweep" mkdir -p "$A7S/corpus" python3 - "$A7S/corpus" <<'PYG' @@ -842,96 +921,109 @@ for i in range( 4 ): with open( os.path.join( out, f"mod{i}.cpp" ), "w" ) as fh: fh.write( "\n".join( lines ) ) PYG -a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0; a7s_floor=0 -# RE-ANCHORED 2026-09-13 (PR #215): the default sweep starts at 760, not 1200. --for's rung zero now triggers on the -# EXACT ceiling (verbs_for.h), so a document 1..15% over its budget drops its three explanatory clauses before the -# allowance is consulted; on this corpus the late-label band (over_ceiling="1" INSIDE the allowance — the residual -# after that drop) therefore sits at 780..810 instead of inside 1200..1500, and the control below would otherwise be -# inert. Swept 700..3300 step 10 on the new binary: default hits at 780 790 800 810, none on the --detail=20 arm. -# RE-ANCHORED 2026-10-07 (knob-honesty-068, orchestrator ruling C3): a capped, CHARGED whose answer is past its ceiling -# paid or not is now served with the rows the cut alone leaves and its next= unpaid — bigger than the paid answer, so the -# capped documents that sat in the late-label band (dd6e4c8e: the --detail=20 arm's 2880..3080, est_tokens > N inside the -# allowance) now cross the allowance on the ladder's disclosed last rung, and no capped document on this corpus is left in -# the band (swept 700..1500 and 2880..3080 step 10). The band itself is untouched by the ruling: an UNCAPPED block owes no -# continuation, so the third spec — a name-exact task serving ONE row — keeps a document whose header floor lands in it -# (480..488 at the time of writing; step 4 so a band 9 budgets wide is always crossed). Fields: label:from:to:step:task:args. -for spec in "default:760:1500:10:serialize the map:" "detail_graph:2880:3080:10:serialize the map:--detail=20 --with-graph" \ - "named_one_row:400:600:4:serializeRow0_3:"; do - s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_rest="${s_rest#*:}" - s_step="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" - for (( N = s_from; N <= s_to; N += s_step )); do +a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_over=0; a7s_cut=0 +# RUNTIME (CI's 1200 s per-gate limit): the sweep keeps only the rungs where the served row set CHANGES — the first and the +# last budget of every distinct row count, plus the band edges — and judges all of them in ONE python pass after the runs +# (it was 131 rungs with three interpreter starts each, ~266 s of this gate's wall time). Measured on the 480bc90b binary at +# step 10 / 50 / 20: default 760..1500 serves 8..20 rows (13 counts over 75 rungs), --detail=20 --with-graph 300..2500 serves +# 4..32 (29 counts over 45 rungs), the named task 1 row everywhere. A rung dropped here served exactly the rows of a kept +# neighbour on both sides, so every ST fit (tightest at a count's FIRST budget) and every maximality (tightest at its LAST) +# is still asserted, and RP still compares each adjacent pair of distinct row sets. +A7S_RUNGS_default="760 770 780 840 850 900 910 960 970 1020 1030 1090 1100 1150 1160 1210 1220 1270 1280 1330 1340 1400 1410 1460 1470 1500" +A7S_RUNGS_detail_graph="300 500 550 600 650 700 750 800 850 900 950 1000 1050 1100 1150 1200 1250 1300 1350 1400 1450 1500 1550 1600 1650 1700 1750 1800 1850 1900 1950 2000 2050 2100 2150 2200 2250 2500" +A7S_RUNGS_named_one_row="400 600" +: >"$A7S/manifest" +for spec in "default:serialize the map:" "detail_graph:serialize the map:--detail=20 --with-graph" "named_one_row:serializeRow0_3:"; do + s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" + eval "s_rungs=\$A7S_RUNGS_$s_label" + # shellcheck disable=SC2086 + ( cd "$A7S" && rwcall corpus --for="$s_task" $s_args --no-cache ) >"$A7S/ref_$s_label.xml" 2>/dev/null # the spec's complete answer + for N in $s_rungs; do # shellcheck disable=SC2086 - ( cd "$A7S" && rwcall corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o.xml" 2>/dev/null - s_rc=$? - a7s_runs=$(( a7s_runs + 1 )) - s_b="$( bytes_of "$A7S/o.xml" )" - s_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - s_root="$( grep -aoE '^]*>' "$A7S/o.xml" | head -1 )" - # fix round 5 (lego-charge): the last rung now names one of two reasons, each only where it is true — the header floor, - # or (both trailing sections under the ceiling) the kept rows. Either is the disclosure; the kept-rows one must also be - # TRUE here: only on the both-flags shape, and with the header alone inside the allowance. - s_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b" unpaid twin above - # holds in full - elif [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml" \ - && sigs_unpaid_twin "$A7S/o.xml" "$s_a" >/dev/null; then - a7s_floor=$(( a7s_floor + 1 )) - elif [ "$s_rc" -ne 0 ] || { [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7S/o.xml"; }; then - a7s_badn=$(( a7s_badn + 1 )) - [ "$a7s_badn" -le 6 ] && a7s_bad="$a7s_bad $s_label@$N=${s_b}/${s_a}B(exit $s_rc)" - elif [ "$s_b" -le "$s_a" ] && [ "${s_root#* over_ceiling=\"1\"}" != "$s_root" ]; then - a7s_inside_labelled=$(( a7s_inside_labelled + 1 )) - fi + ( cd "$A7S" && rwcall corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o_${s_label}_$N.xml" 2>/dev/null + printf '%s %s %s %s %s\n' "$s_label" "$N" "$?" "$A7S/o_${s_label}_$N.xml" "$A7S/ref_$s_label.xml" >>"$A7S/manifest" done done -[ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10; one-row named task 400..600, step 4) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung, or the disclosed unpaid twin ($a7s_floor)" \ - || no "#11 A7 sweep: $a7s_badn of $a7s_runs budgets deliver past the allowance with no ladder rung fired (first:$a7s_bad) — a byte spliced in after the ladder priced the document" -# control: the sweep must cross the band the defect lives in — a root that says over_ceiling="1" while the document -# still fits the allowance (est_tokens > N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. -[ "$a7s_inside_labelled" -gt 0 ] \ - && ok "#11 A7 sweep control: $a7s_inside_labelled budget(s) carry a root over_ceiling=\"1\" INSIDE the allowance — the sweep crosses the late-label band" \ - || no "#11 A7 sweep control: no budget carried over_ceiling=\"1\" inside the allowance — the sweep no longer reaches the est_tokens > N band, re-anchor its ranges" -# TRAIN 26c RE-PIN (2026-10-10) of the three regime arms below — the floor twin, C3 and its near miss. lego-charge charges -# / to the budget at their served size and #362's rides the bundle, so on this corpus the -# merged binary's 4-row floor answer costs ~270 B more than 26b's: at "serialize the map" the cut never leaves 5 rows while -# both the unpaid and the paid answer land over (the paid 4-row answer fits from 920, the cut leaves 5 rows from 920 — no C3 -# window at any budget 700..1400, swept step 10 on the merged binary and on lego-charge's pre-continuation binary 1cfbfdcc). -# The task "map row serializer" on the same corpus has all three regimes: the floor at 890..910 (4 rows, the handle unpaid, -# no last-rung note; the pre-continuation binary fits with 4 rows), C3 at 920..939 (the cut leaves 5 rows — 1cfbfdcc serves -# 5, over, last rung fired — and the paid 4-row answer is over too), the payable near miss from 940 (the paid 4-row answer -# fits, no label). Pinned at 900 / 925 / 970. The sweep above keeps "serialize the map". Assertions unchanged. -# A7 UNPAID TWIN, EXERCISED EVERY RUN (knob-honesty-068; rulings 2026-10-07). The A7 arm and the sweep accept the twin only -# as an alternative, so a fixture that stopped reaching it would leave it untested. On this git-less corpus at -# --token-budget=780 the default bundle's sits at its rank 1..4 floor: the answer the cut alone leaves FITS (the -# pre-continuation binary, 255dc199, serves it at est_tokens=774 with no label), its next= cannot be paid in rows, and the -# document lands past the allowance only by the handle — the twin must hold, with no last-rung disclosure to lean on. +# one pass over every rung: exit 0 + root + est_tokens; NOLBL (no root over_ceiling=, no last-rung note); ST (soft_st's verdict); +# RP (each rung's rows a rank prefix of the next rung's, same spec); NX (shown < total carries next="--for=). Prints +# "runs badn cut over", then up to six "label@N:[why]" lines. +a7s_out="$( python3 - "$A7S/manifest" "$TMP" <<'PYS' +import re, sys +sys.path.insert( 0, sys.argv[ 2 ] ) +import soft_st +runs = badn = cut = over = 0; bad = []; prev = {} +for line in open( sys.argv[ 1 ] ): + label, n, rc, f, ref = line.split() + n = int( n ); runs += 1; why = [] + d = open( f, encoding='utf-8', errors='replace' ).read() + root = re.search( r'^]*>', d ); root = root.group( 0 ) if root else '' + est = re.search( r' est_tokens="(\d+)"', root ) + if rc != '0' or not root or not est: why.append( 'exit-%s/no-root-or-est' % rc ) + if ' over_ceiling=' in root or '[over_ceiling= is 1 on the root: ' in d: why.append( 'labelled' ) + good, msg = soft_st.verdict( f, n, ref ) + if not good: why.append( 'ST:[%s]' % msg ) + rows = soft_st.row_ids( f ) + if not rows or ( label in prev and rows[ :len( prev[ label ] ) ] != prev[ label ] ): why.append( 'not-monotone' ) + prev[ label ] = rows + sigs = re.search( r']*>', d ); sigs = sigs.group( 0 ) if sigs else '' + sh = re.search( r' shown="(\d+)"', sigs ); tot = re.search( r' total="(\d+)"', sigs ) + if sh and tot and int( sh.group( 1 ) ) < int( tot.group( 1 ) ): + cut += 1 + if ' next="--for=' not in sigs: why.append( 'cut-without-next' ) + if est and int( est.group( 1 ) ) > n: over += 1 + if why: + badn += 1 + if len( bad ) < 6: bad.append( '%s@%d:[%s]' % ( label, n, ' '.join( why ) ) ) +print( runs, badn, cut, over ) +for b in bad: print( b ) +PYS +)" +read -r a7s_runs a7s_badn a7s_cut a7s_over <<<"$( printf '%s\n' "$a7s_out" | head -1 )" +a7s_bad="$( printf '%s\n' "$a7s_out" | tail -n +2 | tr '\n' ' ' )" +{ [ -n "$a7s_runs" ] && [ "$a7s_runs" -eq 66 ] && [ "$a7s_badn" -eq 0 ]; } 2>/dev/null \ + && ok "#11 A7 sweep (forbudget-soft): $a7s_runs budgets over a git-less corpus (the first and last budget of every row count: default 760..1500, --detail=20 --with-graph 300..2500, one-row named task 400/600) — every answer exit 0, unlabelled, its rows fit T or the floor and a cut is maximal, rows monotone in N, every cut carries next=" \ + || no "#11 A7 sweep (forbudget-soft): ${a7s_badn:-?} of ${a7s_runs:-?} budgets break the soft target, or the sweep did not judge its 66 rungs (first: $a7s_bad)" +# control (REPLACED, forbudget-soft (rule 4); was ">= 1 budget labelled over_ceiling INSIDE the allowance", a band the soft +# target deleted): the sweep must actually exercise both halves of the contract — rows CUT at the target (else ST and NX ran +# on complete answers only) and an answer whose est_tokens exceeds its N (the soft overshoot, which NOLBL then pins unlabelled). +{ [ "$a7s_cut" -gt 0 ] && [ "$a7s_over" -gt 0 ]; } \ + && ok "#11 A7 sweep control: $a7s_cut budget(s) cut the rows at the target and $a7s_over priced past their N — both halves exercised" \ + || no "#11 A7 sweep control: cut=$a7s_cut over-N=$a7s_over — the sweep no longer reaches a cut or a soft overshoot, re-anchor its ranges" +# A7 FLOOR / NEXT @900 — REPLACED (forbudget-soft (rule 4)). Was the UNPAID TWIN (knob-honesty-068): at the rank 1..4 +# floor the next= rode unpaid, the root said over_ceiling="1" and the legend carried the unpaid clause. The paid/unpaid +# continuation is gone — one render, nothing paid from rows. The same call ("map row serializer" at 900 on this corpus) now +# pins: exit 0; NOLBL and no unpaid clause; ST against the task's no-budget answer (d099ec21: 10 rows, base d4325a8c: the +# 4-row floor, not maximal); and NX — the cut carries next=, and that next= RUNS (from the same root) and serves the +# whole head: every one of total= rows, the cut's rows its rank prefix. ( cd "$A7S" && rwcall corpus --for="map row serializer" --token-budget=900 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? -fl_a="$( awk 'BEGIN{printf "%d", 900*2.36*1.15}' )" -if [ "$fl_rc" -eq 0 ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/floor.xml" \ - && fl_why="$( sigs_unpaid_twin "$A7S/floor.xml" "$fl_a" )"; then - ok "#11 A7 unpaid twin @900: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" +( cd "$A7S" && rwcall corpus --for="map row serializer" --no-cache ) >"$A7S/floor_ref.xml" 2>/dev/null +fl_root="$( ctx_root "$A7S/floor.xml" )"; fl_sigs="$( grep -aoE ']*>' "$A7S/floor.xml" | head -1 )" +fl_tot="$( printf '%s' "$fl_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" +fl_next="$( printf '%s' "$fl_sigs" | sed -nE 's/.* next="(--for=[^"]*)".*/\1/p' | sed "s/'/'/g; s/"/\"/g; s/&/\\&/g" )" +fl_why="" +{ [ "$fl_rc" -eq 0 ] && [ -n "$fl_root" ]; } || fl_why="$fl_why exit-$fl_rc-or-no-root" +[ "${fl_root#* over_ceiling=}" = "$fl_root" ] || fl_why="$fl_why over_ceiling-on-a-soft-target" +! grep -aqF 'next= rides unpaid' "$A7S/floor.xml" || fl_why="$fl_why unpaid-clause" +fl_st="$( soft_st "$A7S/floor.xml" 900 "$A7S/floor_ref.xml" )" || fl_why="$fl_why ST:[$fl_st]" +if [ -z "$fl_next" ] || [ -z "$fl_tot" ]; then + fl_why="$fl_why -cut-without-next=-or-total=" else - no "#11 A7 unpaid twin @900 (exit $fl_rc): ${fl_why:-the ladder last rung fired, so the twin is not the only disclosure} — the floor did not ship its next= with a labelled, honestly priced overshoot" + eval "set -- $fl_next" + ( cd "$A7S" && rwcall corpus "$@" --no-cache ) >"$A7S/floor_next.xml" 2>/dev/null + soft_rows "$A7S/floor.xml" >"$A7S/floor.rows" 2>/dev/null; soft_rows "$A7S/floor_next.xml" >"$A7S/floor_next.rows" 2>/dev/null + { [ "$( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )" = "$fl_tot" ] && rows_prefix "$A7S/floor.rows" "$A7S/floor_next.rows"; } \ + || fl_why="$fl_why next=-does-not-serve-the-head($( wc -l < "$A7S/floor_next.rows" | tr -d ' ' )-of-$fl_tot)" fi -# C3 (orchestrator ruling 2026-10-07): NEVER DROP ROWS TO PAY FOR next= WHEN THE ANSWER IS OVER ITS CEILING ANYWAY. At 790 the -# answer the cut alone leaves is past its ceiling already (255dc199 serves 5 rows there at est_tokens=952, last rung fired); -# paying from rows (dd6e4c8e: 4 rows, est_tokens=885) still lands over, so it bought nothing. The answer must keep the cut's -# own rows — at least 255dc199's 5 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it) — -# and MORE than the paid near miss at 850 serves (4 rows: there paying is what makes the answer fit), with next= riding -# unpaid and over_ceiling="1" + the unpaid clause on the root. RED on dd6e4c8e (4 rows). -# TRAIN 27 RE-PIN (2026-10-11) of C3 and its near miss (the floor twin above is unchanged). Under a spent ceiling the compact -# header no longer carries clauses for sections served as stubs nor a false task_echo / "(total= shown= capped=)" note, and the -# compact dialect pays a measured overrun from the rows (verbs_for.h runForLens), so on this corpus the paid 4-row answer FITS -# from 910 and no budget 700..1500 serves the C3 shape on any task tried (C3 then correctly serves the paid answer: 925 = 4 rows, -# est_tokens=907, no label). The regime lives on a twin of the same corpus WITHOUT the doc comments (smaller rows, so the cut -# alone leaves 5 rows while the 4-row floor is still over): C3 at 740..750 (494f97b6: 740..770, 5 rows, last rung + unpaid -# clause), the payable near miss from 760 (494f97b6: from 780). Pinned at 745 / 790, inside both binaries' windows. RED on a -# mutant whose C3 fallback pays from the rows (4 rows, over). Assertions unchanged. +[ -z "$fl_why" ] \ + && ok "#11 A7 floor/next @900 (forbudget-soft): unlabelled, no unpaid clause, $fl_st; its next= serves all $fl_tot rows, the cut's its prefix" \ + || no "#11 A7 floor/next @900 (forbudget-soft) —$fl_why" +# C3 @745 / NEAR MISS @790 — REPLACED (forbudget-soft (rule 4)). Was orchestrator ruling C3 (never drop rows to pay for next= +# on an answer over its ceiling either way: 745 keeps >= 5 rows, more than the paid 790 answer, labelled) and its payable near +# miss (790: the handle paid from rows, inside the allowance, unlabelled). Under a soft target next= is never paid from rows +# at all, which is what made the 745 answer serve MORE rows than the 790 one; that inversion is now impossible by contract. +# The same doc-less corpus and the same two budgets pin RP: rows(745) is a rank prefix of rows(790), both a rank prefix of +# the task's no-budget answer; both unlabelled with no unpaid clause; both ST-maximal (the handle bought with no row). +# MEASURED: d099ec21 serves 11 / 12 rows; base d4325a8c 5 / 4 (745 labelled) — not monotone. mkdir -p "$A7S/c3/corpus" python3 - "$A7S/c3/corpus" <<'PYG' import os, sys @@ -950,40 +1042,36 @@ for i in range( 4 ): PYG ( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --token-budget=745 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? ( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --token-budget=790 --no-cache ) >"$A7S/paid.xml" 2>/dev/null; pd_rc=$? -sigs_shown(){ grep -aoE ']*>' "$1" | tail -1 | grep -oE ' shown="[0-9]+"' | tr -dc '0-9'; } -c3_sh="$( sigs_shown "$A7S/c3.xml" )"; pd_sh="$( sigs_shown "$A7S/paid.xml" )" -c3_root="$( grep -aoE '^]*>' "$A7S/c3.xml" | head -1 )" -if [ "$c3_rc" -eq 0 ] && [ -n "$c3_sh" ] && [ -n "$pd_sh" ] && [ "$c3_sh" -ge 5 ] && [ "$c3_sh" -gt "$pd_sh" ] \ - && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/c3.xml" && [ "${c3_root#* over_ceiling=\"1\"}" != "$c3_root" ] \ - && grep -aqF 'next= rides unpaid: no row is dropped to pay for it' "$A7S/c3.xml"; then - ok "#11 A7 C3 @745 (doc-less twin): over its ceiling either way, the answer keeps the cut's own $c3_sh rows (>= 5, > the paid 790 answer's $pd_sh) — next= unpaid, over_ceiling=\"1\" + the unpaid clause" -else - no "#11 A7 C3 @745 (doc-less twin, exit $c3_rc): shown=${c3_sh:-?} (want >= 5 and > the paid 790 answer's ${pd_sh:-?}), next= + over_ceiling=\"1\" + the unpaid clause — rows were dropped to pay for a handle that cannot make the answer fit" -fi -# Its near miss (790 since the train-27 re-pin above) — the PAYABLE case, unchanged: paying is what makes the answer fit (255dc199: 5 rows, est_tokens=911, -# over), so the handle is paid (the block sheds its bytes above the floor) — next= rides inside the allowance, and neither -# over_ceiling="1" nor the unpaid clause appears (a label that rode on every capped answer would pass the arms above alone). -pd_b="$( bytes_of "$A7S/paid.xml" )"; pd_a="$( awk 'BEGIN{printf "%d", 790*2.36*1.15}' )" -pd_root="$( grep -aoE '^]*>' "$A7S/paid.xml" | head -1 )" -if [ "$pd_rc" -eq 0 ] && [ "$pd_b" -le "$pd_a" ] && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/paid.xml" \ - && [ "${pd_root#* over_ceiling=\"1\"}" = "$pd_root" ] && ! grep -aqF 'next= rides unpaid' "$A7S/paid.xml"; then - ok "#11 A7 unpaid twin near miss @790 (doc-less twin): the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no unpaid clause" -else - no "#11 A7 unpaid twin near miss @790 (doc-less twin, exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" -fi +( cd "$A7S/c3" && rwcall corpus --for="map row serializer" --no-cache ) >"$A7S/c3_ref.xml" 2>/dev/null +soft_rows "$A7S/c3.xml" >"$A7S/c3.rows" 2>/dev/null; soft_rows "$A7S/paid.xml" >"$A7S/paid.rows" 2>/dev/null; soft_rows "$A7S/c3_ref.xml" >"$A7S/c3_ref.rows" 2>/dev/null +c3_n="$( wc -l < "$A7S/c3.rows" | tr -d ' ' )"; pd_n="$( wc -l < "$A7S/paid.rows" | tr -d ' ' )" +c3_why="" +for c3f in c3:745:$c3_rc paid:790:$pd_rc; do + c3_lbl="${c3f%%:*}"; c3_rest="${c3f#*:}"; c3_bud="${c3_rest%%:*}"; c3_frc="${c3_rest#*:}" + c3_root="$( ctx_root "$A7S/$c3_lbl.xml" )" + { [ "$c3_frc" -eq 0 ] && [ -n "$c3_root" ]; } || c3_why="$c3_why @$c3_bud:exit-$c3_frc-or-no-root" + [ "${c3_root#* over_ceiling=}" = "$c3_root" ] || c3_why="$c3_why @$c3_bud:over_ceiling" + ! grep -aqF 'next= rides unpaid' "$A7S/$c3_lbl.xml" || c3_why="$c3_why @$c3_bud:unpaid-clause" + c3_st="$( soft_st "$A7S/$c3_lbl.xml" "$c3_bud" "$A7S/c3_ref.xml" )" || c3_why="$c3_why @$c3_bud:ST:[$c3_st]" +done +rows_prefix "$A7S/c3.rows" "$A7S/paid.rows" || c3_why="$c3_why rows(745)=$c3_n-not-a-prefix-of-rows(790)=$pd_n" +rows_prefix "$A7S/paid.rows" "$A7S/c3_ref.rows" || c3_why="$c3_why rows(790)-not-a-prefix-of-the-no-budget-answer" +[ -z "$c3_why" ] \ + && ok "#11 A7 C3/near miss (forbudget-soft, doc-less twin): rows(745)=$c3_n is a rank prefix of rows(790)=$pd_n, itself a prefix of the no-budget answer; both unlabelled, no unpaid clause, both ST-maximal" \ + || no "#11 A7 C3/near miss (forbudget-soft, doc-less twin) —$c3_why" } est_11_a7r(){ -# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE. Under --token-budget, --with-graph's block and the -# first --detail body were charged only AFTER had claimed the whole sig-side ceiling, so both rode past the -# budget, and the ladder's last rung then blamed the HEADER floor ("no payload left to trim") while was the -# trimmable payload. MEASURED on the parent binary over this arm's corpus: --detail=20 --with-graph at 2000 tokens = -# 6054 B vs a 5428 B allowance; --detail=3 served its first body as a one-line over_ceiling stub at every budget. -# runForLens now reserves the graph bytes and a MEASURED first-body floor out of the sig claim -# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor). The corpus is git-less, run from a relative path, every -# body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path runs), and its -# 40 signature rows overflow the default sig share (so is capped="1" in every regime below — the negatives -# compare a CUT section, not a whole one). +# ── #11 A7R (lego-charge fix round 4): THE TRAILING-SECTION RESERVE — and its forbudget-soft (rule 4) REPLACEMENT. Under the +# old hard ceiling --with-graph's block and the first --detail body were charged only AFTER had claimed the whole +# sig-side ceiling, so lego-charge reserved the graph bytes and a measured first-body floor OUT of the sig claim +# (forSigClaimWithTrailingReserve / forDetailFirstBodyFloor) and these arms pinned "inside N x 2.36 x 1.15, unlabelled". The +# soft target deletes both the byte promise and the reserve: T is charged against the rows only, and a trailing +# section takes what the rows left of T. So on this corpus the arms now pin SEC — a trailing section NEVER removes a +# row: with --with-graph / --detail the rows are the bare run's rows at the same N — plus ST, NOLBL, and the first body's +# floor (never the one-line stub; a head-cut carries the next= that recovers it). The corpus is git-less, run from a +# relative path, every body is longer than a quarter of the smallest budget's bundle (so the floor's head-cut + next= path +# runs), and its 72 rows overflow the default byte target (so is capped="1", rows cut or shrunk, in every regime below). A7R="$TMP/a7reserve" mkdir -p "$A7R/corpus" python3 - "$A7R/corpus" <<'PYG' @@ -1003,26 +1091,34 @@ PYG A7R_Q="serialize the map row" a7r_out(){ ( cd "$A7R" && rwcall corpus --for="$A7R_Q" "$@" --no-cache ) 2>/dev/null; } a7r_sigs(){ python3 -c 'import re,sys; m=re.search(r"",open(sys.argv[1]).read(),re.S); print(m.group(0) if m else "")' "$1"; } -# A7R-1 (positive): every budget, three shapes — exit 0, inside the allowance, NO over_ceiling on the root (est_tokens fits -# budget_tokens), no ladder last rung, present, and with --detail a first that is not the one-line over_ceiling -# stub; a first body the floor cut carries the next= that serves the rest (rule 5: every cut recoverable). +a7r_out >"$A7R/n0.xml" # the no-budget answer: the ST reference below, and the A7R no-ceiling / A7D default arms +# A7R-1 — REPLACED (forbudget-soft (rule 4)). Was: --with-graph / --detail=3 at 1500..3000 — exit 0, inside N x 2.36 x 1.15, no +# over_ceiling, no last rung, first body served (the reserve made room for it). Now: exit 0; NOLBL and no last-rung note; SEC +# (rows identical to the bare run at the same N — d099ec21: 12/16/21/26 rows each; base d4325a8c: --with-graph 6/11/16/20 and +# --detail=3 4/6/10/15 against the bare 8/13/18/22, the reserve at work); ST against the no-budget answer; and with --detail a +# first that is not the one-line over_ceiling stub, a head-cut one carrying next= (rule 5: every cut recoverable). a7r_bad=""; a7r_runs=0 -for spec in "graph:--with-graph" "detail:--detail=3"; do - r_label="${spec%%:*}"; r_args="${spec#*:}" - for N in 1500 2000 2500 3000; do +for N in 1500 2000 2500 3000; do + a7r_out --token-budget=$N >"$A7R/o0.xml" + soft_rows "$A7R/o0.xml" >"$A7R/o0.rows" 2>/dev/null + for spec in "graph:--with-graph" "detail:--detail=3"; do + r_label="${spec%%:*}"; r_args="${spec#*:}" # shellcheck disable=SC2086 a7r_out --token-budget=$N $r_args >"$A7R/o.xml" r_rc=$? a7r_runs=$(( a7r_runs + 1 )) - r_b="$( bytes_of "$A7R/o.xml" )" - r_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - r_root="$( grep -aoE '^]*>' "$A7R/o.xml" | head -1 )" + r_root="$( ctx_root "$A7R/o.xml" )" + soft_rows "$A7R/o.xml" >"$A7R/o.rows" 2>/dev/null r_why="" [ "$r_rc" -eq 0 ] || r_why="exit $r_rc" [ -n "$r_root" ] && grep -aq '/dev/null || r_why="$r_why ${r_b:-?}B>${r_a}B" - [ "${r_root#* over_ceiling=\"1\"}" = "$r_root" ] || r_why="$r_why over_ceiling" - ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" # either reason (fix round 5) + # round C (rule 4): SEC is the by-design break (a trailing section is reserved before the rows past the cut): RP instead + soft_rows "$A7R/n0.xml" >"$A7R/n0.rows" 2>/dev/null + [ -s "$A7R/o.rows" ] && rows_prefix "$A7R/o.rows" "$A7R/n0.rows" \ + || r_why="$r_why rows:$( wc -l < "$A7R/o.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + r_st="$( soft_st "$A7R/o.xml" "$N" "$A7R/n0.xml" )" || r_why="$r_why ST:[$r_st]" + [ "${r_root#* over_ceiling=}" = "$r_root" ] || r_why="$r_why over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/o.xml" || r_why="$r_why last-rung" if [ "${r_args#*--detail}" != "$r_args" ]; then r_b1="$( grep -aoE ']*>' "$A7R/o.xml" | head -1 )" [ -n "$r_b1" ] || r_why="$r_why no-first-body" @@ -1033,48 +1129,44 @@ for spec in "graph:--with-graph" "detail:--detail=3"; do done done [ "$a7r_runs" -eq 8 ] && [ -z "$a7r_bad" ] \ - && ok "#11 A7R reserve: 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) — inside N x 2.36 x 1.15, no over_ceiling, no last rung, first body served (cut ones carry next=)" \ - || no "#11 A7R reserve: spent the bytes of the graph block / first --detail body —$a7r_bad" -# A7R-K (the PRE-REGISTERED KILL, reports/lego-charge.md fix round 4): with BOTH --detail and --with-graph the reserve is -# off — reserving both cut a gold-relevant owner row out of on the measured question set. So that shape's ranked -# rows keep their claim: never FEWER rows than the same budget's --with-graph run (which reserves the graph alone), -# a delivery past the allowance is disclosed (root over_ceiling="1"), and the first body still gets its floor — not the -# one-line over_ceiling stub, and a head-cut carries next=. + && ok "#11 A7R (forbudget-soft, round C): 8 budgeted runs (--with-graph / --detail=3 at 1500..3000) serve a rank prefix of the no-budget rows, ST against T less the sections, unlabelled, first body served (cut ones carry next=)" \ + || no "#11 A7R no reserve (forbudget-soft): a trailing section changed the rows or the soft target broke —$a7r_bad" +# A7R-K — REPLACED in part (forbudget-soft (rule 4)). Was the pre-registered kill (reports/lego-charge.md fix round 4): with BOTH +# --detail and --with-graph the reserve was off, so the rows were never FEWER than the --with-graph run's; a delivery past +# the allowance was labelled over_ceiling="1"; the first body got its floor. The rows clause tightens to SEC (both flags serve +# exactly the bare rows); the allowance-or-label clause becomes ST + NOLBL; the first-body floor clause is unchanged. a7k_bad="" for N in 2000 2500 3000; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k.xml"; k_rc=$? - a7r_out --token-budget=$N --with-graph >"$A7R/kg.xml" - k_rows="$( grep -aoE ']*>' "$A7R/k.xml" | wc -l | tr -d ' ' )"; g_rows="$( grep -aoE ']*>' "$A7R/kg.xml" | wc -l | tr -d ' ' )" - k_b="$( bytes_of "$A7R/k.xml" )"; k_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k_root="$( grep -aoE '^]*>' "$A7R/k.xml" | head -1 )" + a7r_out --token-budget=$N >"$A7R/kb.xml" + soft_rows "$A7R/k.xml" >"$A7R/k.rows" 2>/dev/null; soft_rows "$A7R/kb.xml" >"$A7R/kb.rows" 2>/dev/null + k_root="$( ctx_root "$A7R/k.xml" )" k_b1="$( grep -aoE ']*>' "$A7R/k.xml" | head -1 )" k_why="" [ "$k_rc" -eq 0 ] && [ -n "$k_root" ] || k_why="exit $k_rc / no root" - [ "${k_rows:-0}" -ge 1 ] && [ "${k_rows:-0}" -ge "${g_rows:-0}" ] 2>/dev/null || k_why="$k_why rows ${k_rows:-?}/dev/null || [ "${k_root#* over_ceiling=\"1\"}" != "$k_root" ] || k_why="$k_why ${k_b:-?}B>${k_a}B-unlabelled" + soft_rows "$A7R/n0.xml" >"$A7R/n0.rows" 2>/dev/null # round C (rule 4): RP instead of SEC, as A7R-1 + [ -s "$A7R/k.rows" ] && rows_prefix "$A7R/k.rows" "$A7R/n0.rows" || k_why="$k_why rows $( wc -l < "$A7R/k.rows" | tr -d ' ' )-not-a-rank-prefix-of-the-no-budget-answer" + k_st="$( soft_st "$A7R/k.xml" "$N" "$A7R/n0.xml" )" || k_why="$k_why ST:[$k_st]" + [ "${k_root#* over_ceiling=}" = "$k_root" ] || k_why="$k_why over_ceiling" [ -n "$k_b1" ] && [ "${k_b1#* over_ceiling=}" = "$k_b1" ] || k_why="$k_why first-body-stub" { [ "${k_b1#* truncated=\"1\"}" = "$k_b1" ] || [ "${k_b1#* next=\"--expand=}" != "$k_b1" ]; } || k_why="$k_why cut-without-next" [ -z "$k_why" ] || a7k_bad="$a7k_bad @$N:[${k_why# }]" done [ -z "$a7k_bad" ] \ - && ok "#11 A7R kill: --detail=20 --with-graph at 2000..3000 keeps at least the --with-graph run's rows, labels any overshoot over_ceiling, serves the first body at its floor" \ - || no "#11 A7R kill: the both-sections shape —$a7k_bad" -# A7R-K2 (fix round 5, owner ruling option B — the A7B contract on a corpus whose first body is LONGER than the floor): -# at 2000 and 2500 tokens --detail=20 --with-graph delivers past the allowance with over_ceiling="1", the last rung names -# the kept rows (never the header floor: the header alone fits), and the head-cut first body's next= RECOVERS EXACTLY the -# lines it cut — the cut CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). + && ok "#11 A7R both sections (forbudget-soft, round C): --detail=20 --with-graph at 2000..3000 serves a rank prefix of the no-budget rows, ST, unlabelled, first body at its floor" \ + || no "#11 A7R both sections (forbudget-soft): —$a7k_bad" +# A7R-K2 — REPLACED in part (forbudget-soft (rule 4)). Was the A7B contract on this corpus: past the allowance with +# over_ceiling="1" and the kept-rows reason (both gone with the ceiling), well-formed despite the note, and the head-cut first +# body's next= RECOVERING EXACTLY the lines it cut. The recovery half is the soft target's NX invariant and is kept unchanged: +# at 2000 and 2500 tokens --detail=20 --with-graph serves its first body head-cut (T minus the rows funds it), and the cut +# CDATA plus the next= answer's CDATA is the function's source, line for line (rule 5). Plus NOLBL and xmllint. a7k2_bad="" for N in 2000 2500; do a7r_out --token-budget=$N --detail=20 --with-graph >"$A7R/k2.xml"; k2_rc=$? - k2_b="$( bytes_of "$A7R/k2.xml" )"; k2_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" - k2_root="$( grep -aoE '^]*>' "$A7R/k2.xml" | head -1 )" - k2_hdr="$( python3 -c 'import sys; d=open(sys.argv[1],"rb").read(); print(d.find(b"/dev/null \ - || k2_why="$k2_why premise/label:${k2_b:-?}B-vs-${k2_a}B" - { grep -aqF "$KEPT_NOTE" "$A7R/k2.xml" && ! grep -aqF "$FLOOR_NOTE" "$A7R/k2.xml" && [ "${k2_hdr:--1}" -gt 0 ] && [ "$k2_hdr" -le "$k2_a" ]; } 2>/dev/null \ - || k2_why="$k2_why reason" - # fix round 6 (checklist 25): the kept-rows note rides in the header XML comment here too — the answer must parse + { [ "$k2_rc" -eq 0 ] && [ -n "$k2_root" ] && [ "${k2_root#* over_ceiling=}" = "$k2_root" ]; } || k2_why="$k2_why exit-$k2_rc/no-root/over_ceiling" + ! grep -aqF '[over_ceiling= is 1 on the root: ' "$A7R/k2.xml" || k2_why="$k2_why last-rung-note" if command -v xmllint >/dev/null 2>&1; then xmllint --noout "$A7R/k2.xml" 2>/dev/null || k2_why="$k2_why xmllint-rejected" fi @@ -1102,8 +1194,8 @@ PYR [ -z "$k2_why" ] || a7k2_bad="$a7k2_bad @$N:[${k2_why# }]" done [ -z "$a7k2_bad" ] \ - && ok "#11 A7R kill+next (by-design, owner option B): --detail=20 --with-graph at 2000/2500 is over_ceiling with the kept-rows reason, and the cut first body's next= returns exactly the lines it cut" \ - || no "#11 A7R kill+next (by-design, owner option B): —$a7k2_bad" + && ok "#11 A7R cut body next= (forbudget-soft): --detail=20 --with-graph at 2000/2500 is unlabelled and well-formed, and the cut first body's next= returns exactly the lines it cut" \ + || no "#11 A7R cut body next= (forbudget-soft): —$a7k2_bad" # A7R-2 (the floor's cut is the one the floor priced): at 1500 tokens the first body is longer than the floor, so it ships # head-cut — truncated="1" with lines= and next= — never dropped and never the bare one-line stub. a7r_out --token-budget=1500 --detail=3 >"$A7R/f.xml" @@ -1115,7 +1207,7 @@ r_hi="$( printf '%s' "$r_b1" | sed -nE 's/.* lines="1-([0-9]+)\/[0-9]+".*/\1/p' || no "#11 A7R floor: the first --detail body at 1500 tokens is not the floor's head-cut of >= 2 lines (got '${r_b1:-none}')" # A7R-N1 (near-miss negative, NO explicit ceiling): the reserve must not exist — is byte-identical with and # without --with-graph (the default regime's graph rides on top, as before). -a7r_out >"$A7R/n0.xml"; a7r_out --with-graph >"$A7R/n1.xml" +a7r_out --with-graph >"$A7R/n1.xml" n0="$( a7r_sigs "$A7R/n0.xml" )"; n1="$( a7r_sigs "$A7R/n1.xml" )" { [ -n "$n0" ] && [ "$n0" = "$n1" ] && [ "${n0#*capped=\"1\"}" != "$n0" ]; } \ && ok "#11 A7R no ceiling: (capped=1) byte-identical with and without --with-graph — nothing reserved without --token-budget" \ @@ -1132,43 +1224,59 @@ w0="$( a7r_sigs "$A7R/w0.xml" )"; w1="$( a7r_sigs "$A7R/w1.xml" )" est_11_a7d(){ # ── #11 A7D (fix round 4, review rv-wrap-recipe-k29 F1; rule 5 — never a silent cut): under --token-budget the compact -# section is no longer DROPPED with no element when the ceiling is spent. The arm corpus is on the conceptual route. -# A7D-1: at 2000 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with -# no budget — its legend clause rides the header, the answer stays inside its budget (the disclosure's bytes are -# reserved), and the next= RECOVERS the rows: that call's serves at least one row of the same total. -a7r_out --token-budget=2000 >"$A7R/h.xml" +# section is never DROPPED with no element when the target is spent. The arm corpus is on the conceptual route. +# forbudget-soft (rule 4): a trailing section now takes T minus the rows already served, and the DEFAULT compact serves +# all its rows (decision D-3: no 1,000 B cap). So the spent-target marker moved from 2000 to 1500 tokens on this corpus (the +# rows now fill more of T: d099ec21 serves 3 of 6 hop rows at 2000, 0 at 1500), the partial cut moved from 5000 to 2000 (5000 +# is wide enough for the whole set), and the no-budget negative inverts: the default is WHOLE (base d4325a8c: 5 of 6, +# capped). Re-anchored with the invariant unchanged where it still holds: every cut is counted and carries next=. +# A7D-1: at 1500 tokens the section is the counted marker — shown="0" total=N capped="1" and next= the same question with no +# budget — its legend clause rides the header, no over_ceiling=, and the next= RECOVERS the rows: that call (the default +# answer) serves every one of the same total= hop rows. +# round C (reserve first; rule 4): the hops are funded from the rows at the cut, so 1500 now serves the whole set — re-anchored +# to 600 (the marker) and 800 (the partial cut), measured on the round C build: 600 shown=0, 800 shown=3 of 6, 900+ whole +a7r_out --token-budget=600 >"$A7R/h.xml" h_tag="$( grep -aoE ']*>' "$A7R/h.xml" | head -1 )" -h_root="$( grep -aoE '^]*>' "$A7R/h.xml" | head -1 )" +h_root="$( ctx_root "$A7R/h.xml" )" h_tot="$( printf '%s' "$h_tag" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" -a7r_out >"$A7R/hd.xml" -hd_tag="$( grep -aoE ']*>' "$A7R/hd.xml" | head -1 )" +hd_tag="$( grep -aoE ']*>' "$A7R/n0.xml" | head -1 )" hd_shown="$( printf '%s' "$hd_tag" | sed -nE 's/" ] && [ "${h_tot:-0}" -ge 1 ] 2>/dev/null \ && grep -aqF '; hops: this budget left out the call-hop rows (one hop of callees for each of total= top rows)' "$A7R/h.xml" && [ "${h_root#* over_ceiling=}" = "$h_root" ] \ - && [ "${hd_shown:-0}" -ge 1 ] 2>/dev/null && [ "$hd_tot" = "$h_tot" ]; } \ - && ok "#11 A7D hops: the spent-ceiling is the counted marker ($h_tag), defined, inside budget; its next= serves $hd_shown of $hd_tot rows" \ - || no "#11 A7D hops: spent-ceiling not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" + && [ -n "$h_root" ] && [ -n "$hd_shown" ] && [ "$hd_shown" = "$h_tot" ] && [ "$hd_tot" = "$h_tot" ]; } \ + && ok "#11 A7D hops: the spent-target at 600 is the counted marker ($h_tag), defined, unlabelled; its next= serves all $hd_shown of $hd_tot rows" \ + || no "#11 A7D hops: spent-target at 600 not the counted, continued marker (got '${h_tag:-none}', next= answer '${hd_tag:-none}', root '${h_root:0:80}…')" # A7D-1 (train 27): the compact dialect above defines the marker in its own short clause — it already reads total= shown= # capped=1 and next= in its un-droppable clauses (verbs_for.h kForHopsBudgetCutLegendCompact). The FULL dialect keeps the -# long clause that defines both: the same marker, the same next=, inside the same budget, with shown=0 capped=1 and next= read. -a7r_out --token-budget=2000 --legend=full >"$A7R/hf.xml" -hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( grep -aoE '^]*>' "$A7R/hf.xml" | head -1 )" -{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] \ +# long clause that defines both: the same marker, the same next=, with shown=0 capped=1 and next= read. +a7r_out --token-budget=600 --legend=full >"$A7R/hf.xml" +hf_tag="$( grep -aoE ']*>' "$A7R/hf.xml" | head -1 )"; hf_root="$( ctx_root "$A7R/hf.xml" )" +{ [ "$hf_tag" = "$h_tag" ] && [ -n "$hf_tag" ] && [ -n "$hf_root" ] \ && grep -aqF '; hops shown=0 capped=1: this budget left out the call-hop rows (one hop of callees for each of total= top rows); next= serves them' "$A7R/hf.xml" \ && [ "${hf_root#* over_ceiling=}" = "$hf_root" ] && ! grep -aqF '; hops: this budget' "$A7R/hf.xml"; } \ - && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), inside budget" \ - || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause or the budget fit is missing" -# A7D-2: a PARTIAL hops cut under the budget carries the same next= (5000 tokens: some rows fit, not all). -a7r_out --token-budget=5000 >"$A7R/hp.xml" + && ok "#11 A7D hops (--legend=full twin): the same marker ($hf_tag), defined by the full clause (shown=0 capped=1, next=), unlabelled" \ + || no "#11 A7D hops (--legend=full twin): got '${hf_tag:-none}' vs compact '${h_tag:-none}', root '${hf_root:0:80}…' — the full clause is missing or the root is labelled" +# A7D-2: a PARTIAL hops cut under the target carries the same next= (800 tokens since round C: some rows fit, not all). +a7r_out --token-budget=800 >"$A7R/hp.xml" hp_tag="$( grep -aoE ']*>' "$A7R/hp.xml" | head -1 )" -{ [ "${hp_tag#*shown=\"0\"}" = "$hp_tag" ] && [ "${hp_tag#* capped=\"1\"}" != "$hp_tag" ] && [ "${hp_tag#* next=\"--for='$A7R_Q'\"}" != "$hp_tag" ]; } \ - && ok "#11 A7D partial hops cut at 5000 tokens carries next= ($hp_tag)" \ - || no "#11 A7D partial hops cut at 5000 tokens: no next= on a cut with rows (got '${hp_tag:-none}')" -# A7D-N (near-miss negative): WITHOUT --token-budget the default regime is unchanged — its cut (capped="1") gets no -# next= from this change (a named deferral: adding it there moves every default --for answer). -{ [ "${hd_tag#* capped=\"1\"}" != "$hd_tag" ] && [ "${hd_tag#* next=}" = "$hd_tag" ]; } \ - && ok "#11 A7D no budget: the default regime's cut is byte-unchanged (capped=1, no next= added): $hd_tag" \ - || no "#11 A7D no budget: the default changed or is not cut (got '${hd_tag:-none}') — the disclosure leaked into the default regime" +hp_sh="$( printf '%s' "$hp_tag" | sed -nE 's//dev/null \ + && ok "#11 A7D partial hops cut at 800 tokens carries next= ($hp_tag)" \ + || no "#11 A7D partial hops cut at 800 tokens: not a partial cut with next= (got '${hp_tag:-none}')" +# A7D-W (forbudget-soft, replaces the old 5000-token partial cut): a target wide enough for the rows AND the hop rows serves the +# whole hop set, like the default answer — stop when complete, nothing left to continue (no capped=, so no next= owed). +a7r_out --token-budget=5000 >"$A7R/hw.xml" +hw_tag="$( grep -aoE ']*>' "$A7R/hw.xml" | head -1 )" +{ [ -n "$hw_tag" ] && [ -n "$hd_tot" ] && [ "$hw_tag" = "" ]; } \ + && ok "#11 A7D wide target (forbudget-soft): at 5000 tokens serves the whole set ($hw_tag)" \ + || no "#11 A7D wide target (forbudget-soft): at 5000 tokens is not the whole set of $hd_tot (got '${hw_tag:-none}')" +# A7D-N — REPLACED (forbudget-soft (rule 4), decision D-3). Was: WITHOUT --token-budget the default regime's cut +# (capped="1") gets no next= (a named deferral). The default compact route now serves ALL its hop rows (no 1,000 B cap), so +# the no-budget is whole: shown == total, capped="0" — nothing cut, nothing to continue. +{ [ -n "$hd_tag" ] && [ -n "$hd_tot" ] && [ "$hd_tag" = "" ] && [ "$hd_tot" -ge 1 ]; } 2>/dev/null \ + && ok "#11 A7D no budget (forbudget-soft): the default serves its whole set ($hd_tag)" \ + || no "#11 A7D no budget (forbudget-soft): the default is not whole (got '${hd_tag:-none}') — the compact route still caps its hop rows" # A9/A10 — the header's own spliced attributes are inside the number. IDENTITY, not a band: for a bundle with # no --detail bodies, est_tokens is markup-only, so it must equal round(delivered bytes / 2.50) EXACTLY @@ -1812,11 +1920,17 @@ jsonEnvelopeCase "--token-budget=600" --token-budget=600 jsonEnvelopeCase "--token-budget=3000" --token-budget=3000 jsonEnvelopeCase "--token-budget=16000" --token-budget=16000 -# the over_ceiling key must actually FIRE somewhere in that sweep, or the §C2 arm above is vacuous. rw src --for="$JSON_TASK" --json --token-budget=200 > "$TMP/fj200.out" 2>/dev/null || true -grep -q '"over_ceiling":true' "$TMP/fj200.out" \ - && ok "§C2 control: over_ceiling DOES fire at a small budget (the charge arm above is not vacuous)" \ - || no "§C2 control: no budget in the sweep produced over_ceiling - the §C2 arms proved nothing" +# §C2 control — REPLACED (forbudget-soft (rule 4)). Was: the over_ceiling key must FIRE at --token-budget=200, or the §C2 arm +# above is vacuous. A --for --json budget is a soft target: est_tokens may exceed it and the key is never emitted for it (it +# survives only at the default regime's runaway guard). The control now pins both halves of that at the same call: the +# overshoot is REAL (est_tokens > 200 — the §C1 identity above already proved that number prices the whole document) and the +# document carries NO over_ceiling key (NOLBL). Base d4325a8c: "over_ceiling":true here. +# ($TMP/fj200.out is the --token-budget=200 capture made just above this arm.) +c2_est="$( python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("est_tokens","")); sys.exit(0 if "over_ceiling" not in d else 1)' "$TMP/fj200.out" 2>/dev/null )"; c2_rc=$? +{ [ "$c2_rc" -eq 0 ] && [ -n "$c2_est" ] && [ "$c2_est" -gt 200 ]; } 2>/dev/null \ + && ok "§C2 control (forbudget-soft): --for --json --token-budget=200 prices past its target (est_tokens=$c2_est) with no over_ceiling key — a soft target is never labelled" \ + || no "§C2 control (forbudget-soft): --for --json --token-budget=200: est_tokens='${c2_est:-unreadable}', over_ceiling key $( [ "$c2_rc" -eq 1 ] && echo present || echo 'absent/unparseable' ) — want est_tokens > 200 and no over_ceiling key" } est_c4(){ @@ -1902,14 +2016,26 @@ band15 "--pack-task --token-budget=1500" "$TMP/p15_pt.xml" ctx 420 [ "$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --pack-task: root budget_tokens=\"1500\" — the ceiling in the SAME unit as est_tokens=" \ || no "#15 --pack-task: root carries no budget_tokens=\"1500\" beside est_tokens= (got '$( root_attr "$TMP/p15_pt.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --pack-task budget is a SOFT target (decision 3) — the section cascade still shapes +# toward it, but nothing is labelled over it and the header is never dropped for it. Was: root over_ceiling="1" at 50 (the +# header floor exceeds the ceiling). Now NOLBL + HL at the same call: no over_ceiling= on the root, task= verbatim and route= +# kept, no "(ceiling)" / task-echo drop; the full dialect reads budget_tokens= as the soft target (packtask.h +# kPackTaskSoftBudgetSentence). The overshoot stays real and priced: est_tokens > 50. rw src --pack-task="$FOR_TASK" --token-budget=50 >"$TMP/p15_pt50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --pack-task --token-budget=50: root over_ceiling=\"1\" (the header floor exceeds the ceiling)" \ - || no "#15 --pack-task --token-budget=50: no over_ceiling=\"1\" on the root while the ledger prose says the floor exceeds the budget" +rw src --pack-task="$FOR_TASK" --token-budget=50 --legend=full >"$TMP/p15_pt50f.xml" 2>/dev/null +pt50_why="" +[ -n "$( perl -0pe 's###gs' "$TMP/p15_pt50.xml" | grep -oE ']*)?>' | head -1 )" ] || pt50_why="$pt50_why no-root" +[ -z "$( root_attr "$TMP/p15_pt50.xml" ctx over_ceiling )" ] || pt50_why="$pt50_why over_ceiling-on-a-soft-target" +[ "$( root_attr "$TMP/p15_pt50.xml" ctx task )" = "$FOR_TASK" ] && [ -n "$( root_attr "$TMP/p15_pt50.xml" ctx route )" ] || pt50_why="$pt50_why task=/route=-dropped" +! grep -aqE '\(ceiling\)|task_echo: dropped' "$TMP/p15_pt50.xml" || pt50_why="$pt50_why header-dropped-for-the-budget" +grep -aqF 'budget_tokens= is the soft token target asked' "$TMP/p15_pt50f.xml" || pt50_why="$pt50_why full-legend-not-soft" +[ -z "$pt50_why" ] \ + && ok "#15 --pack-task --token-budget=50 (forbudget-soft): no over_ceiling=, task=/route= kept, nothing dropped, the full legend reads a soft target" \ + || no "#15 --pack-task --token-budget=50 (forbudget-soft) —$pt50_why" E50="$( root_est "$TMP/p15_pt50.xml" ctx )" [ -n "$E50" ] && [ "$E50" -gt 50 ] 2>/dev/null \ - && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the label and the number agree" \ - || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the budget it is labelled over" + && ok "#15 --pack-task --token-budget=50: est_tokens=$E50 > 50 — the soft overshoot is priced on the root" \ + || no "#15 --pack-task --token-budget=50: est_tokens='$E50' does not exceed the 50-token target (the floor cannot fit 50 tokens)" # (b) --from-trace: the same two shapes on a name-resolved frame printf 'Traceback (most recent call last):\n File "src/graph.h", line 1, in rankGraphTeleport\n' >"$TMP/p15_trace.txt" rw src --from-trace="$TMP/p15_trace.txt" --token-budget=1500 >"$TMP/p15_ft.xml" 2>/dev/null @@ -1917,65 +2043,73 @@ band15 "--from-trace --token-budget=1500" "$TMP/p15_ft.xml" ctx 420 [ "$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )" = "1500" ] \ && ok "#15 --from-trace: root budget_tokens=\"1500\"" \ || no "#15 --from-trace: root carries no budget_tokens=\"1500\" (got '$( root_attr "$TMP/p15_ft.xml" ctx budget_tokens )')" +# forbudget-soft (rule 4): an explicit --from-trace budget is a SOFT target (decision 4): no ladder, the trace echo always ships, +# over_ceiling= only against max_tokens=. Was: root over_ceiling="1" at 50. Now at the same call: est_tokens > 50 (the overshoot +# is priced), no over_ceiling= (no max_tokens= was passed), the echo of the trace file ships, and the full dialect +# reads budget_tokens= as the soft target (tracelocus.h). rw src --from-trace="$TMP/p15_trace.txt" --token-budget=50 >"$TMP/p15_ft50.xml" 2>/dev/null -[ "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" = "1" ] \ - && ok "#15 --from-trace --token-budget=50: root over_ceiling=\"1\"" \ - || no "#15 --from-trace --token-budget=50: no over_ceiling=\"1\" on the root (the prose label alone is what a parser discards)" +rw src --from-trace="$TMP/p15_trace.txt" --token-budget=50 --legend=full >"$TMP/p15_ft50f.xml" 2>/dev/null +FT50="$( root_est "$TMP/p15_ft50.xml" ctx )" +ft50_why="" +{ [ -n "$FT50" ] && [ "$FT50" -gt 50 ]; } 2>/dev/null || ft50_why="$ft50_why est_tokens='${FT50:-absent}'-not-past-50" +[ -z "$( root_attr "$TMP/p15_ft50.xml" ctx over_ceiling )" ] || ft50_why="$ft50_why over_ceiling-on-a-soft-target" +grep -aqF ". SWEPT across the usable range like arm #5 (the small end is where the -# fixed floor dominates): the root carries est_tokens=; delivered bytes sit inside the ladder's own -# allowance (N x kMinBytesPerToken x kCeilingFirstEntryTolerance = N x 2.36 x 1.15, serialize.h) OR the -# root says over_ceiling="1"; and a labelled root's est_tokens exceeds N (the label and the number agree). -# verify-wave2 F2: the byte half above cannot see the defect it was written for. At --token-budget=1600 -# the bundle is 3,485 B against a 4,342 B allowance — so the allowance arm PASSES — while the very same -# root says budget_tokens="1600" est_tokens="1665" and withholds the one attribute that reconciles them. -# over_ceiling= fired only on the ladder's LAST RUNG, so a bundle that overshoots by a little (the common -# case) was silently unlabelled while one that overshoots by a lot was labelled. The property is about the -# two NUMBERS the root itself prints, in the unit it prints them in: est_tokens <= N OR over_ceiling="1", -# on EVERY rung. 1600/1700 are in the sweep because they are the rungs that were red. +# fixed floor dominates): the root carries est_tokens= — KEPT, unchanged. +# REPLACED (forbudget-soft (rule 4)): the two halves that read the old hard ceiling — "delivered <= N x 2.36 x 1.15 OR the +# root says over_ceiling=1" and verify-wave2 F2's "est_tokens > N <=> over_ceiling=1" — assert the deleted contract: an +# explicit --for budget is a soft target, est_tokens may exceed N and nothing labels it (decision 2). The two numbers the +# root prints no longer contradict each other because the legend reads budget_tokens= as a target the rows stop at, not a +# ceiling the document fits. Each rung now pins, on the same src corpus and budgets: NOLBL (no over_ceiling= on the root); +# ST (the rows fit T = N x 2.36 x 0.90 or are the rank 1..4 floor, and a cut is maximal against the no-budget answer +# A7/A8 captured); RP (rows(N) a rank prefix of the next rung's rows); NX (a cut carries next=). MEASURED on +# d099ec21: 4 rows at 100..600, 5 at 800, 10 at 1200, 20 at 2000, every rung unlabelled with est_tokens > N. +# (#398 pool: #11's $TMP/f_for.out is written in another chain, so this section asks the same no-budget question itself — +# rw src --for="$FOR_TASK", the call the pool pre-warms — into its own file: the same bytes, no cross-chain read.) +rw src --for="$FOR_TASK" >"$TMP/p15_for_ref.xml" 2>/dev/null +f15_prev="" for N in 100 300 400 600 800 1200 1500 1600 1700 2000 2500 3000; do rw src --for="$FOR_TASK" --token-budget=$N >"$TMP/p15_for$N.xml" 2>/dev/null FB="$( bytes_of "$TMP/p15_for$N.xml" )" FE="$( root_est "$TMP/p15_for$N.xml" ctx )" FO="$( root_attr "$TMP/p15_for$N.xml" ctx over_ceiling )" - FLIM="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" if [ -z "$FE" ]; then no "#15 --for --token-budget=$N: root carries no est_tokens= ($FB B delivered; the price is not where a parser reads it)" else ok "#15 --for --token-budget=$N: root est_tokens=\"$FE\" ($FB B)" fi - if [ "$FB" -le "$FLIM" ] 2>/dev/null; then - ok "#15 --for --token-budget=$N: $FB B within the $FLIM B ladder allowance" - elif [ "$FO" = "1" ]; then - ok "#15 --for --token-budget=$N: $FB B over the $FLIM B allowance and the ROOT says so (over_ceiling=\"1\")" - { [ -n "$FE" ] && [ "$FE" -gt "$N" ]; } 2>/dev/null \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N — the label and the number agree" \ - || no "#15 --for --token-budget=$N: root over_ceiling=\"1\" but est_tokens='$FE' does not exceed the budget it is labelled over" - else - no "#15 --for --token-budget=$N: $FB B EXCEEDS the $FLIM B allowance with NO over_ceiling=\"1\" on the root (over_ceiling on root='$FO')" - fi - # F2 — the TOKEN property, independent of the byte allowance above: the root prints budget_tokens= and - # est_tokens= in the same unit, so a reader can subtract them. Whenever it does and the answer is - # positive, the root must say over_ceiling="1"; silence there means "inside the budget". - if [ -n "$FE" ] && [ "$FE" -gt "$N" ] 2>/dev/null; then - [ "$FO" = "1" ] \ - && ok "#15 --for --token-budget=$N: est_tokens=$FE > $N and the root says over_ceiling=\"1\"" \ - || no "#15 --for --token-budget=$N: root prints budget_tokens=$N est_tokens=$FE (over by $(( FE - N ))) with NO over_ceiling= — the two numbers contradict each other on one root" - elif [ -n "$FE" ]; then - [ "$FO" = "1" ] \ - && no "#15 --for --token-budget=$N: root says over_ceiling=\"1\" but est_tokens=$FE is inside the $N budget — the label is a lie in the other direction" \ - || ok "#15 --for --token-budget=$N: est_tokens=$FE <= $N and the root is silent (absent = inside the budget)" + soft_rows "$TMP/p15_for$N.xml" >"$TMP/p15_for$N.rows" 2>/dev/null + f15_sigs="$( grep -aoE ']*>' "$TMP/p15_for$N.xml" | head -1 )" + f15_sh="$( printf '%s' "$f15_sigs" | sed -nE 's/.* shown="([0-9]+)".*/\1/p' )"; f15_tot="$( printf '%s' "$f15_sigs" | sed -nE 's/.* total="([0-9]+)".*/\1/p' )" + f15_why="" + [ -n "$( ctx_root "$TMP/p15_for$N.xml" )" ] || f15_why="$f15_why no-root" + [ -z "$FO" ] || f15_why="$f15_why over_ceiling=\"$FO\"-on-a-soft-target" + f15_st="$( soft_st "$TMP/p15_for$N.xml" "$N" "$TMP/p15_for_ref.xml" )" || f15_why="$f15_why ST:[$f15_st]" + [ -z "$f15_prev" ] || rows_prefix "$TMP/p15_for$f15_prev.rows" "$TMP/p15_for$N.rows" || f15_why="$f15_why rows-not-a-superset-of-N=$f15_prev's" + if [ -n "$f15_sh" ] && [ -n "$f15_tot" ] && [ "$f15_sh" -lt "$f15_tot" ] 2>/dev/null; then + [ "${f15_sigs#* next=\"--for=}" != "$f15_sigs" ] || f15_why="$f15_why cut-without-next" fi + [ -z "$f15_why" ] \ + && ok "#15 --for --token-budget=$N (forbudget-soft): unlabelled (est_tokens=${FE:-?}), $f15_st, rows grow monotonically" \ + || no "#15 --for --token-budget=$N (forbudget-soft) —$f15_why" + f15_prev="$N" done # the two root attributes must be DEFINED by the legend of the document that carries them (§B7 class) perl -0pe 's##\1#gs' "$TMP/p15_for3000.xml" | grep -qE '(^|[^[:alnum:]_:.-])est_tokens\s*=' \ && ok "#15 --for: the legend defines est_tokens=" \ || no "#15 --for: est_tokens= rides the root but no legend clause defines it" -grep -o '' "$TMP/p15_for100.xml" | grep -qE '(^|[^[:alnum:]_:.-])over_ceiling\s*=' \ - && ok "#15 --for --token-budget=100: the legend defines over_ceiling=" \ - || no "#15 --for --token-budget=100: over_ceiling= rides the root but no legend clause defines it" +# REPLACED (forbudget-soft (rule 4)): was "the legend at --token-budget=100 defines over_ceiling=" — the soft target never emits +# that label, so the attribute to define is budget_tokens= itself, read as the soft row target the rows stop at. +[ -n "$( root_attr "$TMP/p15_for100.xml" ctx budget_tokens )" ] && grep -aqF "$SOFT_CLAUSE_COMPACT" "$TMP/p15_for100.xml" \ + && ok "#15 --for --token-budget=100 (forbudget-soft): the legend defines budget_tokens= as the soft row target the rows stop at" \ + || no "#15 --for --token-budget=100 (forbudget-soft): budget_tokens= rides the root but the legend does not read it as a soft target" # (c) --handoff: priced, labelled, and withheld= is a boolean with the count beside it rw . --handoff --token-budget=100 >"$TMP/p15_ho100.xml" 2>/dev/null rw . --handoff --token-budget=100000 >"$TMP/p15_hobig.xml" 2>/dev/null @@ -2186,7 +2320,8 @@ est_18(){ # >= E, by the root's own arithmetic — so this arm READS E off a wide run where nothing is dropped and probes # just above it. No pinned byte count: if the corpus or the legend moves, E moves with it and the probe follows. # Both guards against an empty pass are asserted rather than assumed — the wide run must carry the clauses (else -# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). +# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). forbudget-soft (rule 4): +# the soft target deleted rung zero on purpose, so that control now asserts the opposite law (no clause is ever dropped). # GIT-LESS and relative, the #11 A7 sweep's discipline: no at=, no churn, a fixed root=, nothing from the live # repo. --detail=1 is what puts bytes at the BODY rate, which is the half of the defect a bodiless bundle cannot # see; the fixture's one long body exceeds the tight budget's residual, so the first-entry-whole floor emits a @@ -2218,7 +2353,10 @@ rz_note(){ grep -acF '[legend clauses:' "$RZ/o.xml"; } # file defines with no import, so their rows are via="name" and --for's header now defines it (~190 B, never # dropped). The full document prices at est_tokens=1269 (it was 1162, 38 under the old 1200), so 1200 no longer carried # its legend; 1400 is the wide control again with ~130 tokens of headroom. The probe and the control still read E off it. -RZ_WIDE=1400 +# forbudget-soft (rule 4): 1400 -> 4000. Under the soft target the rows' text tier grows with N and nothing in the header is +# charged, so the document at 1400 prices at est_tokens=1770 and the widest form (e= extents on the rows) at 1875 from ~4000 +# (d099ec21); the probe E+5 must sit below the wide control, so the control moved up. Measured: no budget prices past 1875. +RZ_WIDE=4000 rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" # THE CLAUSES THIS ARM COUNTS — all THREE of the droppable trio (CodeRabbit, PR #215). It counted two: the # confidence reading and the tail reading, but not the route= reading the arm's own paragraph above names. @@ -2230,7 +2368,7 @@ rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" RZ_CLAUSE_ROUTE='route= name-exact(X)|subtoken+body' RZ_CLAUSES=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) -grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) if [ -z "$RZ_E" ] || [ "$RZ_WIDE_NOTE" != "0" ] || [ "$RZ_CLAUSES" != "3" ]; then no "#18 rung zero: the wide control (--token-budget=$RZ_WIDE) does not carry its legend (est='${RZ_E:-unreadable}' dropped-note=$RZ_WIDE_NOTE clauses=$RZ_CLAUSES/3) — there is no price to probe against; re-anchor the fixture" @@ -2242,7 +2380,7 @@ else rz_run "$RZ_PROBE"; RZ_PE="$( rz_est )"; RZ_PN="$( rz_note )"; RZ_PB="$( bytes_of "$RZ/o.xml" )" RZ_PC=0 grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) # the ROOT's verdict only (read off the open tag): since lane/cutfix-bodies a marks # a --detail body whose first line alone exceeds the BODY allowance, which is not the root's est_tokens claim, @@ -2254,21 +2392,26 @@ else no "#18 rung zero at --token-budget=$RZ_PROBE dropped a legend it could afford: dropped-note=$RZ_PN clauses=$RZ_PC/3 est_tokens=${RZ_PE:-unreadable} over_ceiling=$RZ_PO ($RZ_PB B) — the same document prices at $RZ_E at --token-budget=$RZ_WIDE, so it fits every budget >= $RZ_E" fi fi - # …and the rung must still FIRE where the kept document genuinely does not fit. Without this, deleting - # rung zero outright would turn the arm above green. - RZ_CTRL=$(( RZ_E - 200 )) - rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" - # …and the tight control asserts the ABSENCE of the same three, route= included: a present-in-wide / - # unchecked-in-tight assertion is the one-sided shape that let the missing clause through. - RZ_CC=0 - grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) - if [ "$RZ_CN" != "0" ] && [ "$RZ_CC" = "0" ]; then - ok "#18 rung zero control at --token-budget=$RZ_CTRL (200 under the $RZ_E the full document prices at): all three clauses dropped and the note says so (est_tokens=$RZ_CE) — the rung still fires when the drop is real" - else - no "#18 rung zero control at --token-budget=$RZ_CTRL: dropped-note=$RZ_CN clauses still riding=$RZ_CC/3 (est_tokens=${RZ_CE:-unreadable}) — rung zero no longer fires at all, so the arm above is green for the wrong reason" - fi + # …the CONTROL — REPLACED (forbudget-soft (rule 4)). Was: 200 under E the rung must still FIRE (all three clauses dropped + # and the '[legend clauses:' note), or deleting rung zero would turn the arm above green. The soft target deletes rung zero + # by design (decision 2: no header ladder — no legend clause is ever dropped for a budget), so the control now asserts the + # law that replaced it, HL, at the same E-200 and at a budget far under the floor (400: base d4325a8c dropped all three + # there, est_tokens=1050, over_ceiling="1"): all three clauses ride, no drop note, no over_ceiling= on the root. The arm + # above then cannot be green for the wrong reason either: it pins the three clauses at E+5, this one below every price. + rz_hl_bad="" + for RZ_CTRL in $(( RZ_E - 200 )) 400; do + rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" + RZ_CC=0 + grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF 'tail: file-grain rows' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + RZ_CO=0; grep -aoE '^]*>' "$RZ/o.xml" | grep -qF 'over_ceiling=' && RZ_CO=1 + { [ -n "$RZ_CE" ] && [ "$RZ_CN" = "0" ] && [ "$RZ_CC" = "3" ] && [ "$RZ_CO" = "0" ]; } \ + || rz_hl_bad="$rz_hl_bad @$RZ_CTRL:[note=$RZ_CN clauses=$RZ_CC/3 over_ceiling=$RZ_CO est_tokens=${RZ_CE:-unreadable}]" + done + [ -z "$rz_hl_bad" ] \ + && ok "#18 no rung zero (forbudget-soft): at --token-budget=$(( RZ_E - 200 )) and 400 (under the $RZ_E the document prices at) all three clauses ride, no drop note, no over_ceiling=" \ + || no "#18 no rung zero (forbudget-soft): a soft target dropped a legend clause or labelled the root —$rz_hl_bad" fi } @@ -2283,7 +2426,7 @@ est_19(){ # this arm's fixture: --signatures-only printed est_tokens=2616 over 8183 B (round(bytes/2.50) = 3273), and at # --token-budget=1500 the root said est_tokens=1360 with no over_ceiling= over a 5043 B document (2017 tokens). # WHAT IS ASSERTED: the #11 identities (markup at 2.50, bodies at 3.80), exact, on three shapes of a document -# that really carries six rows — and under the explicit ceiling, est_tokens > budget iff over_ceiling="1". +# that really carries six rows — and under the explicit budget (a soft target since forbudget-soft) no over_ceiling=. # Git-less and relative (#18's discipline): nothing from the live repo, a fixed root=. HX="$TMP/hdrprice" mkdir -p "$HX" @@ -2321,13 +2464,17 @@ for hx_mode in "--signatures-only" "" "--token-budget=1500"; do hx_out="$( hx_identity "$HX/o.xml" 2>/dev/null || true )" no "#19 rows unpriced ($hx_label): bytes/span/est/expected = ${hx_out:-unreadable} (six rows required; a gap of ~bytes-of-rows/2.50 is the defect)" fi + # REPLACED (forbudget-soft (rule 4)): was "over_ceiling= agrees with est_tokens vs 1500, both ways" — the ceiling verdict + # had to see the rows. A soft target emits no ceiling verdict (the rows are header, never charged against T), + # so the arm pins NOLBL on the same document: the identity above prices the rows, and the root carries no over_ceiling= + # although est_tokens exceeds 1500 (d099ec21: 3726; base d4325a8c labelled it). Read off the root only. if [ "$hx_mode" = "--token-budget=1500" ]; then - HX_E="$( grep -aoE 'est_tokens="[0-9]+"' "$HX/o.xml" | head -1 | tr -dc '0-9' )" - HX_O=0; grep -aqF 'over_ceiling="1"' "$HX/o.xml" && HX_O=1 - if [ -n "$HX_E" ] && { { [ "$HX_E" -gt 1500 ] && [ "$HX_O" = 1 ]; } || { [ "$HX_E" -le 1500 ] && [ "$HX_O" = 0 ]; }; }; then - ok "#19 --token-budget=1500: over_ceiling=$HX_O agrees with est_tokens=$HX_E (the ceiling verdict sees the rows)" + HX_ROOT="$( ctx_root "$HX/o.xml" )" + HX_E="$( printf '%s' "$HX_ROOT" | sed -nE 's/.* est_tokens="([0-9]+)".*/\1/p' )" + if [ -n "$HX_ROOT" ] && [ -n "$HX_E" ] && [ "${HX_ROOT#* over_ceiling=}" = "$HX_ROOT" ]; then + ok "#19 --token-budget=1500 (forbudget-soft): est_tokens=$HX_E with the rows priced, no over_ceiling= on the soft target's root" else - no "#19 --token-budget=1500: over_ceiling=$HX_O disagrees with est_tokens=${HX_E:-unreadable} — the ceiling verdict was made without the rows" + no "#19 --token-budget=1500 (forbudget-soft): root '${HX_ROOT:0:120}' — want est_tokens= present and no over_ceiling= on a soft target" fi fi done From 942cefc541973bd2d6b0bebb1b783e1991f01ca3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 00:12:56 -0400 Subject: [PATCH 128/162] test(forsoftbudgetcheck): E2 splits the two bare-space row readings Merge interaction (forbudget-soft x attrib-multidef): the prov="split" reading now rides the gate's corpus, and like FE-B's via="name" reading it opens with a bare space after the clause before it. Where the e= reading between them is absent (every soft target from 300 to 3000) the r= clause and the split reading read as one segment, so E2 reported the r= clause as dropped although it rides. legend.py treats both readings as clause boundaries, the precedent of the est_tokens splice beside them. The comparison is otherwise unchanged: a copy of the 300-token answer with the r= clause cut out is still reported. Co-Authored-By: Claude Opus 5.5 --- test/forsoftbudgetcheck.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/forsoftbudgetcheck.sh b/test/forsoftbudgetcheck.sh index 35a78929f..2b6a4147d 100755 --- a/test/forsoftbudgetcheck.sh +++ b/test/forsoftbudgetcheck.sh @@ -148,8 +148,11 @@ def clauses( path ): if not com: sys.exit( 3 ) out = set() for c in com: - # " est_tokens= prices…" is spliced after the last clause with a bare space (pre-existing): a clause boundary too - for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s|\s(?=est_tokens= prices))\s*', c ): + # " est_tokens= prices…" is spliced after the last clause with a bare space (pre-existing): a clause boundary too. + # So are the two present-only row readings (FE-B's via="name", attrib-multidef's prov="split"), which open with a + # bare space after whatever clause precedes them: without the boundary the clause before one reads as missing + # wherever the one between them (e= on a d row) is absent (train 28: the split reading rides this corpus) + for seg in re.split( r'\s*(?:\[|\]|;\s|\.\s|\s—\s|\s(?=est_tokens= prices)|\s(?=via="name" on a row)|\s(?=prov="split" on a row))\s*', c ): seg = re.sub( r'\d+', '#', re.sub( r'\s+', ' ', seg ) ).strip( ' .:;' ) if len( seg ) >= 12: out.add( seg ) return out From 3c07fa0d15eeb0c309638c5215d623ddeb06ea87 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 00:19:02 -0400 Subject: [PATCH 129/162] fix(graph): an inert quoted call site has no record, and --callees no longer aborts on one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge interaction (PR #380 x lane declined-sites): #380 counts an Elixir call candidate inside inert quote AST in declinedOut, but it reaches no decline site, so it writes no declinedSite* record; declinedSitesMadeBy's postcondition (records == declined_calls=) then aborted --callees on such a function, e.g. `ripwire test/elixirfix --callees=quoted`. Graph::inertDeclinedCaller keeps each inert site's caller, and both postconditions count it (records + inert sites == declined_calls=). Beside rows, the inert clause now adds that no row names those quoted sites. Gate: declinedsitecheck (L) — red on the merged binary before this commit (exit -5 twice, the sentence missing). Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 22 ++++++++++++++++++++-- src/graphlegend.h | 6 ++++-- test/declinedsitecheck.sh | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/graph.h b/src/graph.h index e29ba076b..6f3331c28 100644 --- a/src/graph.h +++ b/src/graph.h @@ -156,6 +156,10 @@ struct Graph // named by declinedListCallCount[k] calls — what the callers and impact answers read to count, once per call, the declines that could have meant THEIR symbols. std::vector declinedOut; std::size_t inertDeclinedCalls = 0; // quoted call sites counted in declinedOut, with no candidate list + // train 28 (PR #380 x lane declined-sites): the caller of each inert quoted site, in resolve-loop order. Those sites ride + // declinedOut with no candidate list and reach no decline site, so they write no declinedSite* record below; this column is + // what lets declinedSitesMadeBy state its record-count postcondition with them in it (records + inert == declined_calls=). + std::vector inertDeclinedCaller; std::vector declinedListOff{ 0u }; // the leading offset lives here, so a default Graph is already a valid zero-list CSR std::vector declinedListCand; std::vector declinedListCallCount; @@ -7345,6 +7349,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // site on its caller; do not intern target candidates, since it could reach none of them. ++g.declinedOut[ r.fromSymbol ]; ++g.inertDeclinedCalls; + g.inertDeclinedCaller.push_back( r.fromSymbol ); // no declinedSite* record: no candidate list, no decline site disposition = CallDisposition::Declined; continue; } @@ -13301,12 +13306,24 @@ inline std::size_t declinedCallsMadeBy( const Graph& g, std::span // lane declined-sites: the site records (Graph::declinedSite*) behind the two counts above, for the rows // (declinedsites.h). declinedSitesMadeBy: the declines `sources` made — one record per call declinedCallsMadeBy counts. +// The inert quoted sites (#380) `sources` made: counted in declinedOut, never a record (Graph::inertDeclinedCaller). +inline std::size_t inertDeclinedMadeBy( const Graph& g, std::span sources ) noexcept +{ + std::size_t n = 0; + for( const NodeId caller : g.inertDeclinedCaller ) + { + n += std::ranges::contains( sources, caller ) ? 1u : 0u; + } + return n; +} + inline std::vector declinedSitesMadeBy( const Graph& g, std::span sources ) { std::vector sites; if( g.declinedSiteCaller.empty() || sources.empty() ) { - ENSURES( declinedCallsMadeBy( g, sources ) == 0, "no record, so no declined call: every decline site writes one" ); + ENSURES( declinedCallsMadeBy( g, sources ) == inertDeclinedMadeBy( g, sources ), + "no record, so no declined call but an inert quoted site: every decline site writes one" ); return sites; } std::vector isSource( g.declinedOut.size(), 0 ); @@ -13324,7 +13341,8 @@ inline std::vector declinedSitesMadeBy( const Graph& g, std::span sites.push_back( site ); } } - ENSURES( sites.size() == declinedCallsMadeBy( g, sources ), "every declined call a source made has its record, and no other does" ); + ENSURES( sites.size() + inertDeclinedMadeBy( g, sources ) == declinedCallsMadeBy( g, sources ), + "every declined call a source made has its record, and no other does; an inert quoted site has none" ); return sites; } diff --git a/src/graphlegend.h b/src/graphlegend.h index 221ed658e..be5341f05 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -678,6 +678,8 @@ inline std::string declinedCallsLegendWithGates( bool on, bool gateDeclined, boo // twin says so instead (gates review R9) and is the one such an answer carries; everything else in it is the clause above. inline constexpr const char* kDeclinedCallsRowsLegend = "declined_calls=K (absent when 0) counts call SITES the resolver declined to bind: the called name has two or more same-language definitions, none in the caller's file or directory, and no qualifier, receiver type or include chose one, so no edge exists and no count here includes them; the rows below name them (the map header's declined=). Callers form: declined calls that could have meant this selector's definitions; impact form: that could have reached SYM or a symbol in its radius; callees form: declined calls these definitions make. Each call counts once however many candidates it had; the uses verb on the called name lists the sites. "; +// Beside rows the rows clause says the rows name the declined calls; the inert quoted sites it also counts have none. +inline constexpr const char* kInertNoDcRowClause = "No row names those quoted sites: they reach no decline site. "; // inertDeclined (#380): the callees form's Elixir inert quote sites ride the same count with no row (they never reach a // decline site), so their clause follows the gate sentences either way, exactly as declinedCallsLegendWithGates spells it. inline std::string declinedCallsLegendForRows( bool on, bool gateDeclined, bool stdDeclined, bool dcRows, bool inertDeclined = false ) @@ -686,8 +688,8 @@ inline std::string declinedCallsLegendForRows( bool on, bool gateDeclined, bool { return declinedCallsLegendWithGates( on, gateDeclined, stdDeclined, inertDeclined ); } - return std::string( kDeclinedCallsRowsLegend ) + declinedCallsGateLegend( gateDeclined ) + ( inertDeclined ? kInertDeclinedCallsClause : "" ) - + declinedCallsStdLegend( stdDeclined ); + return std::string( kDeclinedCallsRowsLegend ) + declinedCallsGateLegend( gateDeclined ) + + ( inertDeclined ? std::string( kInertDeclinedCallsClause ) + kInertNoDcRowClause : std::string() ) + declinedCallsStdLegend( stdDeclined ); } // The clause, in the gates review's words (R10): what a row is, and above all what it is NOT. Each later sentence rides // only where its attribute does, so an answer pays for the vocabulary it prints. diff --git a/test/declinedsitecheck.sh b/test/declinedsitecheck.sh index 8b392454d..7df0b98d0 100755 --- a/test/declinedsitecheck.sh +++ b/test/declinedsitecheck.sh @@ -537,6 +537,32 @@ rc1, o1 = run( GO, "--callees=doPackage", "--legend=full" ) rc2, o2 = run( GO, "--callees=doPackage", "--legend=full" ) say( rc1 == 0 and rc2 == 0 and o1 == o2 and len( o1 ) > 0, "[C] i_det", "two runs byte-identical" ) +# ── (L) train 28: PR #380's inert quoted sites beside the records ───────────────────────────────────────────────── +# An Elixir call candidate inside inert quote AST is counted in declinedOut (the callees declined_calls=) but reaches no +# decline site, so it writes no record and has no row. Merged with this lane, declinedSitesMadeBy's postcondition +# (records == declined_calls=) aborted --callees on such a function (ENSURES, both branches: a tree with no record at all, +# and a tree whose records belong to other callers). Arms: an Elixir-only tree, then the same file beside a Python tree +# with tier-3 records; and the rows legend says the quoted sites have no row (only where both ride). +print( "=== (L) inert quoted sites (PR #380) carry no record and abort nothing ===" ) +EX = os.path.join( T, "elixirfix" ) +x = ask( EX, "callees", "quoted", "--legend=full" ) +say( x.ok and x.a( "declined_calls" ) == "1" and not x.dc, "[L] l_ex_only", + "elixirfix --callees=quoted: exit 0, declined_calls=\"1\" (the quoted nonexistent(x)), no row", x.why() ) +MIX = os.path.join( TMP, "mix" ) +import shutil +shutil.copytree( PYF, os.path.join( MIX, "py" ) ) +os.makedirs( os.path.join( MIX, "ex" ) ); shutil.copy( os.path.join( EX, "run.exs" ), os.path.join( MIX, "ex", "run.exs" ) ) +x = ask( MIX, "callees", "quoted" ) +say( x.ok and x.a( "declined_calls" ) == "1" and not x.dc, "[L] l_mixed_inert", + "beside Python records: --callees=quoted exit 0, declined_calls=\"1\", no row", x.why() ) +rc, out = run( MIX, "--callees=py/caller/use.py:load_twice", "--legend=full" ) +y = Answer( rc, out, "callees" ) +say( y.ok and len( y.dc ) >= 1 and "No row names those quoted sites" in out and "inert quote AST" in out, "[L] l_rows_legend", + "a Python answer with rows on a tree with quoted sites: rows, the inert clause and its no-row sentence", y.why() ) +rc, out = run( PYF, "--callees=caller/use.py:load_twice", "--legend=full" ) +say( rc == 0 and "No row names those quoted sites" not in out and "inert quote AST" not in out, "[L] l_rows_legend_neg", + "the same answer on the Python tree alone carries neither sentence (present-only)" ) + # ── (J) the predicates can fail ──────────────────────────────────────────────────────────────────────────────────── print( "=== (J) the predicates reject what they must ===" ) fake = Answer( 0, '', "callers" ) From 933c7d66854ac0b929c02b71efb4390fc732ce7c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 00:19:41 -0400 Subject: [PATCH 130/162] fix(lego): Ruby's main object is one implementor, however many directives list it Carried from lane ruby-pair's review: --lego=M counted one main-object implementor per top-level directive, so a file that writes both `self.extend Tasks` and `extend ::Tasks` read implementors="2" for one object. The rows stay one per directive (each names its site); implementors= counts the main object once, and the row's legend clause says so. Gate: subscriptopcheck L1 now pins implementors="1" beside the two rows (red on the merged binary before this commit; L2 keeps a class beside main at 2). CHANGELOG entry follows. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++- src/serialize.h | 6 ++++-- test/subscriptopcheck.sh | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0f508e6..d9103807c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,8 @@ share the path and is not armed. `--uses` lists no subscript site. Parser versio A top-level `include M`, `extend M` or `self.extend M` makes Ruby's main object answer M's methods (rake's dsl_definition.rb ends with `self.extend Rake::DSL`). `--lego=M` now lists it as ``, counted in `implementors=`, with a present-only legend clause. Ruby-only: no other indexed language has +p="FILE:LINE">`, one row per directive, counted once in `implementors=` (every directive mixes into the same object), +with a present-only legend clause. Ruby-only: no other indexed language has an object that a file's top level mixes a module into. Not read: a directive inside a top-level `if`/`begin` or a block, `prepend`, a mixin onto another object, and a module the tree never opens. The ranked `--for` lego section is unchanged. Gate: `test/subscriptopcheck.sh` (L arms). diff --git a/src/serialize.h b/src/serialize.h index c0e9037a1..eabf28b98 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -9124,7 +9124,7 @@ inline std::span legoMainRowsOf( std::span"; + ""; inline const std::vector> kNoImplementorBound{}; // the empty bound map: every edge reads ByName // One row. `line` (S2): the TARGETED answer puts the definition's line on p= (p="file:LINE", the --uses and @@ -9286,7 +9286,7 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector const LegoClosurePage deep = focusId != kNoNode ? legoClosurePage( implementors, targeted.implementorBound != nullptr ? *targeted.implementorBound : kNoImplementorBound, id, targeted.pageLimit, targeted.pageOffset ) : LegoClosurePage{}; if( focusId != kNoNode ) { - hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() + mainRows.size() ) + "\"" + hdr = "\" defs=\"" + std::to_string( definitionCountOfName( ing, id ) ) + "\" implementors=\"" + std::to_string( impls.size() + ( mainRows.empty() ? 0u : 1u ) ) + "\"" + legoClosureAttrs( deep, std::string( pathRel( isym.fileId ) ) + ":" + isym.name, deep.unwalked.empty() ? std::string() : std::string( pathRel( ing.symbols[ deep.unwalked.front() ].fileId ) ) + ":" + ing.symbols[ deep.unwalked.front() ].name, targeted.pageLimit ); @@ -9380,6 +9380,8 @@ inline void packLego( std::FILE* out, const IngestResult& ing, const std::vector { w.write( "" ); } + // train 28 (ruby-pair review): one row per directive (each p= a site), but ONE implementor — every top-level directive + // mixes into the same main object, so implementors= above counts it once however many rows list it for( const RubyMainMixinRow& m : mainRows ) // targeted only (empty otherwise): after the class rows, before the deeper ones { w.write( " " ]; then ok "L1 self.extend Tasks and extend ::Tasks: two main rows at their constants" else no "L1 --lego=Tasks main rows: got '$m'"; fi - if grep -q ']* implementors="2"' "$DIR/l1.xml"; then ok "L1 implementors= counts the main rows" - else no "L1 implementors= should be 2: $( grep -o ']*>' "$DIR/l1.xml" )"; fi + # train 28 (the lane review's carried item; by design, rule 4): the two directives are two ROWS of ONE implementor — both mix + # into the same main object — so implementors= counts it once. Was implementors="2" (one per directive). The two rows stay + # asserted above; L2 below keeps a class beside main at 2. + if grep -q ']* implementors="1"' "$DIR/l1.xml"; then ok "L1 implementors= counts the main object once (two directives, two rows)" + else no "L1 implementors= should be 1 (one main object): $( grep -o ']*>' "$DIR/l1.xml" )"; fi if grep -q ' +692 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **686 gate scripts** and is the authoritative list; +`test/regression.sh` names **692 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 255 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 259 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/EVALS.md b/docs/EVALS.md index 791e77946..d318e6284 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 686 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 692 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **686 gate scripts**, all of which exist on disk. +naming **692 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 686. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 692. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LIMITS.md b/docs/LIMITS.md index c0fd435f5..4ed964749 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 256 | 100 | 149 | **107** | +| 259 | 102 | 151 | **108** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 255 + 7 is the 262 constants this generator parses out of `src/`. +are not counted as caps, and 259 + 7 is the 266 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **139 of 255 caps are classified -(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 116 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **139 of 259 caps are classified +(49 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 120 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 100 files that declare a cap — the 256 caps counted above, and no parameter. +One table for each of the 102 files that declare a cap — the 259 caps counted above, and no parameter. ### `src/abicheck.h` diff --git a/docs/TUNING.md b/docs/TUNING.md index 74e8c71db..2ffd53f40 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 163 | 162 | 112 | 12 | **37** | 75 | +| 167 | 166 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **163 cap -declarations** under **162 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **167 cap +declarations** under **166 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 162 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **39 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 166 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **43 declared since the sweep was prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kCallSitesPerRowCap`, -`kChurnMergeBombMaxFiles`, `kExtendChainCap`, `kExtendedLengthThresholdUnits`, `kFieldIdCapacity`, -+33 more). 111 + 12 + 39 = 162, and `emit` refuses to render a partition that does not add up. The -`tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 163" would be wrong in both halves -at once, which is the shape of error a generated table exists to prevent. +`kChurnMergeBombMaxFiles`, `kDeclinedSiteCandCap`, `kDeclinedSiteCap`, `kDefTableCap`, +37 more). +111 + 12 + 43 = 166, and `emit` refuses to render a partition that does not add up. The `tunable` +column above reads 112 rather than 111 because one name the sweep classified is no longer declared +in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 167" would be wrong in both halves at once, +which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 163-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 167-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 95235b400..10f9421b2 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["686 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["692 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "686", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "692", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["192 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["686 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["692 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], From 0a5d057d90c183b4735c2b1ae91f1e9380999cec Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 00:56:57 -0400 Subject: [PATCH 141/162] test(showcase): a capture case for --quality-kinds showcase_coverage_check (D) on the merged binary: 1 of 185 flags uncovered (--quality-kinds, lane qd-memory-degrade). The case runs a one-commit range for two of the twelve kinds and says what the selector does and does not mean. Co-Authored-By: Claude Opus 5.5 --- test/showcase_capture.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/showcase_capture.py b/test/showcase_capture.py index 772ec407d..36538867c 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -494,6 +494,7 @@ def _strayScope( sub, kind ): add(S4, f"{BIN} . --stray-content={shlex.quote( _famB )} --abi", f"Cross-branch ABI-break gate over {_strayScope( _famB, _famBKind )}: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.", timeout=600) add(S4, f"{BIN} . --whereis=rankGraphTeleport", "Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind=\"def\" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.", timeout=600) add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=all", "The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).", timeout=600) +add(S4, f"{BIN} . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code", "A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why=\"selector\"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.") add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text", "The same page with every t= line whole: the page above previews each line over 300 B (200 B and t_bytes=, the naming this call), and --whereis-full-text replays it with the same listing, window, rows and labels. MCP: full_text=true.") add(S4, f"{BIN} . --whereis=computeOnePairOverlap --with-history", "Same, plus a git-history row (never / removed-by-commit) for names no tree carries.", timeout=600) add(S4, f"{BIN} . --flags", "The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.") From 1532c0db56485d7af288ac7c53ecd2f50dcd59f9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 01:18:07 -0400 Subject: [PATCH 142/162] =?UTF-8?q?docs:=20the=20train=2028=20regeneration?= =?UTF-8?q?=20from=20the=20merged=20binary=20=E2=80=94=20showcase=20captur?= =?UTF-8?q?e,=20COMMANDS.md,=20dictv,=20help=20pins,=20deck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From build/ripwire at 3d3abe61 (every train-28 lane merged; src unchanged since): - docs/captures/COMMANDS_showcase_2026-10-16.md captured (test/showcase_capture.py, 265 commands, clean tree @ 0a5d057d): 185/185 flags covered with the new --quality-kinds case; it re-derives the --at seed and records the live legend dictionary. - docs/COMMANDS.md rebuilt from it (docs/docs_commands_build.py: 188 flags, 172 samples). - CHANGELOG versions paragraph: the session legend dictionary is dictv=f83935bb22c0b728 entries=885 (`ripwire --legend-dict`). - test/printf_parity.manifest: help and help_all re-pinned (UPDATE_GOLDEN_EXPECT matched exactly {help help_all}: the lanes' new flags and help text). - deck: 172 recorded invocations. tools/list is 47,416 B, inside its 47,420 B ceiling (unchanged). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docs/COMMANDS.md | 1216 +-- docs/captures/COMMANDS_showcase_2026-10-16.md | 7729 +++++++++++++++++ present/deck5_ripwire_build.js | 2 +- test/printf_parity.manifest | 4 +- 5 files changed, 8351 insertions(+), 602 deletions(-) create mode 100644 docs/captures/COMMANDS_showcase_2026-10-16.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 922c90877..66d38b560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -253,7 +253,7 @@ branch build has used, so no cache such a build wrote is read as this release's; numbered 9 on #380's branch, which predates the subscript role), and the binding kind byte one (Ruby's main-object mixin); no record changes shape. `kQSnapCacheScheme` 20 → 21: the calls that keep a definition alive moved (proven subscript calls, live `unquote(...)` calls), so the dead kind can differ for an unchanged commit. Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. +cached quality snapshot is recomputed. The session legend dictionary is `dictv=f83935bb22c0b728 entries=885`. ## [0.6.7] — 2026-10-11 diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 23be14265..bf73fd873 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,7 @@ python3 docs/docs_commands_build.py --bin build/ripwire The flag surface below is read from `ripwire --help`, so it cannot disagree with the shipped binary. `test/docscommandscheck.sh` fails if it ever does — in either direction. -Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-11.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-16.md`), trimmed to the first few lines and scrubbed of local paths. It is illustrative, not a golden: run the command yourself for the current shape. @@ -68,20 +68,20 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -104,20 +104,20 @@ _The same capped map without its inventory tier: --top-k=5 leaves files out, so ``` $ ./build/ripwire . --top-k=5 --no-inventory - + - - + + - + - + ... [7 more line(s); run it to see the whole thing] ``` @@ -138,10 +138,10 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ ``` $ ./build/ripwire . --max-tokens=1500 - + - - + + @@ -159,9 +159,9 @@ $ ./build/ripwire . --max-tokens=1500 ### `--token-budget=N[K|M|G]` -**Answers:** cap the whole document at N tokens — a hard gate on the map, a trim-to-fit on the task lenses two personalities depending on the verb: - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's est_tokens exceeds N. +**Answers:** N tokens: a hard gate on the map, a soft row target on the task lenses two personalities depending on the verb: - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's est_tokens exceeds N. -That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone. (test/tokenbudgetcheck.sh reports the live MAPE vs tiktoken o200k when tiktoken is installed; the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged. ASSERTS and fails, vs --max-tokens which shapes to fit — composable: set neither, either, or both (e.g. --max-tokens=16000 --token-budget=16K), but see --max-tokens above: the two Ns are measured in different units. Over budget, nothing of the artifact reaches stdout — only a small record naming withheld_est_tokens= vs budget=, the same vocabulary --recall uses, since est_tokens= is normatively about what a run PRINTED. On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected. --json GATES AT A DIFFERENT NUMBER for the same request, and NOT by a fixed factor: the flag measures the DOCUMENT that was emitted, and whether JSON or XML is smaller flips with RESULT SIZE on this corpus. Small: JSON wins (MEASURED on src --top-k=20: est_tokens 1146 XML vs 899 JSON, ~22% smaller). Large: XML wins instead (MEASURED on src --top-k=200: est_tokens 9405 XML vs 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the same N can pass or fail differently under --json depending on dialect AND size — never assume one direction, measure the request you actually gate. - --for / --pack-task / --from-trace / --run-trace: SHAPES instead of gating — overrides that lens's own default payload budget and trims to fit, always exit 0. --for's header reports est_tokens="N" so its fit is checkable; --pack-task/--from-trace report their budget ledger in the header report line instead. On --for's auto bundle the ceiling is SPLIT, not handed to the signatures first: the sig side's claim caps at the default sig budget and the rest flows to the inline bodies, so a wider ceiling never serves fewer of them (see --for below). Its VERBATIM task echo is bytes no trim can shrink, so past some task length the header floor alone exceeds the ceiling: the lens drops the comment's DUPLICATE echo first (task_echo: dropped (ceiling); task= keeps the verbatim copy), then labels it over_ceiling (--recall: over_ceiling=1) — never a trim it did not actually do. +That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone. (test/tokenbudgetcheck.sh reports the live MAPE vs tiktoken o200k when tiktoken is installed; the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged. ASSERTS and fails, vs --max-tokens which shapes to fit — composable: set neither, either, or both (e.g. --max-tokens=16000 --token-budget=16K), but see --max-tokens above: the two Ns are measured in different units. Over budget, nothing of the artifact reaches stdout — only a small record naming withheld_est_tokens= vs budget=, the same vocabulary --recall uses, since est_tokens= is normatively about what a run PRINTED. On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected. --json GATES AT A DIFFERENT NUMBER for the same request, and NOT by a fixed factor: the flag measures the DOCUMENT that was emitted, and whether JSON or XML is smaller flips with RESULT SIZE on this corpus. Small: JSON wins (MEASURED on src --top-k=20: est_tokens 1146 XML vs 899 JSON, ~22% smaller). Large: XML wins instead (MEASURED on src --top-k=200: est_tokens 9405 XML vs 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the same N can pass or fail differently under --json depending on dialect AND size — never assume one direction, measure the request you actually gate. - --for / --pack-task / --from-trace / --run-trace: a SOFT TARGET, always exit 0 — rows stop once it is reached, never a promise of bytes. --for charges N against its ranked rows only (never the header or a disclosure, so compact and full serve the same rows): rows are added in rank order while they fit, the rank 1..4 rows always ship, and the cut names its continuation (). The bodies, hops and tail fill from what the rows left. A target wider than the answer serves the default answer's rows. --pack-task keeps its section shares (an explicit 6000 serves the default's sections); --from-trace its section pricing. No lens drops a legend clause or the task echo for it, and none labels itself over_ceiling for it: est_tokens= may exceed N (budget_tokens= says it is the target asked). --pack-task without the flag keeps its hard 6K default (over_ceiling when it cannot fit). **Try it** @@ -170,7 +170,7 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--sections`, `--recall`, `--handoff`, `--pr-context`, `--from-trace` @@ -207,7 +207,7 @@ $ ./build/ripwire . --help-task="write a cheerful release announcement" **Answers:** the task lens: say what you are doing, get the signatures and bodies to read first the task lens: ranked signatures + metrics framed for reuse. -The bundle enforces a ~7.5KB default payload budget (tail entries trim first; marks it: T rows handed to the trim, S printed). The r=1 (top-ranked) row carries next="--expand=FILE:NAME" — the one pasteable follow-up, the body that ends the search (defined here, not in the bundle's own header, which the token ladder does not charge). An explicit --token-budget=N overrides the default at the conservative byte rate (SHAPES, exit 0; see --token-budget above) and the header reports the delivered est_tokens. TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The root discloses it: bundle="auto" bodies="N" (bodies="0" reason="budget" when none fit) — on EVERY auto-mode run: a ceiling the signatures alone exhaust still carries the attribute (legend and empty shell dropped there; only the attribute has reserved bytes), and --for --json, which serves no bodies by design, says so with "bundle":"sigs". Only the caller-chosen postures (--signatures-only, --detail=N) are attribute-free. ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. If the anchor's own body does not fit, the bundle serves nothing and says so, and the per-item over-budget comment names what was dropped. COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the ranked map plus a section — the same candidate head's ONE-HOP callee signatures, the block a body carries — and NO body CDATA, disclosed as bundle="compact" bodies="0" reason="compact-route". Read the map, then --expand=SYM the one you want. A ranked function or method whose whole name is a word of the query (by name, not resolved) gets the first rows, past that head: up to 3, each listing its callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=. --auto-bodies restores the body walk there. That shape discloses on every run too: a ceiling the signatures alone exhaust carries bundle="compact" bodies="0" reason="budget" — three distinct reasons, never collapsed (compact-route = the route chose edges, no_candidates = nothing scored, budget = the ceiling was spent). An explicit --token-budget=N is a hard ceiling, split so a wider ceiling never buys less: the signature side's claim is capped at the DEFAULT ~7.5KB sig budget and every byte beyond it flows to the enrichment — at any ceiling at or above the default's effective total the block is byte-identical to the default run's, so every body (or hop row) the default serves still fits. An explicit --pack-top-n is an explicit SIG posture and keeps the whole-ceiling sig claim. --compress composes: the served bodies (auto/anchor and --detail=N alike) go through the same comment-strip --expand uses, disclosed as compress="1" on the element (nothing to strip on the compact route). RANKING CONFIDENCE, disclosed not scored: the root always carries confidence="high|low" margin_pct="N" — derived from the SAME relevance-cliff gap statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect carries the same two keys). low means the ranking is FLAT (no material score cliff and more positive matches than the head shows) — treat the set as a starting point, not an answer; high means a material cliff inside the served head (margin_pct= is that drop as a whole percent) or every positive match already shown. COVERAGE, and the widening page: on a THIN answer only, the root also carries coverage="N" — the IDF-weighted share (whole percent) of the task's subtokens found in the top-ranked symbol's name, doc or body; an unmatched subtoken weighs as the rarest, so a "(#12147)" token honestly lowers it. THIN = coverage under 50, or a ranked head spread over fewer than 3 files: coverage= and its legend clause ride the root (present-only: a confident answer carries neither) and the r=1 row's next= names --for=TASK --limit=40 instead of --expand — the FILE-GRAIN WIDENING PAGE: …, ONE row per positive-score file (~100 B each), ranked file-first by score= = the IDF-weighted share of the task's subtokens the file's top 8 symbols cover between them (a term counts once however often it recurs, so one huge file cannot monopolise; ties by the best symbol's lens score, then path), n= its positive-score symbols, sym= its top symbols. --limit=N sets the rows, --offset=M pages (the house quintet + next= on a cut page). The page takes NO bundle-shaping flag (--json, --format=candidates, --detail, --signatures-only, --auto-bodies, --adaptive, --token-budget, --top-k …): each is refused beside --limit, never ignored. Measured on the routing-loop ladder (RocksDB, frozen 30): the follow-up that completes answers where a body cannot. / COLLAPSE TO A COUNTED STUB BY DEFAULT (L2, a disclosed cut, never a silent one): when the ranked set names an interface with implementors, or a HAS-A field edge into/out of it, the bundle used to spend the full // method contract or row list on it — the STUB instead says only how many rows the section held: (same shape for ). No stub for a section that would be EMPTY (an absent section stays absent). total= is that section's own pre-cap row count (packLego's post-dedup interface count; every matched HAS-A edge for compose, which caps nothing). next= carries the one restoring spelling that returns BOTH sections byte-identical to the un-stubbed render, in ONE call: --sections=lego,compose. +STOPS WHEN COMPLETE: the ranked head (: the relevance floor within the top 40, or --pack-top-n) is the answer and every row of it ships. A ~7.5KB payload TARGET shortens row text toward it (tail signatures and doc excerpts first; with S=T marks a shrink); rows served past it carry past_target="K" (the last K rows). Only a runaway guard (30KB, a bug signal) cuts rows: over_ceiling="1" and serving them all (S printed of the T rows handed to the trim). The r=1 (top-ranked) row carries next="--expand=FILE:NAME" — the one pasteable follow-up, the body that ends the search (defined here, not in the bundle's own header, which the token ladder does not charge). An explicit --token-budget=N is a soft row target at the conservative byte rate (exit 0; see --token-budget above) and the header reports the delivered est_tokens. TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The root discloses it: bundle="auto" bodies="N" (bodies="0" reason="budget" when none fit) — on EVERY auto-mode run: a target the signatures alone exhaust still carries the attribute (with its legend and the empty shell), and --for --json, which serves no bodies by design, says so with "bundle":"sigs". Only the caller-chosen postures (--signatures-only, --detail=N) are attribute-free. ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. If the anchor's own body does not fit, the bundle serves nothing and says so, and the per-item over-budget comment names what was dropped. COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the ranked map plus a section — the same candidate head's ONE-HOP callee signatures, the block a body carries — and NO body CDATA (save the owner body below), disclosed as bundle="compact" bodies="0" reason="compact-route". Read the map, then --expand=SYM the one you want. A ranked function or method whose whole name is a word of the query (by name, not resolved) gets the first rows, past that head: up to 3, each listing its callees to 100 names (capped=1 next= past it); more are counted as qword_cut= with next=. --auto-bodies restores the body walk there. The default serves every hop row. OWNER BODY: an explanatory question (how does, what does, ...) also carries ONE body after the hops, root bodies="1" reason="owner": a ranked callable whose whole name is a query word (the one calling another such owner first), else a method a ranked class owns (parser scope or span) named by a word, else no body - , by name, not resolved, not the only relevant body. Over 6000 B it is cut at a line end with next=. Under --token-budget it rides only when the rows at the target's cut leave room for it whole, else and reason="budget". MCP for serves the same body, --json none; RIPWIRE_NO_BODY_SLOT=1 turns it off (the A/B handle). That shape discloses on every run too: a target the signatures alone exhaust carries bundle="compact" bodies="0" reason="budget" — four distinct reasons, never collapsed (compact-route = the route chose edges, no_candidates = nothing scored, budget = the ceiling was spent, owner = the owner body rides). An explicit --token-budget=N is a soft target: the hop rows, bodies and file tail keep the share a hard N left them, rows fill the rest by rank; --pack-top-n sets the head the target fills. --compress composes: the served bodies (auto/anchor and --detail=N alike) go through the same comment-strip --expand uses, disclosed as compress="1" on the element (on the compact route, the owner body's). RANKING CONFIDENCE, disclosed not scored: the root always carries confidence="high|low" margin_pct="N" — derived from the SAME relevance-cliff gap statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect carries the same two keys). low means the ranking is FLAT (no material score cliff and more positive matches than the head shows) — treat the set as a starting point, not an answer; high means a material cliff inside the served head (margin_pct= is that drop as a whole percent) or every positive match already shown. COVERAGE, and the widening page: on a THIN answer only, the root also carries coverage="N" — the IDF-weighted share (whole percent) of the task's subtokens found in the top-ranked symbol's name, doc or body; an unmatched subtoken weighs as the rarest, so a "(#12147)" token honestly lowers it. THIN = coverage under 50, or a ranked head spread over fewer than 3 files: coverage= and its legend clause ride the root (present-only: a confident answer carries neither) and the r=1 row's next= names --for=TASK --limit=40 instead of --expand — the FILE-GRAIN WIDENING PAGE: …, ONE row per positive-score file (~100 B each), ranked file-first by score= = the IDF-weighted share of the task's subtokens the file's top 8 symbols cover between them (a term counts once however often it recurs, so one huge file cannot monopolise; ties by the best symbol's lens score, then path), n= its positive-score symbols, sym= its top symbols. --limit=N sets the rows, --offset=M pages (the house quintet + next= on a cut page). The page takes NO bundle-shaping flag (--json, --format=candidates, --detail, --signatures-only, --auto-bodies, --adaptive, --token-budget, --top-k …): each is refused beside --limit, never ignored. Measured on the routing-loop ladder (RocksDB, frozen 30): the follow-up that completes answers where a body cannot. / COLLAPSE TO A COUNTED STUB BY DEFAULT (L2, a disclosed cut, never a silent one): when the ranked set names an interface with implementors, or a HAS-A field edge into/out of it, the bundle used to spend the full // method contract or row list on it — the STUB instead says only how many rows the section held: (same shape for ). No stub for a section that would be EMPTY (an absent section stays absent). total= is that section's own pre-cap row count (packLego's post-dedup interface count; every matched HAS-A edge for compose, which caps nothing). next= carries the one restoring spelling that returns BOTH sections byte-identical to the un-stubbed render, in ONE call: --sections=lego,compose. **Try it** @@ -215,20 +215,20 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` - + + + + - - -& p, float alpha = 0.85f ) ... [17 more line(s); run it to see the whole thing] ``` @@ -236,15 +236,15 @@ $ ./build/ripwire . --for="rankGraphTeleport" **Caveats (stated by the binary):** -- marks it: T rows handed to the trim, S printed). -- TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). -- ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. +- STOPS WHEN COMPLETE: the ranked head (: the relevance floor within the top 40, or --pack-top-n) is the answer and every row of it ships. +- with S=T marks a shrink); +- Only a runaway guard (30KB, a bug signal) cuts rows: over_ceiling="1" and serving them all (S printed of the T rows handed to the trim). ### `--sections=lego,compose` **Answers:** (with --for) opt back into the full / render the stub above replaces (with --for=TASK) restore the / sections the ranked bundle collapses to a counted stub by default — this IS the stub's own next= spelling, so pasting it back unchanged is enough. -A comma-separated, order-insensitive, closed set (lego and/or compose, each named at most once); every other section of the bundle is unaffected — the budget is the stubbed answer's. Under an explicit --token-budget the restored sections are charged in full, so can carry fewer rows there. +A comma-separated, order-insensitive, closed set (lego and/or compose, each named at most once); every other section of the bundle is unaffected — the budget is the stubbed answer's, under an explicit --token-budget too (a soft target charges the rows only). **Try it** @@ -252,15 +252,15 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - - - - + + + + inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() ... [21 more line(s); run it to see the whole thing] ``` @@ -279,18 +279,21 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` - + + + + - +... [1 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--auto-bodies` @@ -311,17 +314,17 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - - - - + + + + inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth +struct FileHealth ... [20 more line(s); run it to see the whole thing] ``` @@ -344,16 +347,16 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - - - - - + + + + + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -inline ChurnRanking churnRankedGraph( const MainDispatch& d ) - +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… ... [20 more line(s); run it to see the whole thing] ``` @@ -376,15 +379,15 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - - - - + + + + inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() ... [21 more line(s); run it to see the whole thing] ``` @@ -407,15 +410,15 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - - - + + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) - + P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() -inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) ... [21 more line(s); run it to see the whole thing] ``` @@ -438,15 +441,15 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - - - - -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] - -runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) ... [22 more line(s); run it to see the whole thing] ``` @@ -468,7 +471,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -499,7 +502,7 @@ _The repo's best-in-class instance to imitate before writing new code (picked by $ ./build/ripwire . --exemplar="format byte sizes for humans" - + f, A&&... a ) @@ -528,9 +531,9 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ ``` $ ./build/ripwire . --recall="quality delta gating exit codes" -ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 +ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 -━━ README.md (relevance 8.099) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 219796 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +━━ README.md (relevance 8.028) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -593,20 +596,25 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1367678-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1362430-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 -━━ commands.md (relevance 10.148) ━━ [sections: 1 of 163 selected (191 in doc), section-granular; whole doc 575700 B; lines="3810-3814"; dropped_by_budget=162] [truncated: 1865 of 7738 bytes] -### `--field-affinity[=STRUCT]` +━━ commands.md (relevance 11.045) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 578530 B; lines="555-580"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base -**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? -Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +... [17 more line(s); run it to see the whole thing] ``` ### `--tree` @@ -622,7 +630,7 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + @@ -631,10 +639,10 @@ $ ./build/ripwire . --tree - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -693,9 +701,9 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - + - + @@ -724,20 +732,20 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable ``` $ ./build/ripwire . --no-stable --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -757,11 +765,11 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - + - - + + @@ -780,7 +788,7 @@ $ ./build/ripwire . --around=rankGraphTeleport **Answers:** show SYM's direct callers (1-hop in-edges) who calls SYM (1-hop in-edges). -file:name disambiguates a same-named symbol across files (like --around/--lego); Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere. Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) +file:name disambiguates a same-named symbol across files (like --around/--lego); Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere. Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) A call the resolver DECLINED to bind whose candidates include SYM is a row: a call site, never an edge or a count= row (16 at most, file-first: dc_shown=, dc_next=) **Try it** @@ -797,12 +805,13 @@ $ ./build/ripwire . --callers=DoesNotExist **Caveats (stated by the binary):** - file:name disambiguates a same-named symbol across files (like --around/--lego); +- Each caller row's sites_at= lists its call sites as pasteable file:line tokens (16 at most: sites_total=) A call the resolver DECLINED to bind whose candidates include SYM is a row: a call site, never an edge or a count= row (16 at most, file-first: dc_shown=, dc_next=) ### `--callees=SYM` **Answers:** what SYM calls (1-hop out-edges). -file:name disambiguates like --callers +file:name disambiguates like --callers; each call it makes that the resolver declined to bind is a row, never an edge or a count= row **Try it** @@ -810,13 +819,19 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - - - - + + + + + + + + + + + ``` @@ -824,7 +839,8 @@ $ ./build/ripwire . --callees=rankGraphTeleport **Caveats (stated by the binary):** -- file:name disambiguates like --callers +- file:name disambiguates like --callers; +- each call it makes that the resolver declined to bind is a row, never an edge or a count= row ### `--uses=SYM` @@ -839,16 +855,16 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - - - - - - - + + + + + + - - + + + ``` @@ -873,18 +889,18 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - - - + + + - - - + + + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -910,19 +926,19 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -945,12 +961,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - + + + + + + ``` @@ -967,11 +983,11 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - - + + @@ -993,7 +1009,7 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - @@ -1077,7 +1093,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1138,7 +1154,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 50dfd49c3 +at: 0a5d057d9 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1157,18 +1173,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1192,7 +1208,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1217,7 +1233,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - - + + - + - + @@ -1519,11 +1535,11 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) @@ -1552,16 +1568,16 @@ _Body-elided decl skeletons — recounted on this corpus. Measured as element by $ ./build/ripwire . --pack-signatures --top-k=10 - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -1585,7 +1601,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1612,11 +1628,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - + + + - - + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1788,15 +1804,15 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - + + + + + - - - + + + @@ -1825,18 +1841,18 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - - - - - - - - - + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1853,17 +1869,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1884,15 +1900,15 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - - - - - - + + + + + + + + - ``` @@ -1917,7 +1933,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1952,18 +1968,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - + - + - - + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1983,18 +1999,18 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - + - + - - + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2020,13 +2036,13 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - - - - - - + + + + + + ... [23 more line(s); run it to see the whole thing] ``` @@ -2051,8 +2067,8 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - - + + @@ -2086,16 +2102,16 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - - - + + + + + + - - - + + + @@ -2123,19 +2139,19 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - - + + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2158,11 +2174,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2191,16 +2207,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - + + - - - - + + + + - + ... [22 more line(s); run it to see the whole thing] ``` @@ -2223,16 +2239,16 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - - - + - - + + + + + - ... [22 more line(s); run it to see the whole thing] ``` @@ -2249,19 +2265,19 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - - - + + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2278,19 +2294,19 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2316,7 +2332,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2346,7 +2362,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2372,7 +2388,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2399,19 +2415,19 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - - + + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2508,19 +2524,19 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2622,15 +2638,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + - - + + - + @@ -2653,7 +2669,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2678,19 +2694,19 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - + + - + - - + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2715,16 +2731,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2849 files · 27441 symbols · 26516 edges · 3248 modules (12310 call-graph isolated) +2963 files · 28377 symbols · 27575 edges · 3389 modules (12685 call-graph isolated) Root: `.` -Call-graph isolate provenance: 3292 declaration, 1982 header, 2365 source, 4671 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3248) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1272 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 401 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 259 symbols +## Modules (call-graph clusters; showing 12 of 3389) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1285 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 230 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2740,14 +2756,14 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - - + + + - + + - + @@ -2772,17 +2788,17 @@ $ ./build/ripwire . --mermaid %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
    8097"] - n102["src/infra
    1280"] + n101["src
    8426"] + n102["src/infra
    1294"] end subgraph sg1 ["test"] - n103["test
    5171"] - n392["test/legochargefix
    546"] - n333["test/fixtures/recallpassage
    314"] - n467["test/ownerhopfix/py/pkg
    158"] - n280["test/expandmodefix
    151"] - n282["test/expandsibsfix
    149"] - n608["test/rubyschemafix
    99"] + n103["test
    5323"] + n450["test/legochargefix
    546"] + n375["test/fixtures/recallpassage
    314"] + n525["test/ownerhopfix/py/pkg
    158"] + n322["test/expandmodefix
    151"] + n324["test/expandsibsfix
    149"] + n666["test/rubyschemafix
    99"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2801,19 +2817,19 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + - + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2832,7 +2848,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2897,7 +2913,7 @@ _The same clean verdict with what it folded printed back: every stale acknowledg ``` $ ./build/ripwire . --quality-delta --show-stale - @@ -2926,7 +2942,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2951,7 +2967,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2969,6 +2985,17 @@ $ ./build/ripwire . --quality-delta A named kind's rows are exactly its rows in a full answer (same code, same bars); the header names the others (kinds_skipped=, skipped_why="selector"), stale acks are judged for the named kinds only, and a pass no named kind needs does not run. An unknown name is refused (exit 1) naming the twelve; refused beside --quality-ack or --quality-baseline and on the bare form (the working-tree form and the MCP quality_delta tool have no selector yet). A selection the changed files cannot answer (whole-tree kinds, with or without defect-shape) is never pre-empted by the memory projection: it runs in full, and only a real memory stop refuses it (exit 5) — so the kinds_next= replay is not degraded again. +**Try it** + +_A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B._ + +``` +$ ./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code + +. at=: commit+dirty+shallow. available=0: no score at all (dmm=UNAVAILABLE, reason= says why); never read as 1.000 or 0.000. combine=pooled: root dmm= is summed good over summed good+bad of the 3 properties (ripwire's own). low_loc=/low_cx=/low_params=: a unit is LOW risk at or under these lines / cyclomatic / params. base_units=/base_volume=/target_units=/target_volume=: units with a body and their line span per side. reason=: why no score (no unit's size, complexity or params moved; a tree failed to parse ...). --> -

    @@ -3058,7 +3085,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3088,14 +3115,14 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - - - - - + + + + + + ``` @@ -3142,12 +3169,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3163,12 +3190,12 @@ _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 bec ``` $ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3214,12 +3241,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":748,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3278,11 +3305,11 @@ _The same plan committed: per-file locks, re-verify-before-write, atomic rename, ``` $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":2931,"graph_unresolved":13056,"graph_unindexed":256,"counts_floor":true}}]} +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3296,7 +3323,7 @@ $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply **Answers:** "can I delete this?" — one call joining callers, blast radius, tests and history "can I delete this?" — ONE call composing signals the tool already computes for one already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=), every --uses read/write/import/call/extends site (uses=), how much of the blast radius the tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own zero-caller/internal-linkage shape at defs=1 (dead_code_candidate=). -ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; unmodelled when nothing was found for a kind used by reading or naming it (a variable, class, struct) whose uses those counts cannot see. callers_floor=/uses_floor= mark a count the index holds evidence of a miss for, and next= is the call that lists the rest. A caller row's sites_at= is its call sites as file:line tokens (p= stays the line where the caller is defined). +ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), possible-callers (none of either, but a call the resolver declined could have meant SYM: its rows), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; unmodelled when nothing was found for a kind used by reading or naming it (a variable, class, struct) whose uses those counts cannot see. callers_floor=/uses_floor= mark a count the index holds evidence of a miss for, and next= is the call that lists the rest. A caller row's sites_at= is its call sites as file:line tokens (p= stays the line where the caller is defined). **Try it** @@ -3312,8 +3339,8 @@ $ ./build/ripwire . --safe-delete=DoesNotExist **Caveats (stated by the binary):** - ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. -- FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; - unmodelled when nothing was found for a kind used by reading or naming it (a variable, class, struct) whose uses those counts cannot see. +- callers_floor=/uses_floor= mark a count the index holds evidence of a miss for, and next= is the call that lists the rest. ### `--slice=SYM[:VAR]` @@ -3328,19 +3355,19 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - - - - - - - - - - - - - + + + + + + + + + + + + + ... [3 more line(s); run it to see the whole thing] ``` @@ -3365,17 +3392,17 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + ... [22 more line(s); run it to see the whole thing] @@ -3419,10 +3446,10 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:9117 +$ ./build/ripwire . --at=src/graph.h:9275 - - + + ``` @@ -3446,7 +3473,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3472,11 +3499,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3551,7 +3578,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3585,14 +3612,14 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - + @@ -3621,17 +3648,17 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - - + + - + - + ... [2 more line(s); run it to see the whole thing] @@ -3656,11 +3683,11 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - + + + + + ... [14 more line(s); run it to see the whole thing] ``` @@ -3686,9 +3713,9 @@ _The same page with every t= line whole: the page above previews each line over ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text - - - + + + @@ -3714,10 +3741,10 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - + + - + @@ -3746,7 +3773,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3827,10 +3854,10 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - - - + + + @@ -3839,7 +3866,7 @@ $ ./build/ripwire . --field-affinity - + ... [17 more line(s); run it to see the whole thing] ``` @@ -3862,19 +3889,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - -... [18 more line(s); run it to see the whole thing] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3898,19 +3923,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - -... [18 more line(s); run it to see the whole thing] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit` @@ -3933,19 +3956,17 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - - - - - - - - -... [18 more line(s); run it to see the whole thing] + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--whereis`, `--doc-drift` @@ -3969,7 +3990,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -4004,10 +4025,10 @@ $ ./build/ripwire . --from-trace=- AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9117 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9158 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1751 - #3 0x102e01a44 in main src/main.cpp:4169 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9275 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9316 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 + #3 0x102e01a44 in main src/main.cpp:4173 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -4034,7 +4055,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4124,7 +4145,7 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4161,13 +4182,13 @@ _Fan-out form: one shared core + 3 per-agent slices carved along call-graph comm ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3 - wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4194,11 +4215,11 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) @@ -4267,16 +4288,16 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - - - - - - - + + + + + + + + ``` @@ -4301,21 +4322,20 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - - - - - - - + + + + + + + + - -... [1 more line(s); run it to see the whole thing] +... [2 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4338,7 +4358,7 @@ $ ./build/ripwire . --skipped - @@ -4424,19 +4444,19 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + - - + + + + + - - - - + ... [7 more line(s); run it to see the whole thing] ``` @@ -4459,22 +4479,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~5 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - + + + -... [12 more line(s); run it to see the whole thing] + + + + + + + + +... [8 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4536,7 +4556,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4564,7 +4584,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=80b7e589253a8e76 entries=855 +ripwire legend dictionary ripwire.dict/v1 dictv=f83935bb22c0b728 entries=885 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -4616,18 +4636,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - + - + - - + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4650,10 +4670,10 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + - - + + @@ -4681,20 +4701,20 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - + - - + + - + - + ... [7 more line(s); run it to see the whole thing] ``` @@ -4715,20 +4735,20 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4765,20 +4785,20 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co ``` $ ./build/ripwire . --no-cache --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4796,20 +4816,20 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses ``` $ ./build/ripwire . --no-ignore --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4832,20 +4852,20 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea ``` $ ./build/ripwire . --max-file-size=8K --top-k=3 - + - - + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4912,20 +4932,20 @@ _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and th ``` $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4968,10 +4988,10 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [728 more display lines; full output is 23573 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8048 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5 --no-inventory` + +*The same capped map without its inventory tier: --top-k=5 leaves files out, so the default map appends (program entries, and the files the ranked rows left out) after its rows; --no-inventory drops that tier, and the ranked rows are the same.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --top-k=0 --expand=rankGraphTeleport` + +*NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --token-budget=100` + +*GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* + +**exit code: 3** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 +````` + +## `./build/ripwire . --for="incremental cache invalidation when a file content hash changes"` + +*The task lens: ranked signatures + quality metrics framed for the task.* + +**wall time: 2.02s** + +````` + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline const McpIndex& getIndex( const std::string& root ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFile … [line truncated: 20 more bytes on this line] +inline std::uint64_t blobChecksum( std::string_view s ) noexcept +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes … [line truncated: 25 more bytes on this line] +inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult& … [line truncated: 7 more bytes on this line] +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t> … [line truncated: 25 more bytes on this line] +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& impl … [line truncated: 63 more bytes on this line] +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +struct IngestFileScan +inline std::string spanTierMemoPath( const std::string& diskPath ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cac … [line truncated: 99 more bytes on this line] +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +inline std::uint64_t byteHash( const char* data, std::size_t n ) noexcept +void emitGrepHandleLegend( bool enabled ) +… [111 more display lines; full output is 17069 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +**wall time: 1.12s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; +… [15 more display lines; full output is 4549 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --no-route` + +*Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* + +**wall time: 1.12s** + +````` + + + + + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + +L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +struct RankedGraph +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago& … [line truncated: 20 more bytes on this line] +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… +inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = n… +inline std::string didYouMean( const IngestResult& ing, std::string_view name ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span< … [line truncated: 45 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = n … [line truncated: 12 more bytes on this line] +struct DataSectionsCut +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### 2. Gates for A +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +#### The convergence disclosure contract +… [149 more display lines; full output is 22737 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` + +*T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* + +**wall time: 1.10s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + + + +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --adaptive` + +*Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* + +**wall time: 1.10s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [92 more display lines; full output is 15198 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` + +*Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* + +**wall time: 1.18s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +struct Bm25Params +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [128 more display lines; full output is 17937 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` + +*Same task with the anchor disabled — the contrast the flag exists for.* + +**wall time: 1.13s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +struct Bm25Params +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [136 more display lines; full output is 17955 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lego=Vehicle` + +*Interface -> implementors view: every existing impl of the named interface; the method contract is extracted for the C-family/Java/TS/Python tiers — for a Rust trait (this fixture) it discloses caveat="not-extracted-for-lang" rather than an empty list.* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --exemplar="format byte sizes for humans"` + +*The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* + +````` + + + + + f, A&&... a ) +{ + // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; + // `{}` on a char[N] is a different question that library versions answer differently, and an + // implementation that formats the ARRAY emits the trailing NUL and the uninitialised bytes after it. + // A regex sweep missed sites twice, so the compiler enforces it instead of a reviewer. + // Only a MUTABLE char[N] is rejected. A string literal is const char[N]; every implementation formats + // that as a string, and the hazard here is the reusable buffer that was written short. + static_assert( ( ... && !( std::is_array_v> + && !std::is_const_v>> ) ), + "pass rw::cstr( buf ) for a fixed char buffer: {} on a char[N] is not printf's %s" ); + std::fputs( std::format( f, std::forward( a )... ).c_str(), stream ); +}]]> +````` + +## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` + +*Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* + +````` + + + + +ripwire '.' --verify='calls(runDefaultMap, rankGraphTeleport)' + + +````` + +## `./build/ripwire . --help-task="write a cheerful release announcement"` + +*The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +````` +ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 + +━━ README.md (relevance 8.028) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | +| 5 | The memory guard stopped the run: over `--max-memory` (default 65% of the machine's memory) with no partial answer possible, or a verb other than the default map facing a partial index. One stderr line names the limit and the override. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ CHANGELOG.md (relevance 7.517) ━━ [sections: 1 of 105 selected (423 in doc), section-granular; whole doc 954736 B; lines="139-154"; dropped_by_budget=104] [truncated: 1597 of 3826 bytes] +### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) + +A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change +… [129 more lines, 11714 bytes total] +````` + +## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` + +*The directory-as-knowledge-base pattern: --recall pointed at a 1362430-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* + +````` +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 + +━━ commands.md (relevance 11.045) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 578530 B; lines="555-580"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base + +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? + +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: + +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. + +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … + +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` + +stderr: + +````` +ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pass --no-redact to disable +````` + +## `./build/ripwire . --tree` + +*File-by-file orientation map (top symbols per file).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [368 more display lines; full output is 11197 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 1.71s** + +````` +(empty) +````` + +Artifact written: + +````` + 157091 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8066 bytes on 1 raw line(s)] +````` + + +--- + +# navigate / answer a question + +## `./build/ripwire . --around=rankGraphTeleport` + +*Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [160 more display lines; full output is 7694 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` + +*The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1909 more display lines; full output is 52379 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` + +*The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --legend=full` + +*The same rows under --legend=full: the default is the compact dialect (one compact comment plus schema="ripwire.callers/v1" on the root), and --legend=full restores the prose legend — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB more legend. Works on EVERY XML verb.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** + +````` + + +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +````` + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3648 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface` + +*Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5587 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface --include-builtins` + +*The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5452 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --path=main,rankGraphTeleport` + +*Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* + +````` + + + + + + + + +````` + +## `./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [69 more display lines; full output is 6322 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --situ` + +*Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: 0a5d057d9 + (0 changed files — working tree is clean, nothing to analyze) +````` + +## `./build/ripwire . --test-gate` + +*Pre-PR gate on a CLEAN tree: no obligations, exit 0.* + +**wall time: 2.50s** + +````` + + +````` + +## `./build/ripwire . --grep=DISCLOSE` + +*Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16956 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --match='(if_statement) @i'` + +*The same shape query WITH an explicit capture — identical hits, no auto_captured= attribute.* + +**wall time: 1.03s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16840 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --query="teleport pagerank" --top-k=5` + +*Raw BM25 ranking (debug lens; --for is the real verb).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 3220 bytes on 1 raw line(s)] +````` + + +--- + +# zoom the detail ladder + +## `./build/ripwire . --for="pagerank power iteration" --detail=2` + +*Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* + +**wall time: 1.10s** + +````` + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [156 more display lines; full output is 21122 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --pack-signatures --top-k=10` + +*Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 88.8% fewer bytes at top-10, 86.2% at top-50, 87.4% at top-100 (re-derived 2026-10-10 when the C++ standard-member gate stopped std::vector/std::string calls such as out.push_back() and s.empty() from binding to in-tree namesakes by name: the ranked top-10/50 lost one-line container accessors (svector::push_back, WidePath::c_str, every empty) and gained one-line forwarding helpers and macros (kindIs, ASSUME, formatTo, emitTo) whose signature is nearly the whole body, so less of the set is elidable, from 92.7/88.7/87.6; before that, re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [144 more display lines; full output is 14801 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` + +*Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +## `./build/ripwire . --outline=rankGraphTeleport:1-10 --top-k=0` + +*CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +stderr: + +````` +ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — outlining the whole symbol (use --expand=rankGraphTeleport:1-10 for a body slice) +````` + +## `./build/ripwire . --expand=rankGraphTeleport --top-k=0` + +*Full body + inline callee signatures.* + +````` + + + + +: files the ranked rows show / do not (sum = files=), each named or counted below. : a program entry by its language's convention (C/Go/Rust main, Java/C# static main, a script main its module scope calls, __main__.py), callees as c rows; manifest entries not read. : n= unshown files under dir p=; f= names its code files, the rest (tests, docs, config, test/doc/example dirs) counted. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven outside the tree, no edge. locality_pinned=K: K calls pinned by locality alone (a guess). extent_suspect_syms=K: K defs failed containment, corpus-wide. macro_blanked_files=K: K files indexed from a macro-blanked re-parse. prov=scip|binding|import|split|final-segment: how that edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. : a non-test row an indexed test transitively reaches (absent otherwise, never 0). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, callers + co-changed files. ev=/ev_why=: essential complexity (2+: jumps block extract-method; absent: 1) / the jumps behind it, tag:count. ev_floor=1: ev= is a FLOOR; noreturn calls, macro-hidden exits and unresolved gotos are unseen. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. l=N: start line; only on a same-name overload's row, one row per body (bodyless decls fold into overloads=). --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [89 more display lines; full output is 10597 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [902 more display lines; full output is 23992 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 1.73s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5765 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 1.86s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [307 more display lines; full output is 16168 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 1.78s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 1.80s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5108 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --arch=test/archfix/rules.txt` + +*Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [764 more display lines; full output is 111448 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 1.68s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [686 more display lines; full output is 70498 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +````` + + + + +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf + +````` + +stderr: + +````` +ripwire: lint-rules: test/lintrulesfix/rules/malformed.yaml:5: expected 'key: value' — file skipped +[math degraded] lint-rules: malformed rule file skipped (lintrules.h:399, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +Input file: + +````` +#PROF_TSV_BEGIN one row per scope, aggregated across threads; counters are RAW integers +scope file line calls total_ms l1d_mpki +walk: chase pass x.cpp 9 12 48.500 7.250 +#PROF_TSV_END +````` + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 3428 bytes on 1 raw line(s)] +````` + +The joined finding — past the display cut above, extracted so the join is visible: + +````` +p = p->next +````` + +## `./build/ripwire . --communities` + +*Cluster the call graph into cohesive modules.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [190 more display lines; full output is 16802 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom` + +*Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [135 more display lines; full output is 7330 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom --zoom-levels=3` + +*The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [269 more display lines; full output is 12238 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2963 files · 28377 symbols · 27575 edges · 3389 modules (12685 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 3389) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1285 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 230 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:9077 [pack,allow,insert]** — 182 symbols +- **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [open,get,close]** — 89 symbols +- **src::relForHash@model.h:1674:137245 [clone,read,add]** — 60 symbols +- **test::no@estchargecheck.sh:51:3991 [skip]** — 57 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,call,compute]** — 52 symbols +- **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols +- **src::tokText@defectshape.h:128:5308 [scan,find,join]** — 41 symbols +- **test/ownerhopfix/gain/pkg::target_01@fanout.py:74:1458 [push]** — 41 symbols + +## God files (most depended-on; showing 10 of 217) +- `src/model.h` — 93 dependents +- `src/infra/emit.h` — 86 dependents +- `src/infra/Diagnostics.h` — 61 dependents +- `src/serialize.h` — 42 dependents +- `src/graph.h` — 41 dependents +… [35 more lines, 3912 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60 more display lines; full output is 9257 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +````` +%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. +flowchart LR + subgraph sg0 ["src"] + n101["src
    8426"] + n102["src/infra
    1294"] + end + subgraph sg1 ["test"] + n103["test
    5323"] + n450["test/legochargefix
    546"] + n375["test/fixtures/recallpassage
    314"] + n525["test/ownerhopfix/py/pkg
    158"] + n322["test/expandmodefix
    151"] + n324["test/expandsibsfix
    149"] + n666["test/rubyschemafix
    99"] + n648["test/rubyattrsfix
    85"] + n501["test/massfix
    77"] + end + n73["docs
    844"] + n2[".github
    614"] + n0["(root)
    578"] + subgraph sg5 ["bench"] + n4["bench
    545"] + n5["bench/agentloop
    331"] + n49["bench/locbench/results/r5_pooling
    237"] + n50["bench/locbench/results/r6_expansion
    187"] + n59["bench/recalleval
    113"] + n43["bench/locbench
    107"] + n38["bench/headtohead/r3-headroom-2026-08-03
    102"] + n65["bench/skillrater/results/2026-09-07/arms
    100"] + n62["bench/shotgun
    96"] +… [19 more lines, 1580 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 1.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [930 more display lines; full output is 101731 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --dead-code=src` + +*Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* + +````` + + + + +````` + +## `./build/ripwire . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +````` + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +````` + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 14.83s** + +````` + + +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --show-stale` + +*The same clean verdict with what it folded printed back: every stale acknowledgement as an row and the full header. The regression-free answer above keeps only stale= and stale_by_kind= (and stale_next= names this call), and folds only when that saves a quarter of the bytes. Refused without --quality-delta.* + +**wall time: 12.46s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [122 more display lines; full output is 15040 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --edit-check=rankGraphTeleport` + +*Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* + +**wall time: 8.69s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +````` + + + +````` + +## `./build/ripwire . --pr-context=HEAD~1` + +*The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* + +**wall time: 2.95s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [258 more display lines; full output is 17244 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 11.04s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 12.43s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [167 more display lines; full output is 23733 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` + +*A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* + +**wall time: 1.63s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/ --plan` + +*Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* + +**wall time: 69.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [33517 more display lines; full output is 4640245 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` + +*A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* + +**exit code: 1** — **wall time: 1.43s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zero here would be a failure, not a measurement + (the filter is a substring match against refs/heads names; run bare --stray-content to list them, e.g. --stray-content=feat/) +````` + +## `./build/ripwire . --stray-content=lane/ --abi` + +*Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* + +**exit code: 2** — **wall time: 41.58s** + +````` + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --whereis=rankGraphTeleport` + +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.* + +**wall time: 6.32s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` + +*The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).* + +**wall time: 6.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13355 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code` + +*A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.* + +**wall time: 8.55s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14368 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` + +*Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* + +**wall time: 12.59s** + +````` + + + + + + + +````` + +## `./build/ripwire . --flags` + +*The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [382 more display lines; full output is 23480 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` + +*Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASA` + +*Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --flip: no gate named 'RIPWIRE_ASA' in . (did you mean 'RIPWIRE_ASAN'?) +ripwire: run `ripwire . --flags` for the gate table +````` + +## `./build/ripwire . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it with tests"` + +*NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* + +**wall time: 1.74s** + +````` +{"v":1,"verb":"plan-lanes","at":"0a5d057d9","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28377,"edges":275 … [line truncated: 338 more bytes on this line] +"symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2909,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":654,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1678,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/editcheck.h","n":"editCheckPriceRoot","scope":"rw","key":"33219bb04158a6a8","id":"./src/editcheck.h::rw::editCheckPriceRoot","id_addressable":true,"id_collides_with":0,"l":1189,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":10,"churn":52,"tested":0}, +{"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2685,"ord":0,"overloads":1,"amb":2,"cx":6,"ccx":6,"churn":36,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":1,"cx":8,"ccx":8,"churn":32,"tested":0}, +{"p":"./src/docdrift.h","n":"docDriftNextAttr","scope":"docdrift","key":"7723ed789a1c9c09","id":"./src/docdrift.h::docdrift::docDriftNextAttr","id_addressable":true,"id_collides_with":0,"l":2864,"ord":0,"overloads":1,"amb":0,"cx":6,"ccx":5,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"recordWeakDisclosures","scope":"docdrift","key":"972779210e49c0cf","id":"./src/docdrift.h::docdrift::recordWeakDisclosures","id_addressable":true,"id_collides_with":0,"l":2430,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":1,"churn":36,"tested":0}, +{"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1344,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":48,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":3030,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":11,"churn":447,"tested":0}, +{"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":356,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":19,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":690,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":447,"tested":0}, +{"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5654,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":408,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":48}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":32}, +{"p":"./src/editcheck.h","symbols":1,"churn":52,"ccx":10,"hotspot_rank":39}, +{"p":"./src/main.cpp","symbols":2,"churn":447,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":408,"ccx":2,"hotspot_rank":5}, +{"p":"./src/pageview.h","symbols":1,"churn":19,"ccx":2,"hotspot_rank":146}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":37}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":9,"hotspot_rank":23}]},"blast_radius":{"reaches":47,"files_total":16,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editcheck.h","./src/editplan.h","./src/editpreview.h","./src/flipimpact.h","./src/main.cpp","./src/mcp.h … [line truncated: 182 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-terra","reasoning":"high","rule":"moderate-cross-module","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":1,"max_ccx":11,"sum_ccx":61,"ambiguous_calls":6,"blast_reaches":47,"contract_touches":0,"conflicts":0,"untested":47 … [line truncated: 142 more bytes on this line] +{"id":"lane-1","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3320,"ord":0,"overloads":1,"amb" … [line truncated: 42 more bytes on this line] +{"p":"./src/main.cpp","n":"kJsonRideAlongFlags","scope":"","key":"2a90806fa5a580c1","id":null,"id_addressable":false,"id_collides_with":0,"l":3242,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":447,"tested":0}, +… [67 more display lines; full output is 20350 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +**wall time: 1.92s** + +Input file: + +````` +add a --since filter to the doc-drift verb +add the CLI parse arm and help text for the new filter +write regression tests for the new filter +````` + +````` +{"v":1,"verb":"plan-lanes","at":"0a5d057d9","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28377,"edges":27575,"ambiguous":2970,"unresolved":13753},"carve":null,"core":{"files" … [line truncated: 4 more bytes on this line] +"symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] +{"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":654,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":408,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3320,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, +{"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":10,"cx":14,"ccx":15,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":1,"cx":26,"ccx":38,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1678,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":80,"tested":0}, +{"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5654,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":408,"tested":0}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":1,"churn":503,"ccx":0,"hotspot_rank":6}, +{"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":32}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":99}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":408,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":60}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":4,"hotspot_rank":23}], +"symbols_total":3319,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":13,"module_span":6,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":6,"module_span":6,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":12,"blast_reaches":13,"contract_touches":1,"conflicts":1,"untested":13,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":123,"l":279,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":14, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2941,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3320,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":3020,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":123,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":47,"tested":0}, +{"p":"./src/cli.h","n":"serveHelpSelector","scope":"rw","key":"4b247ee064fc4e94","id":"./src/cli.h::rw::serveHelpSelector","id_addressable":true,"id_collides_with":0,"l":3085,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":503,"tested":0}, +{"p":"./src/query.h","n":"tryParsePredicateOnAll","scope":"query","key":"786acc4dbfec612b","id":"./src/query.h::query::tryParsePredicateOnAll","id_addressable":false,"id_collides_with":1,"l":113,"ord":0,"overloads":2,"amb":0,"cx":1,"ccx":0,"churn":20,"tested":0}, +… [62 more display lines; full output is 18943 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes=99 --task=x` + +*Out-of-range refusal shape for the lane count.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out) +````` + +## `./build/ripwire . --layout=Symbol` + +*CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped enum's underlying type is not a byte layout) +````` + +## `./build/ripwire . --doc-drift` + +*Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* + +````` + + + +
    + + + + + + + + + + + + +
    + + + + + + + + + + + + + +… [233 more display lines; full output is 24560 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --doc-drift --gateability` + +*The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [259 more display lines; full output is 25758 bytes on 1 raw line(s)] +````` + +Tail of the same output — the `` section: + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doc-drift --with-history` + +*Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* + +**wall time: 1.05s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [213 more display lines; full output is 24607 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --from-trace=-` + +*Map a pasted stack trace onto indexed symbols. CHANGED: in_corpus= now reports the real count (was 0).* + +Input file: + +````` +AddressSanitizer:DEADLYSIGNAL +================================================================= +==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9275 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9316 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 + #3 0x102e01a44 in main src/main.cpp:4173 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) +==41337==ABORTING +````` + +````` + + + + + + + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) +int runDefaultMap( const MainDispatch& d ) +int main( int argc, char** argv ) + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; +… [18 more display lines; full output is 4908 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --notes` + +*List all field notes (write-side memory) — the committed .ripwire_notes at the repo root, each with the sha/branch it was recorded at.* + +````` + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI"` + +*ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* + +````` + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 83 more bytes on this line] + +mcp-tools pass-through. Written into the server command only where its argument shape is known to be one more plain argument: the JSON args arrays (cursor/windsurf/gemini), opencode's command arrayinline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + +F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexcept +struct AgentTarget + + + + + + + + + +… [59 more display lines; full output is 9114 bytes on 51 raw line(s)] +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` + +*Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* + +````` + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const WrapListing& listing ) noexcept + +mcp-tools pass-through. Written into the server command only where its argument shape is known t…inline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + + + + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [117 more display lines; full output is 17086 bytes on 10 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +**wall time: 1.43s** + +````` +(empty) +````` + +Artifact written: + +````` + 454586 /aux/ripwire2.cc.json +{"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri +````` + +## `./build/ripwire . --batch=/aux/batch2.txt` + +*One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* + +**wall time: 1.92s** + +Input file: + +````` +for:incremental cache invalidation +callers:rankGraphTeleport +grep:DISCLOSE +lego:Vehicle +````` + +````` + + + + + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) + +Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] + +P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +struct FileHealth +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::ve … [line truncated: 8 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +struct McpIndex +inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& ca … [line truncated: 67 more bytes on this line] +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool … [line truncated: 37 more bytes on this line] +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) +static bool cachePathIsDirectory( const std::string& cachePath ) +inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +… [105 more display lines; full output is 48989 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 168 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) +````` + + +--- + +# self-diagnosis + +## `./build/ripwire . --doctor` + +*Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* + +**exit code: 1** — **wall time: 5.61s** + +````` + + + + + + + + + + +````` + + +--- + +# security + +## `./build/ripwire --scan-skill=skills/ripwire-orient/SKILL.md` + +*Scan a single skill file for injection/exfiltration patterns before installing.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) in skills/ripwire-orient/SKILL.md +````` + +## `./build/ripwire --scan-skills=skills` + +*Scan a whole skills directory (exit 2 = CRITICAL, 1 = WARN). Explicit-DIR form only.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s) skipped, 0 denylisted subtree(s) not descended) +````` + + +--- + +# knobs / modes + +## `./build/ripwire . --rank-by=churn --top-k=5` + +*Rank by git change-frequency prior instead of PageRank.* + +**wall time: 1.80s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~5 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +**wall time: 1.25s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=bogus --top-k=5` + +*An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf|churn|churn-decay) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=columnar` + +*Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* + +````` + + +0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp + +0,1,1,2,3,3,3 +getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap +1184,9314,9960,171,1424,1463,1750 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 +src/mcpindex.h:1288,src/graph.h:9317,src/graph.h:10011,src/eval.h:328,src/main.cpp:1438,src/main.cpp:1477 src/main.cpp:1478 src/main.cpp:1492,src/main.cpp:1870 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +**wall time: 1.10s** + +````` + + + +inline std::string spanTierMemoPath( const std::string& diskPath ) + + +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) + + +### See the map — not just the numbers + + +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) + + +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=bogus` + +*An unknown --format value REFUSES (exit 1), named, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidates) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --json` + +*Machine-parseable JSON, same content, keys mirror the XML attrs.* + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:9314","sites_at":["src/graph.h:9317"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:9960","sites_at":["src/graph.h:10011"]}, +{"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:328"]}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1424","sites_at":["src/main.cpp:1438"]}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1463","sites_at":["src/main.cpp:1477","src/main.cpp:1478","src/main.cpp:1492"]}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1750","sites_at":["src/main.cpp:1870"]}]} +````` + +## `./build/ripwire . --hotspots --json` + +*JSON refusal shape: an unsupported verb refuses loudly instead of silently falling back to XML.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --json is not yet supported for --hotspots — supported: the default map, --for, --pack-task, --callers/--callees, --impact, --quality-delta, --test-gate, --metrics, and --plan-lanes which is JSON-native (e.g. ripwire

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

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

    +

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

    +

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

    +

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

    +

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

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

    +

    + +````` + +## `./build/ripwire . --quality-delta --scope=src/graph.h` + +*OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* + +**wall time: 11.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [133 more display lines; full output is 16890 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --quality-ack --scope=src/graph.h --ack-only=api-surface` + +*The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* + +**exit code: 1** — **wall time: 7.12s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=src/graph.h — refusing, and writing nothing at all: + api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 + those rows belong to whoever is editing those paths. Acking them here writes their debt into a committed ledger under YOUR + reason string, which is how a per-finding ratchet becomes a rubber stamp. Narrow the pattern, or widen the scope if they really are yours. +````` + +## `./build/ripwire . --handoff` + +*The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* + +**wall time: 3.92s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --note-add="lessByScoreDescId: keep this branch-free — it sits inside the PageRank sort comparator"` + +*Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 keep this branch-free — it sits inside the PageRank sort comparator 0a5d057d90c183b4735c2b1ae91f1e9380999cec integration/train-28 +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sortutil.h::rw::sortutil::lessByScoreDescId' — that is the id --for/--expand key notes by +````` + +## `./build/ripwire . --notes` + +*The note is listed under the canonical id, dangling="0" — i.e. it will actually surface. (Before the H1 fix the bare name was stored verbatim and read dangling="1": recorded, and surfaced nowhere.)* + +````` + + + + + + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --note-add="gitOneLine: which one?"` + +*Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'gitOneLine' is ambiguous — it matches 2 definitions in 2 distinct contracts, and a note keys ONE canonical id (it would surface on one of them and look absent on the rest). Qualify one: ./src/handoff.h:gitOneLine, ./src/quality.h:gitOneLine — e.g. --note-add="./src/h … [line truncated: 33 more bytes on this line] +````` + +## `./build/ripwire . --note-add="lessByScoreDescIdd: typo"` + +*A name that resolves to nothing is refused with the read verbs' own did-you-mean — never written as a dead note.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByScoreDescId'?) — a note keys the canonical id a read verb resolves; to note a FILE instead, pass a path (one with a '/' or an extension), which may name a file that does not exist yet +````` + +## `./build/ripwire . --note-add="src/infra/sortutil.h::rw::sortutil::lessByScoreDescId: chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort"` + +*The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 0a5d057d90c183b4735c2b1ae91f1e9380999cec integration/train-28 +````` + +## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` + +*Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h` + +*Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* + +**wall time: 4.13s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: bash test/greptiercheck.sh +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check` + +*The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* + +**wall time: 1.82s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha (post-check skipped); next: --edit-check=src/infra/sortutil.h:lessByScoreDescId +````` + +## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` + +*Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* + +**wall time: 4.35s** + +````` +{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: --uses=src/infra/sortutil.h:nonNegativeFloatDescKey +````` + +## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h` + +*An unknown TARGET refuses and leaves every file byte-identical.* + +**exit code: 1** — **wall time: 1.82s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --dry-run` + +*A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* + +**wall time: 1.84s** + +Input file: + +````` +{ + "version": 1, + "edits": [ + { + "op": "insert_before_symbol", + "target": "nonNegativeFloatDescKey", + "payload": "plan_note.h" + } + ] +} +````` + +````` +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false," … [line truncated: 353 more bytes on this line] +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` + +*The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* + +**wall time: 4.11s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], +"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true}}]} +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json` + +*Neither --dry-run nor --apply: the mode is explicit, so this refuses.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --edit-plan requires exactly one of --dry-run or --apply +````` + +## `./build/ripwire . --quality-delta` + +*After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 11.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [127 more display lines; full output is 15987 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:88 (was=0 now=59) +````` + +## `./build/ripwire . --quality-baseline` + +*REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* + +**exit code: 1** — **wall time: 6.97s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against HEAD — pinning here would absorb + them into the floor, and every later --quality-delta would read clean. First: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey (was=0 now=59) + Commit the tree first, or pass --allow-dirty to pin anyway (the sidecar then records the 2 absorbed, and every + report against it carries baseline_absorbed="2"). +````` + +## `./build/ripwire . --quality-baseline --allow-dirty` + +*The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* + +**wall time: 10.25s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 27497 per-symbol rows, 28379 indexed symbols total) +````` + +Artifact written: + +````` + 4662919 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer cf55cfb2c2c2cdc47c158abf8e111806318c78056458198aa8e7c7fff5bf0393 +head 0a5d057d90c183b4735c2b1ae91f1e9380999cec +dirty 1 +absorbed 2 +ccx 67c15011ee8b 0 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699b +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 11.06s** + +````` + + +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional soft row target (the CLI --for --token-budget). The header carries route= (which ranker … [line truncated: 1061 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 841 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] +{"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"affected","description":"WHICH TESTS TO RUN for a change — test files that transitively reach the changed files/symbols, ranked by evidence (edited > partner-named > hop distance). files = changed files and/or symbols, comma-separated: each item is tried as an indexed PATH pattern first, … [line truncated: 1238 more bytes on this line] +{"name":"path_between","description":"Does A REACH B, and HOW? — the shortest directed CALL path between two symbols, hop-by-hop. reachable=\"0\" hops=\"0\" is a valid 'not reachable' answer — call edges are name-based, so a missing dynamic/callback edge can hide a real path. Named path_between … [line truncated: 709 more bytes on this line] +{"name":"connect","description":"When a task touches 2..16 named symbols, returns the minimal subgraph RELATING them - terminals, the fewest joining intermediaries (with signatures), and call edges in true direction - finding the shared-caller joins a directed path_between cannot; unrelated symbols … [line truncated: 950 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1475 more bytes on this line] +{"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] +{"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] +{"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] +{"name":"slice","description":"WHERE IS THIS VARIABLE DEFINED AND USED inside one function — NAME-BASED intra-procedural def-use rows of one variable inside ONE uniquely-resolved definition (the ARISE slicer, arXiv:2605.03117). symbol alone lists the sliceable locals to pick from; add var (or spel … [line truncated: 1722 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: + +````` +tools= 33 manifest_bytes= 47450 (~tokens at 4 bytes/token: 11862 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +whereis desc_bytes= 701 schema_bytes= 941 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +affected desc_bytes= 790 schema_bytes= 610 required=['files'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 453 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +lego desc_bytes= 360 schema_bytes= 651 required=['type'] +grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] +cochange desc_bytes= 388 schema_bytes= 476 required=['file'] +analyze desc_bytes= 546 schema_bytes= 318 required=[] +mentions desc_bytes= 305 schema_bytes= 486 required=['symbol'] +quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp --mcp-tools=core` + +*--mcp-tools=core: the server lists only the core profile's tools (explore, batch, from_trace, impact, uses, fetch_body, edit_check, quality_delta), so a host that loads every schema at session start pays only for those.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 955 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELAT … [line truncated: 1092 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 14191 (~tokens at 4 bytes/token: 3547 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` + +*MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* + +**wall time: 1.59s** + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= capped= (capped=1 cut). at=: commit+dirty+shallow. next= … [line truncated: 6517 more bytes on this line] +"_index":"[index: files=2963 symbols=28377 hash=e0ec0634]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=' && validxml "$WORK/o"; then ok "(R) --legend=full defines memory_peak= when it rides, and parses as XML" else no "(R) the legend lacks memory_peak= (or invalid XML)"; fi +# CodeRabbit #413: the peak sentence speaks for a changed-files answer too, never "complete" unconditionally. A degraded +# answer whose peak also went over is not reachable through one seam, so the wording is pinned here and the stderr +# twin keeps "the answer is complete" on the full answer above. +if sed -n 's/^\(\).*/\1/p' "$WORK/o" | grep -q -- 'memory_peak=[^.]*(complete, or the changed-files answer ingest_scope= names)'; then + ok "(R) the memory_peak= legend covers both a complete and a changed-files answer" +else no "(R) the memory_peak= legend still reads as complete-only"; fi +qd RIPWIRE_TEST_MEMGUARD=peak:1 "$BIN" . --quality-delta=HEAD~1..HEAD --no-cache +grep -q '^ripwire: memory peak .*: the answer is complete, but' "$WORK/e" \ + && ok "(R) a full answer's peak line still says the answer is complete" \ + || no "(R) the full answer's peak line: $( grep '^ripwire: memory peak' "$WORK/e" | head -c 200 )" # A selection the changed files could not answer (whole-tree kinds, with or without defect-shape) is never pre-empted by # the projection — it is the replay kinds_next= names, and degrading it could only refuse: it runs in full. A real stop # still refuses it, by name, and so does a defect-shape-only selection whose changed files cannot decide defect-shape From a586f7b99afc2acc7a837cc23914601ce7e7119b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 07:35:13 -0400 Subject: [PATCH 157/162] fix(for): the skipped body slot's next= goes through nextFlag CodeRabbit #413 comment 4293498755 (Minor). bodySlotSkippedXml spelled --expand=PATH:LINE:NAME by concatenation, so a path with a space or a shell metacharacter pasted as two arguments and did not serve the body. It is now built by nextFlag like every other next=, which quotes only where a shell would act: ordinary paths are byte-identical. forbodyslotcheck E2b: the fixture copied under a directory whose name holds a space; next= is quoted and splits to one argument (red on the pre-fix binary, unquoted). Co-Authored-By: Claude Opus 5.5 --- src/forowner.h | 2 +- test/forbodyslotcheck.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/forowner.h b/src/forowner.h index 0de4387b2..401ae6473 100644 --- a/src/forowner.h +++ b/src/forowner.h @@ -359,7 +359,7 @@ inline std::string bodySlotSkippedXml( const IngestResult& ing, NodeId id, BodyS std::string x = ""; return x; } diff --git a/test/forbodyslotcheck.sh b/test/forbodyslotcheck.sh index e2fcc024a..ec398f41a 100755 --- a/test/forbodyslotcheck.sh +++ b/test/forbodyslotcheck.sh @@ -71,6 +71,7 @@ # from 3000 in Phase B: the reserve-first share the rows at the cut leave holds the 2.7 KB jwt body from N=4800 on # this fixture; below it the slot is skipped and disclosed — E2, E3) # E2 [P] --token-budget=400: bodies=0 reason=budget; the skipped slot discloses itself on +# E2b [P] the same skip under a directory whose name holds a space: next= is quoted (nextFlag) and splits to one argument # E3 [G/P] N sweep: rows monotone + rank prefix + <= slot-off; body bytes monotone; [P] at 1000000 body == default body # E4 [P] T_REF at --token-budget=800 and MCP budget_tokens=800 (slot-off serves rows past the cut there, and the share # the rows at the cut leave holds the small refresh body): the body is paid INSIDE N — rows(slot) < rows(off), @@ -677,6 +678,13 @@ run --for="$T_D" --token-budget=400 >"$TMP/e400.xml" if r="$( chk rootattr "$TMP/e400.xml" bodies 0 )" && r2="$( chk rootattr "$TMP/e400.xml" reason budget )"; then ok "(E2) --token-budget=400: bodies=0 reason=budget"; else no "(E2) --token-budget=400: ${r:-} ${r2:-}"; fi if r="$( chk bodiesnext "$TMP/e400.xml" '^--expand=src/middleware/jwt/jwt\.ts:' )"; then ok "(E2) --token-budget=400: the skipped slot discloses itself, recoverable: $r"; else no "(E2) --token-budget=400: $r"; fi if r="$( chk prefix "$TMP/e400.xml" "$TMP/d.xml" )"; then ok "(E2) --token-budget=400: $r"; else no "(E2) --token-budget=400: $r"; fi +# E2b (CodeRabbit, #413): the skipped slot's next= is built by nextFlag, so a path a shell would split pastes as ONE argument +rm -rf "$TMP/spc"; cp -R "$FIX" "$TMP/spc" && mv "$TMP/spc/src/middleware/jwt" "$TMP/spc/src/middleware/j wt" +"$BIN" "$TMP/spc" --no-cache --for="$T_D" --token-budget=400 >"$TMP/e400spc.xml" 2>/dev/null +if r="$( chk bodiesnext "$TMP/e400spc.xml" "^--expand='src/middleware/j wt/jwt\.ts:[0-9]+:jwt'\$" )" \ + && [ "$( python3 -c 'import html,re,shlex,sys; m=re.search(r"]*\snext=\"([^\"]*)\"", open(sys.argv[1]).read()); print(len(shlex.split(html.unescape(m.group(1)))) if m else 0)' "$TMP/e400spc.xml" )" = 1 ]; then + ok "(E2b) a path with a space: the skipped slot's next= is quoted and splits to one argument: $r" +else no "(E2b) a path with a space: ${r:-the next= splits into more than one argument}"; fi prev_rows=0; prev_b=0; mono=1; nflag="" for n in 300 500 800 1200 2000 3000 5000 8000 1000000; do run --for="$T_D" --token-budget=$n >"$TMP/s$n.xml"; runoff --for="$T_D" --token-budget=$n >"$TMP/s$n.off.xml" From addb5724c1626b8771ea97eb8b88130981dcc1af Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 07:37:39 -0400 Subject: [PATCH 158/162] docs(readme): the section summary states 192 long flags, and the drift gate reads it CodeRabbit #413 comment 4293498746 (Minor). The "What it answers" summary still said 190 long flags while the sentence below it and --help say 192. readmedriftcheck (B) checked only the "N long flags advertised" sites; it now reads the `N long flags` summary as a third site (red on the previous README at line 985, green here). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- test/readmedriftcheck.sh | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2c3879aee..9556d97c0 100644 --- a/README.md +++ b/README.md @@ -982,7 +982,7 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers

    -190 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin +192 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin Around the core sit 192 long flags advertised in `--help`, across seven families — plus an MCP server, so a coding agent can call any of them mid-task instead of grepping and reading whole files. diff --git a/test/readmedriftcheck.sh b/test/readmedriftcheck.sh index 68db6a434..e26b079f0 100755 --- a/test/readmedriftcheck.sh +++ b/test/readmedriftcheck.sh @@ -103,12 +103,14 @@ fi # the core sentence, not the reference guide's restatement of it in §5. That is E9's failure one arm # over: a second copy of a gated number, free to drift alone. EVERY occurrence is checked now, and a # failure names the line. The loop reads a here-doc, not a pipeline, so `no` sets fail in THIS shell. -b_lines="$( grep -nE '[0-9]+ long flags advertised' "$README" || true )" +# 2026-10-16 (CodeRabbit, #413): the section summary `N long flags` restates it too, and drifted (190 vs 192) — +# it is a third site, read by the same loop. +b_lines="$( grep -nE '[0-9]+ long flags( advertised|)' "$README" || true )" b_seen=0; b_bad="" while IFS= read -r b_ln; do [ -z "$b_ln" ] && continue b_seen=$(( b_seen + 1 )) - b_num="$( printf '%s' "$b_ln" | grep -oE '[0-9]+ long flags advertised' | head -1 | grep -oE '^[0-9]+' )" + b_num="$( printf '%s' "$b_ln" | grep -oE '[0-9]+ long flags( advertised|)' | head -1 | grep -oE '^[0-9]+' )" [ "$b_num" = "$derived" ] || b_bad="$b_bad README.md:${b_ln%%:*}(states $b_num)" done < long flags advertised' site(s) in README.md state $derived, matching the derived count" + ok "(B) all $b_seen ' long flags advertised' / 'N long flags' site(s) in README.md state $derived, matching the derived count" fi # ── (C) mutation control — a wrong count in a COPY must be caught ─────────────────────────────────── From 7b2ecfcaf7aacf2d3ea1cfff369681f693b5e032 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 07:39:31 -0400 Subject: [PATCH 159/162] test(forsectioncollapse): re-anchor the MCP twin (13e) at 6000 tokens, where the section renders CodeRabbit #413 review nitpick (forsectioncollapsecheck.sh 448-451). At budget_tokens 3000 the opted-in sections render no , so the arm's "same rows" held whether or not the sections were charged and it could not fail on that. Like (13d), it now runs at 6000 and asserts what round C pins: a full , no over_ceiling, and rows that never grow with the opt-in (<=). Mutation control: the same arm at 3000 fails (no ). MCP `for` under budget_tokens serves the signatures bundle (uncut here, 40 rows each), so the counts are equal on this fixture. Co-Authored-By: Claude Opus 5.5 --- test/forsectioncollapsecheck.sh | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/forsectioncollapsecheck.sh b/test/forsectioncollapsecheck.sh index 943ee0439..e55500e45 100644 --- a/test/forsectioncollapsecheck.sh +++ b/test/forsectioncollapsecheck.sh @@ -438,17 +438,27 @@ else || no "(13d) --token-budget=6000 --sections=lego,compose: $LC_TBREST vs $LC_TB without it (or over_ceiling, or no full ) — an opted-in section is reserved first under a soft target" fi if command -v python3 >/dev/null 2>&1; then + # lc_mcp ARGS: " " — rows from (a cut bundle) or the rows inside an uncut , + # lego 1 when the full rides, over 1 on over_ceiling="1" lc_mcp(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"test/legochargefix","task":"%s"%s}}}\n' "$LCQ" "$1" \ | "$BIN" --mcp 2>/dev/null | python3 -c " import json,sys,re t = json.loads(sys.stdin.readline())['result']['content'][0]['text'] m = re.search(r' $LCM_DEF rows by default; under budget_tokens the sections cost no row ($LCM_TBREST == $LCM_TB)" \ - || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST (want >= 24, and the last two equal)" +b = re.search(r'(.*?)', t, re.S) +rows = m.group(1) if m else ( str( len( re.findall( r' at all, so "the same rows" held whether or not they were charged. What holds at + # 6000: the full rides, no over_ceiling, and the opted-in sections never ADD a row. (MCP `for` under + # budget_tokens serves the signatures bundle, uncut here, so the row counts are equal; the inequality is the contract.) + [[ "$LCM_DEF" =~ ^[0-9]+$ && "$LCM_TB" =~ ^[0-9]+$ && "$LCM_TBREST" =~ ^[0-9]+$ ]] && [ "$LCM_DEF" -ge 24 ] && [ "$LCM_TBREST" -le "$LCM_TB" ] \ + && [ "${LCM_LEGO:-0}" = 1 ] && [ "${LCM_OVER:-1}" = 0 ] \ + && ok "(13e) MCP for: $LCM_DEF rows by default; under budget_tokens=6000 the opted-in rides whole, no over_ceiling, and adds no row ($LCM_TBREST <= $LCM_TB)" \ + || no "(13e) MCP for: default=$LCM_DEF budget=$LCM_TB budget+sections=$LCM_TBREST lego=${LCM_LEGO:-?} over=${LCM_OVER:-?} (want >= 24, the last <= the middle, a full , no over_ceiling)" else printf ' SKIP %s\n' "(13e) python3 absent: the MCP twin of the lego charge is not checked" fi From 0e6edd91cfe8421531ef2906554fceb722ae49ff Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 07:41:43 -0400 Subject: [PATCH 160/162] docs(skills): --token-budget beside --for --limit is the candidate page, not a refused flag CodeRabbit #413 review, outside-diff comment (map-before-you-read.md 136-137, Minor). The orient skill listed --token-budget among the flags refused beside --for --limit, but since the budgeted-bundle candidate page (#362) that combination answers: a window over the ranked candidates the budgeted bundle cuts, continued by next=. The page now says so, keeps --json, --detail=N, --signatures-only and --top-k as refused (each checked against the binary: exit 1 with a refusal; --token-budget: exit 0, schema ripwire.candidate-page/v1), and spells --detail as the flag takes it. Co-Authored-By: Claude Opus 5.5 --- skills/ripwire-orient/map-before-you-read.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skills/ripwire-orient/map-before-you-read.md b/skills/ripwire-orient/map-before-you-read.md index 75783ce05..326a7dfb2 100644 --- a/skills/ripwire-orient/map-before-you-read.md +++ b/skills/ripwire-orient/map-before-you-read.md @@ -132,9 +132,10 @@ are deciding how much of them to read. - **On `--for`, `--limit=N` is not a cut — it is a WIDER net.** `--for="" --limit=40` answers with the file-grain page (one row per file with a positive-score symbol) instead of the ranked symbol bundle, and `--offset=M` continues it. That is the move when a `--for` answer came back thin (`coverage=` under 50, or - a head spread over fewer than three files) — widen first, then read one file. The bundle-shaping flags - (`--json`, `--detail`, `--signatures-only`, `--token-budget`, `--top-k`) are refused beside it, never - ignored: the page is a document of its own, not a shaped bundle. + a head spread over fewer than three files) — widen first, then read one file. With `--token-budget=N` beside + `--limit=K` the page is the candidate page instead: a `` window over the ranked candidates the budgeted + bundle cuts, continued by its `next=`. The bundle-shaping flags (`--json`, `--detail=N`, `--signatures-only`, + `--top-k`) are refused beside `--limit`, never ignored: the page is a document of its own, not a shaped bundle. - **Spans a service+client split?** `ripwire dir1 dir2 ` merges 2..16 checkouts into ONE labeled map — cheaper than reading each repo separately and cross-referencing an include/import by hand. - ripwire shines on **specific technical** asks. For a **broad common-word** question, plain `rg` + one read From 8234a8c337254ee854e7ba8c0da5a31e5466eec4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 16 Oct 2026 08:43:57 -0400 Subject: [PATCH 161/162] docs(captures): regenerate the showcase capture after the harvest's source moves The graph.h fix shifted rankGraphTeleport, so the published seed (graph.h:9275) no longer resolved (showcasecapturecheck H). Regenerated with test/showcase_capture.py from this tree's binary on a clean tree: the seed is re-derived (graph.h:9279), the rest is the capture's usual churn (wall times, line numbers, counts over this repository). Co-Authored-By: Claude Opus 5.5 --- docs/captures/COMMANDS_showcase_2026-10-16.md | 1643 ++++++++--------- 1 file changed, 816 insertions(+), 827 deletions(-) diff --git a/docs/captures/COMMANDS_showcase_2026-10-16.md b/docs/captures/COMMANDS_showcase_2026-10-16.md index da6301213..5d28b9d2d 100644 --- a/docs/captures/COMMANDS_showcase_2026-10-16.md +++ b/docs/captures/COMMANDS_showcase_2026-10-16.md @@ -2,8 +2,8 @@ - **Date:** 2026-10-16 (regenerated capture; supersedes any older `docs/captures/COMMANDS_showcase_*.md`) - **Lives in `docs/captures/`** — a directory the crawl/retrieval lenses SKIP (`kCrawlSkipDirs`, src/ingest.h): a generated doc that quotes every verb's output out-scores the source for any query about the tool and was measured at 77% of `--recall` on this repo when it sat at the root. `test/argvdiffcheck.sh` harvests its `## `-heading command lines as differential vectors — keep that format. -- **Version:** `ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=3d3abe61d)` -- **Repo:** the ripwire repo @ `0a5d057d` — **CLEAN — `git status --porcelain` is empty**. The diff-aware verbs (`--situ`/`--test-gate`/`--quality-delta`/`--pr-context`/`--map-diff`/`--edit-check`) answer a question about the WORKING TREE, so that condition is part of their answer and every one of their captions below states which tree it recorded against. A clean tree is the honest default for a showcase, so they appear TWICE: once here on the clean tree (their empty/exit-0 shape) and once in the final section against a throwaway `git clone --local` sandbox carrying one deliberate regression, so their real gating shapes are visible without writing a byte into the read-only repo. +- **Version:** `ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=0e6edd91c)` +- **Repo:** the ripwire repo @ `0e6edd91` — **CLEAN — `git status --porcelain` is empty**. The diff-aware verbs (`--situ`/`--test-gate`/`--quality-delta`/`--pr-context`/`--map-diff`/`--edit-check`) answer a question about the WORKING TREE, so that condition is part of their answer and every one of their captions below states which tree it recorded against. A clean tree is the honest default for a showcase, so they appear TWICE: once here on the clean tree (their empty/exit-0 shape) and once in the final section against a throwaway `git clone --local` sandbox carrying one deliberate regression, so their real gating shapes are visible without writing a byte into the read-only repo. - **Corpus:** the ripwire repo itself (dogfood), via `./build/ripwire` - **Sandbox diff** (the last section only): `.ripwire_notes | 2 ++ .ripwire_quality_acks | 3 ++- @@ -26,7 +26,7 @@ ````` - + @@ -64,7 +64,7 @@ ````` - + @@ -102,7 +102,7 @@ ````` - + @@ -132,7 +132,7 @@ - + @@ -217,26 +217,27 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 *The task lens: ranked signatures + quality metrics framed for the task.* -**wall time: 2.02s** +**wall time: 1.63s** ````` - - + incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 - + and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex -inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) -inline const McpIndex& getIndex( const std::string& root ) + +the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no gra…inline const McpIndex& getIndex( const std::string& root ) +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) inline std::uint64_t contentHash64( std::string_view s ) noexcept struct CacheEntry inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFile … [line truncated: 20 more bytes on this line] inline std::uint64_t blobChecksum( std::string_view s ) noexcept -inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes … [line truncated: 25 more bytes on this line] inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult& … [line truncated: 7 more bytes on this line] inline std::string reAbsolutize( std::string_view rel, std::string_view root ) @@ -249,22 +250,21 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cac … [line truncated: 99 more bytes on this line] inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) inline std::uint64_t byteHash( const char* data, std::size_t n ) noexcept -void emitGrepHandleLegend( bool enabled ) -… [111 more display lines; full output is 17069 bytes on 1 raw line(s)] +… [112 more display lines; full output is 17179 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="rankGraphTeleport"` *Name-shaped query: the router picks name-exact BM25 (header says which/why).* -**wall time: 1.12s** +**wall time: 1.69s** ````` - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -275,7 +275,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -297,26 +297,26 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 *Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* -**wall time: 1.12s** +**wall time: 1.54s** ````` - + - + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) inline ChurnRanking churnRankedGraph( const MainDispatch& d ) L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -struct RankedGraph -inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) -inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +struct RankedGraph +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago& … [line truncated: 20 more bytes on this line] inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = n… inline std::string didYouMean( const IngestResult& ing, std::string_view name ) @@ -325,8 +325,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = n … [line truncated: 12 more bytes on this line] struct DataSectionsCut inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) -inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) -### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] ### 2. Gates for A inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 #### The convergence disclosure contract @@ -337,14 +337,14 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 *T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* -**wall time: 1.10s** +**wall time: 1.41s** ````` - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -361,10 +361,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 *Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* -**wall time: 1.10s** +**wall time: 1.55s** ````` - + @@ -401,10 +401,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 *Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* -**wall time: 1.18s** +**wall time: 1.59s** ````` - @@ -434,17 +434,17 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) struct RecallSectionPick inline double binomialChoose( std::size_t n, std::size_t k ) -… [128 more display lines; full output is 17937 bytes on 1 raw line(s)] +… [128 more display lines; full output is 17938 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` *Same task with the anchor disabled — the contrast the flag exists for.* -**wall time: 1.13s** +**wall time: 1.42s** ````` - @@ -474,7 +474,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) struct RecallSectionPick inline double binomialChoose( std::size_t n, std::size_t k ) -… [136 more display lines; full output is 17955 bytes on 1 raw line(s)] +… [136 more display lines; full output is 17956 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lego=Vehicle` @@ -484,7 +484,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 ````` - + @@ -500,7 +500,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 ````` - + f, A&&... a ) @@ -550,7 +550,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=9496 > budget=100 ````` ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 -━━ README.md (relevance 8.028) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +━━ README.md (relevance 8.029) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -574,7 +574,7 @@ refuses by default. -━━ CHANGELOG.md (relevance 7.517) ━━ [sections: 1 of 105 selected (423 in doc), section-granular; whole doc 954736 B; lines="139-154"; dropped_by_budget=104] [truncated: 1597 of 3826 bytes] +━━ CHANGELOG.md (relevance 7.518) ━━ [sections: 1 of 105 selected (423 in doc), section-granular; whole doc 954807 B; lines="139-154"; dropped_by_budget=104] [truncated: 1597 of 3826 bytes] ### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change @@ -583,38 +583,17 @@ A tripped memory guard in the range form used to give exit 5 and nothing, so on ## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` -*The directory-as-knowledge-base pattern: --recall pointed at a 1362430-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* +*The directory-as-knowledge-base pattern: --recall pointed at a 1370299-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* ````` -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 - -━━ commands.md (relevance 11.045) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 578530 B; lines="555-580"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +━━ commands.md (relevance 10.110) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 586190 B; lines="3844-3848"; dropped_by_budget=163] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd - `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, - `.log`, `.json` and extensionless files are **not** documents to it. A directory of those - answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched - your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, - never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir - holds five dumps, and the header says `2 relevant of 2 document files` because only the two - `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not - in the population at all. - -2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so - an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document - SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R - selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got - and what the ceiling cost. … +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] (capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ````` @@ -667,7 +646,7 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas *Self-contained HTML force-directed call graph.* -**wall time: 1.71s** +**wall time: 2.21s** ````` (empty) @@ -676,7 +655,7 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas Artifact written: ````` - 157091 /aux/map2.html + 156961 /aux/map2.html ````` ## `./build/ripwire . --order=stable --top-k=5` @@ -730,7 +709,7 @@ Artifact written: - + @@ -768,7 +747,7 @@ Artifact written: - + @@ -806,7 +785,7 @@ Artifact written: - + @@ -851,10 +830,10 @@ Artifact written: ````` - + - - + + @@ -869,10 +848,10 @@ Artifact written: ````` - + - - + + @@ -884,7 +863,7 @@ Artifact written: *Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* -**exit code: 1** +**exit code: 1** — **wall time: 1.21s** ````` @@ -903,18 +882,18 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - + + - - - - - - + + + + + + ````` @@ -924,10 +903,10 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - - + + @@ -943,35 +922,35 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - + - + - + - + - + - - - - - - - - - - + + + + + + + + + + … [28 more display lines; full output is 3648 bytes on 1 raw line(s)] ````` @@ -982,14 +961,14 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - - - - + + + + + - + @@ -1000,14 +979,14 @@ ripwire: --callers symbol not found: DoesNotExist - + - + … [73 more display lines; full output is 5587 bytes on 1 raw line(s)] @@ -1020,20 +999,20 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - + + - - + + - - + + - - + + @@ -1046,7 +1025,7 @@ ripwire: --callers symbol not found: DoesNotExist - + … [73 more display lines; full output is 5452 bytes on 1 raw line(s)] ````` @@ -1057,12 +1036,12 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - + ````` @@ -1072,10 +1051,10 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - + @@ -1107,8 +1086,8 @@ ripwire: --callers symbol not found: DoesNotExist - - + + @@ -1144,7 +1123,7 @@ ripwire: --callers symbol not found: DoesNotExist ````` - @@ -1163,10 +1142,12 @@ ripwire: --callers symbol not found: DoesNotExist *Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* +**wall time: 1.28s** + ````` ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 0a5d057d9 +at: 0e6edd91c (0 changed files — working tree is clean, nothing to analyze) ````` @@ -1174,7 +1155,7 @@ at: 0a5d057d9 *Pre-PR gate on a CLEAN tree: no obligations, exit 0.* -**wall time: 2.50s** +**wall time: 2.84s** ````` @@ -1189,7 +1170,7 @@ at: 0a5d057d9 ````` captured nodes with their enclosing symbol; grammars=/eligible_files= the scope. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). counts_floor=1: every count is a FLOOR, never a total. hits_capped=1: hits= is a floor. root=: p= relative to it. of_files=: indexed files in all (eligible_files= of them are in the query's languages). --> @@ -1403,14 +1386,14 @@ at: 0a5d057d9 - + - + - + @@ -1421,12 +1404,12 @@ at: 0a5d057d9 - + - + @@ -1442,10 +1425,10 @@ at: 0a5d057d9 *Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* -**wall time: 1.10s** +**wall time: 1.58s** ````` - + @@ -1457,16 +1440,16 @@ at: 0a5d057d9 The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) -struct RankedGraph +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… inline std::uint32_t sliceRdIterationCeiling() noexcept struct PageRankRun -inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept -inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window -inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept -HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept inline std::string powerShellSingleQuote( std::string_view s ) noexcept inline std::string path_prepend_hint( std::string_view dir ) handle_t leftmost_leaf() const @@ -1487,7 +1470,7 @@ at: 0a5d057d9 - + @@ -1525,7 +1508,7 @@ at: 0a5d057d9 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1554,7 +1537,7 @@ at: 0a5d057d9 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1589,7 +1572,7 @@ ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — - + : files the ranked rows show / do not (sum = files=), each named or counted below. : a program entry by its language's convention (C/Go/Rust main, Java/C# static main, a script main its module scope calls, __main__.py), callees as c rows; manifest entries not read. : n= unshown files under dir p=; f= names its code files, the rest (tests, docs, config, test/doc/example dirs) counted. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven outside the tree, no edge. locality_pinned=K: K calls pinned by locality alone (a guess). extent_suspect_syms=K: K defs failed containment, corpus-wide. macro_blanked_files=K: K files indexed from a macro-blanked re-parse. prov=scip|binding|import|split|final-segment: how that edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. : a non-test row an indexed test transitively reaches (absent otherwise, never 0). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, callers + co-changed files. ev=/ev_why=: essential complexity (2+: jumps block extract-method; absent: 1) / the jumps behind it, tag:count. ev_floor=1: ev= is a FLOOR; noreturn calls, macro-hidden exits and unresolved gotos are unseen. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. l=N: start line; only on a same-name overload's row, one row per body (bodyless decls fold into overloads=). --> - + @@ -1815,7 +1798,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan ````` - + @@ -1835,7 +1818,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + @@ -1843,24 +1826,24 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan -… [902 more display lines; full output is 23992 bytes on 1 raw line(s)] +… [908 more display lines; full output is 24123 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --hotspots` *Complexity x recent git churn (maintenance pain).* -**wall time: 1.73s** +**wall time: 2.44s** ````` - - + + - + - + @@ -1872,7 +1855,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + @@ -1883,18 +1866,18 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan -… [14 more display lines; full output is 5765 bytes on 1 raw line(s)] +… [14 more display lines; full output is 5766 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --clones` *Token-normalized duplicate bodies.* -**wall time: 1.86s** +**wall time: 2.43s** ````` - + @@ -1930,38 +1913,38 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Files that change together in git (hidden coupling).* -**wall time: 1.78s** +**wall time: 2.08s** ````` - - + - + - + + + + + + - - - - + + - - - + - + - + @@ -1972,36 +1955,36 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Hotspots scoped to RECENT churn (the regression lens).* -**wall time: 1.80s** +**wall time: 2.48s** ````` - - + + - - + + - + - - - + + + - - + + + - - + @@ -2050,7 +2033,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* -**wall time: 1.68s** +**wall time: 1.92s** ````` @@ -2170,36 +2153,36 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - + + + - - - + - + - + -… [190 more display lines; full output is 16802 bytes on 1 raw line(s)] +… [189 more display lines; full output is 16769 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --zoom` @@ -2208,9 +2191,9 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - - + + @@ -2246,36 +2229,36 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - - - + + + - + - - - - - + - + + + + + -… [269 more display lines; full output is 12238 bytes on 1 raw line(s)] +… [271 more display lines; full output is 12308 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --report` @@ -2287,20 +2270,20 @@ The joined finding — past the display cut above, extracted so the join is visi # ripwire architecture report -2963 files · 28377 symbols · 27575 edges · 3389 modules (12685 call-graph isolated) +2963 files · 28378 symbols · 27578 edges · 3382 modules (12685 call-graph isolated) Root: `.` Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3389) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1285 symbols +## Modules (call-graph clusters; showing 12 of 3382) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1310 symbols - **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 230 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 229 symbols - **docs::`--top-k=N`@COMMANDS.md:59:9077 [pack,allow,insert]** — 182 symbols - **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols - **src/infra::fail@os_win32.cpp:63:2553 [open,get,close]** — 89 symbols -- **src::relForHash@model.h:1674:137245 [clone,read,add]** — 60 symbols +- **src::relForHash@model.h:1674:137245 [clone,read,add]** — 62 symbols - **test::no@estchargecheck.sh:51:3991 [skip]** — 57 symbols - **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,call,compute]** — 52 symbols - **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols @@ -2313,7 +2296,7 @@ Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 - `src/infra/Diagnostics.h` — 61 dependents - `src/serialize.h` — 42 dependents - `src/graph.h` — 41 dependents -… [35 more lines, 3912 bytes total] +… [35 more lines, 3923 bytes total] ````` ## `./build/ripwire . --seams` @@ -2322,7 +2305,7 @@ Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 ````` - + @@ -2362,7 +2345,7 @@ Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
    8426"] + n101["src
    8427"] n102["src/infra
    1294"] end subgraph sg1 ["test"] @@ -2396,11 +2379,11 @@ flowchart LR *Bus-factor: recency-weighted author ownership per file.* -**wall time: 1.71s** +**wall time: 2.05s** ````` - + @@ -2438,7 +2421,7 @@ flowchart LR ````` - + ````` @@ -2460,7 +2443,7 @@ flowchart LR ````` - ````` @@ -2469,11 +2452,11 @@ flowchart LR *On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* -**wall time: 14.83s** +**wall time: 14.04s** ````` - ````` @@ -2487,11 +2470,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The same clean verdict with what it folded printed back: every stale acknowledgement as an row and the full header. The regression-free answer above keeps only stale= and stale_by_kind= (and stale_next= names this call), and folds only when that saves a quarter of the bytes. Refused without --quality-delta.* -**wall time: 12.46s** +**wall time: 14.32s** ````` - @@ -2533,14 +2516,14 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* -**wall time: 8.69s** +**wall time: 1.83s** ````` - - - + + @@ -2552,9 +2535,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* +**wall time: 1.26s** + ````` - ````` @@ -2563,55 +2548,45 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* -**wall time: 2.95s** +**wall time: 3.69s** ````` - - + - + - + - - - - - - - - - - - - - - - - - - - - - -… [258 more display lines; full output is 17244 bytes on 1 raw line(s)] + + + + + + + + + + + + ````` ## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` *Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* -**wall time: 11.04s** +**wall time: 10.41s** ````` - - + + - + @@ -2623,11 +2598,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* -**wall time: 12.43s** +**wall time: 15.21s** ````` - + @@ -2656,24 +2631,24 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g -… [167 more display lines; full output is 23733 bytes on 1 raw line(s)] +… [167 more display lines; full output is 23731 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` *A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* -**wall time: 1.63s** +**wall time: 2.10s** ````` - + - + @@ -2690,11 +2665,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* -**wall time: 69.15s** +**wall time: 81.27s** ````` - + @@ -2730,7 +2705,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* -**exit code: 1** — **wall time: 1.43s** +**exit code: 1** — **wall time: 1.96s** ````` (empty) @@ -2747,11 +2722,11 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* -**exit code: 2** — **wall time: 41.58s** +**exit code: 2** — **wall time: 63.72s** ````` - @@ -2772,14 +2747,14 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.* -**wall time: 6.32s** +**wall time: 8.32s** ````` - - - - + + + + - + ````` @@ -2802,14 +2777,14 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).* -**wall time: 6.15s** +**wall time: 8.16s** ````` - - + + - + @@ -2819,12 +2794,12 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - - - - - - + + + + + + @@ -2833,8 +2808,8 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - - + + … [36 more display lines; full output is 13355 bytes on 1 raw line(s)] ````` @@ -2842,11 +2817,11 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.* -**wall time: 8.55s** +**wall time: 10.31s** ````` - - - + + + @@ -2870,12 +2845,12 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - - - - - - + + + + + + @@ -2884,8 +2859,8 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - - + + … [35 more display lines; full output is 14368 bytes on 1 raw line(s)] ````` @@ -2894,15 +2869,15 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* -**wall time: 12.59s** +**wall time: 13.93s** ````` - - + + - - + + ````` @@ -3001,10 +2976,10 @@ ripwire: run `ripwire . --flags` for the gate table *NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* -**wall time: 1.74s** +**wall time: 2.15s** ````` -{"v":1,"verb":"plan-lanes","at":"0a5d057d9","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28377,"edges":275 … [line truncated: 338 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"0e6edd91c","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28378,"edges":275 … [line truncated: 338 more bytes on this line] "symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, @@ -3021,12 +2996,12 @@ ripwire: run `ripwire . --flags` for the gate table {"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":356,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":19,"tested":0}, {"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":690,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":447,"tested":0}, {"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5654,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":408,"tested":0}], +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5664,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":409,"tested":0}], "files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":48}, {"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":32}, {"p":"./src/editcheck.h","symbols":1,"churn":52,"ccx":10,"hotspot_rank":39}, {"p":"./src/main.cpp","symbols":2,"churn":447,"ccx":12,"hotspot_rank":4}, -{"p":"./src/mcpverbs.h","symbols":1,"churn":408,"ccx":2,"hotspot_rank":5}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":409,"ccx":2,"hotspot_rank":5}, {"p":"./src/pageview.h","symbols":1,"churn":19,"ccx":2,"hotspot_rank":146}, {"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":37}, {"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":9,"hotspot_rank":23}]},"blast_radius":{"reaches":47,"files_total":16,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editcheck.h","./src/editplan.h","./src/editpreview.h","./src/flipimpact.h","./src/main.cpp","./src/mcp.h … [line truncated: 182 more bytes on this line] @@ -3041,7 +3016,7 @@ ripwire: run `ripwire . --flags` for the gate table *NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* -**wall time: 1.92s** +**wall time: 2.25s** Input file: @@ -3052,10 +3027,10 @@ write regression tests for the new filter ````` ````` -{"v":1,"verb":"plan-lanes","at":"0a5d057d9","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28377,"edges":27575,"ambiguous":2970,"unresolved":13753},"carve":null,"core":{"files" … [line truncated: 4 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"0e6edd91c","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2963,"symbols":28378,"edges":27578,"ambiguous":2970,"unresolved":13765},"carve":null,"core":{"files" … [line truncated: 4 more bytes on this line] "symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] {"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":654,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":408,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":654,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":409,"tested":0}, {"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3320,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":503,"tested":0}, {"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, {"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":10,"cx":14,"ccx":15,"churn":36,"tested":0}, @@ -3063,15 +3038,15 @@ write regression tests for the new filter {"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, {"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1678,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":80,"tested":0}, {"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, -{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5654,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":408,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5664,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":409,"tested":0}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], "files":[{"p":"./src/cli.h","symbols":1,"churn":503,"ccx":0,"hotspot_rank":6}, {"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":32}, {"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":99}, -{"p":"./src/mcpverbs.h","symbols":2,"churn":408,"ccx":2,"hotspot_rank":5}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":409,"ccx":2,"hotspot_rank":5}, {"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":60}, {"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":4,"hotspot_rank":23}], -"symbols_total":3319,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"symbols_total":3321,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], "tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":13,"module_span":6,"notes":[], "execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":6,"module_span":6,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":12,"blast_reaches":13,"contract_touches":1,"conflicts":1,"untested":13,"t … [line truncated: 139 more bytes on this line] {"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":123,"l":279,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":14, … [line truncated: 12 more bytes on this line] @@ -3161,9 +3136,11 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped *Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* +**wall time: 1.18s** + ````` - + @@ -3179,29 +3156,31 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped - - - - - - - - - - -… [233 more display lines; full output is 24560 bytes on 1 raw line(s)] + + + + + + + + + + +… [227 more display lines; full output is 23768 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --doc-drift --gateability` *The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* +**wall time: 1.15s** + ````` - + @@ -3217,20 +3196,20 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped - - - - - - - - - - -… [259 more display lines; full output is 25758 bytes on 1 raw line(s)] + + + + + + + + + + +… [253 more display lines; full output is 24966 bytes on 1 raw line(s)] ````` Tail of the same output — the `` section: @@ -3238,7 +3217,6 @@ Tail of the same output — the `` section: ````` - @@ -3247,6 +3225,7 @@ Tail of the same output — the `` section: + @@ -3267,12 +3246,12 @@ Tail of the same output — the `` section: *Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* -**wall time: 1.05s** +**wall time: 1.42s** ````` - - + + @@ -3288,19 +3267,19 @@ Tail of the same output — the `` section: - - - - - - - - - - -… [213 more display lines; full output is 24607 bytes on 1 raw line(s)] + + + + + + + + + + +… [207 more display lines; full output is 23815 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --from-trace=-` @@ -3313,8 +3292,8 @@ Input file: AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9275 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9316 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 #3 0x102e01a44 in main src/main.cpp:4173 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) @@ -3322,25 +3301,25 @@ AddressSanitizer:DEADLYSIGNAL ````` ````` - + - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] - + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) int runDefaultMap( const MainDispatch& d ) int main( int argc, char** argv ) - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -3458,10 +3437,10 @@ AddressSanitizer:DEADLYSIGNAL *Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges.* -**wall time: 1.11s** +**wall time: 1.50s** ````` - + @@ -3473,16 +3452,16 @@ AddressSanitizer:DEADLYSIGNAL The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) -struct RankedGraph +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… inline std::uint32_t sliceRdIterationCeiling() noexcept struct PageRankRun -inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept -inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window -inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept -HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept inline std::string powerShellSingleQuote( std::string_view s ) noexcept inline std::string path_prepend_hint( std::string_view dir ) handle_t leftmost_leaf() const @@ -3498,7 +3477,7 @@ AddressSanitizer:DEADLYSIGNAL *Per-file metrics as CodeCharta cc.json.* -**wall time: 1.43s** +**wall time: 1.62s** ````` (empty) @@ -3507,7 +3486,7 @@ AddressSanitizer:DEADLYSIGNAL Artifact written: ````` - 454586 /aux/ripwire2.cc.json + 454590 /aux/ripwire2.cc.json {"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri ````` @@ -3515,7 +3494,7 @@ Artifact written: *One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* -**wall time: 1.92s** +**wall time: 1.67s** Input file: @@ -3530,7 +3509,7 @@ lego:Vehicle - + @@ -3560,12 +3539,6 @@ lego:Vehicle … [105 more display lines; full output is 48989 bytes on 1 raw line(s)] ````` -stderr: - -````` -ripwire: cache : over its 2048 MiB budget — evicted 168 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) -````` - --- @@ -3575,17 +3548,17 @@ ripwire: cache : over its 2048 MiB budget — evicted 168 blob(s) of other *Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* -**exit code: 1** — **wall time: 5.61s** +**exit code: 1** — **wall time: 8.16s** ````` - - + - - + + - + @@ -3638,13 +3611,13 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s *Rank by git change-frequency prior instead of PageRank.* -**wall time: 1.80s** +**wall time: 2.13s** ````` - - + + @@ -3664,34 +3637,31 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s ````` -## `./build/ripwire . --rank-by=churn-decay --since=HEAD~5 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~8 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` *Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* -**wall time: 1.25s** +**wall time: 1.84s** ````` - - - + + + + - - - - - - - - + + + + - - - - + + + + - + ````` @@ -3717,15 +3687,15 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf ````` - + 0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp 0,1,1,2,3,3,3 getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap -1184,9314,9960,171,1424,1463,1750 +1184,9318,9964,171,1424,1463,1750 fn,fn,fn,fn,fn,fn,fn 0,0,0,0,0,0,0 -src/mcpindex.h:1288,src/graph.h:9317,src/graph.h:10011,src/eval.h:328,src/main.cpp:1438,src/main.cpp:1477 src/main.cpp:1478 src/main.cpp:1492,src/main.cpp:1870 +src/mcpindex.h:1288,src/graph.h:9321,src/graph.h:10015,src/eval.h:328,src/main.cpp:1438,src/main.cpp:1477 src/main.cpp:1478 src/main.cpp:1492,src/main.cpp:1870 ````` @@ -3734,24 +3704,24 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf *Flat top-K export for an external reranker.* -**wall time: 1.10s** +**wall time: 1.40s** ````` - - + + inline std::string spanTierMemoPath( const std::string& diskPath ) - + inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) - + ### See the map — not just the numbers - + inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) - + const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) @@ -3778,9 +3748,9 @@ ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidate *Machine-parseable JSON, same content, keys mirror the XML attrs.* ````` -{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, -{"t":"fn","n":"rankGraph","p":"src/graph.h:9314","sites_at":["src/graph.h:9317"]}, -{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:9960","sites_at":["src/graph.h:10011"]}, +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:9318","sites_at":["src/graph.h:9321"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:9964","sites_at":["src/graph.h:10015"]}, {"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:328"]}, {"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1424","sites_at":["src/main.cpp:1438"]}, {"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1463","sites_at":["src/main.cpp:1477","src/main.cpp:1478","src/main.cpp:1492"]}, @@ -3807,14 +3777,14 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Pagination: 3 items, skipping the first 3 (deterministic seams).* -**wall time: 1.76s** +**wall time: 2.20s** ````` - + - + ````` @@ -3826,7 +3796,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m ````` - + @@ -3864,7 +3834,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m ````` - + @@ -3899,11 +3869,13 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* +**wall time: 1.27s** + ````` - - + + @@ -3927,12 +3899,12 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* -**wall time: 1.77s** +**wall time: 2.10s** ````` - + @@ -3967,12 +3939,12 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* -**wall time: 1.85s** +**wall time: 2.20s** ````` - + @@ -4003,10 +3975,16 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m … [55 more display lines; full output is 7783 bytes on 1 raw line(s)] ````` +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 252 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) +````` + Artifact written: ````` - 25284949 /aux/warm2.ripwirecache + 25291183 /aux/warm2.ripwirecache ````` ## `./build/ripwire . --max-file-size=8K --top-k=3` @@ -4067,12 +4045,12 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Multi-root workspace: ONE merged graph over two roots, paths labeled /.* -**wall time: 1.57s** +**wall time: 1.50s** ````` - + @@ -4099,18 +4077,18 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Self-eval: co-change recall vs BM25.* -**wall time: 14.04s** +**wall time: 14.20s** ````` ripwire --eval (co-change recovery, averaged over 80 historical commits) ranker recall@5 recall@10 recall@20 - ripwire 2.6% 6.7% 9.3% - BM25 16.9% 21.5% 25.9% - BM25sub 16.4% 22.2% 32.3% - BM25body 7.4% 26.2% 37.7% - fused 7.7% 16.3% 27.8% - anchored 7.4% 26.2% 37.7% - same-dir 1.5% 7.3% 11.7% + ripwire 3.3% 7.1% 10.0% + BM25 15.5% 19.7% 24.2% + BM25sub 15.4% 22.2% 31.3% + BM25body 7.4% 24.6% 34.9% + fused 7.8% 15.6% 27.1% + anchored 7.4% 24.6% 34.9% + same-dir 0.8% 6.6% 10.4% random 0.2% 0.3% 0.7% <- floor (random ranking over F=2963 files) note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 / @@ -4127,7 +4105,7 @@ ripwire --eval (co-change recovery, averaged over 80 historical commits) *Known-item retrieval eval: MRR + recall@k per ranker per query mode.* -**wall time: 131.47s** +**wall time: 136.12s** ````` ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) @@ -4136,11 +4114,11 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) ranker query-mode MRR recall@1 recall@5 recall@10 - subtoken name 0.680 54.6% 84.5% 89.9% + subtoken name 0.680 54.6% 84.6% 90.0% subtoken doc-phrase 0.909 88.8% 92.9% 94.0% name-exact name 0.894 81.7% 95.7% 97.1% name-exact doc-phrase 0.014 0.6% 1.9% 2.9% - anchored name 0.685 56.1% 83.6% 88.3% + anchored name 0.686 56.1% 83.7% 88.4% anchored doc-phrase 0.905 88.2% 92.9% 93.6% routed name 0.896 81.9% 95.9% 97.3% routed doc-phrase 0.908 88.8% 92.7% 93.7% @@ -4156,7 +4134,7 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co *Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* -**exit code: 3** — **wall time: 16.86s** +**exit code: 3** — **wall time: 19.80s** Input file: @@ -4266,7 +4244,7 @@ Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after th *Version + short build info.* ````` -ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=3d3abe61d) +ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=0e6edd91c) ````` @@ -4274,27 +4252,27 @@ ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=3d3a # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:9275` +## `./build/ripwire . --at=src/graph.h:9279` *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* ````` - - + + ````` -## `./build/ripwire . --callers=@src/graph.h:9275` +## `./build/ripwire . --callers=@src/graph.h:9279` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - - + + @@ -4315,7 +4293,7 @@ ripwire 0.6.7 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=3d3a stderr: ````` -ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 13436 lines — the seed asked for line 999999) +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 13440 lines — the seed asked for line 999999) ````` ## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` @@ -4324,9 +4302,9 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - + - + ````` @@ -4336,10 +4314,10 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - + - - + + @@ -4381,21 +4359,21 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ````` @@ -4403,19 +4381,21 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* +**wall time: 1.04s** + ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + @@ -4427,26 +4407,26 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + - + pw = biasPrior( g, p );]]> - + & p, float alpha = 0.85f )]]> - + & p, float alpha = 0.85f )]]> @@ -4456,40 +4436,42 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Forward flow: which statements the seed's value reaches, at the default depth bound.* +**wall time: 1.05s** + ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + - + - + - + - + - + 0.0 )]]> - + - + @@ -4531,19 +4513,21 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* +**wall time: 1.01s** + ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + @@ -4553,12 +4537,12 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* -**wall time: 1.08s** +**wall time: 1.32s** ````` rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) ````` @@ -4566,11 +4550,11 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* -**wall time: 1.12s** +**wall time: 1.26s** ````` -DISCLOSE( result, AbiResult::DisclosureWhy::NoMergeBase, "abi: no merge-base for a ref (unrelated history?) — that ref DISCLOSE( Diagnostics::answerRefused, "--arch exits 1 with path:line and the reason on stderr; no report is printed", DISCLOSE( baseline, ArchBaselineRead::DisclosureWhy::SymlinkRefused, "arch: refusing to read the arch baseline sidecar t @@ -4625,7 +4609,7 @@ ripwire: --pattern: this pattern collapses to a single bare token, which is a TE ````` @@ -4906,10 +4890,10 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* -**wall time: 1.13s** +**wall time: 1.62s** ````` - + @@ -4946,10 +4930,10 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* -**wall time: 1.14s** +**wall time: 1.52s** ````` - + @@ -4986,80 +4970,80 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* -**wall time: 1.20s** +**wall time: 1.41s** ````` - + - + L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] -std::optional<int> runQualityDelta( const MainDispatch& d ) -struct DeltaBasis -int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…inline const char* kAcksFile = ".ripwire_quality_acks" -DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) -struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) -inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) -inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome -inline std::string qualityAcksPath( const std::string& root ) -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfS … [line truncated: 15 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…< … [line truncated: 3 more bytes on this line] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at -inline std::string gitHeadSha( const std::string& root ) -inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) -inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) -int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 6 more bytes on this line] -… [146 more display lines; full output is 17609 bytes on 1 raw line(s)] +… [146 more display lines; full output is 17633 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` *The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* -**wall time: 1.09s** +**wall time: 1.46s** ````` - + - + L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] -std::optional<int> runQualityDelta( const MainDispatch& d ) -struct DeltaBasis -int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…inline const char* kAcksFile = ".ripwire_quality_acks" -DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) -struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) -inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) -inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome -inline std::string qualityAcksPath( const std::string& root ) -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfS … [line truncated: 15 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size…< … [line truncated: 3 more bytes on this line] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at -inline std::string gitHeadSha( const std::string& root ) -inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) -inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) -int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 6 more bytes on this line] -… [146 more display lines; full output is 17360 bytes on 1 raw line(s)] +… [146 more display lines; full output is 17384 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --safe-delete=rankGraphTeleport` @@ -5068,10 +5052,10 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? ````` - - - + + @@ -5099,22 +5083,22 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* -**wall time: 2.18s** +**wall time: 3.02s** ````` - + - + - - - + + + + ````` @@ -5123,19 +5107,18 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* -**wall time: 1.96s** +**wall time: 2.59s** ````` - - + + - + ````` @@ -5149,7 +5132,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist - @@ -5175,7 +5158,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist -… [424 more display lines; full output is 45218 bytes on 1 raw line(s)] +… [424 more display lines; full output is 45219 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --no-ignore --top-k=3` @@ -5185,7 +5168,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist ````` - + @@ -5223,7 +5206,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist ````` - + @@ -5267,15 +5250,15 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o **exit code: 4** ````` - + - + const&, float) src/graph.h:9275 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9316 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 #3 0x102e01a44 in main src/main.cpp:4173 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> r( N, 0.f ); std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); return { std::move( r ), run.iterationCount, run.hasConverged }; -}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 288 more bytes on this line] +}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 288 more bytes on this line] ````` ## `./build/ripwire . --run-trace="true"` @@ -5316,7 +5299,7 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o ````` - + ````` @@ -5324,12 +5307,12 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o *A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* -**exit code: 4** — **wall time: 2.46s** +**exit code: 4** — **wall time: 2.52s** ````` - + ````` @@ -5364,11 +5347,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* -**wall time: 4.37s** +**wall time: 4.73s** ````` - @@ -5376,16 +5359,16 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + - + - + - + @@ -5406,8 +5389,8 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - + + @@ -5424,11 +5407,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - + - + @@ -5442,27 +5425,27 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - + - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + @@ -5471,18 +5454,18 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir -… [21 more display lines; full output is 11228 bytes on 1 raw line(s)] +… [21 more display lines; full output is 11233 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --nonlocal-state --limit=8` *Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* -**wall time: 2.20s** +**wall time: 2.38s** ````` - @@ -5518,22 +5501,22 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* -**wall time: 2.50s** +**wall time: 2.84s** ````` - - + - + @@ -5558,11 +5541,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* -**wall time: 1.10s** +**wall time: 1.24s** ````` - @@ -5598,11 +5581,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* -**wall time: 1.17s** +**wall time: 1.22s** ````` - @@ -5636,8 +5619,8 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - + + @@ -5672,11 +5655,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* -**wall time: 9.82s** +**wall time: 10.09s** ````` - + @@ -5709,7 +5692,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* -**wall time: 2.09s** +**wall time: 2.21s** ````` @@ -5825,7 +5808,7 @@ ripwire.connect/v1 : minimal joining subgraph: groups, terminal *Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* -**wall time: 1.70s** +**wall time: 1.74s** ````` @@ -5865,7 +5848,7 @@ ripwire.connect/v1 : minimal joining subgraph: groups, terminal *DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* -**wall time: 1.64s** +**wall time: 1.77s** ````` @@ -5905,7 +5888,7 @@ ripwire.connect/v1 : minimal joining subgraph: groups, terminal *A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped="1", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget.* -**wall time: 1.68s** +**wall time: 1.95s** ````` @@ -5922,7 +5905,7 @@ ripwire.connect/v1 : minimal joining subgraph: groups, terminal *Pasting that findings_next=: the same rule under a 10x budget counts its true total — no count_capped=, no findings_next=.* -**wall time: 1.61s** +**wall time: 1.84s** ````` @@ -5953,7 +5936,7 @@ ripwire.connect/v1 : minimal joining subgraph: groups, terminal *An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* -**exit code: 1** — **wall time: 1.60s** +**exit code: 1** — **wall time: 1.77s** ````` (empty) @@ -5969,7 +5952,7 @@ ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-' *A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* -**exit code: 1** — **wall time: 1.71s** +**exit code: 1** — **wall time: 1.84s** ````` (empty) @@ -5985,7 +5968,7 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- *The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* -**wall time: 1.72s** +**wall time: 1.95s** ````` {"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] @@ -6047,11 +6030,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* -**wall time: 3.62s** +**wall time: 3.90s** ````` -

    @@ -6062,11 +6045,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* -**wall time: 7.60s** +**wall time: 8.43s** ````` -

    @@ -6077,14 +6060,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The range form: tree HEAD vs tree HEAD~3.* -**wall time: 7.41s** +**wall time: 8.05s** ````` - - -

    -

    + + +

    +

    ````` @@ -6092,11 +6075,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* -**wall time: 1.86s** +**wall time: 2.72s** ````` - + @@ -6136,32 +6119,32 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M ````` - + - - + - + + - + - + - + - - - + + + @@ -6174,7 +6157,7 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* -**wall time: 1.74s** +**wall time: 1.81s** ````` (empty) @@ -6183,14 +6166,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M Artifact written: ````` - 157096 /aux/map2.html + 156966 /aux/map2.html ````` ## `./build/ripwire . --index-out=/aux/ci_index` *CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* -**wall time: 3.59s** +**wall time: 4.17s** ````` (empty) @@ -6199,16 +6182,16 @@ Artifact written: stderr: ````` -ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (25284949 bytes, lean family) -ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (52692185 bytes, rich family) +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (25291183 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (52704048 bytes, rich family) ````` Artifact written: ````` - 25284949 /aux/ci_index.lean.ripwirecache - 52692185 /aux/ci_index.rich.ripwirecache - 77977134 total + 25291183 /aux/ci_index.lean.ripwirecache + 52704048 /aux/ci_index.rich.ripwirecache + 77995231 total ````` ## `./build/ripwire . --cache=/aux/ci_index.lean.ripwirecache --top-k=3` @@ -6218,7 +6201,7 @@ Artifact written: ````` - + @@ -6256,7 +6239,7 @@ Artifact written: ````` - + @@ -6298,7 +6281,7 @@ Artifact written: # whose template-argument list spans lines keeps its line break as \n instead of splitting the row. # line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): # the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. - 118866 /aux/pin_census.tsv + 118900 /aux/pin_census.tsv ````` ## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` @@ -6309,7 +6292,7 @@ Artifact written: ````` - + @@ -6327,11 +6310,11 @@ Artifact written: *The ledger-shaped fixture through the same check (exit 2: one gating card).* -**exit code: 2** +**exit code: 2** — **wall time: 1.08s** ````` - + @@ -6343,17 +6326,17 @@ Artifact written: *--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* -**exit code: 1** — **wall time: 5.72s** +**exit code: 1** — **wall time: 7.39s** ````` - - + - - + + - + @@ -6390,14 +6373,14 @@ Everything below runs with `cwd` = the throwaway clone at `/dirty` (`gi *Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* -**wall time: 5.50s** +**wall time: 4.34s** ````` ripwire situational-awareness — 1 changed file(s), 13 symbols in them root: . -at: 0a5d057d9+dirty +at: 0e6edd91c+dirty [1] blast radius: 330 symbols across 73 files transitively depend on these changes (showing 8 of 73 files — shown=8 total=73 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=73) - counts_floor=1 graph_ambiguous=2970 graph_unresolved=13753 graph_unindexed=261 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missing … [line truncated: 45 more bytes on this line] + counts_floor=1 graph_ambiguous=2970 graph_unresolved=13765 graph_unindexed=261 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missing … [line truncated: 45 more bytes on this line] src/mcpverbs.h (34 dependent symbols) src/slice.h (30 dependent symbols) src/quality.h (16 dependent symbols) @@ -6413,8 +6396,8 @@ at: 0a5d057d9+dirty test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) test/rustimport_unit.cpp [hops=4] (run: bash test/rustimportprecisecheck.sh) script_gates_unmodelled=759 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) - [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="4674": - (none — 4674 commits were mined and none co-edited a file outside your diff) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="4692": + (none — 4692 commits were mined and none co-edited a file outside your diff) next: --test-gate ````` @@ -6422,7 +6405,7 @@ at: 0a5d057d9+dirty *The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* -**exit code: 4** — **wall time: 4.45s** +**exit code: 4** — **wall time: 3.44s** ````` @@ -6436,10 +6419,10 @@ at: 0a5d057d9+dirty - + - + @@ -6447,7 +6430,7 @@ at: 0a5d057d9+dirty - + @@ -6455,7 +6438,7 @@ at: 0a5d057d9+dirty - + ````` @@ -6464,11 +6447,11 @@ at: 0a5d057d9+dirty *Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* -**exit code: 2** — **wall time: 22.47s** +**exit code: 2** — **wall time: 23.71s** ````` - @@ -6511,11 +6494,11 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend.* -**exit code: 2** — **wall time: 11.98s** +**exit code: 2** — **wall time: 11.03s** ````` - @@ -6558,10 +6541,10 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* -**exit code: 2** — **wall time: 11.56s** +**exit code: 2** — **wall time: 11.13s** ````` -{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":149,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"0a5d057d9+dirty","renames":60,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 239 more bytes on this line] +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":149,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"0e6edd91c+dirty","renames":60,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 239 more bytes on this line] {"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, @@ -6605,7 +6588,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* -**exit code: 1** — **wall time: 7.40s** +**exit code: 1** — **wall time: 7.33s** ````` (empty) @@ -6623,7 +6606,7 @@ ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing accepte *NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* -**wall time: 7.09s** +**wall time: 7.40s** ````` (empty) @@ -6641,11 +6624,11 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already *Re-run after the partial ack: acked=3, the rest still gate (exit 2).* -**exit code: 2** — **wall time: 11.22s** +**exit code: 2** — **wall time: 11.08s** ````` - @@ -6704,11 +6687,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6720,11 +6703,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire row identical.* -**wall time: 1.85s** +**wall time: 1.48s** ````` - @@ -6736,11 +6719,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6776,13 +6759,13 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire ) and selectors take it. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - - + + @@ -6807,11 +6790,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire of ; dup_loc=/dup_pct=. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). root=: p= relative to it. groups=/type3=: Type-2 and Type-3 group totals over all groups; total= is their sum. exempt_groups=N: groups whose members all sit on fixture/shell-runner paths quality-delta duplication ignores. idiom_groups=/demoted_groups=: groups of one recognized idiom / those quality-delta demotes to minor; floors. clone_groups=N: clusters after merging pairs (rows sharing gid=); a floor under type3_capped=1. total_loc=N: lines of every function body the detector considered; dup_pct= is dup_loc= over it. group exempt=fixture|shell-runner: every member is on such a path; quality-delta duplication ignores it. group idiom=: the recognized shape every member spells: threshold-ladder, switch-name-table, builder-chain. --> - + @@ -6847,12 +6830,12 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire ; v=unknown = no merge base. at=: commit+dirty+shallow. head=: the HEAD commit, bare 9-hex sha (at= adds +dirty). head_ref=: HEAD's branch (HEAD when detached); that branch itself is not scanned. refs=N: local branches scanned (refs/heads only); unmerged + superseded + merged + unknown = refs. blobs=N: distinct git blobs read for the sweep. unmerged=N: refs whose authored work the live line genuinely lacks. superseded=N: refs whose work the live line re-implemented (removed the same base code). merged=N: refs whose work HEAD already has; omitted from the rows. name=: the local branch. tip=: the branch tip commit (9 hex). date=: the tip's committer date, YYYY-MM-DD. files=N: files with stray lines; rows capped at 12, a more element counts the rest (detail=1 lists all). ref superseded=N>: of this ref's stray= lines, those in files the live line re-implemented. unknown=N: refs that could not be analysed (v=unknown, e.g. no merge base); never counted merged. --> - - + + ````` @@ -6867,11 +6850,11 @@ stderr: *CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* -**wall time: 3.16s** +**wall time: 1.80s** ````` - + ````` @@ -6886,11 +6869,11 @@ stderr: *The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* -**wall time: 4.79s** +**wall time: 3.99s** ````` -

    @@ -6901,11 +6884,11 @@ stderr: *OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* -**wall time: 11.22s** +**wall time: 10.74s** ````` - @@ -6947,7 +6930,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* -**exit code: 1** — **wall time: 7.12s** +**exit code: 1** — **wall time: 6.82s** ````` (empty) @@ -6967,11 +6950,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* -**wall time: 3.92s** +**wall time: 2.96s** ````` - + @@ -6996,11 +6979,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr - - - - - + + + + + ````` @@ -7009,8 +6992,10 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* +**wall time: 1.03s** + ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 keep this branch-free — it sits inside the PageRank sort comparator 0a5d057d90c183b4735c2b1ae91f1e9380999cec integration/train-28 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 keep this branch-free — it sits inside the PageRank sort comparator 0e6edd91cfe8421531ef2906554fceb722ae49ff lane/cr-harvest-28 ````` stderr: @@ -7035,7 +7020,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort - + @@ -7087,7 +7072,7 @@ ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByS *The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 0a5d057d90c183b4735c2b1ae91f1e9380999cec integration/train-28 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 0e6edd91cfe8421531ef2906554fceb722ae49ff lane/cr-harvest-28 ````` ## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` @@ -7095,7 +7080,7 @@ src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-16 chose the flat *Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* ````` - + - + - + @@ -7216,16 +7201,16 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* -**wall time: 4.13s** +**wall time: 3.13s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ````` stderr: @@ -7238,7 +7223,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* -**wall time: 1.82s** +**wall time: 1.35s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] @@ -7254,19 +7239,19 @@ ripwire edit: applied atomically; receipt carries region, blob_sha (post-check s *Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* -**wall time: 4.35s** +**wall time: 3.02s** ````` {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ````` stderr: @@ -7279,7 +7264,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *An unknown TARGET refuses and leaves every file byte-identical.* -**exit code: 1** — **wall time: 1.82s** +**exit code: 1** — **wall time: 1.29s** ````` (empty) @@ -7295,7 +7280,7 @@ ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found *A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* -**wall time: 1.84s** +**wall time: 1.50s** Input file: @@ -7313,21 +7298,21 @@ Input file: ````` ````` -{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false," … [line truncated: 353 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false," … [line truncated: 353 more bytes on this line] ````` ## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` *The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* -**wall time: 4.11s** +**wall time: 2.85s** ````` -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true}}]} +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true}}]} ````` ## `./build/ripwire . --edit-plan=/aux/edit_plan.json` @@ -7350,11 +7335,11 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply *After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* -**exit code: 2** — **wall time: 11.66s** +**exit code: 2** — **wall time: 10.58s** ````` - @@ -7397,7 +7382,7 @@ ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: du *REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* -**exit code: 1** — **wall time: 6.97s** +**exit code: 1** — **wall time: 7.17s** ````` (empty) @@ -7416,7 +7401,7 @@ ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against *The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* -**wall time: 10.25s** +**wall time: 10.29s** ````` (empty) @@ -7426,16 +7411,16 @@ stderr: ````` ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") -ripwire: wrote ./.ripwire_quality_baseline (snapshot of 27497 per-symbol rows, 28379 indexed symbols total) +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 27498 per-symbol rows, 28380 indexed symbols total) ````` Artifact written: ````` - 4662919 .ripwire_quality_baseline + 4663100 .ripwire_quality_baseline # ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit -producer cf55cfb2c2c2cdc47c158abf8e111806318c78056458198aa8e7c7fff5bf0393 -head 0a5d057d90c183b4735c2b1ae91f1e9380999cec +producer 4f91846275151921d1985d769d4fd85dbf032735b96a2db0639ffbed3c4a9d5e +head 0e6edd91cfe8421531ef2906554fceb722ae49ff dirty 1 absorbed 2 ccx 67c15011ee8b 0 @@ -7448,11 +7433,11 @@ ccx 6a732f6699b *Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* -**wall time: 11.06s** +**wall time: 11.07s** ````` - ````` @@ -7575,82 +7560,86 @@ quality_delta desc_bytes= 833 schema_bytes= 193 required=[] *MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* -**wall time: 1.59s** +**wall time: 1.85s** ````` {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= capped= (capped=1 cut). at=: commit+dirty+shallow. next= … [line truncated: 6517 more bytes on this line] -"_index":"[index: files=2963 symbols=28377 hash=e0ec0634]","_fresh":"ok"}} +"_index":"[index: files=2963 symbols=28378 hash=2e14003a]","_fresh":"ok"}} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":" - + @@ -106,7 +106,7 @@ _The same capped map without its inventory tier: --top-k=5 leaves files out, so $ ./build/ripwire . --top-k=5 --no-inventory - + @@ -140,7 +140,7 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ $ ./build/ripwire . --max-tokens=1500 - + @@ -215,11 +215,11 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -252,7 +252,7 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - + @@ -279,11 +279,11 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -314,7 +314,7 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + @@ -347,13 +347,13 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - + - + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) inline ChurnRanking churnRankedGraph( const MainDispatch& d ) @@ -379,7 +379,7 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - + @@ -410,7 +410,7 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - @@ -441,14 +441,14 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + - + L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) ... [22 more line(s); run it to see the whole thing] ``` @@ -471,7 +471,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -502,7 +502,7 @@ _The repo's best-in-class instance to imitate before writing new code (picked by $ ./build/ripwire . --exemplar="format byte sizes for humans" - + f, A&&... a ) @@ -533,7 +533,7 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ $ ./build/ripwire . --recall="quality delta gating exit codes" ripwire recall — "quality delta gating exit codes" — 113 relevant of 220 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 -━━ README.md (relevance 8.028) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +━━ README.md (relevance 8.029) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -596,25 +596,20 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1362430-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1370299-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -━━ commands.md (relevance 11.045) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 578530 B; lines="555-580"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 10.110) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 586190 B; lines="3844-3848"; dropped_by_budget=163] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] -... [17 more line(s); run it to see the whole thing] +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ``` ### `--tree` @@ -734,7 +729,7 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable $ ./build/ripwire . --no-stable --top-k=3 - + @@ -768,7 +763,7 @@ $ ./build/ripwire . --around=rankGraphTeleport - + @@ -820,18 +815,18 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - + + - - - - - - + + + + + + ``` @@ -855,10 +850,10 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + @@ -889,16 +884,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - + - + - + @@ -927,14 +922,14 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The $ ./build/ripwire . --external-surface - - - - - + + + + + - + @@ -961,12 +956,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - + - + ``` @@ -983,10 +978,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -1093,7 +1088,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1154,7 +1149,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 0a5d057d9 +at: 0e6edd91c (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1173,18 +1168,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1233,7 +1228,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - + - + @@ -1535,7 +1530,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1570,7 +1565,7 @@ $ ./build/ripwire . --pack-signatures --top-k=10 - + @@ -1601,7 +1596,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1632,7 +1627,7 @@ $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + of ; resolved callees. window: shown= total= capped= (capped=1 cut). sibs_capped=/inc_capped=: 1 = cut. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : that callee matched by name alone (receiver unproven); it does NOT mean the edge is fal … [line truncated: 443 more bytes on this line] -: that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, callers + co-changed files. ev=/ev_why=: essential complexity (2+: jumps block extract-method; absent: 1) / the jumps behind it, tag:count. ev_floor=1: ev= is a FLOOR; noreturn calls, macro-hidden exits and unresolved gotos are unseen. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. l=N: start line; only on a same-name overload's row, one row per body (bodyless decls fold into overloads=). --> - + @@ -1805,7 +1800,7 @@ _File->file dependency graph (god-files, cycles)._ $ ./build/ripwire . --deps - + @@ -1841,12 +1836,12 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - + + - + - + @@ -1869,7 +1864,7 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + @@ -1900,8 +1895,8 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - + + @@ -1933,7 +1928,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1968,18 +1963,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1999,7 +1994,7 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - @@ -2007,9 +2002,9 @@ $ ./build/ripwire . --quality-panel - + - + ... [17 more line(s); run it to see the whole thing] @@ -2036,13 +2031,13 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - - + - - - + + + + ... [23 more line(s); run it to see the whole thing] ``` @@ -2067,7 +2062,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2102,8 +2097,8 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - + + @@ -2174,11 +2169,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2207,16 +2202,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - + - + - - - + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2239,15 +2234,15 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - + - - + - + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2265,7 +2260,7 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + @@ -2294,19 +2289,19 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - + + - - + + - + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2638,15 +2633,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + + + - - - + @@ -2669,7 +2664,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2694,9 +2689,9 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - + + @@ -2731,16 +2726,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2963 files · 28377 symbols · 27575 edges · 3389 modules (12685 call-graph isolated) +2963 files · 28378 symbols · 27578 edges · 3382 modules (12685 call-graph isolated) Root: `.` Call-graph isolate provenance: 3464 declaration, 2049 header, 2464 source, 4708 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3389) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1285 symbols +## Modules (call-graph clusters; showing 12 of 3382) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1310 symbols - **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 407 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 230 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 229 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2757,7 +2752,7 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - + @@ -2788,7 +2783,7 @@ $ ./build/ripwire . --mermaid %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
    8426"] + n101["src
    8427"] n102["src/infra
    1294"] end subgraph sg1 ["test"] @@ -2817,7 +2812,7 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - + @@ -2848,7 +2843,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2913,7 +2908,7 @@ _The same clean verdict with what it folded printed back: every stale acknowledg ``` $ ./build/ripwire . --quality-delta --show-stale - @@ -2942,7 +2937,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2967,7 +2962,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2992,7 +2987,7 @@ _A ref range run for two of the twelve kinds only: the named kinds' rows are exa ``` $ ./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code -

    @@ -3085,7 +3080,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3115,10 +3110,10 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - + + @@ -3169,12 +3164,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3190,12 +3185,12 @@ _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 bec ``` $ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3241,12 +3236,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":759,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3305,11 +3300,11 @@ _The same plan committed: per-file locks, re-verify-before-write, atomic rename, ``` $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":2970,"graph_unresolved":13753,"graph_unindexed":261,"counts_floor":true}}]} +"graph_ambiguous":2970,"graph_unresolved":13765,"graph_unindexed":261,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3355,19 +3350,19 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - - - - - - - - - - - - - + + + + + + + + + + + + + ... [3 more line(s); run it to see the whole thing] ``` @@ -3392,17 +3387,17 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + ... [22 more line(s); run it to see the whole thing] @@ -3446,10 +3441,10 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:9275 +$ ./build/ripwire . --at=src/graph.h:9279 - - + + ``` @@ -3473,7 +3468,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3499,11 +3494,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3578,7 +3573,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3612,7 +3607,7 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + @@ -3648,7 +3643,7 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - @@ -3683,10 +3678,10 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - + + + + ... [14 more line(s); run it to see the whole thing] @@ -3713,9 +3708,9 @@ _The same page with every t= line whole: the page above previews each line over ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text - - - + + + @@ -3741,10 +3736,10 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - + + - + @@ -3854,7 +3849,7 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - @@ -3889,7 +3884,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3923,7 +3918,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3956,8 +3951,8 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - + + @@ -3990,7 +3985,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -4025,8 +4020,8 @@ $ ./build/ripwire . --from-trace=- AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9275 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9316 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9279 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9320 #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1752 #3 0x102e01a44 in main src/main.cpp:4173 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) @@ -4055,7 +4050,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4215,7 +4210,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + @@ -4289,13 +4284,13 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - + @@ -4323,13 +4318,13 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - + @@ -4358,7 +4353,7 @@ $ ./build/ripwire . --skipped - @@ -4446,8 +4441,8 @@ _Rank by git change-frequency prior instead of PageRank._ $ ./build/ripwire . --rank-by=churn --top-k=5 - - + + @@ -4479,22 +4474,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~5 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~8 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - + + + + - - - - - - - - -... [8 more line(s); run it to see the whole thing] + + + + + + + +... [5 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4556,7 +4551,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4636,18 +4631,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4672,7 +4667,7 @@ _Drop matching paths (repeatable) before ranking._ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + @@ -4703,8 +4698,8 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch $ ./build/ripwire . --map-diff --top-k=5 - - + + @@ -4737,7 +4732,7 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + @@ -4787,7 +4782,7 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co $ ./build/ripwire . --no-cache --top-k=3 - + @@ -4818,7 +4813,7 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses $ ./build/ripwire . --no-ignore --top-k=3 - + @@ -4934,7 +4929,7 @@ _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and th $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - + @@ -4989,9 +4984,9 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"