Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
A comprehensive collection of Agent Skills for context engineering, multi-agent architectures, and production agent systems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
researcher/scripts/validate_run.py
1#!/usr/bin/env python32"""Validate publish readiness for a single research run.34This complements validate_repo.py. Repo validation answers whether the corpus is5structurally healthy; run validation answers whether one run is ready to produce6reviewable corpus changes.7"""89from __future__ import annotations1011import argparse12import json13import re14import sys15from dataclasses import asdict, dataclass16from pathlib import Path17from typing import Any181920ROOT = Path(__file__).resolve().parents[2]21VALID_STATES = {22"initialized",23"retrieved",24"evaluated",25"proposed",26"novelty_checked",27"validated",28"pr_ready",29"closed",30}31VALID_CLOSE_STATUS = {"accepted", "rejected", "reference-only", "abandoned"}32PLACEHOLDER_PATTERNS = [33r"\[Short Title\]",34r"Describe the implementable mechanism",35r"State one of:",36r'path: ""',37r'section: ""',38r'summary: ""',39r"Verdict: pending",40]414243@dataclass44class Finding:45severity: str46path: str47message: str484950class RunValidator:51def __init__(self, run_dir: Path) -> None:52self.run_dir = run_dir.resolve()53self.root = ROOT.resolve()54self.findings: list[Finding] = []5556def rel(self, path: Path | str) -> str:57p = Path(path)58try:59return str(p.relative_to(self.root))60except ValueError:61return str(p)6263def error(self, path: Path | str, message: str) -> None:64self.findings.append(Finding("error", self.rel(path), message))6566def warn(self, path: Path | str, message: str) -> None:67self.findings.append(Finding("warning", self.rel(path), message))6869def run(self) -> dict[str, Any]:70if not self.run_dir.exists():71self.error(self.run_dir, "run directory missing")72else:73state_data = self.validate_state()74current_state = (state_data or {}).get("current_state")75if current_state == "closed":76# Closed runs are terminal. validate_run only enforces publish77# readiness for active runs; closed runs are validated by their78# own closure.json plus repo-level checks.79self.validate_closure()80else:81self.validate_queue()82source_status = self.validate_source_evaluation()83self.validate_proposal(source_status)84self.validate_novelty()85self.validate_pr_readiness()86self.validate_closure()8788errors = sum(1 for finding in self.findings if finding.severity == "error")89warnings = sum(1 for finding in self.findings if finding.severity == "warning")90return {91"ok": errors == 0,92"run_dir": self.rel(self.run_dir),93"summary": {"errors": errors, "warnings": warnings},94"findings": [asdict(finding) for finding in self.findings],95}9697def validate_state(self) -> dict[str, Any] | None:98path = self.run_dir / "run-state.json"99data = self.load_json(path)100if not isinstance(data, dict):101return None102current = data.get("current_state")103if current not in VALID_STATES:104self.error(path, f"current_state must be one of {sorted(VALID_STATES)}")105if not isinstance(data.get("state_history"), list) or not data["state_history"]:106self.error(path, "state_history must be a non-empty list")107locked = data.get("locked_surfaces", [])108for required in [109"researcher/rubrics/content-curation.md",110"researcher/mechanisms/registry.jsonl",111".claude-plugin/marketplace.json",112".plugin/plugin.json",113]:114if required not in locked:115self.error(path, f"locked surface missing: {required}")116return data117118def validate_queue(self) -> None:119queue = self.run_dir / "sources" / "queue.jsonl"120if not queue.exists():121self.error(queue, "source queue missing")122return123for line_number, line in enumerate(queue.read_text(encoding="utf-8").splitlines(), start=1):124if not line.strip():125continue126try:127record = json.loads(line)128except json.JSONDecodeError as exc:129self.error(queue, f"line {line_number} invalid JSONL: {exc}")130continue131if not record.get("id") or not record.get("title"):132self.error(queue, f"line {line_number} must include id and title")133134def validate_source_evaluation(self) -> str | None:135eval_dir = self.run_dir / "sources" / "evaluations"136if not eval_dir.exists():137self.error(eval_dir, "source evaluations directory missing")138return None139completed = sorted(path for path in eval_dir.glob("*.json") if "draft" not in path.stem)140drafts = sorted(eval_dir.glob("*draft*.json"))141if drafts and not completed:142self.error(eval_dir, "run has only draft source evaluations")143return None144if not completed:145self.error(eval_dir, "completed source evaluation missing")146return None147148data = self.load_json(completed[0])149if not isinstance(data, dict):150return None151status = data.get("source", {}).get("retrieval_status")152if status != "retrieved":153self.error(completed[0], "publish-ready run requires retrieved source evaluation")154if data.get("decision", {}).get("verdict") == "REJECT":155self.error(completed[0], "rejected source cannot produce publish-ready changes")156return str(status) if status else None157158def validate_proposal(self, source_status: str | None) -> None:159path = self.run_dir / "proposals" / "skill-proposal.md"160if not path.exists():161self.error(path, "skill proposal missing")162return163text = path.read_text(encoding="utf-8")164for pattern in PLACEHOLDER_PATTERNS:165if re.search(pattern, text):166self.error(path, f"proposal still contains placeholder: {pattern}")167required_prefixes = [168"- URL:",169"- Title:",170"- Author or organization:",171"- Source type:",172"- Retrieval status:",173"- Evaluation file:",174"- Decision:",175"- Target path:",176"- Activation scenario:",177"- Verdict:",178"- Max mechanism overlap:",179"- Top mechanism overlaps:",180"- Evidence limitations:",181"- Possible duplication:",182"- Required human review:",183]184for prefix in required_prefixes:185if re.search(rf"^{re.escape(prefix)}\s*$", text, flags=re.MULTILINE):186self.error(path, f"proposal field is blank: {prefix}")187has_evidence_row = False188for line in text.splitlines()[30:]:189if not line.startswith("|") or line.count("|") < 3:190continue191cells = [cell.strip() for cell in line.strip("|").split("|")]192if not cells or cells[0] in {"Claim", "---"} or set("".join(cells)) <= {"-", " "}:193continue194if any(cells):195has_evidence_row = True196break197if source_status != "retrieved" and has_evidence_row:198self.error(path, "proposal cites evidence from a source that is not fully retrieved")199200def validate_novelty(self) -> None:201path = self.run_dir / "reports" / "novelty-result.json"202data = self.load_json(path)203if not isinstance(data, dict):204return205verdict = data.get("verdict")206if verdict not in {"pass", "human_review", "likely_duplicate"}:207self.error(path, "novelty verdict is invalid")208if verdict != "pass" and not data.get("human_review_rationale"):209self.error(path, "non-pass novelty verdict requires human_review_rationale")210211def validate_pr_readiness(self) -> None:212path = self.run_dir / "reports" / "pr-readiness.md"213if not path.exists():214self.error(path, "PR readiness notes missing")215return216text = path.read_text(encoding="utf-8")217for required in ["## Summary", "## Test Plan", "## Risks", "human approval"]:218if required not in text:219self.error(path, f"PR readiness notes missing {required}")220221def validate_closure(self) -> None:222path = self.run_dir / "reports" / "closure.json"223if not path.exists():224return225data = self.load_json(path)226if not isinstance(data, dict):227return228if data.get("status") not in VALID_CLOSE_STATUS:229self.error(path, f"closure status must be one of {sorted(VALID_CLOSE_STATUS)}")230if not data.get("reason"):231self.error(path, "closure reason is required")232233def load_json(self, path: Path) -> Any:234if not path.exists():235self.error(path, "JSON file missing")236return None237try:238return json.loads(path.read_text(encoding="utf-8"))239except json.JSONDecodeError as exc:240self.error(path, f"invalid JSON: {exc}")241return None242243244def main() -> int:245parser = argparse.ArgumentParser(description="Validate publish readiness for a research run")246parser.add_argument("--run-dir", type=Path, required=True)247parser.add_argument("--json", action="store_true")248args = parser.parse_args()249250result = RunValidator(args.run_dir).run()251if args.json:252print(json.dumps(result, indent=2))253else:254summary = result["summary"]255print(256f"Run validation {'passed' if result['ok'] else 'failed'}: "257f"{summary['errors']} errors, {summary['warnings']} warnings"258)259for finding in result["findings"]:260print(f"[{finding['severity']}] {finding['path']}: {finding['message']}")261return 0 if result["ok"] else 1262263264if __name__ == "__main__":265sys.exit(main())266