fix(TASK-018): harden verdict_gate fault routing and widen default_lanes fallback

Adversary finding 1: verdict_gate treated any lane report CONTAINING
'PIPELINE-FAULT:' as a lane fault, so a real review quoting the marker
silently bypassed red-finding counting and sentinel parsing. Lane-fault
detection is now precise — only a report that IS the lane_fault.py
marker (starts with it) routes to the fault rule. Adds the missing
regression tests: reds block despite MERGE-READY, NEEDS-HUMAN without
reds passes with attention, probe INCONCLUSIVE blocks with the
environment note, a gate crash fails closed, and a quoting report is
routed to the red rule, not the fault rule.

Adversary finding 2: default_lanes overwrote caller-forced lanes and
dropped the security lane the deterministic signals guaranteed. The
fallback now unions its defaults with caller-forced lanes (canonicalized
via the same aliases build_items uses) and forces security on
auth/deps/exec signals or hardened posture — selection degrades wider,
never narrower, and stays deterministic via a fixed lane order. Tests
cover forced-lane survival, alias canonicalization, unknown-name
tolerance, each security signal, and determinism.
This commit is contained in:
Dark-Alex-17 committed 2026-09-14 10:50:33 -06:00
1 parent 80c20afcdd
commit ebb9e9af62
4 files changed
+246 -14

No files matched your search

@@ -3,10 +3,13 @@
Reached ONLY via select_lanes' `fallback:` route, when the additive LLM
judgment dies after retries. Emits `forced_lanes` — code-review + adversary,
plus probe when the deterministic signals flagged a consumer-facing
surface — which build_items honors exactly, so selection degrades WIDER,
never narrower. The degradation note goes into `lanes_degraded`; build_items
folds it into lanes_summary so the final report names the degradation.
plus probe when the deterministic signals flagged a consumer-facing surface,
plus security when the attack-surface signals (auth/deps/exec) or a hardened
posture demand it, unioned with any lanes the caller forced (canonicalized
with the same aliases build_items uses) — which build_items honors exactly,
so selection degrades WIDER, never narrower. The degradation note goes into
`lanes_degraded`; build_items folds it into lanes_summary so the final
report names the degradation.
"""
import json
@@ -14,6 +17,27 @@ import os
MAX_DETAIL_CHARS = 500
# Keep in sync with build_items.ALIASES — a caller-forced lane must
# canonicalize the same way here as it would on the non-degraded path.
ALIASES = {
"code-review": "code-review",
"code_review": "code-review",
"code-reviewer": "code-review",
"adversary": "adversary",
"adversarial": "adversary",
"adversarial-review": "adversary",
"security": "security",
"security-review": "security",
"security-reviewer": "security",
"probe": "probe",
"usage": "probe",
"usage-pattern": "probe",
"usage-pattern-testing": "probe",
}
# Stable emission order keeps the fallback deterministic.
LANE_ORDER = ["code-review", "adversary", "security", "probe"]
def load_state():
if path := os.environ.get("GRAPH_STATE_FILE"):
@@ -24,9 +48,24 @@ def load_state():
def main():
state = load_state()
forced = ["code-review", "adversary"]
lanes = {"code-review", "adversary"}
if state.get("consumer_surface"):
forced.append("probe")
lanes.add("probe")
# The deterministic security signals are already in state when this
# fallback runs; a degraded selection must never drop the security lane
# build_items' hard rules would have selected.
if (
state.get("touches_auth")
or state.get("touches_deps")
or state.get("touches_exec")
or state.get("security_posture") == "hardened"
):
lanes.add("security")
# Caller-forced lanes survive degradation — union, never overwrite.
for lane in state.get("forced_lanes") or []:
if canon := ALIASES.get(str(lane).strip().lower()):
lanes.add(canon)
forced = [lane for lane in LANE_ORDER if lane in lanes]
raw = state.get("select_lanes_failure")
detail = raw.strip().replace("\n", " ") if isinstance(raw, str) else ""
if len(detail) > MAX_DETAIL_CHARS: