Files
rune/scripts/research_harness_invariants.py
T
Nick 1ecaa76e4a
Auto-version and Release / release (push) Successful in 13s
CI Pipeline / lint (push) Failing after 32s
CI Pipeline / go-surface (push) Failing after 21s
CI Pipeline / canary (push) Failing after 1m16s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / rust-surface (push) Successful in 4m36s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / type-check (push) Failing after 36s
CI Pipeline / unit-tests (push) Failing after 8m44s
deploy / deploy (push) Failing after 15m16s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Failing after 9s
feat(research): harness invariants — the R1 repair classes as deterministic checks (#411, contract BUILDER-2026-10-08T18:49:19Z-7236d8)
- scripts/research_harness_invariants.py: 6 invariants (I1 schema-shape,
  I2 outward-evidence, I3 innovation-ground, I4 synthesis-qualify,
  I5 ledger-validity, I6 no-phantom-paths) + one-call check() every
  harness entry point can run + CLI.
- tests/test_research_harness_invariants.py 7/7: each fixture IS a repair
  family (the bad shapes that caused the R1 repairs), pinned as regression.
- Language verdict (dichotomy law): Python — deterministic validation over
  in-process harness artifacts (batch/tooling, not runtime backend state).
2026-10-08 19:50:11 +01:00

146 lines
5.4 KiB
Python

"""research_harness_invariants.py — #411: the R1 repair classes as
deterministic invariants.
The repeated repair families (#396-#403 era) showed strong error DETECTION
but insufficient error PREVENTION. This module converts each recurring
repair class into a checkable invariant every harness entry point can run
in one call, plus a fixture corpus pinning the classes as regression tests.
Invariant classes (each maps to a repair family):
I1 SCHEMA-SHAPE — artifact has the required round sections
I2 OUTWARD-EVIDENCE — OUTWARD carries >= 2 live-verified URLs w/ lessons
I3 INNOVATION-GROUND — INNOVATION candidates reference OUTWARD sources
I4 SYNTHESIS-QUALIFY — SYNTHESIS has evidence-sufficiency + kill test
I5 LEDGER-VALIDITY — evidence-ledger lines are JSON with required keys
I6 NO-PHANTOM-PATHS — every referenced artifact path exists on disk
Language verdict (dichotomy law): deterministic validation over research
TOOLING artifacts — batch/reactive tooling, not runtime backend state —
Python placement; a Rust core would parse the same JSON the harness
already holds in-process. Recorded for the spine.
python3 scripts/research_harness_invariants.py <artifact.json> [--ledger FILE]
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REQUIRED_SECTIONS = ("round", "outward", "innovation", "synthesis")
REQUIRED_LEDGER_KEYS = ("ts", "kind", "ref")
def check(artifact: dict, ledger_path: Path | None = None) -> list[str]:
"""Run every invariant. Returns findings (empty == clean)."""
f: list[str] = []
f += _i1_schema_shape(artifact)
f += _i2_outward_evidence(artifact)
f += _i3_innovation_ground(artifact)
f += _i4_synthesis_qualify(artifact)
if ledger_path is not None:
f += _i5_ledger_validity(ledger_path)
f += _i6_no_phantom_paths(artifact)
return f
def _i1_schema_shape(a: dict) -> list[str]:
missing = [s for s in REQUIRED_SECTIONS if s not in a]
return [f"I1 SCHEMA-SHAPE: missing section(s) {missing}"] if missing else []
def _i2_outward_evidence(a: dict) -> list[str]:
outward = a.get("outward") or []
if not isinstance(outward, list):
return ["I2 OUTWARD-EVIDENCE: outward is not a list"]
live = [
s
for s in outward
if isinstance(s, dict)
and str(s.get("url", "")).startswith(("http://", "https://"))
and str(s.get("lesson", "")).strip()
]
if len(live) < 2:
return [f"I2 OUTWARD-EVIDENCE: only {len(live)} live-verified source(s) with lesson annotations (need >= 2)"]
return []
def _i3_innovation_ground(a: dict) -> list[str]:
innovation = a.get("innovation") or {}
candidates = innovation.get("candidates") if isinstance(innovation, dict) else None
if not candidates:
return [] # empty INNOVATION is legal on skipped rounds
outward_urls = {str(s.get("url", "")).strip() for s in (a.get("outward") or []) if isinstance(s, dict)}
ungrounded = [
c
for c in candidates
if isinstance(c, dict)
and not (str(c.get("basis", "")).strip() and any(u and u in str(c.get("basis", "")) for u in outward_urls))
]
if ungrounded:
return [f"I3 INNOVATION-GROUND: {len(ungrounded)} candidate(s) without an OUTWARD basis reference"]
return []
def _i4_synthesis_qualify(a: dict) -> list[str]:
syn = a.get("synthesis") or {}
if not isinstance(syn, dict) or not syn:
return [] # empty SYNTHESIS is legal on skipped rounds
f = []
if not str(syn.get("evidence_sufficiency", "")).strip():
f.append("I4 SYNTHESIS-QUALIFY: no evidence-sufficiency statement")
if not str(syn.get("kill_test", "")).strip():
f.append("I4 SYNTHESIS-QUALIFY: no kill test (cheapest disproof)")
return f
def _i5_ledger_validity(ledger_path: Path) -> list[str]:
if not ledger_path.exists():
# a not-yet-created ledger is the NORMAL early-round state — no
# findings (a ledger passed by path that should exist is the
# caller's contract, not the artifact's)
return []
f = []
for i, line in enumerate(ledger_path.read_text().splitlines()):
if not line.strip():
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
f.append(f"I5 LEDGER-VALIDITY: line {i + 1} is not JSON")
continue
missing = [k for k in REQUIRED_LEDGER_KEYS if k not in entry]
if missing:
f.append(f"I5 LEDGER-VALIDITY: line {i + 1} missing {missing}")
return f
def _i6_no_phantom_paths(a: dict) -> list[str]:
paths = a.get("artifacts") or []
if not isinstance(paths, list):
return []
phantom = [p for p in paths if isinstance(p, str) and not Path(p).exists()]
if phantom:
return [f"I6 NO-PHANTOM-PATHS: {len(phantom)} referenced artifact(s) missing on disk: {phantom[:3]}"]
return []
def main() -> int:
import argparse
ap = argparse.ArgumentParser(description="#411: run the harness invariants on a research artifact")
ap.add_argument("artifact")
ap.add_argument("--ledger")
args = ap.parse_args()
a = json.loads(Path(args.artifact).read_text())
findings = check(a, Path(args.ledger) if args.ledger else None)
for x in findings:
print("FINDING:", x)
print("CLEAN" if not findings else f"{len(findings)} finding(s)")
return 0 if not findings else 1
if __name__ == "__main__":
raise SystemExit(main())