From cdd75d461622ec73ebea7b9a50d251673c263bd6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Tue, 13 Oct 2026 11:57:51 -0400 Subject: [PATCH 1/2] test(legendcoveragecheck): run the roster on a small pool; report call timeouts The gate ran its ~170 independent read-only calls one at a time: 394 s on the dev machine, and over its 1200 s budget (rc=124) on the macOS plain shard twice in three days. Two calls (--pr-context=HEAD~30, with and without a budget) are ~40% of that by themselves; that cost is inside ripwire, not the gate. - The roster and the (E) dropped-note sweep share one runner: a thread pool (LEGENDCOV_JOBS, default min(4, cpus); 1 = the old serial run), heaviest calls submitted first. Every row still runs its own command once and is judged in roster order as before. --doctor, whose answer reads the cache directory, runs serially after the pool drains. - Every call has a budget (LEGENDCOV_CALL_TIMEOUT, default 600 s). A call that times out or dies on a signal prints its own TIMEOUT/ERROR FAIL line naming the call, instead of an empty document read as silent or a partial one judged for gaps. A non-zero exit code is judged as before. No assertion, roster row, floor or expected count changed. Co-Authored-By: Claude Opus 5.5 --- test/legendcoveragecheck.sh | 94 +++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index 2b44b55cd..b23a94299 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -111,10 +111,55 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } echo "legendcoveragecheck: BIN=$BIN" +# THE CALL RUNNER both sweeps below share (2026-10-13, lane/legendcov-speed). The roster is ~170 independent read-only +# calls, and the gate used to run them ONE AT A TIME: 394 s on the dev machine, and over its 1200 s budget (rc=124) on the +# macOS plain shard twice in three days. Two of them (--pr-context=HEAD~30, ±budget) are ~40% of that on their own — the +# cost is inside ripwire (prcontext/cochangePartners), not here. So the calls run on a small pool, the heaviest submitted +# FIRST so they overlap everything else instead of finishing last. Nothing a row asserts changed: every row still runs its +# own command, once, and is judged in roster order exactly as before. Why concurrency is safe here: no roster call writes +# into the corpus (the roster comment says so), and the warm-by-default TMPDIR cache publishes by exclusive temp file + +# rename (ingest_cache.h saveCache), one blob per (root, lean/rich) — two calls racing on a cold root both parse and the +# last rename wins with an equal blob. The ONE exception is --doctor, whose answer reads that cache directory: it runs +# serially after the pool has drained, as it did before (late, on a warm and quiescent cache dir). +# Every call now has a budget (LEGENDCOV_CALL_TIMEOUT, default 600 s), and a call that times out or dies on a signal +# is a TIMEOUT/ERROR row of its own — never an empty document judged as "silent", and never a partial one judged at all +# (CHECKLIST 14). A non-zero EXIT (--quality-delta 2, --doctor 1) is a verdict the verb returns, judged as before. +# LEGENDCOV_JOBS=1 restores the serial run. +cat > "$TMP/lcrun.py" <<'PYR' +import os, subprocess, concurrent.futures as cf +JOBS = max( 1, int( os.environ.get( "LEGENDCOV_JOBS" ) or min( 4, os.cpu_count() or 1 ) ) ) +CALL_TIMEOUT = max( 1, int( os.environ.get( "LEGENDCOV_CALL_TIMEOUT" ) or 600 ) ) +def call( BIN, args ): + """-> (stdout, None) on a completed call (any exit code), (None, reason) on a timeout / signal / no start.""" + try: + p = subprocess.run( [ BIN ] + args, capture_output = True, timeout = CALL_TIMEOUT ) + except subprocess.TimeoutExpired: + return None, f"TIMEOUT (no answer within {CALL_TIMEOUT}s)" + except OSError as e: + return None, f"ERROR (could not start: {e})" + if p.returncode < 0: + return None, f"ERROR (killed by signal {-p.returncode})" + return p.stdout, None +def runAll( BIN, argvs, first = lambda a: False, last = lambda a: False ): + """Run argvs[i] once each; -> list of (stdout, reason) by index. `first` calls are submitted ahead of the rest; + `last` calls run serially once the pool has drained.""" + res = [ None ] * len( argvs ) + pooled = sorted( ( i for i, a in enumerate( argvs ) if not last( a ) ), key = lambda i: not first( argvs[ i ] ) ) + with cf.ThreadPoolExecutor( JOBS ) as ex: + futs = { ex.submit( call, BIN, argvs[ i ] ): i for i in pooled } + for f in cf.as_completed( futs ): res[ futs[ f ] ] = f.result() + for i, a in enumerate( argvs ): + if last( a ): res[ i ] = call( BIN, a ) + return res +def isDoctor( a ): return "--doctor" in a +PYR + python3 - "$BIN" "$ROOT" "$BASELINE" "$TMP" <<'PY' > "$TMP/out" 2>"$TMP/pyerr" import subprocess, re, sys, os BIN, ROOT, BASELINE, TMP = sys.argv[1:5] +sys.path.insert( 0, TMP ) +from lcrun import runAll, isDoctor, JOBS, CALL_TIMEOUT SMALL = os.path.join( ROOT, "src" ) # The roster: one runnable invocation per first-screen shape. Value-taking verbs get a symbol that exists in @@ -374,9 +419,16 @@ def legendOf( doc, anyRoot = False ): def mentioned( a, legend ): return re.search( r'\b' + re.escape( a ) + r'\b', legend ) is not None def defined ( a, legend ): return re.search( r'(?/dev/null; fi cat "$TMP/out" +# A roster call that TIMED OUT or died on a signal has no document to judge: it is named here as what it is, and the gate +# fails on it — it is neither "silent" (C) nor a document with no gaps (A)/(G). +if [ -s "$TMP/callfail" ]; then + while IFS= read -r l; do no "$l"; done <"$TMP/callfail" +elif [ ! -f "$TMP/callfail" ]; then + no "the roster sweep did not record its call outcomes — no verdict below can be trusted" +fi + # (A) the ratchet: no NEW undefined attribute on any first screen. if [ -s "$TMP/new" ]; then no "(A) $( wc -l < "$TMP/new" | tr -d ' ' ) NEW undefined first-screen attribute(s) — define them in that verb's own legend, or add the line to $BASELINE with the reason:" @@ -589,7 +650,20 @@ def legendOf( doc ): return lead.decode( 'utf-8', 'replace' ) def defined( a, legend ): return re.search( r'(? Date: Tue, 13 Oct 2026 15:04:51 -0400 Subject: [PATCH 2/2] test(legendcoveragecheck): judge each dropped-note call on its own, so two failed calls report two lines (CodeRabbit #394) Co-Authored-By: Claude Opus 5.5 --- test/legendcoveragecheck.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index b23a94299..53666b4c2 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -676,11 +676,17 @@ def attrsOf( d ): bad, checked, entered, left, census = [], 0, [], [], [] for dialect, extra in DIALECTS: - try: - dt, dw, dm = ( doc( [ SMALL, QUERY, b ] + extra ) for b in ( TIGHT, WIDE, MARGIN ) ) - except CallFailed as e: - bad.append( f"{dialect}: {e}" ) + # Each of the three calls is judged on its own, so two failed calls report two lines, not the first only. + docs, fails = [], [] + for b in ( TIGHT, WIDE, MARGIN ): + try: + docs.append( doc( [ SMALL, QUERY, b ] + extra ) ) + except CallFailed as e: + fails.append( f"{dialect}: {e}" ) + if fails: + bad.extend( fails ) continue + dt, dw, dm = docs tight, wide = legendOf( dt ), legendOf( dw ) mt, mw = NOTE.search( tight ), NOTE.search( wide ) if not mt: