Files
rune/scripts/fake_worker.py
T
Nick a553fb1363 feat(research): adversarial-gated refinement + narrowed QUALIFY (pipeline correction 2026-09-09)
ADVERSARIAL owns epistemic refinement. QUALIFY owns implementation-readiness.

- ADVERSARIAL_DISPOSITIONS enum: SURVIVES / SURVIVES_WITH_MITIGATIONS /
  EVIDENCE_DEFICIENT / APPLICABILITY_DEFICIENT / INNOVATION_WARRANTED /
  NO_VIABLE_CANDIDATE. Only SURVIVES* unlock SYNTHESIS; every other
  disposition routes targeted refinement to the earliest repairable stage
  (EVIDENCE_DEFICIENT/NO_VIABLE_CANDIDATE → OUTWARD,
  APPLICABILITY_DEFICIENT → APPLICABILITY, INNOVATION_WARRANTED → INNOVATION)
- advance() REQUIRES a valid adversarial disposition — an adversarial pass
  without a disposition cannot unlock anything
- synthesis_readiness() deterministic invariant (§7): disposition unlocks AND
  ≥1 surviving candidate AND mitigations represented (for
  SURVIVES_WITH_MITIGATIONS) AND blocking unknowns = 0
- Falsification tests + required mitigations are first-class capsule fields —
  they reach SYNTHESIS as mandatory inputs, not optional prose (§5)
- QUALIFY narrowed (§8–10): outcomes PROCEED / REFINE_SYNTHESIS (routes to
  SYNTHESIS only) / REOPEN_EARLIER_STAGE (exceptional; requires reason,
  return_to, new_blocking_fact, why_previous_gate_could_not_reasonably_
  have_caught_it). Generic MORE_RESEARCH removed as a QUALIFY outcome.
- Fake worker + all chain tests updated to the disposition/outcome contract

Acceptance §18: 18/18 PASS (test_adversarial_gated_refinement.py).
148 tests pass, ruff clean.
2026-09-09 01:03:13 +01:00

