Rust test comments/assert messages and deep-research script docstrings cited numbered requirement labels (R3/R5) and task labels (TASK-016, TASK-021) that are defined only in an external plan document. Reword each to say what it means on its own: fail-safe crash guards, the validator's fallback-capture warning, task-queue anchors.
117 lines
3.7 KiB
Python
117 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Check that the sources cited in the research report are reachable.
|
|
|
|
Scans the final report for URLs and DOIs, probes each with a HEAD
|
|
request, and writes a `source_check` summary into state so the human
|
|
reviewer sees broken citations at the approval step.
|
|
|
|
Times out per request so a slow source cannot stall the graph.
|
|
|
|
Pipeline-notes fold: end-node templates render unconditionally, so the
|
|
conditional "## Pipeline notes" section of the accepted report is built
|
|
HERE — the last stop before the approval gate on every accepted path,
|
|
including reflexion and feedback re-loops. `pipeline_notes` is the
|
|
rendered section when `pipeline_faults` is non-empty and the empty
|
|
string otherwise, keeping the happy-path `{{report}}{{pipeline_notes}}`
|
|
output byte-identical to the pre-hardening `{{report}}`.
|
|
|
|
Fail-safe: per-URL probes were already guarded; the top level now
|
|
is too. On a crash the summary says sources were NOT checked, a
|
|
PIPELINE-FAULT is recorded (best-effort preserving existing faults), and
|
|
pipeline_notes still renders — the approval gate remains the backstop.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
DOI_RE = re.compile(r"\b(10\.\d{4,9}/[-._;()/:A-Z0-9]+)", re.IGNORECASE)
|
|
URL_RE = re.compile(r"https?://[^\s)\]\}\"'>]+")
|
|
|
|
|
|
def load_state():
|
|
path = os.environ.get("GRAPH_STATE_FILE")
|
|
if path:
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
|
|
|
|
|
def safe_faults():
|
|
try:
|
|
state = load_state()
|
|
return [f for f in (state.get("pipeline_faults") or []) if isinstance(f, str)]
|
|
except Exception: # noqa: BLE001 — best-effort preservation only
|
|
return []
|
|
|
|
|
|
def render_notes(faults):
|
|
entries = [f for f in (faults or []) if isinstance(f, str) and f.strip()]
|
|
if not entries:
|
|
return ""
|
|
return "\n\n## Pipeline notes\n\n" + "\n".join(f"- {e}" for e in entries)
|
|
|
|
|
|
def reachable(url, timeout=5.0):
|
|
req = urllib.request.Request(url, method="HEAD")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
return 200 <= resp.status < 400
|
|
except urllib.error.HTTPError as e:
|
|
return 200 <= e.code < 400
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def main():
|
|
state = load_state()
|
|
report = state.get("report") or ""
|
|
|
|
urls = sorted({u.rstrip(".,;)") for u in URL_RE.findall(report)})
|
|
dois = sorted(set(DOI_RE.findall(report)))
|
|
|
|
results = []
|
|
for url in urls:
|
|
ok = reachable(url)
|
|
results.append(f" {'OK' if ok else 'UNREACHABLE'} {url}")
|
|
for doi in dois:
|
|
url = f"https://doi.org/{doi}"
|
|
if url in urls:
|
|
continue
|
|
ok = reachable(url)
|
|
results.append(f" {'OK' if ok else 'UNREACHABLE'} DOI {doi} ({url})")
|
|
|
|
if not results:
|
|
summary = "No web sources were cited in the report."
|
|
else:
|
|
summary = (
|
|
f"Source reachability ({len(results)} checked):\n"
|
|
+ "\n".join(results)
|
|
)
|
|
|
|
notes = render_notes(state.get("pipeline_faults"))
|
|
print(json.dumps({"source_check": summary, "pipeline_notes": notes}))
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as e: # noqa: BLE001 — a crashed source check must degrade, not die
|
|
faults = safe_faults()
|
|
faults.append(
|
|
f"PIPELINE-FAULT: source verification crashed — {e}; cited sources "
|
|
"were not checked for reachability"
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"source_check": (
|
|
"Source verification crashed (pipeline fault) — cited "
|
|
"sources were NOT checked for reachability."
|
|
),
|
|
"pipeline_notes": render_notes(faults),
|
|
"pipeline_faults": faults,
|
|
}
|
|
)
|
|
)
|