"""Strict parser for the joint output contract (spec ยง5.1).
Never raises: any malformed input yields `ParsedOutput(format_ok=False, ...)`
with `parse_errors` populated. This is required by the Phase 3 gate (fuzz-tested
on >=100 malformed inputs; must never crash) since parse failure feeds the R5
reward guard at training/eval time, not a Python exception.
"""
from __future__ import annotations
import re
from src.contracts.schema import ParsedOutput, TraceStep
_ANALYSIS_RE = re.compile(r"(.*?)", re.DOTALL)
_LABEL_RE = re.compile(r"", re.DOTALL)
_CWE_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL)
_STATEMENTS_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL)
_TRACE_BLOCK_RE = re.compile(r"(.*?)", re.DOTALL)
_STEP_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL)
_EXPLANATION_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL)
_STEP_LINE_RE = re.compile(
r"^\s*line\s+(\d+)\s*:?\s*(.*?)\s*(?:\[\s*(.*?)\s*\])?\s*$", re.IGNORECASE
)
_VALID_LABELS = {"VULNERABLE", "SAFE"}
_CWE_VALUE_RE = re.compile(r"^(CWE-\d+|NONE)$", re.IGNORECASE)
_RANGE_RE = re.compile(r"^\s*(\d+)\s*(?:-\s*(\d+))?\s*$")
def parse_statement_lines(text: str) -> tuple[set[int], list[str]]:
"""Parses 'comma-separated line ranges, e.g. 12-14, 27' -> a set of line numbers."""
lines: set[int] = set()
errors: list[str] = []
text = (text or "").strip()
if not text:
return lines, errors
for chunk in text.split(","):
chunk = chunk.strip()
if not chunk:
continue
m = _RANGE_RE.match(chunk)
if not m:
errors.append(f"unparseable statement range token: {chunk!r}")
continue
start = int(m.group(1))
end = int(m.group(2)) if m.group(2) else start
if end < start:
errors.append(f"inverted range: {chunk!r}")
continue
if end - start > 10_000:
errors.append(f"implausibly large range (rejected): {chunk!r}")
continue
lines.update(range(start, end + 1))
return lines, errors
def _parse_trace(trace_block: str) -> tuple[list[TraceStep], list[str]]:
steps: list[TraceStep] = []
errors: list[str] = []
for step_text in _STEP_RE.findall(trace_block):
m = _STEP_LINE_RE.match(step_text)
if not m:
errors.append(f"unparseable trace step: {step_text!r}")
continue
line_no, code, tag = m.group(1), m.group(2) or "", m.group(3) or ""
try:
steps.append(TraceStep(line=int(line_no), code=code, tag=tag))
except (TypeError, ValueError):
errors.append(f"non-integer line in trace step: {step_text!r}")
return steps, errors
def parse_output(raw: str, contract_version: int = 1) -> ParsedOutput:
"""Parses one model completion against the joint output contract.
Returns format_ok=False (never raises) on any structural violation:
missing block, missing/invalid