Jaycee766's picture
Deploy FrontierLab v0.35 source f7b67b6a3b09
a0e9e53 verified
Raw History Blame Contribute Delete
11.7 kB
"""Source-bound method comparison; researcher assertions are not execution evidence."""
from __future__ import annotations
from copy import deepcopy
from datetime import datetime, timezone
import hashlib
import json
from .models import Run
from .interventions import record_human_event
from .review import approved_claims, review_packet, source_fingerprint
CHECKS = ("data", "code", "environment", "parameters", "protocol", "metrics")
CHECK_STATES = ("unknown", "documented", "missing", "not_applicable")
CRITERIA = ("expected_value", "resources", "validation", "limitations", "novelty")
SCHEMA = "frontierlab.method-evaluation.v1"
def _digest(value):
return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False,
separators=(",", ":"), allow_nan=False).encode()).hexdigest()
def allowed_sources(run: Run) -> list[str]:
if not run.hypotheses:
raise ValueError("Generate hypotheses before evaluating methods")
if run.mode == "demo":
return sorted(p.paper_id for p in run.papers if p.synthetic)
if run.mode != "live":
raise ValueError("Unsupported research mode")
if not run.review or run.review.get("dataset_id") != review_packet(run)["dataset_id"]:
raise PermissionError("Current source-bound human review required")
ids = sorted({c.paper_id for c in approved_claims(run)})
if run.review.get("status") != "READY_FOR_HUMAN_GATED_SYNTHESIS" or len(ids) < 2:
raise PermissionError("Two human-supported sources and complete review required")
if any(p.synthetic for p in run.papers):
raise PermissionError("Synthetic publications cannot enter a live method evaluation")
return ids
def context_id(run: Run) -> str:
allowed_sources(run)
return _digest({
"question": run.question, "mode": run.mode, "model": run.model, "backend": run.backend,
"sources": [source_fingerprint(p) for p in run.papers],
"claims": [c.__dict__ for c in run.audit.verified_claims],
"conflicts": run.audit.conflicts, "hypotheses": run.hypotheses,
"human_review": run.review,
})
def _text(value, label, minimum=1):
if not isinstance(value, str) or not minimum <= len(value.strip()) <= 3000:
raise ValueError(f"{label} requires {minimum}..3000 characters")
return value.strip()
def blank_candidate(candidate_id: str, name: str = "") -> dict:
return {
"candidate_id": candidate_id, "name": name, "evidence_ids": [],
"scope": "",
"checks": {key: {"status": "unknown", "detail": "", "reference": ""}
for key in CHECKS},
"comparison": {key: "" for key in CRITERIA},
}
def _candidate(raw, known):
if not isinstance(raw, dict):
raise ValueError("Candidate must be an object")
cid = _text(raw.get("candidate_id"), "Candidate ID")
name = _text(raw.get("name"), "Method name")
ids = raw.get("evidence_ids")
if (not isinstance(ids, list) or not ids or
any(not isinstance(x, str) or x not in known for x in ids)):
raise ValueError("Method references must use reviewed source IDs")
scope = _text(raw.get("scope"), "Applicability and scope")
checks, gaps = {}, []
supplied = raw.get("checks", {})
if not isinstance(supplied, dict):
raise ValueError("Reproducibility checks must be an object")
for key in CHECKS:
item = supplied.get(key, {})
if not isinstance(item, dict) or item.get("status", "unknown") not in CHECK_STATES:
raise ValueError("Invalid reproducibility check status")
status = item.get("status", "unknown")
detail = item.get("detail", "")
reference = item.get("reference", "")
if not isinstance(detail, str) or not isinstance(reference, str):
raise ValueError("Check details and references must be text")
if status != "unknown":
detail = _text(detail, key + " explanation")
if status == "documented":
reference = _text(reference, key + " location/reference")
checks[key] = {"status": status, "detail": detail.strip(), "reference": reference.strip()}
if status in ("unknown", "missing"):
gaps.append({"item": key, "status": status,
"reason": detail.strip() or "Not assessed; inspect original methods."})
comparison = raw.get("comparison", {})
if not isinstance(comparison, dict):
raise ValueError("Comparison criteria must be an object")
comparison = {key: _text(comparison.get(key), key) for key in CRITERIA}
return {
"candidate_id": cid, "name": name, "evidence_ids": sorted(set(ids)),
"scope": scope, "checks": checks, "comparison": comparison,
"reproducibility_status": "INCOMPLETE_DOCUMENTATION" if gaps else "DOCUMENTED_NOT_EXECUTED",
"open_checks": gaps,
}
def build_evaluation(run: Run, candidates: list[dict], *, selected_id: str | None,
rationale: str) -> dict:
known = allowed_sources(run)
if not isinstance(candidates, list) or not 0 <= len(candidates) <= 4:
raise ValueError("Provide zero to four candidate methods")
normalized = [_candidate(c, known) for c in candidates]
ids = [c["candidate_id"] for c in normalized]
if len(ids) != len(set(ids)):
raise ValueError("Duplicate candidate IDs")
rationale = _text(rationale, "Selection or deferral rationale", minimum=8)
if selected_id is not None and (not isinstance(selected_id, str) or selected_id not in ids):
raise ValueError("Select a known method or defer")
if selected_id is not None and len(normalized) < 2:
raise ValueError("At least two candidates required for a comparative selection; otherwise defer")
status = "READY_FOR_DRAFT_PLANNING" if selected_id is not None else "DEFERRED_INSUFFICIENT_COMPARISON"
result = {
"schema": SCHEMA, "context_id": context_id(run), "status": status,
"origin": "SYNTHETIC_FIXTURE" if run.mode == "demo" else "RESEARCHER_ASSESSED",
"candidates": normalized, "selected_candidate_id": selected_id, "rationale": rationale,
"uncertainty": {
"source_count": len(known),
"machine_flagged_tensions": deepcopy(run.audit.conflicts),
"source_scope": "Abstract-level claim review; method records are researcher assertions.",
"method_references_independently_checked": False,
"confidence_calibrated": False,
"scientific_execution_verified": False,
"note": "Selection is a reasoned draft decision, not a probability of success or proven superiority.",
},
}
result["evaluation_id"] = _digest(result)
return result
def save_evaluation(run: Run, candidates: list[dict], *, selected_id: str | None,
rationale: str) -> dict:
result = build_evaluation(run, candidates, selected_id=selected_id, rationale=rationale)
run.evaluation = result
run.plan = None
run.log.append({"time_utc": datetime.now(timezone.utc).isoformat(),
"role": "Method Evaluation", "status": result["status"],
"note": "Researcher-saved method comparison; evaluation_id=" + result["evaluation_id"]})
record_human_event(run, "METHOD_EVALUATION_SAVED", result["evaluation_id"], note=result["status"])
return result
def require_evaluation(run: Run) -> dict:
saved = run.evaluation
if not isinstance(saved, dict) or saved.get("schema") != SCHEMA:
raise PermissionError("Complete method evaluation before planning")
rebuilt = build_evaluation(run, saved.get("candidates"),
selected_id=saved.get("selected_candidate_id"),
rationale=saved.get("rationale"))
if rebuilt != saved:
raise PermissionError("Method evaluation is stale or changed; review it again")
if saved["status"] != "READY_FOR_DRAFT_PLANNING":
raise PermissionError("Method comparison was deferred; planning remains blocked")
return saved
def selected_method(evaluation: dict) -> dict:
return next(c for c in evaluation["candidates"]
if c["candidate_id"] == evaluation["selected_candidate_id"])
def suggest_methods(run: Run, *, model_client) -> list[dict]:
"""One optional model call; output remains an editable, unreviewed proposal."""
known = allowed_sources(run)
if run.mode != "live" or model_client is None:
raise PermissionError("Method suggestions require a live, connected model")
if run.backend not in ("none", "unspecified") and getattr(model_client, "backend", "unspecified") != run.backend:
raise PermissionError("Use this run's original model backend for method suggestions")
response = model_client.json("Method Evaluation Agent", (
'Propose up to two alternative research methods relevant to the hypotheses. '
'Return {"candidates":[{"name":string,"evidence_ids":[provided IDs],"scope":string,'
'"comparison":{"expected_value":string,"resources":string,"validation":string,'
'"limitations":string,"novelty":string}}]}. '
'Use only provided abstracts and reviewed claims. Say UNKNOWN when details, '
'novelty or resources cannot be inferred. Do not claim code was executed or '
'that structure confidence establishes binding, pH selectivity or agonism. '
'Do not treat complementary pipeline tools as competing methods. '
'Return an empty candidates list if no meaningful alternatives are supported.'
), {"question": run.question, "hypotheses": run.hypotheses,
"papers": [p.__dict__ for p in run.papers if p.paper_id in known],
"reviewed_claims": [c.__dict__ for c in approved_claims(run)]})
raw = response.get("candidates")
if not isinstance(raw, list) or len(raw) > 2:
raise ValueError("Expected zero to two method proposals")
out = []
for i, item in enumerate(raw, 1):
if not isinstance(item, dict):
raise ValueError("Invalid method proposal")
candidate = blank_candidate(f"M{i}", item.get("name", ""))
candidate.update({key: item.get(key) for key in ("evidence_ids", "scope", "comparison")})
checked = _candidate(candidate, known)
out.append({key: checked[key] for key in candidate})
return out
def demo_candidates() -> list[dict]:
"""Invented strategies; no real experiment or method comparison was run."""
out = []
for cid, name, scope, benefit in [
("M1", "[FICTIONAL] Matched-condition replication",
"Compare the same invented binder under matched buffer and assay conditions.",
"Could separate a buffer confound from an apparent pH effect."),
("M2", "[FICTIONAL] Cross-assay confirmation",
"Compare the invented observation across independent assay formats.",
"Could expose assay-format dependence before interpreting the pH response."),
]:
c = blank_candidate(cid, name)
c.update(evidence_ids=["SYNTHETIC:A", "SYNTHETIC:B", "SYNTHETIC:C"], scope=scope)
c["comparison"] = {
"expected_value": benefit,
"resources": "Unknown; obtain protocol, materials and access estimates.",
"validation": "Require independent controls and repeats; no experiment has run.",
"limitations": "Entirely fictional abstracts; actual methods and effect sizes unavailable.",
"novelty": "Not assessed; these are illustrative validation strategies.",
}
out.append(c)
return out