fix(gate): carry the per-function verdicts out of the worktree, and gate the in-tree retry on them

The first version of this parsed 'failed by class:' from the worker's stdout and
was INERT: the worker is gate_stage, which never prints that line (harvest_verify
does, one level down). classes came back empty for all 17 binaries of a batch and
the retry gate that consumed it fired ZERO times — a field that is always empty
makes its consumer a silent no-op (R54). Verified the claim only after re-reading
the log; correcting it here.

Now parallel_gate copies harvest_verify's <stem>.classified.txt out of the
worktree before teardown (it lives in the worktree's own .run/, which is not
symlinked and dies with it) and derives the class summary from those rows. That
also PRESERVES the verdict layer, which until now survived only as a side effect
of gater_lane re-running the whole binary in-tree afterwards (R47).

gater_lane judges the retry on the rows: a class with no per-function diagnostic
is the blind-worktree signature; anything cc1 named is a real compile error and
the serial rebuild would only reproduce it.

Verified live on ov_SC07_000: 'NOT retrying in-tree' fired, and the verdict row
landed at .run/gate_lane/ov_SC07_000.pgate.classified.txt.
This commit is contained in:
Drew T
2026-09-01 00:05:25 -06:00
parent b2d73108e2
commit ae7c68a89a
2 changed files with 42 additions and 7 deletions
+7 -2
View File
@@ -399,9 +399,14 @@ def main():
# worktree was not blind: the draft genuinely does not compile in its TU, and the in-tree
# retry is a serial full-binary build that reproduces the same error. S69 ran 22 of those
# back-to-back and banked 0 — ~20 minutes of a lane whose whole design goal is parallelism.
# Judge on the VERDICT ROWS the worker actually wrote, not on a summary string. A row reads
# `<fn>\t<CLASS>: <file>:<line>: <message>`; the blind-worktree signature is a class with NO
# per-function diagnostic (`CC1-FAIL(no-diagnostic)`), which is precisely what a missing
# generated header or an unstageable link input produces. Anything cc1 named is real.
rows = r.get("verdicts") or []
real = [row for row in rows if "no-diagnostic" not in row and ":" in row.split("\t", 1)[-1]]
cls = (r.get("classes") or "")
real = [c for c in cls.split() if c and "no-diagnostic" not in c]
if real:
if rows and real:
print("[gater] %s: worktree FAILED %d/%d with real cc1 diagnostics (%s) — NOT retrying "
"in-tree; the worktree was not blind, the drafts do not compile in their TU"
% (b, tail["failed"], tail.get("drafts", 0), cls), flush=True)
+35 -5
View File
@@ -318,13 +318,43 @@ def gate_one(idx, pin, job):
# printed EARLIER by harvest_verify, never survived. gater_lane's in-tree retry therefore
# could not tell "the worktree was blind" from "cc1 emitted a real diagnostic naming the
# function", and retried all 22 binaries serially for nothing (measured S69: ~20 min).
cls = ""
for ln in (r.stdout or "").splitlines():
if "failed by class:" in ln:
cls = ln.split("failed by class:", 1)[1].strip()
# THE VERDICT LAYER LIVES IN THE WORKTREE'S OWN .run/ AND DIES WITH IT (P31 S69, R47).
# `harvest_verify` writes `<stem>.classified.txt` — ONE ROW PER FUNCTION with the exact cc1
# diagnostic. `.run/` is not symlinked into a worktree, so those rows were lost and survived
# only because gater_lane re-ran the whole binary IN-TREE afterwards, purely as a side effect.
# Copy them out, and derive the class summary FROM THEM.
#
# THE FIRST ATTEMPT AT THIS PARSED `failed by class:` OUT OF THE WORKER'S STDOUT AND WAS
# INERT: the worker is `gate_stage`, which does not print that line — harvest_verify does,
# one level down. `classes` came back empty for all 17 binaries of the batch and the retry
# gate that consumed it never fired once. A field that is always empty makes its consumer a
# no-op, silently (R54); reading the artifact the tool actually writes cannot drift that way.
verdicts, cls = [], ""
wrun = os.path.join(wt, ".run")
if os.path.isdir(wrun):
for name in sorted(os.listdir(wrun)):
if name.startswith("harvest_failed") and name.endswith(".classified.txt"):
try:
rows = [ln.rstrip("\n") for ln in open(os.path.join(wrun, name)) if ln.strip()]
except OSError:
continue
verdicts.extend(rows)
try:
shutil.copy(os.path.join(wrun, name),
os.path.join(REPO, ".run/gate_lane/%s.pgate.classified.txt" % binary))
except OSError:
pass
# A row is `<fn>\t<CLASS>: <diagnostic>`. The CLASS alone is not enough to decide whether the
# worktree was blind — `CC1-FAIL(no-diagnostic)` is the blind signature, `CC1-FAIL: <file>:<line>:
# <message>` is a real compile error — so keep the whole row and let the consumer judge.
classes = []
for row in verdicts:
part = row.split("\t", 1)[-1]
classes.append(part.split(":", 1)[0].strip())
cls = " ".join(sorted(set(classes)))
return {"binary": binary, "banked": banked, "files": files, "ovl": ovl,
"secs": round(time.time() - t0, 1),
"missing_generated": missing, "classes": cls,
"missing_generated": missing, "classes": cls, "verdicts": verdicts,
"rc": r.returncode, "tail": (r.stdout or r.stderr)[-200:] if not banked else ""}
except Exception as e:
return {"binary": binary, "banked": [], "error": "%s: %s" % (type(e).__name__, str(e)[:160])}