homemaker-layout/src/homemaker/oracle.py

53 lines
1.7 KiB
Python
Raw Normal View History

"""Phase-1 fitness oracle: score a ``.dom`` via Urb's ``urb-fitness.pl``.
This is the only throwaway component. It shells out to the Perl evaluator so we
can validate the Python search core against the trusted fitness before porting
fitness to Python (Phase 2). ``urb-fitness.pl`` reads ``patterns.config`` from
its working directory, so the ``.dom`` must live beside the programme config.
"""
from __future__ import annotations
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
DEFAULT_URB_ROOT = Path("/home/bruno/src/urb")
@dataclass
class Score:
fitness: float
fails: str # raw .fails content (YAML and/or plain lines)
@property
def n_fails(self) -> int:
return sum(1 for line in self.fails.splitlines() if line.strip() and line.strip() != "---")
def score(dom_path: str | Path, urb_root: str | Path = DEFAULT_URB_ROOT) -> Score:
dom_path = Path(dom_path).resolve()
urb_root = Path(urb_root).resolve()
score_file = Path(f"{dom_path}.score")
fails_file = Path(f"{dom_path}.fails")
for f in (score_file, fails_file):
f.unlink(missing_ok=True)
env = {**os.environ, "DEBUG": "1"}
proc = subprocess.run(
["perl", f"-I{urb_root}/lib", str(urb_root / "bin" / "urb-fitness.pl"), dom_path.name],
cwd=dom_path.parent,
env=env,
capture_output=True,
text=True,
)
if not score_file.exists():
raise RuntimeError(
f"urb-fitness.pl produced no score for {dom_path}\n"
f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
)
fitness = float(score_file.read_text().strip())
fails = fails_file.read_text() if fails_file.exists() else ""
return Score(fitness=fitness, fails=fails)