Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
100ed43
perf(testmap): tests-to-run rows without a runner paid the not-deriva…
joyful-ii-V-I Sep 13, 2026
7ab0956
fix(prcontext,tests): the run clause rode a pr-context bundle that co…
joyful-ii-V-I Sep 13, 2026
5d74256
fix(testmap,prcontext): a group reordered the rows it replaced, a byt…
joyful-ii-V-I Sep 13, 2026
ff8d77a
merge(main): main at f6a27167 into the tests-rows lane — both Unrelea…
joyful-ii-V-I Sep 13, 2026
6621370
fix(testmap,packtask,prcontext,partition): the tests section cut wher…
joyful-ii-V-I Sep 13, 2026
470992e
fix(compactlegend,docs): the compact legend promised an escape the em…
joyful-ii-V-I Sep 13, 2026
5797780
Merge remote-tracking branch 'origin/main' into lane/tests-rows
joyful-ii-V-I Sep 13, 2026
4f074f3
Merge remote-tracking branch 'origin/main' into lane/tests-rows
joyful-ii-V-I Sep 13, 2026
ccdb7c0
fix(emit,mcp,tests): a render seam that ignored its own failures, a c…
joyful-ii-V-I Sep 13, 2026
84e5da3
docs(readme): "rows that share their evidence" named no precondition …
joyful-ii-V-I Sep 13, 2026
9609862
fix(emit,readme): a throw from the emitter skipped every line of clea…
joyful-ii-V-I Sep 13, 2026
96be399
Merge remote-tracking branch 'origin/main' into lane/tests-rows
joyful-ii-V-I Sep 13, 2026
b016c87
fix(prcontextcheck): the emitter-throw arm asked the working tree, so…
joyful-ii-V-I Sep 13, 2026
507cdb6
fix(emit): the layer's fault switch named the host project, which is …
joyful-ii-V-I Sep 13, 2026
7bc28ff
fix(prcontextcheck): the flavour probe read a LABEL, so Release faile…
joyful-ii-V-I Sep 13, 2026
c9d6d4e
fix(prcontextcheck): the emitter-throw arm anchored into the live his…
joyful-ii-V-I Sep 14, 2026
f242091
fix(prcontext,emit,testrows): an unmeasured est_tokens disclosed noth…
joyful-ii-V-I Sep 14, 2026
213affc
merge(main): record 49220049 (PR #226 README badge) in the lane's anc…
joyful-ii-V-I Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions .ripwire_quality_acks

Large diffs are not rendered by default.

186 changes: 186 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1276,19 +1276,27 @@ $ ripwire . --test-gate # exit code: 4
<test-gate changed="1" impacted="80" tests="2" untested="76" shown_tests="2" tests_capped="0"
shown_untested="25" untested_capped="1" script_gates_unmodelled="332" at="9cf0b16f3+dirty">
<t p="./test/adaptivecutshapefix/adaptive_cut_shape_test.cpp" run="bash test/adaptivecutshapecheck.sh"/>
<t p="./test/verify_radix.cpp"/>
<t p="./test/verify_radix.cpp" run_unknown="1"/>
<u sym="buildGraph" p="./src/graph.h" ccx="712"/>
<u sym="dispatchMcpLine" p="./src/mcp.h" ccx="428"/>
</test-gate>
```

A `run=` attribute appears only when a runner is derivable from real evidence — a test-dir script
whose stem matches the harness, or whose text names it. No `run=` means *not derivable*, never a
guessed suite command. `script_gates_unmodelled="332"` is the same discipline: script-to-binary is not
a call edge, so those gates are invisible to this walk, and the number says so rather than letting
`tests="2"` read as complete. The `<u>` rows are the untested blast radius: impacted symbols that no
test in the corpus reaches.
whose stem matches the harness, or whose text names it. A row with none says so — `run_unknown="1"`,
never a guessed suite command — and a `<t>` or `<g>` row carries one or the other, never neither. A
`<g hops="2" n="3" p="a,b,c" run_unknown="1"/>` row is **two or more contiguous runner-less rows whose
attributes are byte-identical**, served as one: `n=` is how many, `p=` is their paths verbatim in list
order, and the disclosure is paid once per group rather than once per row. Everything else stays its own
row — a row with a `run=`, a row whose attributes differ from its neighbour's, and a path containing a
comma, which is never grouped at all, so `p=` splits on `,` into exactly `n=` paths. A `shown=`/`total=`
over these rows counts test FILES: a `<g>` row is `n=` of them.

`script_gates_unmodelled="332"` is the same discipline: script-to-binary is not a call edge, so those
gates are invisible to this walk, and the number says so rather than letting `tests="2"` read as
complete. The `<u>` rows are the untested blast radius: impacted symbols that no test in the corpus
reaches.

</details>

Expand Down
31 changes: 30 additions & 1 deletion bench/arb/run_arb.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,40 @@ def trace_files(bin_path, snap, trace_text):


TEST_RE = re.compile(r'<test p="([^"]+)"')
# E1 (2026-09-12): runner-less rows sharing their evidence ride ONE <g … p="a,b,c"/> row, so a group's p= is
# split on ','. THE CONTRACT, as testmap.h states it: a path containing ',' is NEVER grouped — it is served
# as a single row — so splitting a group's p= cannot split a path in half, and there is no comma escape to
# undo. This adapter used to undo a `&#44;` that the seam stopped spelling on 2026-09-13 while decoding NONE
# of the entities it does emit, so `src/a&amp;b.cpp` was scored against a file name that does not exist.
# Both row shapes now go through one decode, and therefore have one path contract.
GROUP_RE = re.compile(r'<g [^>]*?\bp="([^"]+)"')

# The five XML attribute entities plus numeric references — test/testrowpaths.py's xml_unescape, in a file
# that may not import from test/. Local and small on purpose: an XML parser is not a dependency this bench
# adapter is allowed to grow for five substitutions.
_ARB_ENT = {"amp": "&", "lt": "<", "gt": ">", "quot": '"', "apos": "'"}


def xml_unescape(s):
def one(m):
body = m.group(1)
if body.startswith("#"):
try:
return chr(int(body[2:], 16) if body[1:2].lower() == "x" else int(body[1:], 10))
except ValueError:
return m.group(0)
return _ARB_ENT.get(body, m.group(0))
return re.sub(r"&([#0-9A-Za-z]+);", one, s)


def affected_tests(bin_path, snap, changed_file):
out = run_bin_or_none(bin_path, snap, ["--affected=%s" % changed_file])
return [norm_path(m, snap) for m in TEST_RE.findall(out)] if out is not None else []
if out is None:
return []
paths = [xml_unescape(p) for p in TEST_RE.findall(out)]
for grp in GROUP_RE.findall(out):
paths += [xml_unescape(p) for p in grp.split(",") if p != ""]
return [norm_path(m, snap) for m in paths]


def impact_files(bin_path, snap, seeds):
Expand Down
2 changes: 1 addition & 1 deletion skills/ripwire-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ enclosing-chain report) and `@FILE:LINE` in any SYM selector; contract gate: `te
| `grep` (`pattern`) | `--grep=STR` | parallel literal scan + enclosing symbol + matched line |
| `cochange` (`file`) | `--cochange=FILE` | the lockstep git partners of one file |
| `memory_recall` (`task`, `top_k` + `budget_tokens` optional) | `--recall=TASK [--top-k=N] [--max-tokens=N]` | full bodies of the few relevant docs/memory notes, bounded by the SAME default 8000-token body ceiling as the CLI (the header discloses `max_tokens=` and every cut). `budget_tokens` raises the ceiling explicitly when you want everything; `top_k` (default 8) shapes how many docs |
| `situational_awareness` (`diff`/`files` optional) | `--situ` | blast radius, tests_to_run, forgotten co-change partners (the Shotgun Surgery check), hotspot alert — as JSON; defaults to `git diff HEAD` |
| `situational_awareness` (`diff`/`files` optional) | `--situ` | blast radius, tests_to_run, forgotten co-change partners (the Shotgun Surgery check), hotspot alert — as JSON; defaults to `git diff HEAD`. In `tests_to_run`, `situational_awareness` uses `test`; `explore` and edit receipts use `p`. The field is a path string OR an **array** of paths beside `n` — several runner-less tests sharing their attributes, served as one row — and every row carries `run` or `run_unknown: true` |
| `mentions` (`symbol`) | `--mentions=SYM` | which markdown plans/designs discuss a symbol |
| `owners` (`symbol` optional) | `--owners[=SYM]` | bus-factor: recency-weighted author ownership |
| `lego` (`type`) | `--lego=TYPE` | an interface's method contract + every implementor (own-language) |
Expand Down
22 changes: 22 additions & 0 deletions src/compactlegend.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,28 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] =
// --uses=Owner.field's member form (fielduses.h appends kUsesFieldLegend to that answer alone). owner_candidates= is a
// row attribute that exists only beside member=, so one head term defines the whole form.
{ "member", "member=Owner.field: rows use that field; pinned=/amb_sites= rows with one owner/with owner_candidates=K; owners_of_name= fields so named" },
// M21(b) / E1 (2026-09-12): the tests_to_run family's not-derivable disclosure, and the <g> group row it rides
// once per group. Row-level (every dialect puts it on the row), present-only; the <g> reading is qualified
// to that element so a document of single rows never pays for it, and a --flags document's own <g> never
// triggers it (that element carries no run_unknown=).
//
// THE COMPACT TERM SAYS WHAT THE FULL CLAUSE SAYS (review of 6621370f). This row is the compact dialect's
// ONLY reading of <g>, so a reader holding it and nothing else must be able to act on p=. It promised
// "every path verbatim (&#44; a comma)" — an escape testmap.h no longer emits, because a path holding ','
// is not grouped at all now — and it never carried the counts-FILES rule the full clause gained with it.
// So on a comma-path corpus `--affected --legend=compact` told its reader to undo an entity that is not
// there, and disagreed with the full legend about what a section's shown=/total= counts.
// The two facts a <g> consumer cannot act without are now stated here in the FULL CLAUSE'S OWN WORDS: a
// path holding ',' is never grouped, so p= splits into exactly n=; and a shown=/total= over these rows
// counts test FILES. The compact dialect re-spells rather than quotes — that is what makes it compact, and
// kRunHintLegendClause is 350+ B against this table's per-verb charge — so the two cannot be ONE constant.
// test/compactlegendcheck.sh (R) pins them against each other instead: it reads the required phrases OUT
// OF kRunHintLegendClause and fails the next release where either wording drops one or promises &#44;
// again. Cost of saying it: the term goes 99 -> 194 B, charged ONLY on a document that carries a <g> row —
// measured on a six-runner-less-test fixture, `--affected --legend=compact` 501 -> 596 B, and no pinned
// legend on this tree or on any gate fixture moves at all, because every harness here has a runner.
{ "run_unknown", "run_unknown=1: no runner derivable (a guess would be worse)", true },
{ "run_unknown", "<g n= p=a,b,c>: n runner-less rows with equal attrs as ONE row, paths verbatim; a path holding ',' is never grouped, so p= splits into exactly n=; shown=/total= over these rows counts test FILES", true, "g" },
{ "hits_capped", "hits_capped=1: hits= is a floor" },
// Both also ride the map header: est_tokens= alone there under order=stable (the root drops it), over_ceiling=1 there
// under max-tokens. Same number, same reading, so one row reads both places.
Expand Down
30 changes: 21 additions & 9 deletions src/flipimpact.h
Original file line number Diff line number Diff line change
Expand Up @@ -1140,8 +1140,11 @@ inline constexpr const char* kFlipRowLegend =

// The doc comment, the `<flip …>` header attributes, and the four situational rows that qualify them
// (already-lit / also / parent / capped) plus the family roll-up.
// `testFilesRendered` is the count testmap.h's seam returns for the <t> listing this header introduces —
// review of #214: the run-hint clause was spliced unconditionally, so a flip with <tests n="0"> paid 180 B
// for a rule about rows it has none of. The caller renders the rows first and passes the count it got.
inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex,
const std::string& nextInvocation = std::string() )
const std::string& nextInvocation, std::size_t testFilesRendered )
{
rw::emitTo( out, "<!-- ripwire flip: the blast radius of turning ONE gate ON. lights = the code that becomes live: r rows "
"are #if regions, b rows are C++ branch sites (a gate read as a VALUE through a constexpr bool, via= names "
Expand All @@ -1159,8 +1162,9 @@ inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEsc
"UNIT: untested= here counts HOSTS (indexed defs this gate lights that no test reaches). The test gate "
"verb spells untested= over impacted SYMBOLS and the seams verb over cross-directory call EDGES, so the "
"three numbers count three different things and must never be compared or summed across verbs. {}-->",
// M21(b): the run=/run_unknown= rule, from testmap.h's ONE constant.
std::string( rw::kRunHintLegendClause ).c_str(), kFlipRowLegend );
// 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::emitTo( out, "<flip gate=\"{}\" kind=\"{}\" default=\"{}\" dark=\"{}\" runtime=\"{}\" p=\"{}\" l=\"{}\""
" family=\"{}\" regions=\"{}\" loc=\"{}\" branches=\"{}\" bindings=\"{}\""
Expand Down Expand Up @@ -1232,7 +1236,14 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult
const auto rel = [ & ]( std::uint32_t fileId ) { return std::string( relForHash( ing.files[ fileId ], root ) ); };
const auto isTested = [ & ]( NodeId n ) { return n < res.testReach.size() && res.testReach[n]; };

writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ) );
// E1 / review of #214: the <t> 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::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 );
writeFlipLights( out, res, ing, ex, maxRows, pageOffset );

for( const ValueBinding& b : res.bindings )
Expand Down Expand Up @@ -1261,11 +1272,12 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult
// with 26 reachable tests named 25 of them and dropped the 26th because it sorted last. --test-gate's
// own <t> listing has never been windowed for exactly this reason; this listing is the same obligation
// read from a different seed, so it is served whole on every page. SIZE_MAX, not maxRows.
const rw::TestRunnerIndex flipRunners( ing );
writeCappedList( out, "tests", res.tests, SIZE_MAX, [ & ]( std::uint32_t f )
{
rw::emitTo( out, "<t p=\"{}\"{}/>", ex( rel( f ) ).c_str(), rw::runAttrDisclosed( flipRunners, f, ex ).c_str() );
} );
// 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 — `<tests n="N">` with no cut attributes, n= the FILE count,
// which is the same number the header's clause was gated on (flipTests.files, rendered above).
rw::emitTo( out, "<tests n=\"{}\">", res.tests.size() );
rw::emitRaw( out, flipTests.text.c_str() );
rw::emitRaw( out, "</tests>" );
writeCappedList( out, "untested", res.untested, maxRows, [ & ]( NodeId u )
{
const Symbol& s = ing.symbols[u];
Expand Down
16 changes: 7 additions & 9 deletions src/handoff.h
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,12 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In
// TestRunnerIndex is lazy: a packet with no test row reads no runner script.
const rw::TestRunnerIndex hoRunners( ing );
const auto hoEsc = [ & ]( std::string_view t ) { return std::string( escapeXml( t, esc ) ); };
for( const std::uint32_t f : facts.tests )
{
v += "<t p=\"";
v += escapeXml( hoPathRel( f ), esc );
v += "\"";
v += rw::runAttrDisclosed( hoRunners, f, hoEsc );
v += "/>";
}
// E1: <g> where no runner is derivable. The seam returns the FILE count with the rows, and the legend
// below is gated on it — review of #214: this packet spliced the clause unconditionally, and it is
// BYTE-BUDGETED with heuristic rows dropped tail-first, so a packet with <tests n="0"> could evict a real
// row to pay 180 B for a rule about rows it has none of.
const rw::JoinedTestRows hoTests = rw::testRowsList( hoRunners, rw::testRowsOutOf( facts.tests, hoPathRel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, hoEsc );
v += hoTests.text;
v += "</tests></verified>";

// ── heuristic rows, priority order (dropped TAIL-FIRST under a budget) ───────────────────────────
Expand Down Expand Up @@ -388,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::kRunHintLegendClause; // M21(b): the ONE wording, spliced — never a seventh paraphrase
doc += rw::runHintClauseIfRows( hoTests.files ); // M21(b): the ONE wording through the ONE gate — never a seventh paraphrase
if( anySymsCapped ) { doc += handoffSymsCapClause(); } // absent unless an <f> row was cut
doc += kHandoffLegendTail;
doc += "<handoff";
Expand Down
Loading
Loading