221 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""fake_worker.py — deterministic fake MiMo for pipeline integration testing.
NOT the production research worker. This worker produces known, structured
artifacts for each stage so the pipeline plumbing (persistence, handoff,
gating, retry) can be tested in seconds instead of 20-minute real MiMo runs.
Canonical pipeline (RP2, handoff 2026-09-08):
ISSUE → INWARD → OUTWARD → APPLICABILITY
→ optional INNOVATION → ADVERSARIAL → SYNTHESIS → QUALIFY
Per the authoritative build prompt §18:
"DETERMINISTIC TESTS PROVE THE PIPE. REAL MIMO DOES THE RESEARCH."
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from research_task_tree import ResearchTaskTree, STAGES
def fake_issue_gate(tree: ResearchTaskTree) -> dict:
"""ISSUE is deterministic: the Gitea issue content is the DEFINE replacement.
The fake worker emits the compiled gate artifact directly."""
issue = tree.issue
return {
"goal": f"Capability from issue #{issue}",
"why": "recorded in issue body",
"constraints": ["must not regress existing behavior"],
"acceptance": ["issue acceptance criteria"],
"quality": "BUILDABLE",
}
def fake_inward(tree: ResearchTaskTree) -> dict:
# Assert ISSUE-gate artifact was actually supplied
prior = tree.get_prior_artifacts("INWARD")
issue_gate = prior.get("ISSUE", {})
assert issue_gate.get("goal"), "INWARD: ISSUE-gate artifact missing — evidence handoff broken"
return {
"claims": [
{"id": "I1", "claim": "memory_store uses FTS5", "evidence": ["src/rune/memory_store.py:86"], "status": "CURRENT"},
{"id": "I2", "claim": "shard routing is present", "evidence": ["src/rune/shard_manager.py:114"], "status": "CURRENT"},
],
}
def fake_outward(tree: ResearchTaskTree) -> dict:
"""Unified OUTWARD (RP3): one research task, discovery + reading as
SUBSECTIONS of one artifact — not separate stages."""
prior = tree.get_prior_artifacts("OUTWARD")
inward = prior.get("INWARD", {})
claims = inward.get("claims", [])
assert len(claims) >= 2, f"OUTWARD: expected ≥2 INWARD claims, got {len(claims)}"
return {
"discovery": {
"queries": ["fts5 hybrid search", "sqlite-vec alternatives"],
"tool_receipts": [{"tool": "webfetch", "query": "fts5 hybrid search", "status": "completed"}],
"sources": [
{"id": "O1", "url": "https://sqlite.org/fts5.html", "title": "FTS5", "source_class": "software"},
{"id": "O2", "url": "https://github.com/asg017/sqlite-vec", "title": "sqlite-vec", "source_class": "software"},
],
},
"reading": {
"evidence": [
{"id": "E1", "source_id": "O1", "url": "https://sqlite.org/fts5.html",
"claim": "FTS5 supports external content tables", "limitations": ["token porter only"]},
{"id": "E2", "source_id": "O2", "url": "https://github.com/asg017/sqlite-vec",
"claim": "vec0 virtual tables for embedding search", "limitations": []},
],
},
}
def fake_applicability(tree: ResearchTaskTree) -> dict:
"""APPLICABILITY (RP4): classify researched mechanisms against Rune's reality."""
prior = tree.get_prior_artifacts("APPLICABILITY")
outward = prior.get("OUTWARD", {})
sources = outward.get("discovery", {}).get("sources", [])
assert len(sources) >= 2, f"APPLICABILITY: expected ≥2 OUTWARD sources, got {len(sources)}"
return {
"route": "INNOVATION_WARRANTED",
"candidates": [
{
"id": "M1",
"source_ids": ["O1", "O2"],
"classification": "APPLICABLE_WITH_ADAPTATION",
"integration_point": "memory_store.recall",
"benefit": "hybrid recall",
"cost": "low",
"architectural_fit": "fits memory authority",
"authority_implications": "none",
"risks": ["extension availability"],
"required_adaptation": ["fallback when extension missing"],
"evidence": ["E1", "E2"],
},
],
}
def fake_adversarial(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("ADVERSARIAL")
outward = prior.get("OUTWARD", {})
evidence = outward.get("reading", {}).get("evidence", [])
assert len(evidence) >= 2, f"ADVERSARIAL: expected ≥2 evidence items, got {len(evidence)}"
return {
"disposition": "SURVIVES_WITH_MITIGATIONS",
"counter_evidence": [
{"id": "A1", "source_id": "O1", "claim": "FTS5 porter tokenizer may not handle all languages",
"derived_from": ["E1"], "limitations": ["minor"]},
],
"surviving_candidates": ["M1"],
"rejected_candidates": [],
"required_mitigations": ["mode labeling"],
"falsification_tests": ["FT1: harness run with embedding backend unreachable"],
"attacks": [
{"id": "A1", "target_candidate": "M1", "dimension": "failure modes",
"finding": "tokenizer coverage", "evidence": ["E1"], "severity": "minor"},
],
}
def fake_innovation(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("INNOVATION")
inward = prior.get("INWARD", {})
outward = prior.get("OUTWARD", {})
applicability = prior.get("APPLICABILITY", {})
inward_claims = [c["id"] for c in inward.get("claims", [])]
evidence_ids = [e["id"] for e in outward.get("reading", {}).get("evidence", [])]
applicable = [c["id"] for c in applicability.get("candidates", [])]
assert inward_claims, "INNOVATION: no INWARD claims received"
assert evidence_ids, "INNOVATION: no OUTWARD evidence received"
assert applicable, "INNOVATION: no APPLICABILITY candidates received"
return {
"conclusion": "ADOPT",
"candidates": [
{"id": "C1", "category": "ADAPT", "proposal": "hybrid FTS+vec recall",
"derived_from": inward_claims[:1] + evidence_ids[:1] + applicable[:1],
"composition_of": [], "novelty": "adapted",
"expected_gain": "better recall", "risks": ["latency"],
"kill_test": "recall returns >0 rows"},
],
}
def fake_synthesis(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("SYNTHESIS")
innovation = prior.get("INNOVATION", {})
candidates = innovation.get("candidates", [])
assert candidates, "SYNTHESIS: no INNOVATION candidates received"
return {
"selected_design": "hybrid FTS+vec recall",
"rejected_alternatives": [{"design": "pure FTS", "reason": "E2 shows vec adds value"}],
"evidence_used": ["I1", "E1", "E2", "A1", "M1", "C1"],
"architecture_implications": ["memory_store.recall modified"],
"files_affected": ["src/rune/memory_store.py"],
"contracts": ["recall returns thresholded rows"],
"tests_required": ["test_recall_hybrid"],
"failure_modes": ["sqlite-vec not installed"],
"kill_conditions": ["recall returns 0 rows on seeded store"],
"build_instructions": ["modify recall() in memory_store.py", "add tests"],
}
def fake_qualify(tree: ResearchTaskTree) -> dict:
prior = tree.get_prior_artifacts("QUALIFY")
synthesis = prior.get("SYNTHESIS", {})
assert synthesis.get("selected_design"), "QUALIFY: no synthesis received"
# First QUALIFY pass: build instructions incomplete → REFINE_SYNTHESIS
# (narrowed semantics: synthesis-only refinement, no research repeat).
# Record the refine round in the tree so the workflow records a 2nd round.
if tree.state.get("qualify_outcome") != "REFINE_SYNTHESIS":
return {
"outcome": "REFINE_SYNTHESIS",
"reason": "rollback/recovery not specified",
"build_instructions": synthesis.get("build_instructions", []),
}
# After synthesis refinement: PROCEED
return {
"outcome": "PROCEED",
"selected_design": synthesis.get("selected_design", ""),
"evidence_used": synthesis.get("evidence_used", []),
}
FAKE_WORKERS = {
"ISSUE": fake_issue_gate,
"INWARD": fake_inward,
"OUTWARD": fake_outward,
"APPLICABILITY": fake_applicability,
"ADVERSARIAL": fake_adversarial,
"INNOVATION": fake_innovation,
"SYNTHESIS": fake_synthesis,
"QUALIFY": fake_qualify,
}
def run_fake_stage(tree: ResearchTaskTree) -> dict:
stage = tree.current_stage
worker = FAKE_WORKERS.get(stage)
if not worker:
raise ValueError(f"no fake worker for stage {stage}")
return worker(tree)
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Fake stage worker for pipeline testing")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--round", type=int, default=1)
parser.add_argument("--artifact-dir", required=True)
parser.add_argument("--out-dir", required=True)
args = parser.parse_args(argv[1:])
tree = ResearchTaskTree(issue=args.issue, round_no=args.round, artifact_dir=args.artifact_dir)
if tree.is_complete:
print("research tree already complete")
return 0
artifact = run_fake_stage(tree)
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
stage = tree.current_stage
(out_dir / f"stage_{stage}.json").write_text(json.dumps(artifact, indent=2))
print(f"fake stage {stage} complete")
return 0
if __name__ == "__main__":
raise SystemExit(main_with(sys.argv))