From 98035399e554fa81546488b8e5edc333527bfc90 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 10:57:28 -0400 Subject: [PATCH 01/20] perf(testmap,situ): a runner command pasted the whole checkout prefix on every row that had one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A change report states its absolute root once, in the envelope, and every path below it is relative to that root — that is what test/rootrelemitcheck.sh ARM 1/2/5 prove, and what makes the document independent of where the tree is checked out. One emitter never joined: testmap.h's spell(), which builds the run= command, pasted diskPath() verbatim. On an absolute root `--test-gate` therefore printed the checkout prefix three times (the root= anchor, next=, and every row's run=) and `--situ` once per runnable test line — a per-ROW cost against a per-DOCUMENT fact. The sweep missed it because test/fixture holds no runner script at all: every test row reads run_unknown="1" there, so the one emitter that pastes a PATH INSIDE A COMMAND was never exercised. TestRunnerIndex now takes the run's crawl root and spells the command through the same rw::sarif::rootRelativeUri every p= beside it goes through; the hand-rolled leading-"./" strip becomes that one call rather than a second rule. The root is passed at all fourteen construction sites (--affected, --exercises, --test-gate XML and JSON, --situ, --pr-context, --handoff, --pack-task XML and JSON, --flags --flip, the MCP situational_awareness twin and the edit receipt), so the twelve emitters that share the index cannot disagree about the spelling. A multi-root run, whose disk path is under no single root, keeps the absolute command: an unrelativizable command must stay pasteable rather than become relative to a root that does not contain it. The rule is stated where it is consumed — kRunHintLegendClause gains "A run= command is relative to root=." (27 B, rows-gated like the rest of that clause) and --situ's [2] header says "a (run: …) is relative to root:". Measured with wc -c on a clean tree, same cache, absolute root: ripwire tree (root 132 chars) --test-gate=src/testmap.h 5,327 -> 5,100 B RocksDB @0e2801ac (root 66) --test-gate=db/write_batch.cc 9,793 -> 9,696 B --situ=db/write_batch.cc 7,445 -> 7,412 B --affected=db/write_batch.cc 6,971 -> 6,941 B The saving is one root spelling per echo minus the 27 B legend clause, so it scales with checkout depth and with the number of rows that HAVE a runner. Gate: test/rootrelemitcheck.sh ARM 9 — a fixture with a real runner script (mention evidence) at two checkout depths, over the eight verbs that echo a command. It asserts one anchor per document, zero absolute paths elsewhere, depth-independence, and that the printed run= actually EXECUTES from the declared root. Red first on the unchanged binary: 8 FAIL rows (test-gate 3 leaks of 4 occurrences, situ 2 of 3, affected 2 of 3, exercises 1 of 2, plus the run=/next= runnability and disclosure rows). Green after. test/runhintcheck.sh's pins move with the contract: the arm that pinned run= to "the root spelling the caller passed" now asserts the absolute and relative scans print the SAME command, and that it runs from the root. test/printf_parity.manifest re-pinned for pack_task alone (the legend clause); 41 of 42 verbs unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 24 ++++++++ src/flipimpact.h | 2 +- src/handoff.h | 2 +- src/mcpedit.h | 4 +- src/mcpverbs.h | 2 +- src/packtask.h | 4 +- src/prcontext.h | 2 +- src/situ.h | 10 +-- src/testmap.h | 22 ++++--- src/verbs_change.h | 4 +- test/printf_parity.manifest | 2 +- test/rootrelemitcheck.sh | 117 ++++++++++++++++++++++++++++++++++++ test/runhintcheck.sh | 39 +++++++----- 13 files changed, 197 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccee4dc9..26fdec12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,30 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — one absolute root per change report + +`--test-gate`, `--situ` and `--affected` state the crawl root once, in the envelope (`root=` in XML and +JSON, the `root:` line in `--situ`'s text), and every path below it is relative to that root — which is +what makes the document independent of where the tree is checked out. One emitter never joined: the +`run=` command. It pasted the stored disk path verbatim, so on an absolute root `--test-gate` printed +the checkout prefix three times (the anchor, `next=`, and every `` row's `run=`) and `--situ` once per +runnable test line: a per-row cost against a per-document fact. The runner index now takes the run's root +and spells the command through the same relativizer every `p=` beside it uses, at all fourteen sites that +build one, so the twelve emitters sharing it cannot disagree; a multi-root run, whose disk path lies under +no single root, keeps the absolute command rather than become relative to a root that does not contain it. +The rule is stated where it is consumed: the shared run-hint clause gains "A run= command is relative to +root=." (27 B, emitted only on a document that has rows) and `--situ`'s `[2]` header says "a (run: …) is +relative to root:". Measured with `wc -c` on a clean tree with the same warm cache and an absolute root: +on this repo (root 132 chars) `--test-gate=src/testmap.h` 5,327 → 5,100 B; on RocksDB @0e2801ac (root 66 +chars) `--test-gate=db/write_batch.cc` 9,793 → 9,696 B, `--situ=db/write_batch.cc` 7,445 → 7,412 B and +`--affected=db/write_batch.cc` 6,971 → 6,941 B. The saving is one root spelling per echo less the legend +clause, so it grows with checkout depth and with how many rows have a runner at all. The gate is a new +ARM 9 in `test/rootrelemitcheck.sh`: a fixture carrying a real runner script, at two checkout depths, over +the eight verbs that echo a command — one anchor per document, no absolute path anywhere else, +byte-identical documents at both depths, and the printed `run=` actually executed from the declared root. +Red first on the unchanged binary (8 FAIL rows); `test/runhintcheck.sh`'s pins move with the contract, and +`test/printf_parity.manifest` moves for `--pack-task` alone, the one verb whose legend text changed. + ### Changed — tests-to-run rows without a runner are grouped by hop distance Every tests-to-run row that had no derivable runner said so on the row — `run_unknown="1"` in XML, diff --git a/src/flipimpact.h b/src/flipimpact.h index 528d5cab..78a1633c 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -1263,7 +1263,7 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult // read from a different seed, so it is served whole on every page. SIZE_MAX, not maxRows. // E1: rows without a runner are grouped (testmap.h's seam), so the listing is rendered whole and wrapped here // exactly as writeCappedList wraps an uncut list — `` with no cut attributes, n= the FILE count. - const rw::TestRunnerIndex flipRunners( ing ); + const rw::TestRunnerIndex flipRunners( ing, root ); rw::emitTo( out, "", res.tests.size() ); rw::emitRaw( out, rw::testRowsJoined( flipRunners, rw::testRowsOutOf( res.tests, rel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, ex ).c_str() ); rw::emitRaw( out, "" ); diff --git a/src/handoff.h b/src/handoff.h index 3cd88b90..5b336709 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -277,7 +277,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In // SAME facts — carried run="bash test/…" for those same files. The packet whose whole purpose is to be // read by the NEXT session was the one that said least. Built here, inside the section's scope, because // TestRunnerIndex is lazy: a packet with no test row reads no runner script. - const rw::TestRunnerIndex hoRunners( ing ); + const rw::TestRunnerIndex hoRunners( ing, root ); const auto hoEsc = [ & ]( std::string_view t ) { return std::string( escapeXml( t, esc ) ); }; v += rw::testRowsJoined( hoRunners, rw::testRowsOutOf( facts.tests, hoPathRel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, hoEsc ); // E1: where no runner is derivable v += ""; diff --git a/src/mcpedit.h b/src/mcpedit.h index 10e41610..1fbc7d85 100644 --- a/src/mcpedit.h +++ b/src/mcpedit.h @@ -1031,7 +1031,7 @@ namespace mcpedit // The SAME answer --affected= gives, through the SAME function — see // testmap.h::affectedAnswerForFile for why this used to be a private walk and what that cost. const AffectedAnswer ans = rw::affectedAnswerForFile( ing, g, fileId ); - const TestRunnerIndex runners( ing ); + const TestRunnerIndex runners( ing, root ); const auto jesc = []( std::string_view t ) { return mcpdetail::jsonEscape( std::string( t ) ); }; const std::string prefix = rw::sarif::rootPrefixOf( root ); std::string out = ",\"tests_to_run\":["; @@ -1150,7 +1150,7 @@ namespace mcpedit // evidence order now, so next= suggests the changed/partner test ahead of a deeper graph hop const std::uint32_t firstTest = withTests ? rw::firstTestFileForFile( ing, g, editedFile ) : rw::kNoFile; *nextOut = receiptNextFor( fileIdentity, symbolName, out, - firstTest == rw::kNoFile ? std::string() : TestRunnerIndex( ing ).commandFor( firstTest ) ); + firstTest == rw::kNoFile ? std::string() : TestRunnerIndex( ing, root ).commandFor( firstTest ) ); } return out; } diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 07ea2ac1..634e3ad7 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1247,7 +1247,7 @@ inline std::string situationDiffJson( const std::string& root, const std::string // §B6 M11: the run= hint index, from the SAME source --affected/--situ/--test-gate/--pr-context read // (testmap.h). runFieldJson is that header's JSON call shape, so "absent means NOT DERIVABLE" — the // load-bearing half of the rule — is decided in one place for every emitter rather than re-decided here. - const TestRunnerIndex runners( ing ); + const TestRunnerIndex runners( ing, root ); const auto jsonEsc = []( std::string_view sv ) { return mcpdetail::jsonEscape( std::string( sv ) ); }; // M10: this verb reads git (the diff itself, plus an 18-month co-change mine below) and, before this diff --git a/src/packtask.h b/src/packtask.h index 16cdebae..a0616b25 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1421,7 +1421,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // inside the section's scope, because it is lazy — a bundle with no test row reads no runner script. // §B14 — std::string rows, not char[512]: a row carries TWO unbounded interpolands (the test path AND // the runner command), so it was the widest of the six breaching sites. - const rw::TestRunnerIndex runners( ing ); + const rw::TestRunnerIndex runners( ing, in.rootArg ); const std::string ptPrefix = in.rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( in.rootArg ); const std::vector ptRows = rw::testRowsOutOf( testFiles, [ & ]( std::uint32_t f ) -> std::string_view { @@ -1661,7 +1661,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, { char b[ 96 ]; rw::formatTo( b, sizeof( b ), ",\"tests_total\":{},\"tests_kept\":{},\"tests_to_run\":[", tests.totalUnits, tests.keptUnits ); j += b; } // §A9.5: the JSON sibling of the XML run= above — situ's tests_to_run already carries it, and one // computation path must not serialize two different obligations. - const rw::TestRunnerIndex jsonRunners( ing ); + const rw::TestRunnerIndex jsonRunners( ing, in.rootArg ); const auto jrun = [ & ]( std::string_view s ) { return jsonStr( s ); }; const std::vector jsonRows = rw::testRowsRendered( jsonRunners, rw::testRowsOutOf( testFiles, jPathRel ), rw::TestRowShape{ rw::RowDialect::Json, "p" }, jrun, &testPartition ); for( std::size_t i = 0; i < testsShown && i < jsonRows.size(); ++i ) diff --git a/src/prcontext.h b/src/prcontext.h index 48bfffca..e8fedb53 100644 --- a/src/prcontext.h +++ b/src/prcontext.h @@ -955,7 +955,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest const auto allOwners = gitFileAuthors( root, ing, UINT32_MAX, 182.5, onlyRoot ); // §A9.5 / §P11.4: run= on the named test rows, from the SAME index --affected/--situ/--test-gate read. - const TestRunnerIndex prRunners( ing ); // built once, like coSets/allOwners — the bundle re-renders + const TestRunnerIndex prRunners( ing, root ); // built once, like coSets/allOwners — the bundle re-renders // One-time file→defined-symbols index (in id order == file/line order), so each changed file reads its // symbols in O(1) instead of re-scanning all N symbols (A4-P10). Buckets fill in ascending id order. diff --git a/src/situ.h b/src/situ.h index 0f369fb7..c42c7521 100644 --- a/src/situ.h +++ b/src/situ.h @@ -595,11 +595,11 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges rw::emitTo( out, " [2] tests to run ({}){}", tests.size(), tests.empty() ? ": (none transitively reach these files)\n" : " — evidence order: [changed] you edited it, [partner] named after a changed file, then hops (1 = calls a changed symbol directly); " - "a line (n): a, b lists n files sharing that evidence with no derivable runner:\n" ); + "a line (n): a, b lists n files sharing that evidence with no derivable runner; a (run: …) is relative to root:\n" ); // §P11.4: this section says "tests to run" and named files that are not commands. The runner is appended // where one is DERIVABLE and omitted where it is not — see testmap.h; a guessed command is worse than none. // E1: runner-less rows with equal evidence are ONE `[hops=N] (n): a, b` line — testmap.h's seam, the multiset unchanged - const TestRunnerIndex situRunners( ing ); + const TestRunnerIndex situRunners( ing, root ); rw::emitRaw( out, testRowsJoined( situRunners, evidenceRowsOut( testRows, EvDialect::Text, situPathRel ), TestRowShape{ RowDialect::Text, {}, " " }, []( std::string_view s ) { return std::string( s ); } ).c_str() ); // §B7.3: this section inherits --affected's blind spot without --affected's disclosure — a shell harness @@ -1158,7 +1158,7 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const rw::rootRelPathsLegend( !tgRootAttr.empty() ) ); // §P11.4: this gate EXITS 4 on the obligation, so its rows carry the command that discharges it — where // one is derivable. Absent run= = not derivable (testmap.h states why a fallback would be a lie). - const TestRunnerIndex gateRunners( ing ); + const TestRunnerIndex gateRunners( ing, root ); // shown_tests= / tests_capped= are DERIVED from the rows this document actually emits, not asserted. // tests_capped= was the string literal "0" — a disclosure that could never become "1", so if a row // cap were ever added the attribute would keep saying nothing was cut while something was. It is kept @@ -1229,7 +1229,7 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co const std::size_t testRows = r.tests.size() + r.shellGates.obligations.size(); // same rows-gate as the XML twin, so the two dialects disclose the SAME facts about the same run rather // than one carrying a root the other omits (test/mcpclidiffcheck.sh's parity question). - const TestRunnerIndex gateRunnersJ( ing ); // P3 (L7): the root's next= needs the runner index before the rows + const TestRunnerIndex gateRunnersJ( ing, root ); // P3 (L7): the root's next= needs the runner index before the rows const bool tgJHasRows = ( testRows > 0 || !r.untested.empty() ); const std::string tgJRootJson = ( root.empty() || !tgJHasRows ) ? std::string() : ( ",\"root\":\"" + jsonStr( root ) + "\"" ); // The XML twin's derived pair, mirrored key-for-key: "tests_capped":false was a literal here too. @@ -1245,7 +1245,7 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co graphCountFloorAttrJson( g ).c_str(), // M15: the JSON twin's gauge + "counts_floor":true rw::cstr( pageJson ), atJson.c_str(), tgJRootJson.c_str(), // M12: root= rides only when the document has rows (same gate as the XML twin) nextFieldJson( testGateNextInvocation( ing, r, gateRunnersJ ) ).c_str() ); // P3 (L7): the XML twin's next= - const TestRunnerIndex gateRunners( ing ); // §P11.4, the JSON sibling of the XML run= + const TestRunnerIndex gateRunners( ing, root ); // §P11.4, the JSON sibling of the XML run= const auto jesc = []( std::string_view s ) { return jsonStr( s ); }; rw::emitRaw( out, testRowsJoined( gateRunners, evidenceRowsOut( r.testRows, EvDialect::Json, tgJPathRel ), TestRowShape{ RowDialect::Json, "p" }, jesc, "," ).c_str() ); // E1: the XML twin's , "p" an array for( std::size_t i = 0; i < r.shellGates.obligations.size(); ++i ) diff --git a/src/testmap.h b/src/testmap.h index d5e41312..ddcc45d1 100644 --- a/src/testmap.h +++ b/src/testmap.h @@ -26,6 +26,7 @@ #include "docparse.h" // docparse::detail::readWholeFile — the canonical whole-file byte read (reused, not re-rolled) #include "mention.h" // mention_detail::baseNameOf + stripExt — the ONE basename/stem pair binstale.h/gitmine.h reuse #include "infra/namesplit.h" // namesplit::isIdentChar — the canonical ASCII identifier-byte predicate +#include "sarif.h" // rootPrefixOf / rootRelativeUri — the ONE relativizer every p= emitter already shares (A3) #include #include @@ -489,7 +490,14 @@ inline std::vector exercisedSymbols( const IngestResult& ing, const Grap class TestRunnerIndex { public: - explicit TestRunnerIndex( const IngestResult& ing ) : ing_( &ing ) + // A3 (one absolute root per document): `root` is the run's own crawl root, and its ONLY use is to spell + // the command below relative to it — the same rootPrefixOf/rootRelativeUri pair every p= emitter uses. + // Defaulted to "" so a caller that has no root (or a multi-root run, where the disk path is not under any + // single root) keeps the absolute spelling: an unrelativizable command must stay pasteable, never become + // a path relative to a root that does not contain it. + explicit TestRunnerIndex( const IngestResult& ing, std::string_view root = {} ) + : ing_( &ing ), + rootPrefix_( root.empty() || !ing.realPaths.empty() ? std::string() : rw::sarif::rootPrefixOf( root ) ) { for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) { @@ -595,15 +603,15 @@ class TestRunnerIndex // is dropped for readability; the result is pasteable from the repo root. std::string spell( std::uint32_t runnerFile ) const { - std::string_view p = diskPath( *ing_, runnerFile ); - if( p.rfind( "./", 0 ) == 0 ) - { - p = p.substr( 2 ); - } + const std::string& disk = diskPath( *ing_, runnerFile ); + // A3: root-relative, like every p= beside it. rootRelativeUri strips a leading "./" unconditionally, + // so the readability strip the pre-A3 code did by hand is the SAME call now, not a second rule. + std::string_view p = rw::sarif::rootRelativeUri( disk, rootPrefix_ ); return std::string( runnerVerb( p ) ) + " " + std::string( p ); } const IngestResult* ing_; + std::string rootPrefix_; // A3: "" ⇒ the command keeps its stored spelling std::vector runners_; mutable std::vector texts_; mutable bool textsLoaded_ = false; @@ -916,7 +924,7 @@ inline constexpr std::string_view kRunHintLegendClause = "run= is the command that discharges a test row; run_unknown=\"1\" means none is derivable for that " "harness (a guess would be worse than none) — a or row carries one or the other, never neither. " " is 2+ runner-less rows with equal attributes served as ONE row: n= how many, p= their paths " - "in list order (, a comma in a path), every path verbatim. "; + "in list order (, a comma in a path), every path verbatim. A run= command is relative to root=. "; // The clause is a rule about ROWS, so a legend splices it only when the rendered rows are non-empty — a // tests="0" answer pays nothing for it (--affected/--exercises; --test-gate and --pack-task gate it the same way). diff --git a/src/verbs_change.h b/src/verbs_change.h index 300fab29..19ca0fb9 100644 --- a/src/verbs_change.h +++ b/src/verbs_change.h @@ -128,7 +128,7 @@ std::optional runAffected( const MainDispatch& d ) // run=/run_unknown=/ clause only when there are rows for it to be a rule about — a tests="0" answer, // the common clean case, pays nothing for it. The index is constructed here (not hoisted into // MainDispatch) because it is lazy — a run with no test row reads no script. - const rw::TestRunnerIndex runners( ing ); + const rw::TestRunnerIndex runners( ing, d.root ); std::vector afRows; afRows.reserve( answer.rows.size() ); for( rw::TestRow row : answer.rows ) // by value: a matched test file's changed= is spelled seed_kind="test" on this verb @@ -240,7 +240,7 @@ std::optional runExercises( const MainDispatch& d ) const std::string harnessAttr = exercisesHarnessAttr( ing, sel.testFiles ); // §A9.1, empty for a .cpp/.py harness // §P11.4 / E1: the seed rows are the tests you are about to re-run — rendered before the legend so the // run=/run_unknown=/ clause rides only a document that has rows (testmap.h's seam; grouped where no runner is derivable) - const rw::TestRunnerIndex runners( ing ); + const rw::TestRunnerIndex runners( ing, d.root ); const auto exPathRel = [ & ]( std::uint32_t f ) -> std::string_view { return exSingleRoot ? rw::sarif::rootRelativeUri( ing.files[f], exRootPrefix ) : std::string_view( ing.files[f] ); diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 2e689938..48ab995f 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -18,7 +18,7 @@ path 0 7058d89f7bab7aabe0a5cbf921959bc8a2c346fe65888b776996f879f04a58a2 e3b0c442 connect 0 31c7e3a689a6dcddf5eae17283740823005c3efdfdf3945be5770b2660a152b7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 b171aa1e5c28b47827f9148c2a5fc6948fb22938d3da21e6551b12432f492472 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_signatures 0 4a22aeda1d2e36fd390065dac1c07955e9f8942e7ba858435281863653f373ba e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_task 0 acc6ad4d23144bb195865986006ce3600bce6761f4971553ee9d0424c20f97d0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_task 0 f9ad4cee3de1ea1c9945961aed388874eb164aad903a1cc372ea9f7b9c9a76af e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 6014e2f18ba2d587f70d59fcbe62f64c38e8c556291006ae3dfd76ef7259315a e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 23b824f67984709f3c01dc7cc97c4c682b2afbaa5b63677a79ce56a1a58e6454 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 skipped 0 5b9749fdafec842ae75c6ad07e9eac8eb435653f8e5fc1ce452853fc36ff96f2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/rootrelemitcheck.sh b/test/rootrelemitcheck.sh index 84e60283..edaa8791 100755 --- a/test/rootrelemitcheck.sh +++ b/test/rootrelemitcheck.sh @@ -494,6 +494,123 @@ for spelling in abs rel; do fi done +# ── ARM 9 — THE COMMAND ECHOES: one absolute root per document, even when a runner IS derivable ───────── +# A3 (PLAN_OUTPUT_ROUTING_LOOP §1.5): the arms above sweep a fixture with NO runner script, so every test row +# reads run_unknown="1" and the one emitter that pastes a PATH INSIDE A COMMAND — testmap.h's spell() — was +# never exercised by them. It spelled diskPath(), i.e. the whole checkout prefix, so `--test-gate` on an +# absolute root printed the root three times (the anchor, next=, and every row's run=) and `--situ` +# printed it once per runnable test line. That is a per-ROW cost against a per-DOCUMENT fact, exactly what +# ARM 1/2/5 exist to forbid; it simply had no fixture that could see it. +# +# So this arm builds one: the fixture plus a real runner script whose text names the changed harness (the +# MENTION evidence kind), which makes run= and next= materialize on every verb that echoes a command. It then +# asserts the same three properties the matrix above asserts — one anchor, no other absolute path, depth +# independence — AND the property that makes relativizing safe: the echoed command still RUNS from the root. +A9="$TMP/runner"; rm -rf "$A9"; mkdir -p "$A9"; cp -R "$FIX/." "$A9/" +cat > "$A9/test_geometry.cpp" <<'A9EOF' +#include "geometry.h" + +double test_distance( Point a, Point b ) +{ + return distance( a, b ); +} +A9EOF +cat > "$A9/test_geometry.sh" <<'A9EOF' +#!/usr/bin/env bash +# the corpus's runner: its TEXT names geometry.cpp, which is the MENTION evidence testmap.h derives run= from +exit 0 +A9EOF +chmod +x "$A9/test_geometry.sh" +seed_git "$A9" +# the same corpus one directory deeper, so the one-anchor claim is a MEASURED bound here too +A9D="$TMP/ddddddddd/ddddddddd/ddddddddd/ddddddddd/runner"; rm -rf "$A9D"; mkdir -p "$A9D"; cp -R "$A9/." "$A9D/" +seed_git "$A9D" +A9_DELTA=$(( ${#A9D} - ${#A9} )) + +A9_VERBS=( + "test-gate:--test-gate=geometry.cpp" + "test-gate-json:--test-gate=geometry.cpp|--json" + "situ:--situ=geometry.cpp" + "affected:--affected=distance" + "exercises:--exercises=test_geometry.cpp" + "pr-context:--pr-context" + "pack-task:--pack-task=compute the area" + "handoff:--handoff" +) +# the runner had better be derivable, or every assertion below is vacuous +if ! run_at "$A9" "--test-gate=geometry.cpp" | grep -q 'run="'; then + no "ARM9 the fixture derives NO run= at all — every assertion below would be a false green" +else + ok "ARM9 fixture: a runner IS derivable (run= is emitted), so the command echoes are live" +fi +for entry in "${A9_VERBS[@]}"; do + name="${entry%%:*}"; spec="${entry#*:}" + run_at "$A9" "$spec" > "$TMP/a9.short" + run_at "$A9D" "$spec" > "$TMP/a9.deep" + read -r lk tot anc < "$TMP/a9.ms" + mask "$A9D" < "$TMP/a9.deep" > "$TMP/a9.md" + if cmp -s "$TMP/a9.ms" "$TMP/a9.md"; then + ok "ARM9 $name depth-independent with a live runner" + else + no "ARM9 $name differs with checkout depth once a run= is derivable (+$(( $( wc -c < "$TMP/a9.deep" ) - $( wc -c < "$TMP/a9.short" ) ))B over ${A9_DELTA} chars)" + fi +done + +# The point of a relative command is that it is still runnable — from the root the document declares. +# Pulled out of the loop because it EXECUTES what the document printed, which is the whole claim: an agent +# that cd's to root= and pastes run= gets the runner, not a "No such file or directory". +a9_cmd(){ tr '<' '\n' < "$1" | sed -n 's/.* run="\([^"]*\)".*/\1/p' | head -1; } +run_at "$A9" "--test-gate=geometry.cpp" > "$TMP/a9.tg" +A9CMD="$( a9_cmd "$TMP/a9.tg" )" +A9NEXT="$( tr '<' '\n' < "$TMP/a9.tg" | sed -n 's/.* next="\([^"]*\)".*/\1/p' | head -1 )" +if [ -z "$A9CMD" ]; then + no "ARM9 --test-gate emitted no run= — the runnability assertion would be a false green" +else + case "$A9CMD" in + */) no "ARM9 run=\"$A9CMD\" ends in a separator" ;; + *"$A9"*) no "ARM9 run=\"$A9CMD\" still carries the absolute checkout prefix" ;; + *) ok "ARM9 run=\"$A9CMD\" is root-relative" ;; + esac + if ( cd "$A9" && eval "$A9CMD" >/dev/null 2>&1 ); then + ok "ARM9 the printed run= actually runs from root= ($A9CMD)" + else + no "ARM9 the printed run= does NOT run from root= ($A9CMD) — a relative command that cannot be pasted is worse than an absolute one" + fi +fi +if [ -n "$A9NEXT" ] && [ "$A9NEXT" = "$A9CMD" ]; then + ok "ARM9 next= pastes the same root-relative command as the first row's run= ($A9NEXT)" +elif [ -n "$A9NEXT" ]; then + no "ARM9 next=\"$A9NEXT\" disagrees with the first row's run=\"$A9CMD\"" +fi +# --situ is the text dialect of the same echo: its `(run: …)` recipe and its `root:` line. +run_at "$A9" "--situ=geometry.cpp" > "$TMP/a9.situ" +# a ROW's recipe, never the section header's own "(run: …)" mention of the convention +A9SITU="$( sed -n 's/^ \{8\}.*(run: \([^)]*\)).*/\1/p' "$TMP/a9.situ" | grep -v 'not derivable' | head -1 )" +if [ -z "$A9SITU" ]; then + no "ARM9 --situ printed no (run: …) recipe — the text dialect's echo is untested" +else + case "$A9SITU" in + *"$A9"*) no "ARM9 --situ's (run: $A9SITU) still carries the absolute checkout prefix" ;; + *) ok "ARM9 --situ's (run: $A9SITU) is root-relative" ;; + esac + grep -q 'relative to root' "$TMP/a9.situ" \ + && ok "ARM9 --situ says its run recipe is relative to the root it declares" \ + || no "ARM9 --situ prints a relative run recipe and never says what it is relative to" +fi + # ── the MCP dialect ───────────────────────────────────────────────────────────────────────────────────── if ! python3 "$ROOT/test/rootrelemitmcp.py" "$BIN" "$SHORT" "$DEEP"; then fail=1 diff --git a/test/runhintcheck.sh b/test/runhintcheck.sh index b1e75d60..bb0c82cf 100755 --- a/test/runhintcheck.sh +++ b/test/runhintcheck.sh @@ -52,23 +52,34 @@ runof(){ printf '%s' "$2" | grep -oE "<[a-z]+ p=\"[^\"]*$1\"( (seed_kind|changed A="$( run --affected=src/core.cpp )" # ── 1) MENTION evidence: the *check.sh that names the harness becomes its run= ──────────────────────── -[ "$( runof 'mything_harness.cpp' "$A" )" = "bash $R/test/mythingcheck.sh" ] \ +[ "$( runof 'mything_harness.cpp' "$A" )" = "bash test/mythingcheck.sh" ] \ && ok "--affected: mention-derived run= on mything_harness.cpp" \ || no "--affected mention hint wrong: '$( runof 'mything_harness.cpp' "$A" )'" # ── 2) STEM evidence: foo.cpp <-> foo.sh ───────────────────────────────────────────────────────────── -[ "$( runof 'samename.cpp' "$A" )" = "bash $R/test/samename.sh" ] \ +[ "$( runof 'samename.cpp' "$A" )" = "bash test/samename.sh" ] \ && ok "--affected: stem-derived run= on samename.cpp" \ || no "--affected stem hint wrong: '$( runof 'samename.cpp' "$A" )'" -# ── 2b) run= is spelled with the SAME root the caller passed, exactly as p= is ──────────────────────── +# ── 2b) run= is spelled RELATIVE TO root=, exactly as the p= beside it is ──────────────────────────── # A hint whose path spelling disagreed with the p= beside it would be a second vocabulary for "where this -# file is" — the §P8 defect, in the one attribute meant to be pasted into a shell. Scanned as ".", both -# are repo-relative and the command is pasteable from the repo root. +# file is" — the §P8 defect, in the one attribute meant to be pasted into a shell. +# A3 (2026-09-13, PLAN_OUTPUT_ROUTING_LOOP §1.5): this arm used to pin run= to "the root spelling the caller +# passed", which made an ABSOLUTE scan print the whole checkout prefix inside the command — one absolute +# path per ROW against a per-DOCUMENT fact (test/rootrelemitcheck.sh ARM 9 is the emission contract). run= +# is now root-relative under BOTH spellings, so the two runs agree byte-for-byte on the command and the +# document is independent of where the tree is checked out. REL="$( cd "$R" && perl -e 'alarm 20; exec @ARGV' "$BIN" . --affected=src/core.cpp --no-cache 2>/dev/null )" [ "$( runof 'mything_harness.cpp' "$REL" )" = "bash test/mythingcheck.sh" ] \ - && ok "run= follows the caller's root spelling (scanned as '.', run=\"bash test/mythingcheck.sh\")" \ + && ok "run= is root-relative under a relative scan (run=\"bash test/mythingcheck.sh\")" \ || no "run= root spelling wrong under a relative scan: '$( runof 'mything_harness.cpp' "$REL" )'" +[ "$( runof 'mything_harness.cpp' "$REL" )" = "$( runof 'mything_harness.cpp' "$A" )" ] \ + && ok "run= is the SAME command under an absolute and a relative root — no checkout prefix rides the row" \ + || no "run= differs between an absolute scan ('$( runof 'mything_harness.cpp' "$A" )') and a relative one ('$( runof 'mything_harness.cpp' "$REL" )')" +# …and it must still RUN from the root the document declares, which is the whole point of relativizing it. +( cd "$R" && eval "$( runof 'mything_harness.cpp' "$A" )" >/dev/null 2>&1 ) \ + && ok "the printed run= executes from the declared root" \ + || no "the printed run= does not execute from the declared root — a relative command that cannot be pasted is worse than an absolute one" # ── 3) NO evidence → NO run=. The half that keeps the attribute trustworthy. ────────────────────────── case "$A" in @@ -79,18 +90,18 @@ esac # ── 4) the same hint on --test-gate, the verb that EXITS 4 on the obligation ────────────────────────── G="$( run --test-gate=src/core.cpp )" -[ "$( runof 'mything_harness.cpp' "$G" )" = "bash $R/test/mythingcheck.sh" ] \ +[ "$( runof 'mything_harness.cpp' "$G" )" = "bash test/mythingcheck.sh" ] \ && ok "--test-gate rows carry the same run= (the exit-4 obligation is now dischargeable)" \ || no "--test-gate run= missing/wrong: '$( runof 'mything_harness.cpp' "$G" )'" # ── 4b) …and in its --json sibling, under the same key ─────────────────────────────────────────────── GJ="$( run --test-gate=src/core.cpp --json )" -case "$GJ" in *'"run":"bash '*'/test/mythingcheck.sh"'*) ok "--test-gate --json tests_to_run rows carry \"run\"" ;; +case "$GJ" in *'"run":"bash test/mythingcheck.sh"'*) ok "--test-gate --json tests_to_run rows carry \"run\"" ;; *) no "--test-gate --json has no run key: $GJ" ;; esac # ── 4c) …and on --situ's text report ───────────────────────────────────────────────────────────────── S="$( run --situ=src/core.cpp )" -case "$S" in *'(run: bash '*'/test/mythingcheck.sh)'*) ok "--situ tests-to-run lines carry the run command" ;; +case "$S" in *'(run: bash test/mythingcheck.sh)'*) ok "--situ tests-to-run lines carry the run command" ;; *) no "--situ tests-to-run lines have no run command" ;; esac # ── 5) determinism + G4 ────────────────────────────────────────────────────────────────────────────── @@ -108,10 +119,10 @@ fi # this is the MENTION path on a corpus with 255 candidate runner scripts — where a wrong tie-break or # an over-eager match would show up immediately. RA="$( perl -e 'alarm 90; exec @ARGV' "$BIN" "$ROOT" --affected=src/graph.h 2>/dev/null )" -[ "$( runof 'cloneband_harness.cpp' "$RA" )" = "bash $ROOT/test/clonebandcheck.sh" ] \ +[ "$( runof 'cloneband_harness.cpp' "$RA" )" = "bash test/clonebandcheck.sh" ] \ && ok "repo: cloneband_harness.cpp -> run=\"bash test/clonebandcheck.sh\"" \ || no "repo: cloneband_harness.cpp run= wrong: '$( runof 'cloneband_harness.cpp' "$RA" )'" -[ "$( runof 'connectcore_harness.cpp' "$RA" )" = "bash $ROOT/test/connectcorecheck.sh" ] \ +[ "$( runof 'connectcore_harness.cpp' "$RA" )" = "bash test/connectcorecheck.sh" ] \ && ok "repo: connectcore_harness.cpp -> run=\"bash test/connectcorecheck.sh\"" \ || no "repo: connectcore_harness.cpp run= wrong: '$( runof 'connectcore_harness.cpp' "$RA" )'" @@ -121,12 +132,12 @@ RA="$( perl -e 'alarm 90; exec @ARGV' "$BIN" "$ROOT" --affected=src/graph.h 2>/d # by a bundle that also carries bodies, callers and notes should not have to leave the bundle to find # the command. Both now read the same TestRunnerIndex; absence still means "not derivable". P="$( run --pack-task="drive mid through core" )" -[ "$( runof 'mything_harness.cpp' "$P" )" = "bash $R/test/mythingcheck.sh" ] \ +[ "$( runof 'mything_harness.cpp' "$P" )" = "bash test/mythingcheck.sh" ] \ && ok "--pack-task rows carry run= (same index as affected/situ/test-gate/exercises)" \ || no "--pack-task run= missing/wrong: '$( runof 'mything_harness.cpp' "$P" )'" PJ="$( run --pack-task="drive mid through core" --json )" -case "$PJ" in *'"run":"bash '*'/test/mythingcheck.sh"'*) ok "--pack-task --json tests_to_run rows carry \"run\"" ;; +case "$PJ" in *'"run":"bash test/mythingcheck.sh"'*) ok "--pack-task --json tests_to_run rows carry \"run\"" ;; *) no "--pack-task --json has no run key in tests_to_run" ;; esac # --pr-context needs real git history, so the fixture becomes a repo HERE — after every arm above has run @@ -136,7 +147,7 @@ if command -v git >/dev/null 2>&1; then && git add -A && git commit -qm base ) >/dev/null 2>&1 printf 'int extra() { return mid(); }\n' >> "$R/src/core.cpp" PR="$( run --pr-context )" - [ "$( runof 'mything_harness.cpp' "$PR" )" = "bash $R/test/mythingcheck.sh" ] \ + [ "$( runof 'mything_harness.cpp' "$PR" )" = "bash test/mythingcheck.sh" ] \ && ok "--pr-context rows carry run= (the review lens's obligation is dischargeable)" \ || no "--pr-context run= missing/wrong: '$( runof 'mything_harness.cpp' "$PR" )'" case "$PR" in From 0617c09016e9724da34cab9cffbc2ed1b689ffd1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 11:14:54 -0400 Subject: [PATCH 02/20] perf(situ): the one report with no attributes said every disclosure twice as a paragraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --situ has no XML root, so every fact it owed a reader was written as a sentence and the sentences kept growing: the graph-count floor clause 601 B, the decl/def partner header 228 B, the tests-to-run header 267 B, the script-gate caveat 158 B. A byte attribution over the frozen question set put ~800 B per answer in those four lines — repeated on every call, and carrying facts (a floor, two gauges, a cap, an ordering, a blind-spot count) that a consumer can only act on once they are NAMED. METHODOLOGY §9 already says where honesty lives: in the attributes, not in the sentence around them. Each of the five becomes an attribute line, spelled exactly as the XML and JSON dialects already spell the same fact, so the three share one vocabulary: floor line counts_floor=1 graph_ambiguous= graph_unresolved= graph_unindexed= 601 -> 198 B partners decl/def partners (N) not_dependents=1 228 -> 134 B [1] header prcontext_cap=20 beside shown=/total=/capped= -55 B [2] header order=evidence, the attribute --affected's root already carries 267 -> 220 B script gates script_gates_unmodelled=N, the counter --affected publishes 158 -> 132 B Nothing is dropped. Every floor, cap and caveat survives, and the two readings with no attribute form — how to read a zero, and what [changed]/[partner]/hops mean on a row — stay as the shortest sentence that defines them. Measured with wc -c, same cache, same commit: this repo --situ=src/situ.h 2,320 -> 1,836 B (-484) --situ=src/testmap.h 2,302 -> 1,818 B (-484) RocksDB @0e2801ac --situ=db/write_batch.cc 7,412 -> 6,781 B (-631) That is below the ~800 B the attribution predicted: the prediction assumed the gauge names could go unglossed, and they carry a four-word gloss here instead. Gate: new test/situshapecheck.sh, one arm per converted disclosure — the attribute is present, its value agrees with the XML sibling's where one exists (graph_unindexed= against --affected's root, script_gates_unmodelled= against --affected's), the reading survives, and a per-line byte ratchet so the prose cannot creep back. Red first on the previous binary: 10 FAIL rows. Listed in test/regression.sh in this commit (612 -> 613 gates, generator re-run). test/floormarkcheck.sh (9) stays green — it matches "counts_floor=1" and "is a FLOOR, never a total", and situshapecheck mirrors both so a regression reds in both. test/rootrelemitcheck.sh ARM 6's --situ row extraction is re-pinned: the section's closing disclosure is now an attribute line rather than a parenthesised sentence, and a row path never contains "=". Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 24 ++++ README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/graphlegend.h | 23 ++-- src/situ.h | 29 +++-- test/regression.sh | 2 +- test/rootrelemitcheck.sh | 5 +- test/situshapecheck.sh | 213 +++++++++++++++++++++++++++++++++ 9 files changed, 281 insertions(+), 31 deletions(-) create mode 100755 test/situshapecheck.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 26fdec12..4d7849f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,30 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — `--situ`'s disclosures are attributes + +`--situ` is the only report with no XML root to hang attributes on, so every disclosure it owed was written +as a sentence, and the sentences grew: the graph-count floor clause ran 601 B, the decl/def partner header +228 B, the tests-to-run header 267 B and the script-gate caveat 158 B — about 800 B of prose on every call, +carrying facts a reader can only act on once they are named. They are now named. The floor line is +`counts_floor=1 graph_ambiguous=N graph_unresolved=N graph_unindexed=N (map-header gauges) — every count +above is a FLOOR, never a total; a zero is "none found", never "none exists"` (601 → 198 B), using the same +attribute spellings the XML and JSON dialects already use, so the three share one vocabulary. The partner +header carries `not_dependents=1` (228 → 134 B), section `[1]` carries `prcontext_cap=20` where it used to +spell `--pr-context`'s own cap as an aside, section `[2]` carries `order=evidence` — the attribute +`--affected`'s root already carries for the same ordering (267 → 220 B) — and the script-gate blind spot is +`script_gates_unmodelled=N`, the same counter `--affected` publishes, with its cause kept (158 → 132 B). +Nothing was dropped: every floor, cap and caveat survives, and the readings that have no attribute form (how +to read a zero; what `[changed]`/`[partner]`/`hops` mean on a row) stay as the shortest sentence that defines +them. Measured with `wc -c`, same cache, same commit: on this repo `--situ=src/situ.h` 2,320 → 1,836 B +(−484) and `--situ=src/testmap.h` 2,302 → 1,818 B; on RocksDB @0e2801ac `--situ=db/write_batch.cc` 7,412 → +6,781 B (−631). The gate is the new `test/situshapecheck.sh`: one arm per converted disclosure, each +asserting the attribute is present, that its value agrees with the XML sibling's where one exists +(`graph_unindexed=`, `script_gates_unmodelled=`), that the reading survives, and a per-line byte ratchet so +the prose cannot creep back; 10 of its rows are red on the previous binary. `test/floormarkcheck.sh` keeps +the two anchor phrases it matches — `counts_floor=1` and "is a FLOOR, never a total" — and situshapecheck +mirrors them, so a regression reds in both. + ### Changed — one absolute root per change report `--test-gate`, `--situ` and `--affected` state the crawl root once, in the envelope (`root=` in XML and diff --git a/README.md b/README.md index 5079d8d1..cd6d59e9 100644 --- a/README.md +++ b/README.md @@ -1799,9 +1799,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-612 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +613 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 **612 gate scripts** and is the authoritative list; +`test/regression.sh` names **613 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 b93fe69f..b11bf6ff 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` | 612 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 613 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) @@ -5834,7 +5834,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 **612 gate scripts**, all of which exist on disk. +naming **613 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`. @@ -6846,7 +6846,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 612. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 613. 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 2d53e3ba..82fd532d 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1068,7 +1068,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["612 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 + ["613 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."], @@ -1092,7 +1092,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, "612", "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, "613", "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", @@ -1342,7 +1342,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["179 long flags · 33 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["612 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["613 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 70 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/graphlegend.h b/src/graphlegend.h index cf5bb562..64067545 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -168,12 +168,19 @@ inline std::string graphCountFloorBrief( bool hasUnindexed ) return std::string( kGraphCountFloorBriefLegend ) + graphUnindexedLegend( hasUnindexed ); } -// The same two facts as PROSE, for the one graph-count report that is text (--situ's [1] blast radius). -// The trailing %s is the #66 gauge's prose clause — EMPTY when nothing went unindexed, so this dialect -// keeps the same omit-at-zero reading as the XML/JSON attribute rather than printing a bare "0" the other -// two dialects never print. Rendered through graphUnindexedTextClause() below, never spelled at the site. +// The same facts for the one graph-count report that is TEXT (--situ's [1] blast radius) — as ATTRIBUTES, +// not as a paragraph. A5 (2026-09-13, PLAN_OUTPUT_ROUTING_LOOP §1.5): this line ran 601 B on every --situ +// answer to say four things that are names with values — counts_floor, the two resolver gauges and, when a +// file could not be read at all, the third — plus the two readings a consumer cannot supply for itself: +// that the counts are floors, and how to read a zero. The names are now spelled exactly as the XML/JSON +// dialects spell them (graph_ambiguous=/graph_unresolved=/graph_unindexed=, counts_floor=1), so the three +// dialects share one vocabulary, and the two readings stay as the short clause after them. Nothing was +// dropped: METHODOLOGY §9 puts honesty in the attributes, and the sentence was never the honest part. +// The trailing {} is the #66 gauge — EMPTY when nothing went unindexed, so this dialect keeps the same +// omit-at-zero reading as the attribute rather than printing a bare "0" the other two never print. +// Gate: test/situshapecheck.sh (1); test/floormarkcheck.sh (9) keeps the two anchor phrases verbatim. inline constexpr const char* kGraphCountFloorTextLine = - " counts_floor=1: every count above is a FLOOR, never a total (call edges are name-based; dynamic dispatch, callbacks and macros can be missing) — read a zero as \"none found\", never as \"none exists\"; graph_ambiguous={} graph_unresolved={} is the whole graph's resolver gauge (calls split over several defs / calls whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved={}\n"; // std::format FORMAT: two gauge totals + the clause + " counts_floor=1 graph_ambiguous={} graph_unresolved={}{} (map-header gauges) — every count above is a FLOOR, never a total; a zero is \"none found\", never \"none exists\"\n"; // std::format FORMAT: two gauge totals + the clause // The #66 clause for the prose dialect. "" at zero — the absence IS the confident case, same as the attribute. inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) @@ -182,10 +189,8 @@ inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) { return {}; } - char buf[256]; // literal ~180 B + one %zu at 20 digits = ~198 B worst case; snprintf truncates regardless - rw::formatTo( buf, sizeof( buf ), - "; graph_unindexed={} is a third gauge — files no grammar in this build could read at all (the map header's unindexed=), whose calls produce no reference and so raise neither gauge above", - unindexedFiles ); + char buf[64]; // literal 18 B + one size_t at 20 digits = 38 B worst case; snprintf truncates regardless + rw::formatTo( buf, sizeof( buf ), " graph_unindexed={}", unindexedFiles ); return buf; } diff --git a/src/situ.h b/src/situ.h index c42c7521..ee5568fd 100644 --- a/src/situ.h +++ b/src/situ.h @@ -361,19 +361,19 @@ inline constexpr std::size_t kSituPartnerFileRowsShown = 4; // section [1] — // XML root to carry attributes — and the exact pasteable follow-up. All of it appears ONLY on a cut section: // an untruncated section is byte-unchanged, and no section ever prints capped=0. inline std::string situShowingNote( std::size_t shown, std::size_t rowTotal, const char* rowNoun, - std::string_view nextInvocation = {}, std::string_view extraProse = {} ) + std::string_view nextInvocation = {}, std::string_view extraAttrs = {} ) { if( rowTotal <= shown ) { return {}; } - // ORDER IS THE CONTRACT: prose first, then the machine triple, then `next:` LAST — a pasteable command has - // to run to the end of the parenthetical or a reader cannot tell where it stops. `extraProse` is the one - // section-specific sentence (section [1] pointing at --pr-context's own cap) that used to be spliced in by - // hand at size() - 1, which put it AFTER the command. + // ORDER IS THE CONTRACT: the reading first, then the machine attributes, then `next:` LAST — a pasteable + // command has to run to the end of the parenthetical or a reader cannot tell where it stops. A5: + // `extraAttrs` is the one section-specific fact (section [1] naming --pr-context's own cap), and it is an + // ATTRIBUTE spelled beside the triple rather than the 68 B sentence it used to be spliced in as. std::string note = " (showing " + std::to_string( shown ) + " of " + std::to_string( rowTotal ) + " " + rowNoun; - note += std::string( extraProse ); note += " — shown=" + std::to_string( shown ) + " total=" + std::to_string( rowTotal ) + " capped=1"; + note += std::string( extraAttrs ); if( !nextInvocation.empty() ) { note += "; next: " + std::string( nextInvocation ); @@ -430,7 +430,9 @@ inline void writeSituDeclDefRows( std::FILE* out, const std::vector&1 | sed 's/^/ | /' fi # 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 agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck 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 clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck 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 nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck 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 pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck 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 recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck 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 declinedlistcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck 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 clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck 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 nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck 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 pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck 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 recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck 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 declinedlistcheck; 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/rootrelemitcheck.sh b/test/rootrelemitcheck.sh index edaa8791..121422a8 100755 --- a/test/rootrelemitcheck.sh +++ b/test/rootrelemitcheck.sh @@ -371,12 +371,15 @@ for spelling in abs rel; do # the JSON twin carries "p" in BOTH arrays — narrow to tests_to_run so this compares like with like j_rows=$( cd "$CD" && "$BIN" "$RT" --test-gate=geometry.cpp --json 2>/dev/null \ | sed -n 's/.*"tests_to_run":\[\([^]]*\)\].*/\1/p' | tr ',' '\n' | sed -n 's/.*"p":"\([^"]*\)".*/\1/p' ) + # A5 re-pin (2026-09-13): the section's closing script-gate disclosure is now the attribute line + # `script_gates_unmodelled=N — …`, which starts with a non-space non-"(" byte and so was read as a path + # row. A row path never contains "=", so that is the discriminator. # M21(b) re-pin (capture-audit 2026-09-04, lane L8): every --situ tests-to-run line now ends in a run # recipe OR its "(run: not derivable)" disclosure, so the old "the line contains no '(' " extraction # matched nothing and this arm read red while the SPELLING it exists to compare was correct. Re-pinned to # the new contract: take the path FIELD off a row line, not the whole line. s_rows=$( cd "$CD" && "$BIN" "$RT" --situ=geometry.cpp 2>/dev/null \ - | sed -n '/tests to run/,/^ \[3\]/p' | awk '/^ [^ (]/ { print $1 }' ) + | sed -n '/tests to run/,/^ \[3\]/p' | awk '/^ [^ (]/ && $1 !~ /=/ { print $1 }' ) if [ -z "$a_rows" ]; then no "ARM6/$spelling --affected emitted NO test row for distance — the arm would be a false green" continue diff --git a/test/situshapecheck.sh b/test/situshapecheck.sh new file mode 100755 index 00000000..5f36902e --- /dev/null +++ b/test/situshapecheck.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# situshapecheck.sh — --situ's DISCLOSURE SHAPE: every fact the report used to say in a sentence is said as +# an attribute, and nothing it disclosed has gone missing. +# +# test/situshapecheck.sh +# RIPWIRE_BIN=asan/ripwire test/situshapecheck.sh +# +# WHY THIS EXISTS. --situ is the mid-task report, and it is the one verb with no XML root to hang attributes +# on — so every disclosure it owed was written as prose, and the prose grew: the graph-count floor clause ran +# 601 B, the decl/def partner header 229 B, the tests-to-run header 267 B, and the script-gates caveat 152 B, +# on a report whose whole ANSWER (the [2] rows) is what the agent acts on. A byte attribution over the frozen +# question set (PLAN_OUTPUT_ROUTING_LOOP §1.2) put ~800 B per answer in those four sentences, repeated on every +# call, carrying facts a reader can only use if they are NAMED — which is what an attribute is. +# +# METHODOLOGY §9 is the rule this gate enforces: honesty lives in ATTRIBUTES, and a shortened sentence may not +# quietly drop a floor, a cap, or a caveat. So each arm below names ONE disclosure the prose carried and +# asserts the attribute form still carries it — by name, with a reading — plus a byte ratchet per line so the +# prose cannot creep back. test/floormarkcheck.sh keeps the two anchor phrases; this gate mirrors them, so a +# regression reds HERE too rather than only in a gate about a different property. +# +# Exit 0 = ALL PASS, non-zero = SOME FAILED. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +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 (cmake --build build -j)"; exit 2; } + +# ── the fixture: a header/implementation pair with a test partner, a same-stem DECOY in another directory, +# and a caller, so [1] has a blast radius AND a decl/def partner list. Seeded as a git repo because --situ +# mines co-change; the dates are fixed so the report is reproducible. +FX="$TMP/fx" +mkdir -p "$FX/core" "$FX/other" "$FX/app" +cat > "$FX/core/widget.h" <<'EOF' +#pragma once +int widgetArea( int w, int h ); +int widgetPerimeter( int w, int h ); +EOF +cat > "$FX/core/widget.cc" <<'EOF' +#include "core/widget.h" + +int widgetArea( int w, int h ) +{ + return w * h; +} + +int widgetPerimeter( int w, int h ) +{ + return 2 * ( w + h ); +} +EOF +cat > "$FX/core/widget_test.cc" <<'EOF' +#include "core/widget.h" + +int test_widget_area() +{ + return widgetArea( 2, 3 ); +} +EOF +cat > "$FX/core/widget.inl" <<'EOF' +inline int widgetSquare( int s ) { return s * s; } +EOF +cat > "$FX/core/gadget.cc" <<'EOF' +int gadgetSpin( int n ) { return n + 1; } +EOF +cat > "$FX/other/widget.cc" <<'EOF' +int otherWidget( int n ) { return n - 1; } +EOF +cat > "$FX/app/main.cc" <<'EOF' +#include "core/widget.h" + +int appMain() +{ + return widgetArea( 4, 5 ) + widgetPerimeter( 4, 5 ); +} +EOF +( cd "$FX" && git init -q -b main >/dev/null 2>&1 + git config user.email rw@example.invalid; git config user.name ripwire + git add -A >/dev/null 2>&1 + GIT_AUTHOR_DATE='2026-01-01T00:00:00 +0000' GIT_COMMITTER_DATE='2026-01-01T00:00:00 +0000' \ + git commit -q -m seed >/dev/null 2>&1 ) || true + +OUT="$TMP/situ.txt" +"$BIN" "$FX" --situ=core/widget.cc >"$OUT" 2>/dev/null +REPO_OUT="$TMP/repo.txt" +"$BIN" "$ROOT" --situ=src/graph.h >"$REPO_OUT" 2>/dev/null + +[ -s "$OUT" ] || { no "the fixture produced no --situ report at all — every arm below would be a false green"; echo "situshapecheck: SOME FAILED"; exit 1; } +grep -q '^ \[1\] blast radius' "$OUT" || { no "the fixture's report has no [1] section — fixture broken"; echo "situshapecheck: SOME FAILED"; exit 1; } +ok "fixture: --situ=core/widget.cc produced a report with a [1] section" + +# the one line matching a pattern, and its byte length +line_of(){ grep -m1 -- "$1" "$2"; } +len_of(){ local l; l="$( line_of "$1" "$2" )"; printf '%s' "${#l}"; } + +# ── (1) THE GRAPH-COUNT FLOOR — four gauges, one line, both anchor phrases ─────────────────────────────── +# Was 601 B of prose on every answer. The facts it owed: counts_floor, the two resolver gauges, the third +# (unindexed) gauge when the build could not read some files at all, that every count is a FLOOR, and how to +# read a zero. All six survive; only the sentence around them is gone. +FL="$( line_of 'counts_floor=1' "$REPO_OUT" )" +if [ -z "$FL" ]; then + no "(1) --situ states no counts_floor at all" +else + for tok in 'counts_floor=1' 'graph_ambiguous=' 'graph_unresolved='; do + case "$FL" in *"$tok"*) ok "(1) floor line carries $tok" ;; *) no "(1) floor line lost $tok: $FL" ;; esac + done + # the two phrases test/floormarkcheck.sh matches, mirrored here so a regression reds in both gates + case "$FL" in *'is a FLOOR, never a total'*) ok "(1) floor line keeps the anchor phrase 'is a FLOOR, never a total'" ;; + *) no "(1) floor line lost floormarkcheck's anchor phrase: $FL" ;; esac + case "$FL" in *'none found'*) ok "(1) floor line keeps the zero reading ('none found')" ;; + *) no "(1) floor line lost the zero reading: $FL" ;; esac + # the third gauge rides exactly when the map header's unindexed= is non-zero (#66's attribute⇒clause rule) + # the oracle is the XML sibling over the SAME corpus: --affected's root carries graph_unindexed= when, and + # only when, some file no grammar in this build could read was crawled. (The map header's own unindexed= is + # an extension HISTOGRAM, not a count, so it cannot serve as the oracle here.) + UNIDX="$( "$BIN" "$ROOT" --affected=src/graph.h 2>/dev/null | tr ' ' '\n' | sed -n 's/^graph_unindexed="\([0-9]*\)".*/\1/p' | head -1 )" + if [ "${UNIDX:-0}" -gt 0 ]; then + case "$FL" in *"graph_unindexed=$UNIDX"*) ok "(1) floor line carries graph_unindexed=$UNIDX, the same gauge --affected's root carries" ;; + *) no "(1) --affected says graph_unindexed=$UNIDX and the floor line does not carry it: $FL" ;; esac + else + case "$FL" in *'graph_unindexed='*) no "(1) floor line claims graph_unindexed= on a corpus with nothing unindexed" ;; + *) ok "(1) floor line omits graph_unindexed= — nothing was unindexed" ;; esac + fi + N="$( len_of 'counts_floor=1' "$REPO_OUT" )" + [ "$N" -le 200 ] && ok "(1) floor line is ${N} B (ratchet 200) — an attribute line, not a paragraph" \ + || no "(1) floor line is ${N} B, over the 200 B ratchet: the prose has crept back" +fi + +# ── (2) THE DECL/DEF PARTNER HEADER — the "NOT dependents" caveat becomes an attribute ─────────────────── +# The 229 B sentence existed to stop a reader treating the partner rows as transitive dependents. That is a +# NAMEABLE fact: not_dependents=1, beside the count. +PH="$( line_of 'decl/def partners' "$OUT" )" +if [ -z "$PH" ]; then + no "(2) the fixture's report lists no decl/def partners — the arm would be a false green" +else + case "$PH" in *'not_dependents=1'*) ok "(2) partner header carries not_dependents=1" ;; + *) no "(2) partner header does not name the NOT-dependents caveat as an attribute: $PH" ;; esac + case "$PH" in *'(2)'*|*'(1)'*|*'(3)'*) ok "(2) partner header still states how many partners there are" ;; + *) no "(2) partner header lost its count: $PH" ;; esac + N="$( len_of 'decl/def partners' "$OUT" )" + [ "$N" -le 140 ] && ok "(2) partner header is ${N} B (ratchet 140)" \ + || no "(2) partner header is ${N} B, over the 140 B ratchet" +fi + +# ── (3) SECTION [1]'s pr-context ASIDE — a cap is a number, so it is an attribute ──────────────────────── +# "--pr-context's own per-file blast-radius list is also capped, at 20" is one number and one target. +B1="$( line_of '\[1\] blast radius' "$REPO_OUT" )" +case "$B1" in + *'capped=1'*) ok "(3) [1] keeps pageview.h's shown=/total=/capped= triple" ;; + *) no "(3) [1] lost its cut disclosure: $B1" ;; +esac +case "$B1" in + *'prcontext_cap=20'*) ok "(3) [1] names --pr-context's own cap as prcontext_cap=20" ;; + *) no "(3) [1] does not carry prcontext_cap=20: $B1" ;; +esac +case "$B1" in + *"pr-context's own per-file blast-radius list is also capped"*) + no "(3) [1] still spells the pr-context cap as a sentence" ;; + *) ok "(3) [1] no longer spells the pr-context cap as a sentence" ;; +esac + +# ── (4) SECTION [2]'s EVIDENCE ORDER — the same attribute its XML sibling carries ──────────────────────── +# --affected's root says order="evidence"; --situ said the same thing in 127 B of prose and never named it. +B2="$( line_of '\[2\] tests to run' "$REPO_OUT" )" +case "$B2" in + *'order=evidence'*) ok "(4) [2] names its ordering as order=evidence, like --affected's root" ;; + *) no "(4) [2] does not carry order=evidence: $B2" ;; +esac +case "$B2" in + *'[changed]'*) ok "(4) [2] keeps a reading of the evidence tags the rows carry" ;; + *) no "(4) [2] dropped the reading of [changed]/[partner]/hops — the tags would be undefined" ;; +esac +N="$( len_of '\[2\] tests to run' "$REPO_OUT" )" +[ "$N" -le 230 ] && ok "(4) [2] header is ${N} B (ratchet 230)" \ + || no "(4) [2] header is ${N} B, over the 230 B ratchet" + +# ── (5) THE SCRIPT-GATES BLIND SPOT — the same counter --affected carries as an attribute ──────────────── +SG="$( line_of 'script_gates_unmodelled=' "$REPO_OUT" )" +AFF="$( "$BIN" "$ROOT" --affected=src/graph.h 2>/dev/null | tr ' ' '\n' | sed -n 's/^script_gates_unmodelled="\([0-9]*\)".*/\1/p' | head -1 )" +if [ -z "$SG" ]; then + no "(5) --situ does not name its script-gate blind spot as script_gates_unmodelled=" +else + ok "(5) --situ carries script_gates_unmodelled= as an attribute" + case "$SG" in *"script_gates_unmodelled=$AFF"*) ok "(5) --situ and --affected report the SAME count ($AFF)" ;; + *) no "(5) --situ's count disagrees with --affected's script_gates_unmodelled=\"$AFF\": $SG" ;; esac + case "$SG" in *'not call edges'*) ok "(5) the blind spot keeps its cause (script-to-binary edges are not call edges)" ;; + *) no "(5) the blind spot lost its cause: $SG" ;; esac + N="$( len_of 'script_gates_unmodelled=' "$REPO_OUT" )" + [ "$N" -le 140 ] && ok "(5) script-gates line is ${N} B (ratchet 140)" \ + || no "(5) script-gates line is ${N} B, over the 140 B ratchet" +fi + +# ── (6) NOTHING WENT MISSING, AND THE REPORT GOT SMALLER ──────────────────────────────────────────────── +# The whole point: fewer bytes, same facts. Every disclosure token above must be present in ONE report. +MISSING="" +for tok in 'counts_floor=1' 'graph_ambiguous=' 'graph_unresolved=' 'order=evidence' 'script_gates_unmodelled=' 'capped=1' 'prcontext_cap='; do + grep -q -- "$tok" "$REPO_OUT" || MISSING="$MISSING $tok" +done +[ -z "$MISSING" ] && ok "(6) one report carries every disclosure attribute" \ + || no "(6) the report is missing:$MISSING" +# determinism, on the verb this gate reshapes +"$BIN" "$FX" --situ=core/widget.cc >"$TMP/d1" 2>/dev/null +"$BIN" "$FX" --situ=core/widget.cc >"$TMP/d2" 2>/dev/null +cmp -s "$TMP/d1" "$TMP/d2" && ok "(6) --situ is byte-identical across two runs" || no "(6) --situ is not deterministic" + +echo +if [ "$fail" -eq 0 ]; then echo "situshapecheck: ALL PASS"; else echo "situshapecheck: SOME FAILED"; fi +exit "$fail" From fe0a13a9f3b164d95cc1b2aeef063bc2f08b7856 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 11:54:42 -0400 Subject: [PATCH 03/20] feat(situ): the files a change drags with it were the ones no walk could reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --situ's [1] section answers "what depends on this" with a caller walk, and the files that actually move WITH a changed file are mostly invisible to it: a header does not call the source that implements it, an .inl has no grammar in any build so it is not in the index at all, and a harness the graph cannot link (a fixture-built test, a generated main) is reached by nothing. A byte-and-answer attribution over a frozen 30-question set found two incomplete answers sitting exactly there. Section [1] now lists them, under the decl/def partners and the floor clause: lexical siblings (3) not_dependents=1 — same directory and stem as a changed file (header/impl partner, test, .inl); static, not a graph result: core/widget.h core/widget.inl core/widget_test.cc The rule is the dumbest one that is always right — SAME DIRECTORY, and the same filename stem or the stem-partner convention testmap.h already owns (_test, test_, Test, _unittest, _spec). Same directory is load-bearing, not a speed trick: a same-stem file in another directory is a NAMESAKE, and listing namesakes would make the block noise on exactly the large trees it is for. The candidate population is the CRAWL's rather than the INDEX's, so the .inl/.ipp/.tcc partner a C++ change most often has to edit is named; the crawl's unsupported-extension ROW list is itself capped, which is the one way this list can be short of the truth, and that is disclosed as unindexed_rows_floor=1. Capped at 8 with shown=/total=/ capped=1 and a pasteable next:, raisable with --limit like the report's other two listings. The block is ADDITIVE to the decl/def partners above it: a file is often both, and suppressing the overlap was tried and reverted because it removed widget.h from "the siblings of widget.cc" — the one row a reader of the block is looking for — to save about 20 B. It sits after the floor clause because it carries no graph-derived count for that clause to qualify. The MCP situational_awareness twin carries the same list as "siblings" with "siblings_total". Both surfaces relativize through a STORED-path form of the same relativizer, because an unindexed sibling has no fileId at all — the one file this report names that the index does not hold. Measured with wc -c, same cache, same commit: RocksDB @0e2801ac --situ=db/write_batch.cc 6,781 -> 6,967 B, a one-row block naming db/write_batch_test.cc that no other section of that report reaches. On this repo every source file is a lone .h, so no file has a lexical sibling and the report is byte-unchanged. Gate: test/situshapecheck.sh arms (7)-(7d) — a fixture with a .h/.cc/_test.cc/.inl quadruple, a same-stem DECOY in another directory and a same-directory different-stem file (both must be absent), a nine-sibling stem for the cap and its disclosure and for --limit's relief, a no-git copy of the same tree proving the block is static (which is also why it cannot leak), and the MCP twin agreeing row for row. Red first: 4 FAIL rows. docs/LIMITS.md and docs/TUNING.md regenerated for the new row cap (208 -> 209 caps). Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 28 +++++++++ docs/LIMITS.md | 11 ++-- docs/TUNING.md | 10 +-- src/mcpverbs.h | 30 ++++++++- src/situ.h | 139 ++++++++++++++++++++++++++++++++++++++++- test/situshapecheck.sh | 120 +++++++++++++++++++++++++++++++++++ 6 files changed, 324 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7849f4..3802cff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,34 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `--situ` lists a changed file's lexical siblings + +The files that move WITH a changed file are usually its neighbours by name, and the caller walk can reach +none of them: a header does not call the source that implements it, an `.inl` is not indexed by any grammar +in any build, and a harness the graph cannot link — a fixture-built test, a generated `main` — is reached by +nothing. A byte-and-answer attribution over a frozen 30-question set found two answers incomplete for +exactly that reason. Section `[1]` of `--situ` now lists them, under the decl/def partners and the floor +clause: `lexical siblings (N) not_dependents=1 — same directory and stem as a changed file (header/impl +partner, test, .inl); static, not a graph result`, then one root-relative path per row. The rule is the +dumbest one that is always right — same directory, and the same filename stem or the stem-partner convention +the tests-to-run rows already use (`_test`, `test_`, `Test`, `_unittest`, `_spec`). Same +directory is load-bearing rather than a speed trick: a same-stem file in another directory is a namesake, not +a partner, and listing namesakes would make the block noise on exactly the large trees it is for. The +candidate population is the CRAWL's, not the index's, so an `.inl`/`.ipp`/`.tcc` partner — the sibling a C++ +change most often has to edit, and one no grammar can read — is named; the crawl's unsupported-extension row +list is itself capped, and the one case where that can shorten this list is disclosed as +`unindexed_rows_floor=1`. The block is capped at 8 rows with `shown=`/`total=`/`capped=1` and a pasteable +`next:`, and `--limit=N` raises it like the report's other two listings. The MCP `situational_awareness` +twin carries the same list as `siblings` with `siblings_total`. It costs what it lists: measured with +`wc -c` on RocksDB @0e2801ac, `--situ=db/write_batch.cc` 6,781 → 6,967 B (+186 for a one-row block naming +`db/write_batch_test.cc`, which no other section of that report reaches); on this repo, where every source +file is a lone `.h`, no file has a lexical sibling and the report is byte-unchanged. Gate: +`test/situshapecheck.sh` arms (7)–(7d) on a fixture with a `.h`/`.cc`/`_test.cc`/`.inl` quadruple, a +same-stem DECOY in another directory and a same-directory file with a different stem — both must be absent — +plus a nine-sibling stem for the cap and its disclosure, a no-git copy of the same tree proving the block is +static (and therefore cannot leak), and the MCP twin agreeing row for row. All four arms red on the previous +binary. + ### Changed — `--situ`'s disclosures are attributes `--situ` is the only report with no XML root to hang attributes on, so every disclosure it owed was written diff --git a/docs/LIMITS.md b/docs/LIMITS.md index d4b95a0e..c8ed7b1b 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -16,10 +16,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 208 | 83 | 116 | **92** | +| 209 | 83 | 117 | **92** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 208 + 7 is the 215 constants this generator parses out of `src/`. +are not counted as caps, and 209 + 7 is the 216 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -37,8 +37,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. **111 of 208 caps are classified -(37 INDEXING, 39 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **111 of 209 caps are classified +(37 INDEXING, 39 OUTPUT, 35 BOUNDARY); the remaining 98 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 @@ -88,7 +88,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 83 files that declare a cap — the 208 caps counted above, and no parameter. +One table for each of the 83 files that declare a cap — the 209 caps counted above, and no parameter. ### `src/abicheck.h` @@ -790,6 +790,7 @@ Discloses: `tests_capped`, `untested_capped` | `kSituBlastFilesShown` | `8` | OUTPUT | section [1] — blast-radius file rows; a raisable DEFAULT | | `kSituPartnerFileRowsShown` | `4` | OUTPUT | section [1] — decl/def partner rows | | `kSituPartnerRowsShown` | `8` | OUTPUT | section [3] — co-change partner rows; a raisable DEFAULT | +| `kSituSiblingRowsShown` | `8` | — | section [1] — L-D lexical sibling rows; a raisable DEFAULT | ### `src/skillscan.h` diff --git a/docs/TUNING.md b/docs/TUNING.md index e02d15fb..556c802f 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,18 +14,18 @@ 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 | | --- | --- | --- | --- | --- | --- | -| 126 | 125 | 112 | 12 | **37** | 75 | +| 127 | 126 | 112 | 12 | **37** | 75 | The first two columns are not the same number, and the gap is not a rounding: `src/` holds -**126 cap declarations** under **125 distinct names** (`kRowCap` declared in more than one file). The -sweep patches by NAME, so `112 + 12` accounts for the 125 NAMES — not the 126 declarations. Quoting -"113 of 126" would be wrong in both halves at once, which is exactly the shape of error a +**127 cap declarations** under **126 distinct names** (`kRowCap` declared in more than one file). The +sweep patches by NAME, so `112 + 12` accounts for the 126 NAMES — not the 127 declarations. Quoting +"113 of 127" would be wrong in both halves at once, which is exactly 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 126-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 127-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 634e3ad7..73837624 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1236,9 +1236,14 @@ inline std::string situationDiffJson( const std::string& root, const std::string // CLI text twin (situ.h::writeSituation) states the SAME fact on its own leading "root: …" line. const bool situJSingleRoot = ing.realPaths.empty(); const std::string situJRootPrefix = situJSingleRoot ? sarif::rootPrefixOf( root ) : std::string(); + // L-D: the STORED-path form is the primitive (an unindexed sibling has no fileId); the fileId form is it. + const auto situJPathRelStr = [ & ]( std::string_view p ) -> std::string_view + { + return situJSingleRoot ? sarif::rootRelativeUri( p, situJRootPrefix ) : p; + }; const auto situJPathRel = [ & ]( std::uint32_t f ) -> std::string_view { - return situJSingleRoot ? sarif::rootRelativeUri( ing.files[f], situJRootPrefix ) : std::string_view( ing.files[f] ); + return situJPathRelStr( ing.files[f] ); }; const auto fileObj = [ & ]( std::uint32_t f ) -> std::string @@ -1338,7 +1343,28 @@ inline std::string situationDiffJson( const std::string& root, const std::string // F2: and the window the co-change zero below was mined in — a JSON reader that only sees an empty // `forgotten` array cannot tell "no partners" from "the window mined nothing", and this surface is the // one where that reads most like an answer. - out += "]" + declDefAndWindowJson( facts, situJPathRel ) + ",\"forgotten\":["; + // L-D: the same lexical siblings the CLI report's [1] section lists — the ONE list here whose members + // may not be indexed at all (an unindexed .inl has no fileId), so they carry the STORED spelling through + // the string relativizer rather than the fileId one. Served whole, like every other array in this payload. + std::string situJSibs = ",\"siblings\":["; + { + bool first = true; + for( const std::string& p : facts.siblings.paths ) + { + if( !first ) + { + situJSibs += ","; + } + first = false; + situJSibs += "{\"file\":\"" + mcpdetail::jsonEscape( std::string( situJPathRelStr( p ) ) ) + "\"}"; + } + } + situJSibs += "],\"siblings_total\":" + std::to_string( facts.siblings.paths.size() ); + if( facts.siblings.unindexedRowsFloor ) + { + situJSibs += ",\"siblings_unindexed_rows_floor\":true"; // the crawl's unsupported-extension ROW list was itself cut + } + out += "]" + declDefAndWindowJson( facts, situJPathRel ) + situJSibs + ",\"forgotten\":["; { bool first = true; for( std::size_t i = situJForgot.begin; i < situJForgot.end; ++i ) diff --git a/src/situ.h b/src/situ.h index ee5568fd..2414e86c 100644 --- a/src/situ.h +++ b/src/situ.h @@ -355,6 +355,7 @@ inline std::vector declDefPartners( const IngestResult& ing, con inline constexpr std::size_t kSituBlastFilesShown = 8; // section [1] — blast-radius file rows; a raisable DEFAULT inline constexpr std::size_t kSituPartnerRowsShown = 8; // section [3] — co-change partner rows; a raisable DEFAULT inline constexpr std::size_t kSituPartnerFileRowsShown = 4; // section [1] — decl/def partner rows +inline constexpr std::size_t kSituSiblingRowsShown = 8; // section [1] — L-D lexical sibling rows; a raisable DEFAULT // §B12.1 gave this the "showing N of M " form so a reader could see the gap without a second sentence; // C1 F-10 adds the machine half — pageview.h's shown=/total=/capped= spelled in prose, because --situ has no @@ -419,6 +420,104 @@ inline std::string situNextInvocation( std::string_view selector, std::size_t ne return verb + " --limit=" + std::to_string( needed ); } +// ── L-D — a changed file's LEXICAL siblings ────────────────────────────────────────────────────────── +// The files that move WITH a changed file are usually its neighbours by NAME, and the caller-walk can reach +// none of them: a header does not call the source that implements it, an .inl is not indexed at all, and a +// harness the graph cannot link (a fixture-built test, a generated main) is reached by nothing. On the frozen +// question set (PLAN_OUTPUT_ROUTING_LOOP §1.5, lesson L-D) two answers were incomplete for exactly that +// reason. So section [1] lists them, and the rule is deliberately the dumbest one that is always right: +// +// SAME DIRECTORY, and the same filename stem — or the stem-partner convention testmap.h already owns +// (_test, test_, Test, _unittest, _spec), so widget.cc names widget_test.cc. +// +// SAME DIRECTORY is load-bearing, not a performance trick: a same-stem file in another directory is a +// NAMESAKE, not a partner (RocksDB has db/version_set.cc and utilities/…/version_set_test.cc that are about +// different things), and listing namesakes would make the block noise on exactly the large corpora it is for. +// +// The candidate population is the crawl's, not the INDEX's: a `.inl`, `.ipp` or `.tcc` partner has no grammar +// in any build, so it never enters ing.files, and it is the sibling a C++ change most often has to edit. Those +// come from ing.crawlSkips.unsupported — whose ROW list is capped even though its count is exact, which is the +// one place this list can be short of the truth and is disclosed as unindexed_rows_floor=1 when it applies. +// +// STATIC BY CONSTRUCTION: no git, no graph, no history window. That is what makes it cheap, and it is also +// why it cannot leak a future commit into an answer about the present. +struct SituSiblings +{ + std::vector paths; // the STORED spelling (relativized by the caller), path-ascending, unique + bool unindexedRowsFloor = false; // the crawl's unsupported-extension ROW list was itself cut +}; + +inline std::string_view situDirOf( std::string_view path ) noexcept +{ + const std::size_t slash = path.rfind( '/' ); + return slash == std::string_view::npos ? std::string_view() : path.substr( 0, slash ); +} + +inline std::string_view situStemOf( std::string_view path ) noexcept +{ + return mention_detail::stripExt( mention_detail::baseNameOf( path ) ); +} + +// ADDITIVE, deliberately: a file may be BOTH a decl/def partner (symbol identity) and a lexical sibling +// (name), and the header/implementation pair is the commonest case of exactly that. Suppressing the overlap +// was tried and reverted — it removed `widget.h` from "the siblings of widget.cc", which is the one row a +// reader of this block is looking for, to save about 20 B. The two blocks answer two questions, and each +// answers its own whole. +inline SituSiblings lexicalSiblings( const IngestResult& ing, const std::vector& changedFile ) +{ + SituSiblings out; + std::vector changedPaths; + for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) + { + if( changedFile[f] ) + { + changedPaths.push_back( ing.files[f] ); + } + } + if( changedPaths.empty() ) + { + return out; + } + const auto isSiblingOfAnyChanged = [ & ]( std::string_view cand ) noexcept + { + for( std::string_view c : changedPaths ) + { + if( cand == c || situDirOf( cand ) != situDirOf( c ) ) + { + continue; + } + if( situStemOf( cand ) == situStemOf( c ) || isTestPartnerOf( cand, c ) || isTestPartnerOf( c, cand ) ) + { + return true; + } + } + return false; + }; + const auto consider = [ & ]( std::string_view cand ) + { + if( isSiblingOfAnyChanged( cand ) ) + { + out.paths.emplace_back( cand ); + } + }; + for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) + { + if( !changedFile[f] ) + { + consider( ing.files[f] ); + } + } + for( const SkippedFile& sk : ing.crawlSkips.unsupported ) + { + consider( sk.path ); + } + std::sort( out.paths.begin(), out.paths.end() ); + out.paths.erase( std::unique( out.paths.begin(), out.paths.end() ), out.paths.end() ); + out.unindexedRowsFloor = !out.paths.empty() + && ing.crawlSkips.unsupported.size() < ing.crawlSkips.unsupportedFiles; + return out; +} + // Section [1]'s decl/def rows and section [3]'s empty-co-change line, as their own emitters: writeSituation // is already this file's largest function and the quality bar counts what a caller ADDS to it, so a fact that // is nameable gets a name. `pathRel` is the caller's own root-relative spelling, passed in rather than @@ -441,6 +540,29 @@ inline void writeSituDeclDefRows( std::FILE* out, const std::vector +inline void writeSituSiblingRows( std::FILE* out, const SituSiblings& sibs, PathRelStrFn pathRel, + std::string_view nextInvocation, int pageLimit, int pageOffset ) +{ + if( sibs.paths.empty() ) + { + return; + } + const PageWindow win = pageWindow( sibs.paths.size(), effectiveRowCap( pageLimit, int( kSituSiblingRowsShown ) ), pageOffset ); + const std::size_t shown = win.end - win.begin; + rw::emitTo( out, " lexical siblings ({}) not_dependents=1{}{} — same directory and stem as a changed file (header/impl partner, test, .inl); static, not a graph result:\n", + sibs.paths.size(), + sibs.unindexedRowsFloor ? " unindexed_rows_floor=1" : "", + situShowingNote( shown, sibs.paths.size(), "files", nextInvocation ).c_str() ); + for( std::size_t i = win.begin; i < win.end; ++i ) + { + const std::string_view rp = pathRel( sibs.paths[i] ); + rw::emitTo( out, " {}\n", std::string_view( rp.data(), rp.size() ) ); + } +} + // F2: the two causes the old "(none, or no git history)" conflated, told apart by the only fact that // separates them — how many commits the window actually contained. inline void writeSituEmptyCochangeLine( std::FILE* out, std::size_t coCommits ) @@ -467,9 +589,15 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // from the document, same as every structured verb's root= attribute). const bool situSingleRoot = ing.realPaths.empty(); const std::string situRootPrefix = situSingleRoot ? rw::sarif::rootPrefixOf( root ) : std::string(); + // L-D names one kind of file the index does not hold (an unindexed .inl sibling), so the relativizer is + // split in two: the STORED-path form is the primitive, and the fileId form is that same call. + const auto situPathRelStr = [ & ]( std::string_view p ) -> std::string_view + { + return situSingleRoot ? rw::sarif::rootRelativeUri( p, situRootPrefix ) : p; + }; const auto situPathRel = [ & ]( std::uint32_t fileId ) -> std::string_view { - return situSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ fileId ], situRootPrefix ) : std::string_view( ing.files[ fileId ] ); + return situPathRelStr( ing.files[ fileId ] ); }; std::uint32_t nChanged = 0; @@ -562,7 +690,8 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges reach.size(), affected.size(), blastNote.c_str() ); // F3: the decl/def partner FIRST — it is the answer to "what else has to change with this file" that the // dependent-symbol ranking below can never produce, because a header does not depend on its own source. - writeSituDeclDefRows( out, declDefPartners( ing, changedFile ), situPathRel ); + const std::vector situPartners = declDefPartners( ing, changedFile ); + writeSituDeclDefRows( out, situPartners, situPathRel ); { // H5/M15: the same floor + gauge the XML graph verbs mark, in this report's prose — through the SAME // fold, graphGaugeTotals, that graphGaugeAttrXml and graphGaugeAttrJson go through. PR #72 (382e66e6) // introduced that fold in the same commit that widened the gauge to three, precisely to stop the two @@ -572,6 +701,10 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges const auto [gaugeAmb, gaugeUnresolved] = graphGaugeTotals( g.ambOut, g.unresolvedOut ); rw::emitTo( out, kGraphCountFloorTextLine, gaugeAmb, gaugeUnresolved, graphUnindexedTextClause( g.unindexedFiles ).c_str() ); } + // L-D: the lexical neighbours of the changed files, which the caller walk above can never reach. + const SituSiblings situSibs = lexicalSiblings( ing, changedFile ); + writeSituSiblingRows( out, situSibs, situPathRelStr, + situNextInvocation( page.selector, situSibs.paths.size() ), page.limit, page.offset ); for( std::size_t i = blastPage.begin; i < blastPage.end; ++i ) { const std::string_view rp = situPathRel( affected[i] ); @@ -696,6 +829,7 @@ struct SituationFacts // Carried on the FACTS, not re-derived per surface, so the CLI report, the MCP JSON and --handoff cannot // disclose it three ways or two of them forget. std::vector declDef; // F3: files defining the SAME (scope, name) symbols as the changed set — the header/impl pair the transitive list cannot reach + SituSiblings siblings; // L-D: same-directory, same-stem neighbours of the changed files — STORED spellings (an unindexed .inl has no fileId) std::string coWindow; // the window label its co-change was mined in ("18mo@HEAD"), empty only if never mined std::size_t coCommits = 0; // commits that window actually contained — 0 ⇒ the zero above is not a measurement std::vector> hotspots; // (changed file, cx×churn score) for high-risk changed files (score desc, path asc) @@ -778,6 +912,7 @@ inline SituationFacts computeSituationFacts( const std::string& root, const Inge // popen per probed file — up to 40 — the O(files)-subprocess storm). Deterministic for a fixed HEAD. const auto coSets = gitCommitFileSets( root, ing, "18 months ago", 30 ); facts.declDef = declDefPartners( ing, changedFile ); // F3: same relationship, same rule, one implementation + facts.siblings = lexicalSiblings( ing, changedFile ); // L-D: the same list the CLI report's [1] prints facts.coWindow = defaultWindowLabel( root, "18mo" ); // F2: the composed zero's window travels WITH the zero facts.coCommits = coSets.size(); HashMap partnerDeg; diff --git a/test/situshapecheck.sh b/test/situshapecheck.sh index 5f36902e..a31ccacb 100755 --- a/test/situshapecheck.sh +++ b/test/situshapecheck.sh @@ -71,6 +71,14 @@ EOF cat > "$FX/other/widget.cc" <<'EOF' int otherWidget( int n ) { return n - 1; } EOF +# the WIDE stem: nine same-directory, same-stem siblings, so the block's cap and its disclosure are live +for f in wide.h wide_test.cc wide_unittest.cc wide_spec.cc wideTest.cc; do + printf '#pragma once\nint wideThing_%s( int n );\n' "$( printf '%s' "$f" | tr './-' '___' )" > "$FX/core/$f" +done +printf 'int wideThing( int n ) { return n; }\n' > "$FX/core/wide.cc" +for f in wide.inl wide.ipp wide.hpp wide.hxx; do + printf 'inline int wideInline_%s( int n ) { return n; }\n' "$( printf '%s' "$f" | tr './-' '___' )" > "$FX/core/$f" +done cat > "$FX/app/main.cc" <<'EOF' #include "core/widget.h" @@ -208,6 +216,118 @@ done "$BIN" "$FX" --situ=core/widget.cc >"$TMP/d2" 2>/dev/null cmp -s "$TMP/d1" "$TMP/d2" && ok "(6) --situ is byte-identical across two runs" || no "(6) --situ is not deterministic" +# ── (7) LEXICAL SIBLINGS (L-D) — the files that move WITH a changed file, which no graph walk can reach ── +# A change to core/widget.cc almost always touches core/widget.h and core/widget_test.cc, and neither is a +# transitive DEPENDENT: a header does not call its own implementation, and a test the graph cannot link (a +# fixture-built harness, a generated main) is reached by nothing. The frozen-30 attribution put two of our +# incomplete answers exactly there. This block is lexical and static — same directory, same stem — so it +# costs no history and cannot leak a future commit into an answer about the present. +SIB="$( grep -m1 'lexical siblings' "$OUT" )" +if [ -z "$SIB" ]; then + no "(7) --situ lists no lexical siblings for core/widget.cc" +else + ok "(7) --situ has a lexical-siblings block: $SIB" + sib_rows(){ sed -n '/lexical siblings/,/^ \[2\]/p' "$1" | awk '/^ [^ (]/ && NF == 1 && $1 !~ /=/ { print $1 }'; } + ROWS="$( sib_rows "$OUT" )" + for want in core/widget.h core/widget_test.cc core/widget.inl; do + printf '%s\n' "$ROWS" | grep -qx -- "$want" \ + && ok "(7) siblings include $want" \ + || no "(7) siblings do NOT include $want (rows: $( printf '%s' "$ROWS" | tr '\n' ' ' ))" + done + # the DECOY: same stem, different directory. A sibling is a neighbour, not a namesake. + printf '%s\n' "$ROWS" | grep -qx -- 'other/widget.cc' \ + && no "(7) siblings wrongly include other/widget.cc — a same-stem file in a DIFFERENT directory" \ + || ok "(7) siblings exclude other/widget.cc (same stem, different directory)" + # the neighbour that is not a namesake + printf '%s\n' "$ROWS" | grep -qx -- 'core/gadget.cc' \ + && no "(7) siblings wrongly include core/gadget.cc — same directory, different stem" \ + || ok "(7) siblings exclude core/gadget.cc (same directory, different stem)" + # the changed file itself is not its own sibling + printf '%s\n' "$ROWS" | grep -qx -- 'core/widget.cc' \ + && no "(7) siblings list the changed file itself" \ + || ok "(7) siblings exclude the changed file itself" + # root-relative, like every other path in the report + BAD="$( printf '%s\n' "$ROWS" | grep -E '^(/|\./)' | head -1 )" + [ -z "$BAD" ] && ok "(7) sibling paths are root-relative" || no "(7) sibling path '$BAD' is absolute or ./-prefixed" + case "$SIB" in + *'not_dependents=1'*) ok "(7) the block says these are NOT transitive dependents (not_dependents=1)" ;; + *) no "(7) the block does not say these rows are not dependents: $SIB" ;; + esac +fi + +# ── (7b) BOUNDED, and the bound DISCLOSED ─────────────────────────────────────────────────────────────── +WIDE="$TMP/wide.txt" +"$BIN" "$FX" --situ=core/wide.cc >"$WIDE" 2>/dev/null +WSIB="$( grep -m1 'lexical siblings' "$WIDE" )" +if [ -z "$WSIB" ]; then + no "(7b) the wide-stem file lists no siblings at all — the cap arm would be a false green" +else + case "$WSIB" in + *'capped=1'*) ok "(7b) a stem with more siblings than the cap discloses capped=1: $WSIB" ;; + *) no "(7b) the sibling block is cut without saying so: $WSIB" ;; + esac + case "$WSIB" in + *'shown='*'total='*) ok "(7b) the cut names shown= and total=" ;; + *) no "(7b) the cut names no shown=/total= pair: $WSIB" ;; + esac + case "$WSIB" in + *'next: --situ'*) ok "(7b) the cut carries a pasteable next: that widens it" ;; + *) no "(7b) the cut offers no relief: $WSIB" ;; + esac + WROWS="$( sed -n '/lexical siblings/,/^ \[2\]/p' "$WIDE" | awk '/^ [^ (]/ && NF == 1 && $1 !~ /=/ { print $1 }' | grep -c . )" + WTOTAL="$( printf '%s' "$WSIB" | sed -n 's/.*total=\([0-9]*\).*/\1/p' )" + [ "${WROWS:-0}" -lt "${WTOTAL:-0}" ] && ok "(7b) ${WROWS} rows of ${WTOTAL} — the count is the population, not the rows" \ + || no "(7b) shown rows (${WROWS}) do not sit under total=${WTOTAL}" + # --limit raises it, exactly as it raises [1] and [3] + "$BIN" "$FX" --situ=core/wide.cc --limit=40 >"$TMP/wide40.txt" 2>/dev/null + W40="$( sed -n '/lexical siblings/,/^ \[2\]/p' "$TMP/wide40.txt" | awk '/^ [^ (]/ && NF == 1 && $1 !~ /=/ { print $1 }' | grep -c . )" + [ "${W40:-0}" -gt "${WROWS:-0}" ] && ok "(7b) --limit=40 widens the sibling block (${WROWS} -> ${W40} rows)" \ + || no "(7b) --limit did not widen the sibling block (${WROWS} -> ${W40})" +fi + +# ── (7c) STATIC: the block does not depend on git history ─────────────────────────────────────────────── +# The lesson this implements is deliberately LEXICAL: it must answer the same way on a tree with no history +# at all, which is also what makes it unable to leak a future commit into an answer about the present. +NOGIT="$TMP/nogit"; rm -rf "$NOGIT"; mkdir -p "$NOGIT" +( cd "$FX" && tar cf - --exclude .git . ) | ( cd "$NOGIT" && tar xf - ) +"$BIN" "$NOGIT" --situ=core/widget.cc >"$TMP/nogit.txt" 2>/dev/null +NG="$( sed -n '/lexical siblings/,/^ \[2\]/p' "$TMP/nogit.txt" | awk '/^ [^ (]/ && NF == 1 && $1 !~ /=/ { print $1 }' )" +GT="$( sed -n '/lexical siblings/,/^ \[2\]/p' "$OUT" | awk '/^ [^ (]/ && NF == 1 && $1 !~ /=/ { print $1 }' )" +if [ -z "$NG" ]; then + no "(7c) the sibling block vanished on a tree with no git history — it is not static" +elif [ "$NG" = "$GT" ]; then + ok "(7c) the sibling block is identical with and without git history — static, so it cannot leak" +else + no "(7c) the sibling block differs with and without git history: [$( printf '%s' "$NG" | tr '\n' ' ' )] vs [$( printf '%s' "$GT" | tr '\n' ' ' )]" +fi + +# ── (7d) the MCP twin answers the same question with the same list ────────────────────────────────────── +if command -v python3 >/dev/null 2>&1; then + MCPOUT="$TMP/mcp.json" + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"situational_awareness","arguments":{"path":"'"$FX"'","diff":"core/widget.cc"}}}' \ + | "$BIN" --mcp >"$MCPOUT" 2>/dev/null + MROWS="$( python3 - "$MCPOUT" <<'PYEOF' +import json, sys +last = [l for l in open(sys.argv[1]) if l.strip()][-1] +r = json.loads(last) +try: + inner = json.loads(r["result"]["content"][0]["text"]) +except Exception: + print("__NONE__"); raise SystemExit +sibs = inner.get("siblings") +if sibs is None: + print("__MISSING__"); raise SystemExit +print(" ".join(sorted(s.get("file", "") for s in sibs))) +PYEOF +)" + case "$MROWS" in + __MISSING__|__NONE__) no "(7d) the MCP situational_awareness twin carries no siblings list ($MROWS)" ;; + *core/widget.h*) ok "(7d) the MCP twin carries the same siblings ($MROWS)" ;; + *) no "(7d) the MCP twin's siblings disagree with the CLI report: $MROWS" ;; + esac +fi + echo if [ "$fail" -eq 0 ]; then echo "situshapecheck: ALL PASS"; else echo "situshapecheck: SOME FAILED"; fi exit "$fail" From 52e07cca89a7a8c0c5f466c609579efbfdaac4f6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 11:57:32 -0400 Subject: [PATCH 04/20] =?UTF-8?q?chore(gatecount):=20the=20merged=20gate?= =?UTF-8?q?=20count=20is=20614=20=E2=80=94=20the=20generator's=20own=20num?= =?UTF-8?q?ber=20after=20two=20lanes=20each=20added=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both lanes added a gate (forwidencheck and situshapecheck) and each committed the count its own tree could see. The merge resolves the absorb loop to the union, so the count is a build product again rather than either side's reading — regenerated by docs/gatecount_build.py, which is what test/gatecountcheck.sh re-derives. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 4 ++-- docs/EVALS.md | 6 +++--- present/deck5_ripwire_build.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cd6d59e9..afe59de3 100644 --- a/README.md +++ b/README.md @@ -1799,9 +1799,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-613 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +614 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 **613 gate scripts** and is the authoritative list; +`test/regression.sh` names **614 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 ba1b8978..073cf774 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` | 613 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 614 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) @@ -5834,7 +5834,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 **613 gate scripts**, all of which exist on disk. +naming **614 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`. @@ -6846,7 +6846,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 613. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 614. 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 82fd532d..e2512732 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1068,7 +1068,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["613 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 + ["614 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."], @@ -1092,7 +1092,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, "613", "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, "614", "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", @@ -1342,7 +1342,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["179 long flags · 33 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["613 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["614 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 70 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 f06b0c1779e0c3e46382008c36330c4322c5008e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 12:06:27 -0400 Subject: [PATCH 05/20] refactor(situ): the sibling rule re-rolled a directory helper the repo already had --quality-delta across the lane reported the clone directly: situDirOf was a 44-token copy of siblift.h's siblift_detail::dirOf, the other same-directory lens, and writeSituSiblingRows took the three page fields loose (6 params, bar 5) where every other --situ emitter takes the SituPageArgs the caller already holds. Both are the reuse-first rule, and both were a fresh symbol's worth of debt, not an inherited one: dirOf is now called, not copied, and the sibling test is its own named predicate (isLexicalSiblingOf) so lexicalSiblings reads as the two loops it is. Output is byte-identical; test/situshapecheck.sh ALL PASS either way. Co-Authored-By: Claude Opus 5 (1M context) --- src/situ.h | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/situ.h b/src/situ.h index 2414e86c..30f5127f 100644 --- a/src/situ.h +++ b/src/situ.h @@ -19,6 +19,7 @@ #include "gitstamp.h" // r26-stamp Task A: gitstamp::atAttr — the at="[+dirty]" root anchor #include "testmap.h" // §P11.4: TestRunnerIndex / runAttr — the run= hint on a named test row #include "didyoumean.h" // H6: nearestIndexedFileClause — the ONE path near-miss suggester, shared with the MCP arm +#include "siblift.h" // L-D: siblift_detail::dirOf — the ONE "directory of a path" primitive, reused not re-rolled #include "serialize.h" // L2: jsonStr() — writeTestGateReportJson's escaping (self-contained: don't rely on // include-order in whichever TU pulls situ.h in first) #include "pageview.h" // §A3a: the ONE paging/truncation vocabulary — the @@ -447,15 +448,22 @@ struct SituSiblings bool unindexedRowsFloor = false; // the crawl's unsupported-extension ROW list was itself cut }; -inline std::string_view situDirOf( std::string_view path ) noexcept +// dirOf is siblift.h's (the other same-directory lens), stem is mention.h's pair — the two primitives this +// rule needs both already exist, and a third spelling of either is the clone --quality-delta reports. +inline std::string_view situStemOf( std::string_view path ) noexcept { - const std::size_t slash = path.rfind( '/' ); - return slash == std::string_view::npos ? std::string_view() : path.substr( 0, slash ); + return mention_detail::stripExt( mention_detail::baseNameOf( path ) ); } -inline std::string_view situStemOf( std::string_view path ) noexcept +// One changed file's test: same directory, and the same stem or testmap.h's stem-partner convention. Named +// so lexicalSiblings below reads as the two loops it is (candidates x changed files) rather than four levels. +inline bool isLexicalSiblingOf( std::string_view cand, std::string_view changed ) noexcept { - return mention_detail::stripExt( mention_detail::baseNameOf( path ) ); + if( cand == changed || siblift_detail::dirOf( cand ) != siblift_detail::dirOf( changed ) ) + { + return false; + } + return situStemOf( cand ) == situStemOf( changed ) || isTestPartnerOf( cand, changed ) || isTestPartnerOf( changed, cand ); } // ADDITIVE, deliberately: a file may be BOTH a decl/def partner (symbol identity) and a lexical sibling @@ -478,26 +486,15 @@ inline SituSiblings lexicalSiblings( const IngestResult& ing, const std::vector< { return out; } - const auto isSiblingOfAnyChanged = [ & ]( std::string_view cand ) noexcept + const auto consider = [ & ]( std::string_view cand ) { for( std::string_view c : changedPaths ) { - if( cand == c || situDirOf( cand ) != situDirOf( c ) ) + if( isLexicalSiblingOf( cand, c ) ) { - continue; + out.paths.emplace_back( cand ); + return; } - if( situStemOf( cand ) == situStemOf( c ) || isTestPartnerOf( cand, c ) || isTestPartnerOf( c, cand ) ) - { - return true; - } - } - return false; - }; - const auto consider = [ & ]( std::string_view cand ) - { - if( isSiblingOfAnyChanged( cand ) ) - { - out.paths.emplace_back( cand ); } }; for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) @@ -543,19 +540,19 @@ inline void writeSituDeclDefRows( std::FILE* out, const std::vector -inline void writeSituSiblingRows( std::FILE* out, const SituSiblings& sibs, PathRelStrFn pathRel, - std::string_view nextInvocation, int pageLimit, int pageOffset ) +inline void writeSituSiblingRows( std::FILE* out, const SituSiblings& sibs, PathRelStrFn pathRel, const SituPageArgs& page ) { if( sibs.paths.empty() ) { return; } - const PageWindow win = pageWindow( sibs.paths.size(), effectiveRowCap( pageLimit, int( kSituSiblingRowsShown ) ), pageOffset ); + const PageWindow win = pageWindow( sibs.paths.size(), effectiveRowCap( page.limit, int( kSituSiblingRowsShown ) ), page.offset ); const std::size_t shown = win.end - win.begin; rw::emitTo( out, " lexical siblings ({}) not_dependents=1{}{} — same directory and stem as a changed file (header/impl partner, test, .inl); static, not a graph result:\n", sibs.paths.size(), sibs.unindexedRowsFloor ? " unindexed_rows_floor=1" : "", - situShowingNote( shown, sibs.paths.size(), "files", nextInvocation ).c_str() ); + situShowingNote( shown, sibs.paths.size(), "files", + situNextInvocation( page.selector, sibs.paths.size() ) ).c_str() ); for( std::size_t i = win.begin; i < win.end; ++i ) { const std::string_view rp = pathRel( sibs.paths[i] ); @@ -703,8 +700,7 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges } // L-D: the lexical neighbours of the changed files, which the caller walk above can never reach. const SituSiblings situSibs = lexicalSiblings( ing, changedFile ); - writeSituSiblingRows( out, situSibs, situPathRelStr, - situNextInvocation( page.selector, situSibs.paths.size() ), page.limit, page.offset ); + writeSituSiblingRows( out, situSibs, situPathRelStr, page ); for( std::size_t i = blastPage.begin; i < blastPage.end; ++i ) { const std::string_view rp = situPathRel( affected[i] ); From 985e2bea056507ee041d734d5ad846028d5d4711 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 12:07:49 -0400 Subject: [PATCH 06/20] chore(quality): ack the nine contract-change rows A3's root parameter is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --quality-delta over the whole lane (ff8d77a1..HEAD) gates on nine api-surface rows, and all nine are ONE change: TestRunnerIndex's constructor takes the run's crawl root, and the eight sites that build one pass the root they already hold. That parameter IS the fix — without it the runner command cannot be spelled relative to the root the document declares (test/rootrelemitcheck.sh ARM 9) — so the finding is accurate and acknowledged rather than argued with. Acked by SYMBOL, nine rows, one reason; the default HEAD-vs-worktree delta reads gating="0" with no acks at all. Co-Authored-By: Claude Opus 5 (1M context) --- .ripwire_quality_acks | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 37736a64..397eea5f 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -2,6 +2,7 @@ # format: ack <16-hex-key> [cid=<16-hex-content-id>] [by=] — one per line, kept SORTED by (kind,key) on every write (merge-friendly) ack api-surface 048bde69cfc80a6f 2 cid=2dc85eb4d5e1cb33 lane V1 N2 (f5913f3): grepTierAttrs/grepTierKeys gain floorAlreadyEmitted, resolveCandidates gains capFired — one explicit parameter each, every caller updated in the same commit ack api-surface 060a064b6ffa7775 7 cid=605cbb1f768828e0 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign +ack api-surface 085e3d408c2c4a35 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 0a3d16d6f3139408 10 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack api-surface 0d6e1106ea5949d5 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. @@ -10,10 +11,12 @@ ack api-surface 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 ack api-surface 10f47dd5a3f35d86 5 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack api-surface 131068a6cedf0864 3 cid=6df1f8794b53f5a2 E1 follow-up (#214 CI): prLegendText gains the one pre-render fact that gates testmap.h's run=/run_unknown=/ clause (corpusHasTests) — its single caller writePrContext passes it; the unconditional clause put defaultceilingcheck's 120-file no-test bundle 25 tokens over its 8000 default budget ack api-surface 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack api-surface 155d74d341a49f3c 2 cid=abcde655ff1c0826 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 15754e3561a34f40 7 cid=e3721579f68947f6 deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack api-surface 163c0a0eb3219fa9 5 cid=9e7d5dab8c14a887 R2: prEmptyRootTail gains the truncated= parameter it needs to carry budget-floor-exceeded — deliberate, 1 caller, incompatible=0 (--edit-check contract-change); prEmptyRootPrice is the new file-scope helper that decides the label and re-prices, keeping writePrContext's own complexity and LOC unchanged | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack api-surface 1689c98fa4eac33e 4 cid=f5ec9b69e2526e08 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 195e2b4deba2cee7 4 cid=c2581959226700a8 V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. +ack api-surface 1a15386c2d1e47af 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 1c11c9480374c3a4 5 cid=27984fbee9fe12a9 by=src/* lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 1c873f03ef665f93 8 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 1da01868deceb731 7 cid=bce36ef4a75ab3fb by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) @@ -64,6 +67,7 @@ ack api-surface 5383f63ad718935c 7 cid=170e3fe466966e87 L10: printLintRuleTallyR ack api-surface 5391ffd9aa5765bf 6 cid=8a2aed10e80ed270 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack api-surface 53b3f823688054b4 5 cid=137a04e79db61a58 by=src/* §N6-C .gitignore-by-default: the crawl gains an ignore mode. The two api-surface/params rows are ONE deliberate contract change — ingest()/collectSources() take a trailing defaulted respectGitignore, the only way a CLI flag can reach the crawl without a global; the three short-horizon-churn rows are this lane's own edits to the flag ledger, the crawl and the --skipped verb, which is what adding a flag with a disclosure IS; collectSources +3 ccx / +11 LOC is what remains after the probe, the mode and the prune fan-out were extracted into probeIgnoreSet/recordDirPrune (it was +15/+43 inline). ack api-surface 55e8f57eb5817b60 3 cid=39ba1a3dc3bff198 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. +ack api-surface 56acdf9b5c314a14 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 5710beada2095a34 9 cid=3371683faa811117 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. | prior: the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack api-surface 574641dcc1bdf0ec 13 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 5774f0f445361430 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -76,6 +80,8 @@ ack api-surface 61e5df9e1e40ff70 5 cid=e2155dd6082b880a E2 (terminality round A, ack api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 654551bf984cc299 5 cid=913136f787574436 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 6e58b0a307757079 24 cid=cb5c8aaa7451a632 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. +ack api-surface 6eef859578c8c376 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. +ack api-surface 6f2394762f82a855 2 cid=abcde655ff1c0826 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 75720b711509b5b9 5 cid=a54cd0ffa32a9952 lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack api-surface 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. | prior: the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack api-surface 77eb2194156050d0 4 cid=ae974822986c10b4 lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. @@ -88,6 +94,7 @@ ack api-surface 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminali ack api-surface 7ed8ad2c213537a4 7 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack api-surface 7f2c3eefdf6e512e 3 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack api-surface 7f5e07a10dd97563 4 cid=1cc3fb32d2789e8a P2-2 regex hoist: passesPredicates takes the per-predicate compiled-regex table as one explicit parameter, every caller updated in the same commit (lane F; --lint/--match byte-identical) +ack api-surface 802e513731103806 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 80b08c75913ae76c 8 cid=2b1373e91d63396e by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack api-surface 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 84b6bfc164c989e8 2 cid=87c39ba5f968fb34 M21(a) sa sym=/p=: staleAcksXml takes the caller's XML escaper as a template parameter (+1 param) because sym= carries a canonical id — corpus text — and quality.h sits BELOW serialize.h in the include order. testmap.h's runHint uses the same seam for the same reason; the alternative was including serialize.h from quality.h, which inverts the order. @@ -99,6 +106,7 @@ ack api-surface 8a92173ded649e17 14 cid=f76e97c84ac7c168 by=src/* lane 2 of the ack api-surface 8d58de9bb922f582 4 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack api-surface 925094be92085dae 3 cid=714cea1e1b31a1ae A6: rollbackMessage gained a 'cause' parameter one commit after this lane introduced it (57fe5fc). It is a header-inline helper in namespace rw::editplan with two callers, both in the same function in the same file; no consumer outside this lane ever saw the 2-arg form. The parameter is what lets the concurrent-write abort reuse the rollback disposition wording instead of growing a second copy of it. ack api-surface 92ac9caf38b8aab0 4 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh +ack api-surface 93848617cae1fe4e 2 cid=a42863f577100fa6 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 95cc88ca4aab7039 4 cid=25c411d4871bda46 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 96fdcdff2f0ff0f7 4 R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). ack api-surface 983814f2b5912a90 3 cid=fe2cfa34166f503a M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -114,6 +122,7 @@ ack api-surface aeed75863f7b617d 3 --lint reads the corpus ONCE (audit lane B2, ack api-surface b496a273ae1564ef 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface b65c78efe3e73a34 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface b689422adfc04435 5 cid=cdb3a6ea16c83186 lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked +ack api-surface b6a24afef32a68a8 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface bb2c0b847815a0ca 4 cid=9eb5f97927595a07 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1: the MCP edit_check verb mirrors the CLI pre-apply preview through the SAME editpreview::run, so the two surfaces cannot answer differently (gate arm N pins them document-for-document). new_body is optional and defaulted; every existing call site is untouched and the verb stays readOnlyHint true — passing it previews, it never writes. ack api-surface bcb3377087f2e034 7 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface be8176514288abc7 2 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. From 34467eca210e3220316062d9f80b672a65cc0978 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 12:26:40 -0400 Subject: [PATCH 07/20] fix(gates): three --situ readers treated the new attribute line as a test row, and two new arms used the banned one-line verdict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full suite caught what the targeted set could not. A5 turned --situ's closing script-gate disclosure from a parenthesised sentence into the attribute line `script_gates_unmodelled=N — …`, and three gates had discriminated rows from prose by "does the line start with (": testrowruncheck (8) and (12) and rootrelemitcheck ARM 6 all then read that disclosure as a path row. The fix is the right discriminator and it had to be got right twice: a ROW's first token is a path, but a GROUPED row opens with `[hops=N]`, so "the first token contains =" would have dropped every row's paths and silently weakened the multiset assertion — the check is on the LEADING bytes (`^[a-z_]+=`), which only an attribute line has. testgatepagecheck (c) pinned the pr-context cap as a trailing SENTENCE inside the [1] parenthetical; it now pins the same meaning through prcontext_cap=, the attribute that replaced it. test/gateexitcheck.sh (G2) was right about situshapecheck: two of its arms reported a verdict through a single-line `… && ok … || no …`, which prints FAIL for an arm that passed when the PASS write is interrupted (bash's SIGCHLD has no SA_RESTART). Both are wrapped. Co-Authored-By: Claude Opus 5 (1M context) --- test/rootrelemitcheck.sh | 5 +++-- test/situshapecheck.sh | 14 ++++++++++++-- test/testgatepagecheck.sh | 13 ++++++++++--- test/testrowruncheck.sh | 9 ++++++++- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/test/rootrelemitcheck.sh b/test/rootrelemitcheck.sh index 121422a8..90852cb5 100755 --- a/test/rootrelemitcheck.sh +++ b/test/rootrelemitcheck.sh @@ -373,13 +373,14 @@ for spelling in abs rel; do | sed -n 's/.*"tests_to_run":\[\([^]]*\)\].*/\1/p' | tr ',' '\n' | sed -n 's/.*"p":"\([^"]*\)".*/\1/p' ) # A5 re-pin (2026-09-13): the section's closing script-gate disclosure is now the attribute line # `script_gates_unmodelled=N — …`, which starts with a non-space non-"(" byte and so was read as a path - # row. A row path never contains "=", so that is the discriminator. + # row. An attribute line OPENS with `name=`; a grouped row opens with `[hops=N]`, so the discriminator is + # the leading bytes, not "contains =". # M21(b) re-pin (capture-audit 2026-09-04, lane L8): every --situ tests-to-run line now ends in a run # recipe OR its "(run: not derivable)" disclosure, so the old "the line contains no '(' " extraction # matched nothing and this arm read red while the SPELLING it exists to compare was correct. Re-pinned to # the new contract: take the path FIELD off a row line, not the whole line. s_rows=$( cd "$CD" && "$BIN" "$RT" --situ=geometry.cpp 2>/dev/null \ - | sed -n '/tests to run/,/^ \[3\]/p' | awk '/^ [^ (]/ && $1 !~ /=/ { print $1 }' ) + | sed -n '/tests to run/,/^ \[3\]/p' | awk '/^ [^ (]/ && $1 !~ /^[a-z_]+=/ { print $1 }' ) if [ -z "$a_rows" ]; then no "ARM6/$spelling --affected emitted NO test row for distance — the arm would be a false green" continue diff --git a/test/situshapecheck.sh b/test/situshapecheck.sh index a31ccacb..5a8ce58e 100755 --- a/test/situshapecheck.sh +++ b/test/situshapecheck.sh @@ -214,7 +214,12 @@ done # determinism, on the verb this gate reshapes "$BIN" "$FX" --situ=core/widget.cc >"$TMP/d1" 2>/dev/null "$BIN" "$FX" --situ=core/widget.cc >"$TMP/d2" 2>/dev/null -cmp -s "$TMP/d1" "$TMP/d2" && ok "(6) --situ is byte-identical across two runs" || no "(6) --situ is not deterministic" +if cmp -s "$TMP/d1" "$TMP/d2" +then + ok "(6) --situ is byte-identical across two runs" +else + no "(6) --situ is not deterministic" +fi # ── (7) LEXICAL SIBLINGS (L-D) — the files that move WITH a changed file, which no graph walk can reach ── # A change to core/widget.cc almost always touches core/widget.h and core/widget_test.cc, and neither is a @@ -248,7 +253,12 @@ else || ok "(7) siblings exclude the changed file itself" # root-relative, like every other path in the report BAD="$( printf '%s\n' "$ROWS" | grep -E '^(/|\./)' | head -1 )" - [ -z "$BAD" ] && ok "(7) sibling paths are root-relative" || no "(7) sibling path '$BAD' is absolute or ./-prefixed" + if [ -z "$BAD" ] + then + ok "(7) sibling paths are root-relative" + else + no "(7) sibling path '$BAD' is absolute or ./-prefixed" + fi case "$SIB" in *'not_dependents=1'*) ok "(7) the block says these are NOT transitive dependents (not_dependents=1)" ;; *) no "(7) the block does not say these rows are not dependents: $SIB" ;; diff --git a/test/testgatepagecheck.sh b/test/testgatepagecheck.sh index ff26b2db..ee1d9557 100755 --- a/test/testgatepagecheck.sh +++ b/test/testgatepagecheck.sh @@ -145,9 +145,16 @@ S="$( run "$SITU" --situ=src/target.cpp )" # §B12.1 PIN UPDATE: "(showing 8" carried no UNIT and no remainder, so the disclosure did not say what 8 # counted. The pin now asserts the two MEANING halves — the count is qualified by its unit AND by the total # it is 8 of — instead of the old bare literal. -printf '%s' "$S" | grep -qE 'across [0-9]+ files transitively depend on these changes \(showing 8 of [0-9]+ files; --pr-context' \ - && ok "(c) --situ discloses the 8-row cap with its UNIT and remainder (\"showing 8 of N files\")" \ - || no "(c) --situ's blast-radius header does not disclose the cap: $( printf '%s' "$S" | sed -n '2p' )" +# A5 PIN UPDATE (2026-09-13): --pr-context's own cap was a trailing SENTENCE inside this parenthetical and is +# now the attribute prcontext_cap=20, spelled beside pageview.h's shown=/total=/capped= triple. The MEANING +# this arm pins is unchanged — the unit, the remainder, and the fact that the sibling verb is capped too — so +# the pattern follows the attribute rather than the sentence. +if printf '%s' "$S" | grep -qE 'across [0-9]+ files transitively depend on these changes \(showing 8 of [0-9]+ files — shown=8 total=[0-9]+ capped=1 prcontext_cap=[0-9]+' +then + ok "(c) --situ discloses the 8-row cap with its UNIT, remainder and the sibling verb's own cap (prcontext_cap=)" +else + no "(c) --situ's blast-radius header does not disclose the cap: $( printf '%s' "$S" | grep -m1 '\[1\] blast radius' )" +fi S2="$( run "$R" --situ=src/lib.cpp )" printf '%s' "$S2" | grep -qE 'across (0|[1-8]) files transitively depend on these changes$' \ && ok "(c') --situ omits the cap note when files <= 8 (byte-neutral small case)" \ diff --git a/test/testrowruncheck.sh b/test/testrowruncheck.sh index 77afe6cd..5364f996 100755 --- a/test/testrowruncheck.sh +++ b/test/testrowruncheck.sh @@ -190,7 +190,10 @@ else fi # ── ARM 8 — --situ's TEXT dialect (situ.h) ───────────────────────────────────────────────────────────── SITU="$( rw --situ )" -SITU_ROWS="$( printf '%s\n' "$SITU" | sed -n '/tests to run/,/^ \[3\]/p' | grep -E '^ [^ (]' )" +# A5 (2026-09-13): the section's closing script-gate disclosure is now the attribute line +# `script_gates_unmodelled=N — …` rather than a parenthesised sentence, so "starts with (" no longer +# excludes it. A ROW's first token is a path, and a path never contains "=" — that is the discriminator. +SITU_ROWS="$( printf '%s\n' "$SITU" | sed -n '/tests to run/,/^ \[3\]/p' | grep -E '^ [^ (]' | grep -vE '^ [a-z_]+=' )" if [ -z "$SITU_ROWS" ]; then no "(8) --situ: fixture produced no 'tests to run' rows — the arm cannot bite"; printf '%s\n' "$SITU" | head -30 else @@ -345,6 +348,10 @@ s_paths, s_groups = [], 0 for line in sec.split( "\n" ): if not line.startswith( " " ) or line.startswith( " (" ): continue body = line[8:] + # A5: the section's closing disclosure is an attribute line (`script_gates_unmodelled=N — …`), not a row. + # An attribute line OPENS with `name=`; a group row opens with `[hops=N]`, which is why this tests the + # leading bytes rather than "does the first token contain =". + if re.match( r'[a-z_]+=', body ): continue gm = re.match( r'(\[[^\]]*\] )?\((\d+)\): (.*?) \(run: not derivable\)$', body ) if gm: ps = gm.group( 3 ).split( ", " ); s_groups += 1 From 26930ae3d4eaa44da674b9a9da4a333d95724546 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 15:35:23 -0400 Subject: [PATCH 08/20] fix(situ,testmap,sarif,mcpedit,flip): a relative command with no anchor, and the roots that never declared themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten findings from a review of #219, every one of them a document a reader holding it could not resolve, plus a lifetime bug the gates caught on the way. A RELATIVE COMMAND IS ONLY AS GOOD AS ITS ANCHOR. The run-hint clause claimed "relative to root=" unconditionally — including on a MULTI-root run, which declares no root= at all and (correctly) keeps the absolute command. The spelling and the sentence now answer to ONE predicate, testmap.h's runsAreRootRelative( ing, root ), so they cannot disagree; all eight legend sites and the TestRunnerIndex read it. --flags --flip spelled every p= through relForHash and declared no root either: now carries root= with the one sentence that defines it. The MCP edit receipt, the surface that hands a caller a command to PASTE, spelled "file", every tests_to_run[].run and its stderr "next:" relative to a root it never named; it now carries "root", single-root only. And --help still said run= is "spelled with the same root you scanned" — not true since 98035399; --help and docs/COMMANDS.md now say what the code does. A "./" THAT RETURNED TOO EARLY. sarif.h rootRelativeUri stripped a leading "./" and RETURNED before the root prefix was tried: right for the root ".", wrong for every other relative spelling. `ripwire ./corp` stores "./corp/test/x.sh", the early return yielded "corp/test/x.sh", and pasting that from the declared root is `cd ./corp && bash corp/test/x.sh` — rc 127. Both sides now drop the optional "./" first and compare what is left; the root "." case stays byte-identical. rootrelemitcheck ARM 9b is the matrix this deserved: ".", "corp", "./corp", "corp/", an absolute path and a symlink all print the SAME command, and every printed command is EXECUTED from the root it names. A SMALL BLOCK PAGED WITH SOMEONE ELSE'S WINDOW. The lexical-siblings block honoured page.offset — section [1]'s blast-radius offset. --offset=20 printed "shown=0 total=9 capped=1" with a next= offering --limit=9, relief that cannot restore rows an OFFSET removed; --offset=7 dropped six rows silently. Cap and --limit, no offset, like the decl/def rows above it. unindexed_rows_floor was computed only for a NON-EMPTY list and the emitter suppressed the empty one, so a cut that removed the only candidate printed nothing at all — the silent zero METHODOLOGY §9 forbids. The floor is a property of the CANDIDATE LIST, so it rides the empty case too, in both dialects. The MCP twin's siblings_total was the length of the array beside it; it now states siblings_capped explicitly. AN ATTRIBUTE WITHOUT A READING IS A TOKEN. Four readings went out with the four sentences A5 compressed: the floor's cause (call edges are name-based), what an unindexed file IS, which header the resolver gauges come from, and whose cap prcontext_cap= is. --situ is the one dialect with no legend to look a name up in (it refuses --legend=compact), so each gauge keeps a short gloss and situshapecheck arm (8) asserts the READING, not the token. Two ratchets moved with them (floor 200 -> 360, partner 140 -> 220): a ratchet that forbids a restored disclosure is aimed at the wrong thing. A DANGLING VIEW THE GATES FOUND. runHintClauseIfRows now BUILDS its clause (the root sentence is conditional), and PackTaskHeaderParts holds string_views: binding runClause straight to the returned temporary read freed memory the moment the full expression ended. It showed as exactly that — packtaskcheck's bundle both malformed and non-deterministic, two runs two sha256s, and xmlwellformed red on --pack-task --json. The clause is owned by a named local now, like report and droppedPositiveAttr beside it. MEASURED, wc -c, this lane's base binary (6621370f) against this one over the SAME tree, so the pair carries all three --situ entries together. This repo (root 131 chars): --situ=src/graph.h 4,448 -> 2,955 B, --situ=src/situ.h 2,332 -> 2,040 B, --situ=src/testmap.h 2,325 -> 2,033 B, --test-gate=src/testmap.h 5,455 -> 5,247 B. RocksDB @0e2801ac (root 66 chars): --situ=db/write_batch.cc 7,489 -> 7,376 B, --test-gate 9,946 -> 9,868 B, --affected 7,124 -> 7,113 B. The four compressed lines, by situshapecheck's own ${#line} on this repo at --situ=src/graph.h (the partner header on the gate's fixture): 601 -> 344, 228 -> 209, 233 -> 220, 167 -> 132 — 1,229 -> 905 B. The CHANGELOG carried a different before-pair for two of those lines than the gate did; one number, one corpus, both now the gate's. RED FIRST: situshapecheck shows 17 FAIL rows against 6621370f's binary (arms (8)-(11) are new); receiptpostcheck (18), rootrelemitcheck ARM 9b/9c/9d and runhintcheck 2c/2d are red there too. Two gate self-checks were wrong the same way the code was — an empty run= made `eval ""` succeed, and an empty next= fell out of an if/elif chain printing neither PASS nor FAIL — so each now reds on the outcome it exists to forbid. PINS MOVED: testgatelegendbudgetcheck 3,000 -> 3,070 B for the 56 B conditional root sentence (measured 2,957 -> 3,013 B on its src/model.h fixture), and test/printf_parity.manifest for pack_task and help_all, the two labels whose text this round changed. situStemOf was a fourth spelling of stripExt( baseNameOf( p ) ); one mention.h pathStem serves all four sites. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 126 +++++++++++++++----- docs/COMMANDS.md | 2 +- docs/LIMITS.md | 2 +- src/cli.h | 6 +- src/flipimpact.h | 24 +++- src/graphlegend.h | 13 ++- src/handoff.h | 2 +- src/mcpedit.h | 10 ++ src/mcpverbs.h | 12 +- src/mention.h | 5 + src/packtask.h | 8 +- src/partition.h | 2 +- src/prcontext.h | 11 +- src/sarif.h | 22 ++-- src/situ.h | 52 +++++---- src/testmap.h | 36 ++++-- src/verbs_change.h | 4 +- test/printf_parity.manifest | 4 +- test/receiptpostcheck.sh | 22 ++++ test/rootrelemitcheck.sh | 119 ++++++++++++++++++- test/runhintcheck.sh | 59 +++++++++- test/situshapecheck.sh | 188 +++++++++++++++++++++++++++++- test/testgatelegendbudgetcheck.sh | 14 ++- 23 files changed, 642 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86025ec..927b8658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,68 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — a relative command with no anchor, and the roots that never declared themselves + +A second review of the three `--situ` entries below found ten defects, every one of them a document +that could not be resolved by the reader holding it, and all ten are fixed here. + +A RELATIVE COMMAND IS ONLY AS GOOD AS ITS ANCHOR. Making `run=` root-relative is what makes a change +report independent of where the tree is checked out — and it makes every one of those commands useless +to a reader who cannot tell what they are relative to. Four surfaces had exactly that hole. The shared +run-hint clause claimed "relative to root=" unconditionally, including on a MULTI-root run, which +declares no `root=` at all and (correctly) keeps the absolute command: the spelling and the sentence now +answer to one predicate, `testmap.h runsAreRootRelative`, so they cannot disagree. `--flags --flip` +spelled every `p=` relative to the crawl root and declared no root either; `` now carries `root=` +with the one sentence that defines it, like every other verb. The MCP edit receipt — the surface that +hands a caller a command to paste — spelled `file`, every `tests_to_run[].run` and its stderr `next:` +relative to a root it never named; it now carries `"root"`, single-root only, the same condition every +other `root=` keeps. And `--help` still told the reader `run=` was "spelled with the same root you +scanned", which stopped being true in this lane; `--help` and `docs/COMMANDS.md` now say what the code +does. + +A "./" THAT RETURNED TOO EARLY. `sarif.h rootRelativeUri` stripped a stored path's leading `./` and +RETURNED, before the root prefix was ever tried. That is right for the root `.` and wrong for every +other relative spelling: `ripwire ./corp` stores `./corp/test/x.sh`, the early return yielded +`corp/test/x.sh`, and pasting that from the root the document declares is `cd ./corp && bash +corp/test/x.sh` — rc 127. Both sides now drop the optional `./` first and compare what is left, which +leaves the one case the early return got right byte-identical. `test/rootrelemitcheck.sh` ARM 9b turns +the old spelling pair into a matrix: `.`, `corp`, `./corp`, `corp/`, an absolute path and a symlink all +print the SAME command, and each printed command is EXECUTED from the root it names. + +A SMALL BLOCK PAGED WITH SOMEONE ELSE'S WINDOW. The new lexical-siblings block honoured `page.offset` — +which is section `[1]`'s blast-radius offset. `--situ=F --offset=20` printed `shown=0 total=9 capped=1` +with a `next:` offering `--limit=9`, relief that cannot restore rows an OFFSET removed, and `--offset=7` +dropped six rows silently. It is a small fixed block with a cap, like the decl/def partner rows above it: +cap and `--limit`, no offset. In the same family, `unindexed_rows_floor` was computed only for a NON-EMPTY +list and the emitter suppressed the empty one, so the case where the crawl's 500-row cut removed the only +candidate printed nothing at all — a silent zero, which is the one thing METHODOLOGY §9 forbids outright. +The floor is a property of the candidate list, not of the answer: it is recorded whenever that list was +short, and the block speaks at zero. The MCP twin's `siblings_total` was the length of the array beside it +— a tautology — and now states `siblings_capped` explicitly beside a population. + +AN ATTRIBUTE WITHOUT A READING IS A TOKEN, NOT A DISCLOSURE. The compression below shortened four +sentences into attributes, and four readings went with them: what makes the counts a floor (call edges are +name-based), what an unindexed file IS, which header the resolver gauges come from, and whose cap +`prcontext_cap=` is. `--situ` is the one dialect with no legend anywhere to look a name up in — it refuses +`--legend=compact` — so each gauge keeps a short gloss, and `test/situshapecheck.sh` arm (8) asserts the +READING, not the token. Two byte ratchets moved with them (floor 200 → 360, partner 140 → 220): a ratchet +that forbids a restored disclosure is a ratchet aimed at the wrong thing. + +Measured with `wc -c`, this lane's base binary (`6621370f`) against this one over the SAME tree, so the +pair carries all three `--situ` entries below together. On this repo (root 131 chars): +`--situ=src/graph.h` 4,448 → 2,955 B, `--situ=src/situ.h` 2,332 → 2,040 B, `--situ=src/testmap.h` +2,325 → 2,033 B, `--test-gate=src/testmap.h` 5,455 → 5,247 B. On RocksDB @0e2801ac (root 66 chars): +`--situ=db/write_batch.cc` 7,489 → 7,376 B, `--test-gate=db/write_batch.cc` 9,946 → 9,868 B, +`--affected=db/write_batch.cc` 7,124 → 7,113 B. Gates: `test/situshapecheck.sh` (17 rows red on that base +binary, arms (8)–(11) new), `test/rootrelemitcheck.sh` ARM 9b/9c/9d, `test/runhintcheck.sh` 2c/2d, +`test/receiptpostcheck.sh` (18). Two gate self-checks were wrong in the same way the code was — an empty +`run=` made `eval ""` succeed, and an empty `next=` fell out of an if/elif chain printing neither PASS nor +FAIL — so each now reds on the outcome it exists to forbid. `situStemOf` was a fourth spelling of +`stripExt( baseNameOf( p ) )`; one `mention.h pathStem` now serves all four call sites. Pins moved: +`test/testgatelegendbudgetcheck.sh` 3,000 → 3,070 B for the 56 B conditional root sentence (measured +2,957 → 3,013 B on its `src/model.h` fixture), and `test/printf_parity.manifest` for `pack_task` and +`help_all`, the two labels whose text this round changed. + ### Added — `--situ` lists a changed file's lexical siblings The files that move WITH a changed file are usually its neighbours by name, and the caller walk can reach @@ -22,8 +84,9 @@ none of them: a header does not call the source that implements it, an `.inl` is in any build, and a harness the graph cannot link — a fixture-built test, a generated `main` — is reached by nothing. A byte-and-answer attribution over a frozen 30-question set found two answers incomplete for exactly that reason. Section `[1]` of `--situ` now lists them, under the decl/def partners and the floor -clause: `lexical siblings (N) not_dependents=1 — same directory and stem as a changed file (header/impl -partner, test, .inl); static, not a graph result`, then one root-relative path per row. The rule is the +clause: `lexical siblings (N) not_dependents=1 — same directory and stem as a changed file (its header/impl +partner, its test, its .inl): NOT transitive dependents, so they are absent from the list below; lexical and +static, never a graph result`, then one root-relative path per row. The rule is the dumbest one that is always right — same directory, and the same filename stem or the stem-partner convention the tests-to-run rows already use (`_test`, `test_`, `Test`, `_unittest`, `_spec`). Same directory is load-bearing rather than a speed trick: a same-stem file in another directory is a namesake, not @@ -31,12 +94,16 @@ a partner, and listing namesakes would make the block noise on exactly the large candidate population is the CRAWL's, not the index's, so an `.inl`/`.ipp`/`.tcc` partner — the sibling a C++ change most often has to edit, and one no grammar can read — is named; the crawl's unsupported-extension row list is itself capped, and the one case where that can shorten this list is disclosed as -`unindexed_rows_floor=1`. The block is capped at 8 rows with `shown=`/`total=`/`capped=1` and a pasteable -`next:`, and `--limit=N` raises it like the report's other two listings. The MCP `situational_awareness` -twin carries the same list as `siblings` with `siblings_total`. It costs what it lists: measured with -`wc -c` on RocksDB @0e2801ac, `--situ=db/write_batch.cc` 6,781 → 6,967 B (+186 for a one-row block naming -`db/write_batch_test.cc`, which no other section of that report reaches); on this repo, where every source -file is a lone `.h`, no file has a lexical sibling and the report is byte-unchanged. Gate: +`unindexed_rows_floor=1` — on the EMPTY list too, because a cut that removes the only candidate is exactly +the case a silent zero would hide. The block is capped at 8 rows with `shown=`/`total=`/`capped=1` and a +pasteable `next:`, and `--limit=N` raises it like the report's other two listings; it does NOT take section +`[1]`'s `--offset`, which is the blast-radius window's, so no offset can empty it. The MCP +`situational_awareness` twin carries the same list as `siblings` with `siblings_total`, `siblings_capped` +(always emitted: that payload serves every row, and an absent flag would be the silence this rule forbids) +and `siblings_unindexed_rows_floor`. It costs what it lists: the block's own rendered lines on RocksDB +@0e2801ac at `--situ=db/write_batch.cc` are 276 B — a 244 B header and one 30 B row naming +`db/write_batch_test.cc`, which no other section of that report reaches; on this repo, where every source +file is a lone `.h`, no file has a lexical sibling and the block prints nothing at all. Gate: `test/situshapecheck.sh` arms (7)–(7d) on a fixture with a `.h`/`.cc`/`_test.cc`/`.inl` quadruple, a same-stem DECOY in another directory and a same-directory file with a different stem — both must be absent — plus a nine-sibling stem for the cap and its disclosure, a no-git copy of the same tree proving the block is @@ -47,20 +114,27 @@ binary. `--situ` is the only report with no XML root to hang attributes on, so every disclosure it owed was written as a sentence, and the sentences grew: the graph-count floor clause ran 601 B, the decl/def partner header -228 B, the tests-to-run header 267 B and the script-gate caveat 158 B — about 800 B of prose on every call, -carrying facts a reader can only act on once they are named. They are now named. The floor line is -`counts_floor=1 graph_ambiguous=N graph_unresolved=N graph_unindexed=N (map-header gauges) — every count -above is a FLOOR, never a total; a zero is "none found", never "none exists"` (601 → 198 B), using the same -attribute spellings the XML and JSON dialects already use, so the three share one vocabulary. The partner -header carries `not_dependents=1` (228 → 134 B), section `[1]` carries `prcontext_cap=20` where it used to -spell `--pr-context`'s own cap as an aside, section `[2]` carries `order=evidence` — the attribute -`--affected`'s root already carries for the same ordering (267 → 220 B) — and the script-gate blind spot is -`script_gates_unmodelled=N`, the same counter `--affected` publishes, with its cause kept (158 → 132 B). +228 B, the tests-to-run header 233 B and the script-gate caveat 167 B — 1,229 B of prose on every call, +carrying facts a reader can only act on once they are named. They are now named, and the four lines together +are 905 B. The floor line is `counts_floor=1 graph_ambiguous=N graph_unresolved=N graph_unindexed=N (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; a zero is "none found", never "none exists"` (601 → 344 B), +using the same attribute spellings the XML and JSON dialects already use, so the three share one vocabulary. +The partner header carries `not_dependents=1` (228 → 209 B), section `[1]` carries `prcontext_cap=20` where +it used to spell `--pr-context`'s own cap as an aside, section `[2]` carries `order=evidence` — the attribute +`--affected`'s root already carries for the same ordering (233 → 220 B) — and the script-gate blind spot is +`script_gates_unmodelled=N`, the same counter `--affected` publishes, with its cause kept (167 → 132 B). An +attribute is shorter than a sentence; it is not shorter than the FACT, so every gauge keeps a short gloss — +this is the one dialect with no legend anywhere to look a name up in (`--situ` refuses `--legend=compact`). Nothing was dropped: every floor, cap and caveat survives, and the readings that have no attribute form (how to read a zero; what `[changed]`/`[partner]`/`hops` mean on a row) stay as the shortest sentence that defines -them. Measured with `wc -c`, same cache, same commit: on this repo `--situ=src/situ.h` 2,320 → 1,836 B -(−484) and `--situ=src/testmap.h` 2,302 → 1,818 B; on RocksDB @0e2801ac `--situ=db/write_batch.cc` 7,412 → -6,781 B (−631). The gate is the new `test/situshapecheck.sh`: one arm per converted disclosure, each +them. The four line lengths above are the gate's own `${#line}`, measured on this repo at +`--situ=src/graph.h` (the partner header on the gate's fixture at `--situ=core/widget.cc`, since this repo +has no decl/def partner for `graph.h`), against the binary this lane branched from (`6621370f`) over the same +tree — one number, one corpus, and the gate's header carries the same table. The whole-report numbers for +this lane are in the review entry above, where they belong: the same pair of binaries also carries the +relativized `run=` and the new sibling block, so no single entry owns them. The gate is the new +`test/situshapecheck.sh`: one arm per converted disclosure, each asserting the attribute is present, that its value agrees with the XML sibling's where one exists (`graph_unindexed=`, `script_gates_unmodelled=`), that the reading survives, and a per-line byte ratchet so the prose cannot creep back; 10 of its rows are red on the previous binary. `test/floormarkcheck.sh` keeps @@ -79,12 +153,12 @@ and spells the command through the same relativizer every `p=` beside it uses, a build one, so the twelve emitters sharing it cannot disagree; a multi-root run, whose disk path lies under no single root, keeps the absolute command rather than become relative to a root that does not contain it. The rule is stated where it is consumed: the shared run-hint clause gains "A run= command is relative to -root=." (27 B, emitted only on a document that has rows) and `--situ`'s `[2]` header says "a (run: …) is -relative to root:". Measured with `wc -c` on a clean tree with the same warm cache and an absolute root: -on this repo (root 132 chars) `--test-gate=src/testmap.h` 5,327 → 5,100 B; on RocksDB @0e2801ac (root 66 -chars) `--test-gate=db/write_batch.cc` 9,793 → 9,696 B, `--situ=db/write_batch.cc` 7,445 → 7,412 B and -`--affected=db/write_batch.cc` 6,971 → 6,941 B. The saving is one root spelling per echo less the legend -clause, so it grows with checkout depth and with how many rows have a runner at all. The gate is a new +root=: run it from there." (56 B, emitted only on a document that has rows AND a single root — a multi-root +run declares no `root=` and keeps the absolute command, so the sentence would be a false claim there) and +`--situ`'s `[2]` header says "a (run: …) is relative to root:". The saving is one root spelling per echo +less that clause, so it grows with checkout depth and with how many rows have a runner at all; the +whole-report numbers for this lane are in the review entry above, measured against the binary it branched +from over one tree. The gate is a new ARM 9 in `test/rootrelemitcheck.sh`: a fixture carrying a real runner script, at two checkout depths, over the eight verbs that echo a command — one anchor per document, no absolute path anywhere else, byte-identical documents at both depths, and the printed `run=` actually executed from the declared root. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index bad0c752..983db8bb 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1131,7 +1131,7 @@ $ ./build/ripwire . --handoff **Answers:** before a PR: name the tests to run and the untested blast radius; -exit 4 if either is non-empty agent self-check before a PR (pair with --quality-delta): names the tests to run + the UNTESTED blast radius; exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff) run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries run="" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem matches the harness's, or whose TEXT names the harness file. Spelled with the same root you scanned, so it pastes straight into a shell. NO run= means NOT DERIVABLE -- never a guessed suite command +exit 4 if either is non-empty agent self-check before a PR (pair with --quality-delta): names the tests to run + the UNTESTED blast radius; exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff) run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries run="" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem matches the harness's, or whose TEXT names the harness file. Spelled RELATIVE to the root= the document declares, so it pastes into a shell run from there, and the document does not change with where the tree is checked out (a MULTI-ROOT run declares no single root, so it stays absolute). NO run= means NOT DERIVABLE -- never a guessed suite command **Try it** diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 8ff145a5..f92da175 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -558,7 +558,7 @@ Discloses: **none** ### `src/mcpverbs.h` -Discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `unindexed_candidates_capped` +Discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `siblings_capped`, `unindexed_candidates_capped` | constant | value | class | note | | --- | --- | --- | --- | diff --git a/src/cli.h b/src/cli.h index 41b061ea..c890ad14 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1164,8 +1164,10 @@ inline constexpr char kHelpHead[] = " exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff)\n" " run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries\n" " run=\"\" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem\n" - " matches the harness's, or whose TEXT names the harness file. Spelled with the same root you scanned,\n" - " so it pastes straight into a shell. NO run= means NOT DERIVABLE -- never a guessed suite command\n" + " matches the harness's, or whose TEXT names the harness file. Spelled RELATIVE to the root= the\n" + " document declares, so it pastes into a shell run from there, and the document does not change\n" + " with where the tree is checked out (a MULTI-ROOT run declares no single root, so it stays\n" + " absolute). NO run= means NOT DERIVABLE -- never a guessed suite command\n" " --grep=STR | --regex=PAT search for a literal or a regex; every hit comes back with its enclosing symbol\n" " literal / regex search + enclosing symbol + the matched line. SPAN-TIERED by default (see\n" " --grep-in below): the scan itself is exhaustive, the ANSWER serves one tier and discloses\n" diff --git a/src/flipimpact.h b/src/flipimpact.h index f8965485..76d9baa8 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -1143,8 +1143,11 @@ inline constexpr const char* kFlipRowLegend = // `testFilesRendered` is the count testmap.h's seam returns for the listing this header introduces — // review of #214: the run-hint clause was spliced unconditionally, so a flip with paid 180 B // for a rule about rows it has none of. The caller renders the rows first and passes the count it got. +// `rootRelativeRuns` is testmap.h's runsAreRootRelative for this run, decided by writeFlip (which holds the +// ingest and the root) and passed in: the legend sentence and the command spelling answer to one predicate. inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex, - const std::string& nextInvocation, std::size_t testFilesRendered ) + const std::string& nextInvocation, std::size_t testFilesRendered, + bool rootRelativeRuns, std::string_view rootAttr ) { rw::emitTo( out, "", // M21(b): the run=/run_unknown= rule, from testmap.h's ONE constant — rows-gated, through // the ONE gate every other legend asks (runHintClauseIfRows). - std::string( rw::runHintClauseIfRows( testFilesRendered ) ).c_str(), kFlipRowLegend ); + rw::runHintClauseIfRows( testFilesRendered, rootRelativeRuns ).c_str(), kFlipRowLegend ); + // Review of #219: every p= this verb prints is already spelled relative to the crawl root (relForHash), + // and declared no root at all — so a consumer holding the document could resolve none of them, + // and the run= commands beside them had nothing to be relative to either. The attribute and the one + // sentence that defines it are emitted together, the same pairing every other verb's root= keeps. + rw::emitRaw( out, rw::rootRelPathsLegend( !rootAttr.empty() ) ); rw::emitTo( out, "", + " hosts=\"{}\" filescope=\"{}\" downstream=\"{}\" dependents=\"{}\" tests=\"{}\" untested=\"{}\" files=\"{}\"{}{}>", ex( res.name ).c_str(), darkflags::gateKindTag( res.kind ), ex( res.def ).c_str(), res.isDark ? 1 : 0, res.isRuntime ? 1 : 0, ex( res.defSite.path ).c_str(), res.defSite.line, res.family.size(), res.totalRegions, res.totalLines, res.branches.size(), res.bindings.size(), res.hosts.size(), res.fileScopeLights, res.downstream.size(), res.dependents, res.tests.size(), res.untested.size(), res.filesScanned, - rw::nextAttrXml( nextInvocation ).c_str() ); + rw::nextAttrXml( nextInvocation ).c_str(), std::string( rootAttr ).c_str() ); // the contradiction row: this gate is ALREADY lit by the winning declaration, and dark only in the other if( !res.isDark ) @@ -1239,11 +1247,15 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult // E1 / review of #214: the listing is rendered HERE, before the header, so the header's run-hint clause // can be gated on the rows this document will actually carry. TestRunnerIndex stays lazy — it reads a runner // script only when asked about a file, and an empty res.tests asks about none. - const rw::TestRunnerIndex flipRunners( ing ); + const rw::TestRunnerIndex flipRunners( ing, root ); const rw::JoinedTestRows flipTests = rw::testRowsList( flipRunners, rw::testRowsOutOf( res.tests, rel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, ex ); - writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ), flipTests.files ); + // The root every p= above is relative to. Single-root only, exactly like every other verb's root= + // (ing.realPaths is non-empty only on a multi-root merge, where there is no single root to name). + const std::string flipRootAttr = rw::runsAreRootRelative( ing, root ) ? ( " root=\"" + ex( root ) + "\"" ) : std::string(); + writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ), flipTests.files, + rw::runsAreRootRelative( ing, root ), flipRootAttr ); writeFlipLights( out, res, ing, ex, maxRows, pageOffset ); for( const ValueBinding& b : res.bindings ) diff --git a/src/graphlegend.h b/src/graphlegend.h index 64067545..1b350198 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -178,9 +178,14 @@ inline std::string graphCountFloorBrief( bool hasUnindexed ) // dropped: METHODOLOGY §9 puts honesty in the attributes, and the sentence was never the honest part. // The trailing {} is the #66 gauge — EMPTY when nothing went unindexed, so this dialect keeps the same // omit-at-zero reading as the attribute rather than printing a bare "0" the other two never print. -// Gate: test/situshapecheck.sh (1); test/floormarkcheck.sh (9) keeps the two anchor phrases verbatim. +// Review of #219: an attribute with no reading is a token, not a disclosure — and this is the ONE dialect +// with no legend to look a token up in (--situ refuses --legend=compact; compactlegendcheck (R)). So the +// floor's CAUSE (a name-based call graph) and what an unindexed file IS stay on the line. They are the +// price of having no legend, not prose the compression was entitled to. +// Gate: test/situshapecheck.sh (1) and (8); test/floormarkcheck.sh (9) keeps the two anchor phrases. inline constexpr const char* kGraphCountFloorTextLine = - " counts_floor=1 graph_ambiguous={} graph_unresolved={}{} (map-header gauges) — every count above is a FLOOR, never a total; a zero is \"none found\", never \"none exists\"\n"; // std::format FORMAT: two gauge totals + the clause + " counts_floor=1 graph_ambiguous={} graph_unresolved={}{} (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; a zero is \"none found\", never \"none exists\"\n"; // std::format FORMAT: two gauge totals + the clause // The #66 clause for the prose dialect. "" at zero — the absence IS the confident case, same as the attribute. inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) @@ -189,8 +194,8 @@ inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) { return {}; } - char buf[64]; // literal 18 B + one size_t at 20 digits = 38 B worst case; snprintf truncates regardless - rw::formatTo( buf, sizeof( buf ), " graph_unindexed={}", unindexedFiles ); + char buf[128]; // literal 63 B + one size_t at 20 digits = 83 B worst case; snprintf truncates regardless + rw::formatTo( buf, sizeof( buf ), " graph_unindexed={} (files no grammar in this build could read at all)", unindexedFiles ); return buf; } diff --git a/src/handoff.h b/src/handoff.h index eff645c2..c191d40f 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -386,7 +386,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In const auto assemble = [ & ]( std::size_t keepRows, std::size_t withheld ) { std::string doc = kHandoffLegendHead; - doc += rw::runHintClauseIfRows( hoTests.files ); // M21(b): the ONE wording through the ONE gate — never a seventh paraphrase + doc += rw::runHintClauseIfRows( hoTests.files, rw::runsAreRootRelative( ing, root ) ); // M21(b): the ONE wording through the ONE gate — never a seventh paraphrase if( anySymsCapped ) { doc += handoffSymsCapClause(); } // absent unless an row was cut doc += kHandoffLegendTail; doc += "& segments ) noexcept diff --git a/src/packtask.h b/src/packtask.h index 160da4c4..ed64ec3e 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1808,9 +1808,15 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // mcpattrparitycheck still sees one spelling on every root. droppedPositiveAttr += lr.capAttrs; + // The clause is now BUILT (the root-relative sentence is conditional, so the seam composes a string + // rather than handing back one of two constants), and PackTaskHeaderParts holds VIEWS — so it is owned + // by a named local here, like report and droppedPositiveAttr above it. Binding the view straight to the + // returned temporary is a dangling read the moment the full expression ends, and it showed as exactly + // that: packtaskcheck's bundle was both malformed and non-deterministic (two runs, two sha256s). + 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, - rw::runHintClauseIfRows( tests.kept ) }; // the ONE gate: the section's own kept count + runClauseStr }; 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 diff --git a/src/partition.h b/src/partition.h index b074714c..8912ac08 100644 --- a/src/partition.h +++ b/src/partition.h @@ -609,7 +609,7 @@ inline std::string packTaskPartitionText( const IngestResult& ing, const Graph& sliceTests += part.testsKept; } whole += ""; whole += bundleOpen( "core", -1, core ); whole += core.xml; diff --git a/src/prcontext.h b/src/prcontext.h index d76c92c4..28bca25d 100644 --- a/src/prcontext.h +++ b/src/prcontext.h @@ -615,7 +615,7 @@ inline std::string prBudgetTail( std::size_t changedFiles, std::uint32_t skipped // (CodeRabbit on #214): a test elsewhere in the corpus, or a testCap=0 level, bought the clause for a document // with no row. Measured on test/defaultceilingcheck.sh's 120-file, no-test fixture: unconditional, 7,989 -> // 8,025 tokens, over the 8,000 default budget; gated, 7,989. -inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindexed, bool withRunClause ) +inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindexed, bool withRunClause, bool rootRelativeRuns ) { return std::string( ""; } @@ -928,14 +928,15 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // E1: both legend forms are built now and ONE is written later, once the body is known (prBodyHasTestRow); // the envelope is priced without the clause and the pricer adds runClauseBytes for a rows-bearing body. - const std::string legendText = prLegendText( escBase, g.unindexedFiles > 0, false ); + const bool prRootRelRuns = rw::runsAreRootRelative( ing, root ); + const std::string legendText = prLegendText( escBase, g.unindexedFiles > 0, false, prRootRelRuns ); const std::string anchorNoteText = prAnchorNoteText( anchorAttr ); // The clause-bearing form is built ONCE, and only if it is the form that gets written — the difference // between the two is exactly kRunHintLegendClause (prLegendText splices that constant and nothing else), // so the pricer reads the constant's size rather than a second rendering's. const auto writeHead = [ & ]( std::size_t testFiles ) { - const std::string legend = testFiles > 0 ? prLegendText( escBase, g.unindexedFiles > 0, true ) : legendText; + const std::string legend = testFiles > 0 ? prLegendText( escBase, g.unindexedFiles > 0, true, prRootRelRuns ) : legendText; std::fwrite( legend.data(), 1, legend.size(), out ); std::fwrite( anchorNoteText.data(), 1, anchorNoteText.size(), out ); }; @@ -948,7 +949,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // R2/N4: the price context (see prPriceDocument) — the envelope and every root attribute that does not // vary per candidate trim level, gathered once. const PrPriceCtx priceCtx{ .g = &g, .sharedAttrs = &sharedAttrs, .anchor = &anchor, .baseEscaped = &escBase, .atAttrs = &atAttrStr, - .envelopeBytes = envelopeBytes, .runClauseBytes = rw::kRunHintLegendClause.size(), + .envelopeBytes = envelopeBytes, .runClauseBytes = rw::runHintClauseIfRows( 1, prRootRelRuns ).size(), .changedFiles = changed.size(), .skippedModeOnly = skippedModeOnly, .budgetTokens = budgetTokens, .isDefaultBudget = budget.isDefault }; const auto priceOf = [ & ]( std::string_view body, std::size_t testFiles, std::size_t level, const std::string& truncatedRaw, const std::string& windowAttrs ) diff --git a/src/sarif.h b/src/sarif.h index cf55cf21..34bb13fb 100644 --- a/src/sarif.h +++ b/src/sarif.h @@ -126,18 +126,24 @@ inline const char* sarifLevel( std::string_view sev ) // which strips the same way for the same reason). SARIF wants a plain root-relative URI regardless of // which spelling the caller used, so this normalizes BOTH shapes against `rootPrefix` (the run's root, // trailing '/' already stripped — see rootPrefixOf below) rather than assuming a leading "./". +// Review of #219: the leading-"./" strip used to RETURN, before the prefix was ever tried. That is correct +// for the root "." (where the stored spelling is "./x" and "x" is the answer) and wrong for every other +// relative root: `ripwire ./corp` stores "./corp/test/x.sh", the early return yielded "corp/test/x.sh", and +// pasting that from the root the document declares is `cd ./corp && bash corp/test/x.sh` — rc 127. Both +// sides carry the same optional "./", so both sides drop it FIRST and the prefix comparison runs on what is +// left. Root "." then normalizes to "." , matches no path, and the answer is the "./"-stripped file exactly +// as before — the one case the old early return got right is the one case this keeps byte-identical. inline std::string_view rootRelativeUri( std::string_view file, std::string_view rootPrefix ) { - if( file.rfind( "./", 0 ) == 0 ) + const auto dropLeadingDot = []( std::string_view p ) noexcept + { return p.rfind( "./", 0 ) == 0 ? p.substr( 2 ) : p; }; + const std::string_view f = dropLeadingDot( file ); + const std::string_view r = dropLeadingDot( rootPrefix ); + if( !r.empty() && f.size() > r.size() + 1 && f.compare( 0, r.size(), r ) == 0 && f[ r.size() ] == '/' ) { - return file.substr( 2 ); + return f.substr( r.size() + 1 ); } - if( !rootPrefix.empty() && file.size() > rootPrefix.size() + 1 - && file.compare( 0, rootPrefix.size(), rootPrefix ) == 0 && file[ rootPrefix.size() ] == '/' ) - { - return file.substr( rootPrefix.size() + 1 ); - } - return file; + return f; } // Normalize a scan root for rootRelativeUri above: drop trailing '/' so the prefix strips cleanly diff --git a/src/situ.h b/src/situ.h index 8aa5b473..c3b45665 100644 --- a/src/situ.h +++ b/src/situ.h @@ -448,13 +448,8 @@ struct SituSiblings bool unindexedRowsFloor = false; // the crawl's unsupported-extension ROW list was itself cut }; -// dirOf is siblift.h's (the other same-directory lens), stem is mention.h's pair — the two primitives this -// rule needs both already exist, and a third spelling of either is the clone --quality-delta reports. -inline std::string_view situStemOf( std::string_view path ) noexcept -{ - return mention_detail::stripExt( mention_detail::baseNameOf( path ) ); -} - +// dirOf is siblift.h's (the other same-directory lens) and the stem is mention.h's pathStem — both +// primitives already existed, and a third spelling of either is the clone --quality-delta reports. // One changed file's test: same directory, and the same stem or testmap.h's stem-partner convention. Named // so lexicalSiblings below reads as the two loops it is (candidates x changed files) rather than four levels. inline bool isLexicalSiblingOf( std::string_view cand, std::string_view changed ) noexcept @@ -463,7 +458,8 @@ inline bool isLexicalSiblingOf( std::string_view cand, std::string_view changed { return false; } - return situStemOf( cand ) == situStemOf( changed ) || isTestPartnerOf( cand, changed ) || isTestPartnerOf( changed, cand ); + return mention_detail::pathStem( cand ) == mention_detail::pathStem( changed ) + || isTestPartnerOf( cand, changed ) || isTestPartnerOf( changed, cand ); } // ADDITIVE, deliberately: a file may be BOTH a decl/def partner (symbol identity) and a lexical sibling @@ -510,8 +506,11 @@ inline SituSiblings lexicalSiblings( const IngestResult& ing, const std::vector< } std::sort( out.paths.begin(), out.paths.end() ); out.paths.erase( std::unique( out.paths.begin(), out.paths.end() ), out.paths.end() ); - out.unindexedRowsFloor = !out.paths.empty() - && ing.crawlSkips.unsupported.size() < ing.crawlSkips.unsupportedFiles; + // Review of #219: this used to be gated on a NON-EMPTY result, and the emitter suppressed an empty + // block — so when the crawl's 500-row cut removed the only candidate, the report said nothing at all. + // That is the silent zero non-negotiable #3 forbids: the cut is a property of the CANDIDATE LIST, not + // of the answer, so it is recorded whenever the row list was short and the block speaks even at zero. + out.unindexedRowsFloor = ing.crawlSkips.unsupported.size() < ing.crawlSkips.unsupportedFiles; return out; } @@ -528,7 +527,8 @@ inline void writeSituDeclDefRows( std::FILE* out, const std::vector inline void writeSituSiblingRows( std::FILE* out, const SituSiblings& sibs, PathRelStrFn pathRel, const SituPageArgs& page ) { - if( sibs.paths.empty() ) + // Review of #219: an empty list is NOT nothing to say when the candidate list itself was cut — that zero + // is a floor, and a floor a reader cannot see is a confident wrong answer. Silence is kept only for the + // honest empty: nothing found, and nothing was hidden from the search. + if( sibs.paths.empty() && !sibs.unindexedRowsFloor ) { return; } - const PageWindow win = pageWindow( sibs.paths.size(), effectiveRowCap( page.limit, int( kSituSiblingRowsShown ) ), page.offset ); - const std::size_t shown = win.end - win.begin; - rw::emitTo( out, " lexical siblings ({}) not_dependents=1{}{} — same directory and stem as a changed file (header/impl partner, test, .inl); static, not a graph result:\n", + // …and the block does NOT take section [1]'s offset. Review of #219: it did, so `--situ=F --offset=20` + // printed "shown=0 total=9 capped=1" with a next= offering --limit=9 — relief that cannot restore rows an + // OFFSET removed — and --offset=7 dropped six rows silently. This is a small fixed block with a cap and + // --limit, like the decl/def partner rows above it, not a paged listing. + const std::size_t cap = effectiveRowCap( page.limit, int( kSituSiblingRowsShown ) ); + const std::size_t shown = sibs.paths.size() < cap ? sibs.paths.size() : cap; + rw::emitTo( out, " lexical siblings ({}){}{} — same directory and stem as a changed file (its header/impl partner, its test, its .inl): " + "NOT transitive dependents, so they are absent from the list below; lexical and static, never a graph result{}\n", sibs.paths.size(), - sibs.unindexedRowsFloor ? " unindexed_rows_floor=1" : "", + " not_dependents=1", situShowingNote( shown, sibs.paths.size(), "files", - situNextInvocation( page.selector, sibs.paths.size() ) ).c_str() ); - for( std::size_t i = win.begin; i < win.end; ++i ) + situNextInvocation( page.selector, sibs.paths.size() ) ).c_str(), + sibs.unindexedRowsFloor + ? " — unindexed_rows_floor=1: the crawl rows at most 500 unreadable-extension files, and it hit that cut here, " + "so a sibling no grammar can read may be missing and this count is a FLOOR" + : "" ); + for( std::size_t i = 0; i < shown; ++i ) { const std::string_view rp = pathRel( sibs.paths[i] ); rw::emitTo( out, " {}\n", std::string_view( rp.data(), rp.size() ) ); @@ -682,7 +694,7 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges const std::size_t blastShown = blastPage.end - blastPage.begin; const std::string blastNote = situShowingNote( blastShown, affected.size(), "files", situNextInvocation( page.selector, affected.size() ), - " prcontext_cap=20" ); // A5: --pr-context's own per-file blast-radius list is capped at 20 too + " prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too)" ); rw::emitTo( out, " [1] blast radius: {} symbols across {} files transitively depend on these changes{}\n", reach.size(), affected.size(), blastNote.c_str() ); // F3: the decl/def partner FIRST — it is the answer to "what else has to change with this file" that the @@ -1289,7 +1301,7 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const // H2H-Graft F1: the evidence clause (testmap.h's ONE wording) rides the rows-gated half, like the run= rule. rw::emitTo( out, "{}", kTestGateLegend, tgHasRows ? kTestGateRowLegend : "", std::string_view( kTestRowEvidenceLegend.data(), tgHasRows ? int( kTestRowEvidenceLegend.size() ) : 0 ), - runHintClauseIfRows( testRows ), // the ONE gate: this clause is about rows, so an untested-only report pays nothing + runHintClauseIfRows( testRows, runsAreRootRelative( ing, root ) ), // the ONE gate: this clause is about rows, so an untested-only report pays nothing rw::graphUnindexedLegend( g.unindexedFiles > 0 ), // #66: exactly when the root carries the attribute rw::rootRelPathsLegend( !tgRootAttr.empty() ) ); // §P11.4: this gate EXITS 4 on the obligation, so its rows carry the command that discharges it — where diff --git a/src/testmap.h b/src/testmap.h index 5a1ded9b..ff585f3e 100644 --- a/src/testmap.h +++ b/src/testmap.h @@ -487,6 +487,14 @@ inline std::vector exercisedSymbols( const IngestResult& ing, const Grap // // COST: the candidate scripts' texts are read at most ONCE per invocation and only LAZILY — nothing is read // until a row actually asks for a hint, so every verb that emits no test row pays nothing at all. +// A3 / review of #219: run= is spelled relative to root= exactly when the run HAS one root and declares it. +// A multi-root run's disk path lies under no single root, so its command must stay absolute — and the legend +// sentence below is gated on this SAME predicate, so the spelling and the claim cannot disagree. +inline bool runsAreRootRelative( const IngestResult& ing, std::string_view root ) noexcept +{ + return ing.realPaths.empty() && !root.empty(); +} + class TestRunnerIndex { public: @@ -497,7 +505,7 @@ class TestRunnerIndex // a path relative to a root that does not contain it. explicit TestRunnerIndex( const IngestResult& ing, std::string_view root = {} ) : ing_( &ing ), - rootPrefix_( root.empty() || !ing.realPaths.empty() ? std::string() : rw::sarif::rootPrefixOf( root ) ) + rootPrefix_( runsAreRootRelative( ing, root ) ? rw::sarif::rootPrefixOf( root ) : std::string() ) { for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) { @@ -525,6 +533,10 @@ class TestRunnerIndex return cache_.emplace( fileId, derive( fileId ) ).first->second; } + // Whether the commands this index spells are relative to a root — the SAME fact the legend sentence + // is gated on, read off the index rather than re-derived at each legend site. + bool rootRelative() const noexcept { return !rootPrefix_.empty(); } + std::string commandForScript( std::uint32_t fileId ) const { return fileId < ing_->files.size() && runnerVerb( ing_->files[fileId] ) != nullptr ? spell( fileId ) : std::string(); } @@ -548,8 +560,7 @@ class TestRunnerIndex // primitives binstale.h, gitmine.h and docdrift.h already stem paths with. Re-rolling them here is // exactly the new-clone-of-a-reused-helper --quality-delta reports, and it would also fork the // "strip the LAST dot" convention that every other stemming call site in this repo shares. - static std::string_view stemOf( std::string_view p ) noexcept - { return mention_detail::stripExt( mention_detail::baseNameOf( p ) ); } + static std::string_view stemOf( std::string_view p ) noexcept { return mention_detail::pathStem( p ); } void loadTexts() const { @@ -963,12 +974,23 @@ inline constexpr std::string_view kRunHintLegendClause = "verbatim in list order — a path holding ',' is never grouped, so p= splits into exactly n= paths. A " "shown=/total= over these rows counts test FILES: a row is n= of them. "; +// A3 / review of #219: the ROOT-RELATIVE half of the rule, and it is CONDITIONAL. Spliced unconditionally — +// as the first cut of A3 did — this sentence told a multi-root reader, in a document carrying no root= at +// all, that its absolute command was relative to something. runsAreRootRelative (above) decides BOTH the +// spelling and the sentence, so the two cannot disagree. Gate: rootrelemitcheck ARM 9c, runhintcheck 2d. +inline constexpr std::string_view kRunRootRelSentence = + "A run= command is relative to root=: run it from there. "; + // The clause is a rule about ROWS, so a legend splices it only when the document actually renders one — a // tests="0" answer pays nothing for it. THE gate, taking the count testRowsList returns (or, for a section // that cut its own rows, that section's kept count): one rule, one spelling, asked by all eight sites. -inline std::string_view runHintClauseIfRows( std::size_t testFilesRendered ) noexcept +inline std::string runHintClauseIfRows( std::size_t testFilesRendered, bool rootRelativeRuns ) { - return testFilesRendered == 0 ? std::string_view() : kRunHintLegendClause; + if( testFilesRendered == 0 ) + { + return {}; + } + return std::string( kRunHintLegendClause ) + ( rootRelativeRuns ? std::string( kRunRootRelSentence ) : std::string() ); } // ── P9 (capture-audit 2026-09-04) — the tests_to_run row set for ONE changed file ──────────────────── @@ -1210,7 +1232,7 @@ inline std::vector suiteMemberStems( const std::vector for( const std::string& token : tokens ) { if( token.find( '/' ) == std::string::npos ) { continue; } - const std::string_view stem = mention_detail::stripExt( mention_detail::baseNameOf( token ) ); + const std::string_view stem = mention_detail::pathStem( token ); if( !stem.empty() ) { stems.emplace_back( stem ); } } appendForListStems( tokens, stems ); @@ -1271,7 +1293,7 @@ inline ShellGateIndex buildShellGateIndex( const IngestResult& ing, const std::v { const std::string_view path = ing.files[f]; if( !isTestPath( path ) || !path.ends_with( ".sh" ) || mention_detail::baseNameOf( path ) == "regression.sh" ) { continue; } - const std::string_view stem = mention_detail::stripExt( mention_detail::baseNameOf( path ) ); + const std::string_view stem = mention_detail::pathStem( path ); if( std::find( registeredTokens.begin(), registeredTokens.end(), stem ) == registeredTokens.end() ) { continue; } addRegisteredShellGate( ing, changedFiles, f, index ); } diff --git a/src/verbs_change.h b/src/verbs_change.h index 6e62dd9c..c22980a8 100644 --- a/src/verbs_change.h +++ b/src/verbs_change.h @@ -150,7 +150,7 @@ std::optional runAffected( const MainDispatch& d ) "{}" // H2H-Graft F1: the evidence-order clause, testmap.h's ONE wording (changed= is spelled seed_kind="test" here: the argument matched it) "order=evidence says so on the root; partners= counts the partner rows. " "{}" // M21(b)/E1: the run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated - "{}{}-->{}", rw::kTestRowEvidenceLegend, rw::runHintClauseIfRows( afRowsXml.files ), + "{}{}-->{}", rw::kTestRowEvidenceLegend, rw::runHintClauseIfRows( afRowsXml.files, rw::runsAreRootRelative( ing, d.root ) ), // H1: the decl→def residue resolveAffectedSeeds summed over the symbol items. A file:name item whose // definitions were dropped seeded the walk with declarations alone, which reached the reader as a bare // tests="0" — on the verb whose answer is the list of tests to run. Exactly when the root carries it. @@ -252,7 +252,7 @@ std::optional runExercises( const MainDispatch& d ) " = the seed test files the pattern matched; = the covered symbols, PageRank desc. " "harness=script|mixed says the seed set contains shell gates, whose subprocess coverage this walk cannot see. " "{}" // M21(b)/E1: the run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated - "{}{}-->{}", rw::runHintClauseIfRows( exRowsXml.files ), rw::graphCountFloorBrief( g.unindexedFiles > 0 ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str(), rw::rootRelPathsLegend( exSingleRoot ) ); + "{}{}-->{}", rw::runHintClauseIfRows( exRowsXml.files, rw::runsAreRootRelative( ing, d.root ) ), rw::graphCountFloorBrief( g.unindexedFiles > 0 ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str(), rw::rootRelPathsLegend( exSingleRoot ) ); const std::string exRootAttr = exSingleRoot ? ( " root=\"" + ex( cfg.roots[0] ) + "\"" ) : std::string(); rw::emitTo( stdout, "", ex( cfg.exercisesFile ).c_str(), sel.testFiles.size(), shownSeed, diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 1f7d24e4..57067592 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -18,7 +18,7 @@ path 0 7058d89f7bab7aabe0a5cbf921959bc8a2c346fe65888b776996f879f04a58a2 e3b0c442 connect 0 31c7e3a689a6dcddf5eae17283740823005c3efdfdf3945be5770b2660a152b7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 b171aa1e5c28b47827f9148c2a5fc6948fb22938d3da21e6551b12432f492472 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_signatures 0 4a22aeda1d2e36fd390065dac1c07955e9f8942e7ba858435281863653f373ba e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_task 0 b4a88debacae6771a3c7e050fe0ed882c33e40faf80afd056e843a0e5095fe75 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_task 0 1d9eb9e7ae3b0a418118cda95e90d3a678c836650922ee64a4f779a3e590ad26 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 6014e2f18ba2d587f70d59fcbe62f64c38e8c556291006ae3dfd76ef7259315a e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 23b824f67984709f3c01dc7cc97c4c682b2afbaa5b63677a79ce56a1a58e6454 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 skipped 0 5b9749fdafec842ae75c6ad07e9eac8eb435653f8e5fc1ce452853fc36ff96f2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 b06980d52e4991e57563059d8be986bb4614779e20ccfd3790aac1ee076d2512 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 ab9dee52240f70055fa4d82d6b928ef52f4f5781c6a80ee39edb49805b892719 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 b11003a2f5fb35b595cf2f5de5b479afc0e2f89d8aef64c1e7009e9fd2b11243 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 53e10dcbbed0cfe9dd6bdc8e3e23d275d66151d43bdfda6dd58b1fd44147f79c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/receiptpostcheck.sh b/test/receiptpostcheck.sh index 8ece1fd7..906e5960 100755 --- a/test/receiptpostcheck.sh +++ b/test/receiptpostcheck.sh @@ -589,5 +589,27 @@ else || no "(17) the wrap blurb neither prescribes nor mentions the receipt's edit_check" fi +# ── ARM 18 — the receipt's own ROOT, so its root-relative echoes can be resolved ─────────────────────── +# Review of #219 (A3): the receipt's "file", its tests_to_run[].run recipes and its stderr "next:" are all +# spelled RELATIVE to the crawl root — which is right, and useless on its own: an MCP client runs in its own +# working directory and the receipt named no root at all. Its JSON siblings (--test-gate --json, the +# situational_awareness payload) have carried "root" all along; the receipt is the one that hands the caller +# a command to paste, so it is the one that least afforded to omit it. +R18="$( cd "$TMP/w" && "$BIN" . --insert-before-symbol=report --edit-payload="$TMP/insert.py" 2>/dev/null )" +if [ -z "$R18" ]; then + no "(18) the edit verb produced no receipt — the arm would be a false green" +else + printf '%s' "$R18" | python3 -c ' +import sys, json +r = json.load( sys.stdin ) +root = r.get( "root" ) +assert root, "the receipt carries no \"root\" key, so its relative file=/run=/next= cannot be resolved" +f = r.get( "file", "" ) +assert not f.startswith( "/" ), "file=%r is absolute; the root key exists to make it relative" % f +print( "OK" )' >/dev/null 2>&1 \ + && ok "(18) the receipt declares the root its file=/run=/next= are relative to" \ + || no "(18) the receipt's paths are root-relative and it declares NO root — nothing in it resolves from a client cwd" +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" diff --git a/test/rootrelemitcheck.sh b/test/rootrelemitcheck.sh index 4e1b2ebb..7e108d1f 100755 --- a/test/rootrelemitcheck.sh +++ b/test/rootrelemitcheck.sh @@ -592,9 +592,13 @@ else no "ARM9 the printed run= does NOT run from root= ($A9CMD) — a relative command that cannot be pasted is worse than an absolute one" fi fi -if [ -n "$A9NEXT" ] && [ "$A9NEXT" = "$A9CMD" ]; then +# Review of #219: an EMPTY next= used to fall out of this chain printing neither ok nor no — a silent pass +# on the one attribute the arm exists to read. --test-gate's next= is never optional on a document with rows. +if [ -z "$A9NEXT" ]; then + no "ARM9 --test-gate printed NO next= on a document with test rows — the follow-up is not optional" +elif [ "$A9NEXT" = "$A9CMD" ]; then ok "ARM9 next= pastes the same root-relative command as the first row's run= ($A9NEXT)" -elif [ -n "$A9NEXT" ]; then +else no "ARM9 next=\"$A9NEXT\" disagrees with the first row's run=\"$A9CMD\"" fi # --situ is the text dialect of the same echo: its `(run: …)` recipe and its `root:` line. @@ -613,6 +617,117 @@ else || no "ARM9 --situ prints a relative run recipe and never says what it is relative to" fi +# ── ARM 9b — EVERY ROOT SPELLING NAMES THE SAME COMMAND, AND IT RUNS ──────────────────────────────────── +# Review of #219: rootRelativeUri() returned on a stored path's leading "./" BEFORE it tried the root prefix, +# so `ripwire ./corp` stored "./corp/test/x.sh", the relativizer stripped only the "./" and the document said +# run="bash corp/test/x.sh" — which, pasted from the root the document declares, is +# (cd ./corp && bash corp/test/x.sh) => rc 127. The same wrong prefix rode every p= and every sibling row. +# So the spelling sweep is a matrix, not a pair: a command is the SAME bytes under every way of naming one +# root, and it EXECUTES from that root under each of them. +A9B="$TMP/spell"; rm -rf "$A9B"; mkdir -p "$A9B/corp" +cp -R "$A9/." "$A9B/corp/" +rm -rf "$A9B/corp/.git"; seed_git "$A9B/corp" +ln -s corp "$A9B/link" 2>/dev/null +a9b_run(){ # a9b_run → the first run= the test-gate prints + ( cd "$1" && printf '' | "$BIN" "$2" --test-gate=geometry.cpp 2>/dev/null ) \ + | tr '<' '\n' | sed -n 's/.* run="\([^"]*\)".*/\1/p' | head -1 +} +A9B_REF="$( a9b_run "$A9B/corp" "." )" +if [ -z "$A9B_REF" ]; then + no "ARM9b the spelling fixture derives no run= — every arm below would be a false green" +else + ok "ARM9b reference spelling (root '.') says run=\"$A9B_REF\"" + # each row: a working directory and the root spelling used from it + for pair in "$A9B/corp|." "$A9B|corp" "$A9B|./corp" "$A9B|corp/" "$A9B|$A9B/corp" "$A9B|link"; do + cwd="${pair%%|*}"; spell="${pair#*|}" + got="$( a9b_run "$cwd" "$spell" )" + if [ "$got" = "$A9B_REF" ]; then + ok "ARM9b root spelled '$spell' says the same run=\"$got\"" + else + no "ARM9b root spelled '$spell' says run=\"$got\" but root '.' says \"$A9B_REF\" — one root, two commands" + fi + if [ -z "$got" ]; then + no "ARM9b root spelled '$spell' printed NO run= — nothing to execute" + elif ( cd "$A9B/corp" && eval "$got" >/dev/null 2>&1 ); then + ok "ARM9b the command printed under '$spell' executes from the root it names" + else + no "ARM9b the command printed under '$spell' does NOT execute from the root it names (run=\"$got\")" + fi + done +fi + +# ── ARM 9c — MULTI-ROOT: no single root, so no relative command AND no claim of one ───────────────────── +# Review of #219: TestRunnerIndex keeps the absolute command on a multi-root run (there is no single root +# for it to be relative to), but the legend clause was spliced unconditionally — so a document with NO root= +# at all told the reader its commands were relative to it. The spelling and the sentence must be decided by +# the SAME predicate; this arm pins both halves against each other. +A9M1="$TMP/mr/A"; A9M2="$TMP/mr/B"; rm -rf "$TMP/mr"; mkdir -p "$A9M1" "$A9M2" +cp -R "$A9/." "$A9M1/"; rm -rf "$A9M1/.git"; seed_git "$A9M1" +cp -R "$FIX/." "$A9M2/"; seed_git "$A9M2" +MR="$( "$BIN" "$A9M1" "$A9M2" --affected=distance 2>/dev/null )" +if [ -z "$MR" ]; then + printf ' SKIP ARM9c the multi-root run emitted nothing on this fixture\n' +else + MRRUN="$( printf '%s' "$MR" | tr '<' '\n' | sed -n 's/.* run="\([^"]*\)".*/\1/p' | head -1 )" + MRCLAUSE=no; case "$MR" in *"relative to root="*) MRCLAUSE=yes ;; esac + MRROOT=no; case "$MR" in *' root="'*) MRROOT=yes ;; esac + if [ -z "$MRRUN" ]; then + printf ' SKIP ARM9c the multi-root run derived no run= (root= present: %s)\n' "$MRROOT" + else + # the multi-root document declares no single root=, so the command MUST stay absolute… + # the command is ` `; it is the PATH that must stay absolute, so test the argument + MRPATH="${MRRUN##* }" + case "$MRPATH" in + /*) ok "ARM9c multi-root keeps an absolute run=\"$MRRUN\" — there is no single root to be relative to" ;; + *) no "ARM9c multi-root printed a RELATIVE run=\"$MRRUN\" in a document with root= $MRROOT — unresolvable" ;; + esac + # …and the legend must not claim otherwise + if [ "$MRCLAUSE" = yes ] && [ "$MRROOT" = no ]; then + no "ARM9c multi-root says \"relative to root=\" in a document that declares no root= — a false claim" + else + ok "ARM9c multi-root does not claim its command is relative to a root it never declares" + fi + fi +fi + +# ── ARM 9d — --flags --flip: a document full of root-relative paths that declared no root ─────────────── +# Review of #219: writeFlip spells every p= through relForHash( …, root ) — root-relative — and its rows +# now carry a root-relative run= as well, but itself declared no root= at all. A consumer holding that +# document cannot resolve one path in it. The fixture is test/flagsfix (the gate corpus that actually HAS a +# dark feature), copied to two depths so the one-anchor claim is measured here too. +FLAGSFIX="$ROOT/test/flagsfix" +if [ ! -d "$FLAGSFIX" ]; then + no "ARM9d test/flagsfix is missing — the --flip anchor arm cannot run" +else + A9F="$TMP/flip"; A9FD="$TMP/ddddddddd/ddddddddd/ddddddddd/ddddddddd/flip" + rm -rf "$A9F" "$A9FD"; mkdir -p "$A9F" "$A9FD" + cp -R "$FLAGSFIX/." "$A9F/"; cp -R "$FLAGSFIX/." "$A9FD/" + seed_git "$A9F"; seed_git "$A9FD" + "$BIN" "$A9F" --flags --flip=FIXTURE_DARK_FEATURE --no-cache >"$TMP/flip.s" 2>/dev/null + "$BIN" "$A9FD" --flags --flip=FIXTURE_DARK_FEATURE --no-cache >"$TMP/flip.d" 2>/dev/null + if [ ! -s "$TMP/flip.s" ]; then + no "ARM9d --flags --flip emitted nothing on test/flagsfix — the arm would be a false green" + else + read -r lk tot anc <]* root="'; then + ok "ARM9d --flip declares the root its p= rows are relative to" + else + no "ARM9d --flip prints root-relative p= rows and declares NO root= — nothing in it can be resolved" + fi + [ "$anc" -le 1 ] && ok "ARM9d --flip declares the absolute root ${anc} time(s) — once per document" \ + || no "ARM9d --flip declares the absolute root ${anc} times" + mask "$A9F" < "$TMP/flip.s" > "$TMP/flip.ms" + mask "$A9FD" < "$TMP/flip.d" > "$TMP/flip.md" + cmp -s "$TMP/flip.ms" "$TMP/flip.md" \ + && ok "ARM9d --flip is depth-independent" \ + || no "ARM9d --flip differs with checkout depth" + fi +fi + # ── the MCP dialect ───────────────────────────────────────────────────────────────────────────────────── if ! python3 "$ROOT/test/rootrelemitmcp.py" "$BIN" "$SHORT" "$DEEP"; then fail=1 diff --git a/test/runhintcheck.sh b/test/runhintcheck.sh index bb0c82cf..2e270e57 100755 --- a/test/runhintcheck.sh +++ b/test/runhintcheck.sh @@ -77,9 +77,62 @@ REL="$( cd "$R" && perl -e 'alarm 20; exec @ARGV' "$BIN" . --affected=src/core.c && ok "run= is the SAME command under an absolute and a relative root — no checkout prefix rides the row" \ || no "run= differs between an absolute scan ('$( runof 'mything_harness.cpp' "$A" )') and a relative one ('$( runof 'mything_harness.cpp' "$REL" )')" # …and it must still RUN from the root the document declares, which is the whole point of relativizing it. -( cd "$R" && eval "$( runof 'mything_harness.cpp' "$A" )" >/dev/null 2>&1 ) \ - && ok "the printed run= executes from the declared root" \ - || no "the printed run= does not execute from the declared root — a relative command that cannot be pasted is worse than an absolute one" +# Review of #219: an EMPTY run= makes `eval ""` succeed, so this arm passed on the one outcome it exists to +# forbid — a row with no command at all. The emptiness is checked BEFORE anything is executed. +RUNCMD="$( runof 'mything_harness.cpp' "$A" )" +if [ -z "$RUNCMD" ] +then + no "the row carries NO run= at all — there is no command to execute, and an empty eval would pass" +elif ( cd "$R" && eval "$RUNCMD" >/dev/null 2>&1 ) +then + ok "the printed run= executes from the declared root ($RUNCMD)" +else + no "the printed run= does not execute from the declared root ($RUNCMD) — a relative command that cannot be pasted is worse than an absolute one" +fi + +# ── 2c) EVERY spelling of one root names the SAME command ──────────────────────────────────────────── +# Review of #219: `ripwire ./sub` stored "./sub/test/x.sh" and the relativizer stripped only the leading +# "./", so the document said run="bash sub/test/x.sh" — wrong from the root it declares. The root is one +# place, however the caller spells it, so the command must be one string. +PARENT="$( dirname "$R" )"; LEAF="$( basename "$R" )" +for spell in "$LEAF" "./$LEAF" "$LEAF/"; do + GOT="$( cd "$PARENT" && perl -e 'alarm 20; exec @ARGV' "$BIN" "$spell" --affected=src/core.cpp --no-cache 2>/dev/null )" + GOTRUN="$( runof 'mything_harness.cpp' "$GOT" )" + if [ "$GOTRUN" = "bash test/mythingcheck.sh" ] + then + ok "root spelled '$spell' says run=\"$GOTRUN\"" + else + no "root spelled '$spell' says run=\"$GOTRUN\", not the root-relative \"bash test/mythingcheck.sh\"" + fi + if [ -n "$GOTRUN" ] && ( cd "$R" && eval "$GOTRUN" >/dev/null 2>&1 ) + then + ok "the command printed under '$spell' executes from that root" + else + no "the command printed under '$spell' does not execute from that root" + fi +done + +# ── 2d) MULTI-ROOT: the absolute command stays, and no legend claims otherwise ──────────────────────── +# There is no single root for a command to be relative to, so the spelling keeps the disk path — and the +# sentence that says "relative to root=" must not be spliced into a document that declares no root=. +MR2="$TMP/mr2"; rm -rf "$MR2"; mkdir -p "$MR2/src" +printf 'int other() { return 1; }\n' > "$MR2/src/other.cpp" +MRDOC="$( perl -e 'alarm 40; exec @ARGV' "$BIN" "$R" "$MR2" --affected=src/core.cpp --no-cache 2>/dev/null )" +MRRUN2="$( runof 'mything_harness.cpp' "$MRDOC" )" +if [ -z "$MRDOC" ]; then + printf ' SKIP 2d the multi-root run emitted nothing on this fixture\n' +elif [ -z "$MRRUN2" ]; then + printf ' SKIP 2d the multi-root run derived no run= for mything_harness.cpp\n' +else + case "${MRRUN2##* }" in + /*) ok "2d multi-root keeps the absolute command ($MRRUN2) — there is no single root to be relative to" ;; + *) no "2d multi-root printed a relative command ($MRRUN2) in a document with no single root" ;; + esac + case "$MRDOC" in + *"relative to root="*) no "2d multi-root splices \"relative to root=\" into a document that declares no root=" ;; + *) ok "2d multi-root does not claim a relativity it does not have" ;; + esac +fi # ── 3) NO evidence → NO run=. The half that keeps the attribute trustworthy. ────────────────────────── case "$A" in diff --git a/test/situshapecheck.sh b/test/situshapecheck.sh index 5a8ce58e..7d9ebe9a 100755 --- a/test/situshapecheck.sh +++ b/test/situshapecheck.sh @@ -18,6 +18,23 @@ # prose cannot creep back. test/floormarkcheck.sh keeps the two anchor phrases; this gate mirrors them, so a # regression reds HERE too rather than only in a gate about a different property. # +# THE BYTE TABLE — one number per line, one corpus, named here so nothing else has to restate it. Measured +# by this gate's own `${#line}` (characters), on THIS repo at `--situ=src/graph.h` except the partner header, +# which is measured on the fixture below at `--situ=core/widget.cc` (this repo has no decl/def partner for +# graph.h). The "before" column is what these same arms printed, red, against the pre-A5 binary: +# +# line before after ratchet +# graph-count floor 601 344 360 +# decl/def partner header 228 209 220 +# [2] tests-to-run header 267 220 230 +# script-gate disclosure 165* 132 140 (* derived from the pre-A5 literal with this +# corpus's count: the arm did not exist yet) +# +# The ratchets sit above the after values, not on them: this gate forbids the PARAGRAPH coming back, and a +# later lane adding one honest word to a reading should not have to move a pin to do it. Review of #219 moved +# the floor and partner ratchets UP (200 -> 360, 140 -> 220) when the readings those lines had dropped were +# restored — the reading is the disclosure, and a ratchet that forbids it is a ratchet aimed at the wrong thing. +# # Exit 0 = ALL PASS, non-zero = SOME FAILED. set -u @@ -135,8 +152,12 @@ else *) ok "(1) floor line omits graph_unindexed= — nothing was unindexed" ;; esac fi N="$( len_of 'counts_floor=1' "$REPO_OUT" )" - [ "$N" -le 200 ] && ok "(1) floor line is ${N} B (ratchet 200) — an attribute line, not a paragraph" \ - || no "(1) floor line is ${N} B, over the 200 B ratchet: the prose has crept back" + if [ "$N" -le 360 ] + then + ok "(1) floor line is ${N} B (ratchet 360) — an attribute line, not a paragraph" + else + no "(1) floor line is ${N} B, over the 360 B ratchet: the prose has crept back" + fi fi # ── (2) THE DECL/DEF PARTNER HEADER — the "NOT dependents" caveat becomes an attribute ─────────────────── @@ -151,8 +172,12 @@ else case "$PH" in *'(2)'*|*'(1)'*|*'(3)'*) ok "(2) partner header still states how many partners there are" ;; *) no "(2) partner header lost its count: $PH" ;; esac N="$( len_of 'decl/def partners' "$OUT" )" - [ "$N" -le 140 ] && ok "(2) partner header is ${N} B (ratchet 140)" \ - || no "(2) partner header is ${N} B, over the 140 B ratchet" + if [ "$N" -le 220 ] + then + ok "(2) partner header is ${N} B (ratchet 220)" + else + no "(2) partner header is ${N} B, over the 220 B ratchet" + fi fi # ── (3) SECTION [1]'s pr-context ASIDE — a cap is a number, so it is an attribute ──────────────────────── @@ -338,6 +363,161 @@ PYEOF esac fi +# ── (8) EVERY READING SURVIVES — an attribute without a reading is a token, not a disclosure ──────────── +# Review of #219: A5 is a compression of the SENTENCE, never of the FACT, and --situ is the one dialect with +# no legend anywhere to look the fact up in (it refuses --legend=compact; test/compactlegendcheck.sh (R)). +# So a gauge name here has to carry its own short gloss. These arms assert the READING, not the token — the +# four that the first cut of A5 dropped, plus the two attributes it introduced. +READINGS="$TMP/readings"; : > "$READINGS" +read_arm(){ # read_arm