fix(TASK-032): enforce incomplete⇒BLOCKED structurally; anchor adversary degraded detection
This commit is contained in:
4 files changed
+882
-30
No files matched your search
@@ -57,7 +57,11 @@ those rules are *structure*:
|
||||
sentinel is a lane FAILURE, never a pass; the critical count from the
|
||||
code-review report's summary line (raw 🔴 count if absent) blocks
|
||||
regardless of its verdict line; a code-review lane whose own verdict line
|
||||
records a pipeline fault blocks (a degraded review is never a pass);
|
||||
records a pipeline fault blocks (a degraded review is never a pass); the
|
||||
adversary's own degraded-run header (the `Criteria: …` line directly under
|
||||
its sentinel recording a pipeline fault, died criterion checks, or its
|
||||
verdict script's crash stub) blocks as a pipeline fault,
|
||||
not as a DIVERGES finding;
|
||||
`NEEDS-HUMAN` without 🔴 passes but
|
||||
is surfaced under *Human attention required*; probe `INCONCLUSIVE` blocks
|
||||
with an environment note — it is never treated as PASS or FAIL.
|
||||
@@ -86,7 +90,8 @@ those rules are *structure*:
|
||||
only lanes that **faulted** — a missing report (stood in for by a synthetic
|
||||
`PIPELINE-FAULT: <lane> lane produced no report` marker so it never renders as
|
||||
SKIPPED), a non-string report, the `lane_fault` marker, a sentinel-less
|
||||
report, or a nested graph's own degraded-run wording. A real verdict
|
||||
report, or a nested graph's own degraded-run wording (the code-review verdict
|
||||
line / the adversary's header line directly under its sentinel). A real verdict
|
||||
(DIVERGES, FAIL, INCONCLUSIVE, NEEDS-HUMAN, …) is never re-run: the review's
|
||||
judgment stands. Each lane gets up to 3 attempts, all inside a wall-clock
|
||||
budget (`MAX_RETRY_ELAPSED_SECS`) sized to fit the graph timeout.
|
||||
@@ -105,6 +110,13 @@ budget (`MAX_RETRY_ELAPSED_SECS`) sized to fit the graph timeout.
|
||||
guards append to an existing `signals_error` rather than overwrite it, and
|
||||
`build_items.py` preserves the prior `lanes_summary`.
|
||||
|
||||
A non-empty `GAUNTLET_REVIEW_INCOMPLETE:` line structurally implies
|
||||
`GAUNTLET: BLOCKED`: the verdict gate forces every lane named in
|
||||
`review_incomplete` to a BLOCKED row (`review incomplete after N attempt(s)`)
|
||||
and a matching blocker, so a restored earlier report can never turn an
|
||||
incomplete review into a pass — and malformed `review_incomplete` bookkeeping
|
||||
is itself recorded as a `pipeline` fault, never dropped.
|
||||
|
||||
Whenever at least one lane (or the pipeline) could not complete, the first
|
||||
line reads `GAUNTLET: BLOCKED` followed immediately by
|
||||
`GAUNTLET_REVIEW_INCOMPLETE: <lane>[, <lane>]`. The names are the exhausted
|
||||
@@ -124,7 +136,7 @@ still appear under `## Blockers` and must be fixed as usual.
|
||||
| Lane | Sentinel | Blocks on |
|
||||
|------|----------|-----------|
|
||||
| code-reviewer | `**Verdict: MERGE-READY \| NEEDS-HUMAN**` | critical count from the report's summary line (raw 🔴 count if absent); missing sentinel; own verdict line records a pipeline fault |
|
||||
| adversary | `ADVERSARIAL_REVIEW: CONFORMS \| DIVERGES` | DIVERGES; missing sentinel |
|
||||
| adversary | `ADVERSARIAL_REVIEW: CONFORMS \| DIVERGES` | DIVERGES; own degraded-run header (pipeline fault, died criterion checks, or its verdict script's crash stub); missing sentinel |
|
||||
| security-reviewer | `SECURITY_REVIEW: PASS \| FAIL` | FAIL; missing sentinel |
|
||||
| probe | `USAGE_PROBE: PASS \| FAIL \| INCONCLUSIVE` | FAIL; INCONCLUSIVE (environment note); missing sentinel |
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ hard rules:
|
||||
- a faulted lane is re-run only while it has attempts left AND the
|
||||
gauntlet is inside its wall-clock budget; otherwise it is recorded in
|
||||
review_incomplete and its fault flows to the verdict gate as-is;
|
||||
- malformed review_incomplete bookkeeping (not a list, or non-str
|
||||
entries) keeps its str entries and records a PIPELINE-FAULT in
|
||||
signals_error — never silently dropped, never split into lane names;
|
||||
- the gate itself must never retry by accident: any internal error
|
||||
records a PIPELINE-FAULT in signals_error and falls through to the
|
||||
verdict gate, which blocks on it.
|
||||
@@ -57,6 +60,15 @@ SENTINELS = {
|
||||
# instead of overwriting it, and report the lanes it had already declined.
|
||||
crash_context = {"signals_error": "", "review_incomplete": []}
|
||||
|
||||
# Duplicated verbatim in the sibling gate script — the two copies must stay identical.
|
||||
ADVERSARY_DEGRADED_HEADER = re.compile(
|
||||
r"Criteria: .*("
|
||||
r"degraded run: pipeline fault recorded"
|
||||
r"|degraded run: \d+ criterion check\(s\) died"
|
||||
r"|verdict computation error:"
|
||||
r")"
|
||||
)
|
||||
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
@@ -65,6 +77,18 @@ def load_state():
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def adversary_degraded(report):
|
||||
# The adversary's verdict.py emits its degraded-run header on the line
|
||||
# directly under the FIRST sentinel; anchoring there keeps a report that
|
||||
# merely quotes the wording later (observations, complaints) a real verdict.
|
||||
lines = report.splitlines()
|
||||
for i, ln in enumerate(lines):
|
||||
if re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", ln):
|
||||
nxt = lines[i + 1] if i + 1 < len(lines) else ""
|
||||
return bool(ADVERSARY_DEGRADED_HEADER.match(nxt))
|
||||
return False
|
||||
|
||||
|
||||
def nested_degraded(lane, report):
|
||||
# The nested graphs complete with a sentinel even when a stage inside
|
||||
# them died; their own degraded-run wording marks a partial review,
|
||||
@@ -80,13 +104,7 @@ def nested_degraded(lane, report):
|
||||
)
|
||||
)
|
||||
if lane == "adversary":
|
||||
return bool(
|
||||
re.search(
|
||||
r"^Criteria: .*degraded run: pipeline fault recorded",
|
||||
report,
|
||||
re.MULTILINE,
|
||||
)
|
||||
)
|
||||
return adversary_degraded(report)
|
||||
return False
|
||||
|
||||
|
||||
@@ -100,15 +118,38 @@ def is_faulted(lane, report):
|
||||
return nested_degraded(lane, report)
|
||||
|
||||
|
||||
def attempt_count(attempts, lane):
|
||||
try:
|
||||
return int(attempts.get(lane) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
prior = state.get("signals_error")
|
||||
crash_context["signals_error"] = prior if isinstance(prior, str) else ""
|
||||
incomplete = list(state.get("review_incomplete") or [])
|
||||
crash_context["review_incomplete"] = incomplete
|
||||
attempts = dict(state.get("lane_attempts") or {})
|
||||
kept = dict(state.get("kept_results") or {})
|
||||
signals_error = prior if isinstance(prior, str) else ""
|
||||
crash_context["signals_error"] = signals_error
|
||||
out = {}
|
||||
declared = state.get("review_incomplete")
|
||||
if isinstance(declared, list):
|
||||
incomplete = [name for name in declared if isinstance(name, str)]
|
||||
malformed = len(incomplete) != len(declared)
|
||||
else:
|
||||
incomplete = []
|
||||
malformed = declared is not None
|
||||
if malformed:
|
||||
fault = (
|
||||
"PIPELINE-FAULT: malformed review_incomplete bookkeeping "
|
||||
f"({repr(declared)[:80]})"
|
||||
)
|
||||
signals_error = f"{signals_error}; {fault}" if signals_error else fault
|
||||
crash_context["signals_error"] = signals_error
|
||||
out["signals_error"] = signals_error
|
||||
crash_context["review_incomplete"] = incomplete
|
||||
raw_attempts = state.get("lane_attempts")
|
||||
attempts = dict(raw_attempts) if isinstance(raw_attempts, dict) else {}
|
||||
kept = dict(state.get("kept_results") or {})
|
||||
faulted = []
|
||||
|
||||
for lane, (items_key, results_key) in LANES.items():
|
||||
@@ -124,7 +165,7 @@ def main():
|
||||
kept[lane] = report
|
||||
if is_faulted(lane, report):
|
||||
faulted.append(lane)
|
||||
attempts[lane] = int(attempts.get(lane) or 0) + 1
|
||||
attempts[lane] = attempt_count(attempts, lane) + 1
|
||||
|
||||
# A missing/zero stamp reads as an ancient start and declines every
|
||||
# retry — the safe direction when the clock's origin is unknown.
|
||||
|
||||
@@ -17,6 +17,11 @@ re-reads, no judgment. The gate's hard rules:
|
||||
(code-reviewer's verdict.py / render.py degraded-run wording) blocks —
|
||||
the nested graph completed, but with a dead domain lane, verifier, or
|
||||
synthesis behind it, so its critical count is not trustworthy;
|
||||
- an adversary lane whose OWN header line (the line directly under its
|
||||
sentinel) records a degraded run — a pipeline fault, died criterion
|
||||
checks, or its verdict script's crash stub — blocks as a PIPELINE-FAULT,
|
||||
not as a DIVERGES finding: the nested graph completed with a dead stage
|
||||
behind it, so its verdict is not a judgment on the code;
|
||||
- a non-zero critical count in the code-review report blocks regardless
|
||||
of the lane's own verdict line — derived from render.py's deterministic
|
||||
summary line (`*Reviewed N files, found X critical, …*`); the raw 🔴
|
||||
@@ -31,7 +36,12 @@ re-reads, no judgment. The gate's hard rules:
|
||||
- an incomplete review — lanes retry_gate gave up on (review_incomplete)
|
||||
or a pipeline-level fault (reported under the `pipeline` pseudo-lane) —
|
||||
emits the GAUNTLET_REVIEW_INCOMPLETE: machine line and a "Review
|
||||
incomplete" section: that is a fault to re-run, never a code finding;
|
||||
incomplete" section: that is a fault to re-run, never a code finding.
|
||||
Every lane named in review_incomplete is forced to a BLOCKED row, so a
|
||||
restored earlier report can never turn an incomplete review into a
|
||||
pass: a non-empty machine line structurally implies GAUNTLET: BLOCKED.
|
||||
Malformed review_incomplete bookkeeping (not a list, or non-str
|
||||
entries) is itself recorded as a `pipeline` fault, never dropped;
|
||||
- the gate itself must never crash into a silent pass: any internal
|
||||
error emits GAUNTLET: BLOCKED naming the error, with the pipeline
|
||||
marked incomplete.
|
||||
@@ -41,6 +51,16 @@ import json
|
||||
import os
|
||||
import re
|
||||
|
||||
# Duplicated verbatim in the sibling gate script — the two copies must stay identical.
|
||||
ADVERSARY_DEGRADED_HEADER = re.compile(
|
||||
r"Criteria: .*("
|
||||
r"degraded run: pipeline fault recorded"
|
||||
r"|degraded run: \d+ criterion check\(s\) died"
|
||||
r"|verdict computation error:"
|
||||
r")"
|
||||
)
|
||||
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
@@ -48,11 +68,26 @@ def load_state():
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def adversary_degraded(report):
|
||||
# The adversary's verdict.py emits its degraded-run header on the line
|
||||
# directly under the FIRST sentinel; anchoring there keeps a report that
|
||||
# merely quotes the wording later (observations, complaints) a real verdict.
|
||||
lines = report.splitlines()
|
||||
for i, ln in enumerate(lines):
|
||||
if re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", ln):
|
||||
nxt = lines[i + 1] if i + 1 < len(lines) else ""
|
||||
return bool(ADVERSARY_DEGRADED_HEADER.match(nxt))
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
blockers, attention, lane_lines, reports = [], [], [], []
|
||||
blockers, attention, reports = [], [], []
|
||||
rows = {}
|
||||
malformed = set()
|
||||
lane_attempts = state.get("lane_attempts") or {}
|
||||
lane_attempts = state.get("lane_attempts")
|
||||
if not isinstance(lane_attempts, dict):
|
||||
lane_attempts = {}
|
||||
|
||||
def lane_report(key):
|
||||
arr = state.get(key) or []
|
||||
@@ -65,10 +100,16 @@ def main():
|
||||
return json.dumps(item)
|
||||
|
||||
def record(lane, status, detail=""):
|
||||
lane_lines.append(f"| {lane} | {status} | {detail} |")
|
||||
rows[lane] = (status, detail)
|
||||
|
||||
def attempt_count(lane):
|
||||
try:
|
||||
return int(lane_attempts.get(lane) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def attempts_suffix(lane):
|
||||
n = int(lane_attempts.get(lane) or 0)
|
||||
n = attempt_count(lane)
|
||||
return f" ×{n}" if n > 1 else ""
|
||||
|
||||
def fault_excerpt(report):
|
||||
@@ -178,7 +219,17 @@ def main():
|
||||
else:
|
||||
reports.append(("adversary", adv))
|
||||
m = re.search(r"ADVERSARIAL_REVIEW:\s*(CONFORMS|DIVERGES)", adv)
|
||||
if m and m.group(1) == "CONFORMS":
|
||||
if adversary_degraded(adv):
|
||||
blockers.append(
|
||||
"adversary: PIPELINE-FAULT — degraded run, pipeline fault recorded "
|
||||
"inside the adversary; a degraded lane is never a pass"
|
||||
)
|
||||
record(
|
||||
"adversary",
|
||||
"BLOCKED",
|
||||
f"PIPELINE-FAULT (degraded lane{attempts_suffix('adversary')})",
|
||||
)
|
||||
elif m and m.group(1) == "CONFORMS":
|
||||
record("adversary", "GREEN", "CONFORMS")
|
||||
elif m:
|
||||
blockers.append(
|
||||
@@ -244,21 +295,57 @@ def main():
|
||||
)
|
||||
record("probe", "BLOCKED", "missing verdict sentinel")
|
||||
|
||||
# retry_gate restores an exhausted lane's LAST report, which may be an
|
||||
# earlier clean verdict — an incomplete lane must block regardless of
|
||||
# what its row parsed to.
|
||||
declared = state.get("review_incomplete")
|
||||
incomplete = set()
|
||||
malformed_bookkeeping = None
|
||||
if isinstance(declared, list):
|
||||
incomplete = {name for name in declared if isinstance(name, str)}
|
||||
if any(not isinstance(name, str) for name in declared):
|
||||
malformed_bookkeeping = repr(declared)[:80]
|
||||
elif declared is not None:
|
||||
malformed_bookkeeping = repr(declared)[:80]
|
||||
for lane in sorted(incomplete - {"pipeline"}):
|
||||
if rows.get(lane, ("", ""))[0] == "BLOCKED":
|
||||
continue
|
||||
after = f" after {n} attempt(s)" if (n := attempt_count(lane)) else ""
|
||||
blockers.append(
|
||||
f"{lane}: review incomplete{after} — the lane never produced a usable verdict"
|
||||
)
|
||||
record(lane, "BLOCKED", f"review incomplete{after}")
|
||||
|
||||
# A gauntlet-level fault (build_items crash, dead parse stage) means the
|
||||
# lanes above were never built — SKIPPED rows alone must not read as PASS.
|
||||
signals_error = state.get("signals_error")
|
||||
incomplete = set(state.get("review_incomplete") or [])
|
||||
if isinstance(signals_error, str) and "PIPELINE-FAULT:" in signals_error:
|
||||
pipeline_fault = isinstance(signals_error, str) and "PIPELINE-FAULT:" in signals_error
|
||||
if pipeline_fault:
|
||||
blockers.append(f"pipeline: {signals_error}")
|
||||
incomplete.add("pipeline")
|
||||
if malformed_bookkeeping is not None:
|
||||
blockers.append(
|
||||
"pipeline: PIPELINE-FAULT — malformed review_incomplete bookkeeping "
|
||||
f"({malformed_bookkeeping}); never a pass"
|
||||
)
|
||||
incomplete.add("pipeline")
|
||||
if "pipeline" in incomplete and not any(b.startswith("pipeline:") for b in blockers):
|
||||
blockers.append(
|
||||
"pipeline: review incomplete — a pipeline stage never completed "
|
||||
"(no fault text recorded)"
|
||||
)
|
||||
|
||||
verdict = "BLOCKED" if blockers else "PASS"
|
||||
verdict = "BLOCKED" if blockers or incomplete else "PASS"
|
||||
|
||||
incomplete_lines = []
|
||||
for name in sorted(incomplete):
|
||||
if name == "pipeline":
|
||||
incomplete_lines.append(f"- pipeline: {signals_error}")
|
||||
elif n := int(lane_attempts.get(name) or 0):
|
||||
incomplete_lines.append(
|
||||
f"- pipeline: {signals_error}"
|
||||
if pipeline_fault
|
||||
else "- pipeline: review incomplete (no fault text recorded)"
|
||||
)
|
||||
elif n := attempt_count(name):
|
||||
incomplete_lines.append(f"- {name}: no completed review after {n} attempt(s)")
|
||||
else:
|
||||
incomplete_lines.append(f"- {name}: no completed review")
|
||||
@@ -269,7 +356,8 @@ def main():
|
||||
)
|
||||
|
||||
md = "\n".join(
|
||||
["| lane | status | detail |", "|------|--------|--------|"] + lane_lines
|
||||
["| lane | status | detail |", "|------|--------|--------|"]
|
||||
+ [f"| {lane} | {status} | {detail} |" for lane, (status, detail) in rows.items()]
|
||||
)
|
||||
md += "\n\n## Lane selection\n" + (state.get("lanes_summary") or "(none recorded)")
|
||||
if blockers:
|
||||
|
||||
Reference in new issue
Block a user