Skip to content

Commit 9525b1c

Browse files
committed
feat(engine): oracle bridge — CDD card -> atlas oracle grade (fail-closed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dc66a44 commit 9525b1c

2 files changed

Lines changed: 303 additions & 0 deletions

File tree

prd_taskmaster/oracle_bridge.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Oracle bridge: map a CDD card to a Graded Card verdict via the atlas oracle CLI.
2+
3+
Fail-closed: any ambiguity (missing verdict, unparseable output, CLI crash) yields
4+
("FAIL", {...}) — never "PASS" and never an uncaught exception from the grading path.
5+
OracleCardError is the one intended raise, for a missing/unreadable/invalid card.
6+
"""
7+
from __future__ import annotations
8+
9+
import json
10+
import os
11+
import subprocess
12+
from pathlib import Path
13+
14+
15+
class OracleCardError(Exception):
16+
"""Raised when a CDD card cannot be graded (e.g. missing 'grading' block)."""
17+
18+
19+
def _oracle_cmd() -> list[str]:
20+
"""Configurable CLI invocation. Default: the 'atlas' binary on PATH.
21+
22+
Override with ATLAS_ORACLE_CMD (shell-split), e.g.:
23+
ATLAS_ORACLE_CMD="node /path/to/atlas-protocol/apps/cli/dist/index.js"
24+
"""
25+
raw = os.environ.get("ATLAS_ORACLE_CMD")
26+
if raw:
27+
import shlex
28+
return shlex.split(raw)
29+
return ["atlas"]
30+
31+
32+
def grade_card(
33+
*,
34+
card_path: str | Path,
35+
repo_path: str | Path,
36+
commit_sha: str,
37+
held_root: str | Path,
38+
evidence_dir: str | Path,
39+
ledger_dir: str | Path,
40+
oracle_cmd: list[str] | None = None,
41+
) -> tuple[str, dict]:
42+
"""Grade a submission via the atlas oracle CLI.
43+
44+
Returns (verdict, detail) where verdict is "PASS" or "FAIL".
45+
46+
FAIL-CLOSED: any error (missing verdict, unparseable output, CLI crash)
47+
yields ("FAIL", {...}) — never raises after the card has been validated.
48+
49+
Raises:
50+
OracleCardError: if the card file is missing, unreadable, or has no 'grading' block.
51+
"""
52+
card_path = Path(card_path)
53+
54+
try:
55+
card = json.loads(card_path.read_text())
56+
except (OSError, json.JSONDecodeError) as exc:
57+
raise OracleCardError(f"cannot read card {card_path}: {exc}") from exc
58+
59+
if "grading" not in card:
60+
raise OracleCardError(
61+
f"card {card_path} has no 'grading' block; cannot grade"
62+
)
63+
64+
cmd = (oracle_cmd or _oracle_cmd()) + [
65+
"oracle", "grade",
66+
"--repo", str(repo_path),
67+
"--commit", commit_sha,
68+
"--card", str(card_path),
69+
"--held", str(held_root),
70+
"--evidence", str(evidence_dir),
71+
"--ledger", str(ledger_dir),
72+
]
73+
74+
try:
75+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
76+
except (OSError, subprocess.SubprocessError) as exc:
77+
return ("FAIL", {"error": f"oracle CLI invocation failed: {exc}"})
78+
79+
try:
80+
parsed = json.loads(proc.stdout)
81+
except json.JSONDecodeError:
82+
return (
83+
"FAIL",
84+
{
85+
"error": "oracle CLI produced no parseable JSON verdict",
86+
"returncode": proc.returncode,
87+
"stdout": proc.stdout[:500],
88+
"stderr": proc.stderr[:500],
89+
},
90+
)
91+
92+
verdict = parsed.get("verdict")
93+
if verdict not in ("PASS", "FAIL"):
94+
return ("FAIL", {"error": "oracle CLI verdict missing/invalid", "parsed": parsed})
95+
96+
return (verdict, parsed)
97+
98+
99+
def grade_task(
100+
*,
101+
repo_root: str | Path,
102+
task_id,
103+
commit_sha: str,
104+
held_root: str | Path,
105+
evidence_dir: str | Path,
106+
ledger_dir: str | Path,
107+
oracle_cmd: list[str] | None = None,
108+
) -> tuple[str, dict]:
109+
"""Convenience: locate the CDD card for a task and grade it.
110+
111+
Looks for .atlas-ai/cdd/task-<id>.json under repo_root.
112+
113+
Raises:
114+
OracleCardError: if the card file does not exist or fails validation.
115+
"""
116+
card_path = Path(repo_root) / ".atlas-ai" / "cdd" / f"task-{task_id}.json"
117+
if not card_path.exists():
118+
raise OracleCardError(f"no CDD card at {card_path}")
119+
return grade_card(
120+
card_path=card_path,
121+
repo_path=repo_root,
122+
commit_sha=commit_sha,
123+
held_root=held_root,
124+
evidence_dir=evidence_dir,
125+
ledger_dir=ledger_dir,
126+
oracle_cmd=oracle_cmd,
127+
)

tests/core/test_oracle_bridge.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""Oracle bridge tests — all subprocess calls are mocked; no real CLI/podman invoked."""
2+
3+
import json
4+
from pathlib import Path
5+
from types import SimpleNamespace
6+
7+
import pytest
8+
9+
from prd_taskmaster.oracle_bridge import grade_card, grade_task, OracleCardError
10+
11+
12+
# ---------------------------------------------------------------------------
13+
# Helpers
14+
# ---------------------------------------------------------------------------
15+
16+
def _write_graded_card(path: Path, extra: dict | None = None) -> Path:
17+
"""Write a minimal valid Graded Card (has the required 'grading' key)."""
18+
card = {"id": "C-001", "grading": {"criteria": "all tests pass"}}
19+
if extra:
20+
card.update(extra)
21+
path.write_text(json.dumps(card))
22+
return path
23+
24+
25+
def _fake_proc(stdout: str = "", stderr: str = "", returncode: int = 0):
26+
"""Return a fake subprocess.CompletedProcess-like object."""
27+
return SimpleNamespace(stdout=stdout, stderr=stderr, returncode=returncode)
28+
29+
30+
def _common_args(tmp_path: Path):
31+
"""Return common keyword args for grade_card, pointing at tmp_path sub-dirs."""
32+
return dict(
33+
repo_path=tmp_path / "repo",
34+
commit_sha="abc123",
35+
held_root=tmp_path / "held",
36+
evidence_dir=tmp_path / "evidence",
37+
ledger_dir=tmp_path / "ledger",
38+
)
39+
40+
41+
# ---------------------------------------------------------------------------
42+
# Test 1: PASS round-trip + argv mapping
43+
# ---------------------------------------------------------------------------
44+
45+
def test_pass_roundtrip_and_argv_mapping(tmp_path, monkeypatch):
46+
"""grade_card returns ("PASS", full_dict) and passes correct CLI args."""
47+
card_path = _write_graded_card(tmp_path / "card.json")
48+
args = _common_args(tmp_path)
49+
50+
captured_cmd = []
51+
52+
def fake_run(cmd, **kwargs):
53+
captured_cmd.extend(cmd)
54+
return _fake_proc(stdout='{"verdict":"PASS","exitCode":0,"ledgerEventId":"x"}')
55+
56+
monkeypatch.setattr("prd_taskmaster.oracle_bridge.subprocess.run", fake_run)
57+
58+
oracle_cmd = ["atlas"]
59+
verdict, detail = grade_card(card_path=card_path, oracle_cmd=oracle_cmd, **args)
60+
61+
assert verdict == "PASS"
62+
assert detail == {"verdict": "PASS", "exitCode": 0, "ledgerEventId": "x"}
63+
64+
# argv mapping checks
65+
assert "oracle" in captured_cmd
66+
assert "grade" in captured_cmd
67+
assert "--repo" in captured_cmd
68+
assert str(args["repo_path"]) in captured_cmd
69+
assert "--commit" in captured_cmd
70+
assert "abc123" in captured_cmd
71+
assert "--card" in captured_cmd
72+
assert str(card_path) in captured_cmd
73+
assert "--held" in captured_cmd
74+
assert str(args["held_root"]) in captured_cmd
75+
assert "--evidence" in captured_cmd
76+
assert str(args["evidence_dir"]) in captured_cmd
77+
assert "--ledger" in captured_cmd
78+
assert str(args["ledger_dir"]) in captured_cmd
79+
80+
81+
# ---------------------------------------------------------------------------
82+
# Test 2: Missing 'grading' block → OracleCardError
83+
# ---------------------------------------------------------------------------
84+
85+
def test_missing_grading_block_raises(tmp_path, monkeypatch):
86+
"""A card without a 'grading' key must raise OracleCardError (before shelling out)."""
87+
card_path = tmp_path / "card_no_grading.json"
88+
card_path.write_text(json.dumps({"id": "C-002", "title": "Some task"}))
89+
90+
# subprocess.run should NOT be called — we assert by not patching it;
91+
# if the bridge reaches it the test will fail for a different reason.
92+
# But to be clean and not accidentally run a real 'atlas' binary, patch it too.
93+
def fake_run(cmd, **kwargs):
94+
raise AssertionError("subprocess.run should not be called when grading block absent")
95+
96+
monkeypatch.setattr("prd_taskmaster.oracle_bridge.subprocess.run", fake_run)
97+
98+
with pytest.raises(OracleCardError, match="no 'grading' block"):
99+
grade_card(card_path=card_path, **_common_args(tmp_path))
100+
101+
102+
# ---------------------------------------------------------------------------
103+
# Test 3: Fail-closed on unparseable output (non-JSON stdout)
104+
# ---------------------------------------------------------------------------
105+
106+
def test_fail_closed_on_unparseable_output(tmp_path, monkeypatch):
107+
"""Non-JSON stdout (e.g. 'podman: command not found') yields ("FAIL", {...}), never raises."""
108+
card_path = _write_graded_card(tmp_path / "card.json")
109+
110+
def fake_run(cmd, **kwargs):
111+
return _fake_proc(stdout="podman: command not found", returncode=127)
112+
113+
monkeypatch.setattr("prd_taskmaster.oracle_bridge.subprocess.run", fake_run)
114+
115+
verdict, detail = grade_card(card_path=card_path, oracle_cmd=["atlas"], **_common_args(tmp_path))
116+
117+
assert verdict == "FAIL"
118+
assert "error" in detail
119+
assert detail["returncode"] == 127
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# Test 4: Fail-closed on CLI invocation error (FileNotFoundError)
124+
# ---------------------------------------------------------------------------
125+
126+
def test_fail_closed_on_invocation_error(tmp_path, monkeypatch):
127+
"""FileNotFoundError from subprocess.run yields ("FAIL", {...}), never raises."""
128+
card_path = _write_graded_card(tmp_path / "card.json")
129+
130+
def fake_run(cmd, **kwargs):
131+
raise FileNotFoundError("atlas: No such file or directory")
132+
133+
monkeypatch.setattr("prd_taskmaster.oracle_bridge.subprocess.run", fake_run)
134+
135+
verdict, detail = grade_card(card_path=card_path, oracle_cmd=["atlas"], **_common_args(tmp_path))
136+
137+
assert verdict == "FAIL"
138+
assert "error" in detail
139+
assert "oracle CLI invocation failed" in detail["error"]
140+
141+
142+
# ---------------------------------------------------------------------------
143+
# Test 5: FAIL verdict passthrough
144+
# ---------------------------------------------------------------------------
145+
146+
def test_fail_verdict_passthrough(tmp_path, monkeypatch):
147+
"""A FAIL verdict from the oracle is passed through as ("FAIL", detail)."""
148+
card_path = _write_graded_card(tmp_path / "card.json")
149+
150+
def fake_run(cmd, **kwargs):
151+
return _fake_proc(stdout='{"verdict":"FAIL"}', returncode=1)
152+
153+
monkeypatch.setattr("prd_taskmaster.oracle_bridge.subprocess.run", fake_run)
154+
155+
verdict, detail = grade_card(card_path=card_path, oracle_cmd=["atlas"], **_common_args(tmp_path))
156+
157+
assert verdict == "FAIL"
158+
assert detail == {"verdict": "FAIL"}
159+
160+
161+
# ---------------------------------------------------------------------------
162+
# Test 6: grade_task missing card → OracleCardError
163+
# ---------------------------------------------------------------------------
164+
165+
def test_grade_task_missing_card_raises(tmp_path):
166+
"""grade_task raises OracleCardError when no CDD card exists for the given task."""
167+
# tmp_path has no .atlas-ai/cdd/task-1.json
168+
with pytest.raises(OracleCardError, match="no CDD card at"):
169+
grade_task(
170+
repo_root=tmp_path,
171+
task_id=1,
172+
commit_sha="deadbeef",
173+
held_root=tmp_path / "held",
174+
evidence_dir=tmp_path / "evidence",
175+
ledger_dir=tmp_path / "ledger",
176+
)

0 commit comments

Comments
 (0)