Viewing files at commit 4f4412a18356Commit detailsView latest

Browse files
scripts/oakbench/command_semantics.py 109 lines · 4.5 KB · python
1
"""The command-semantics contract, loaded from config/command_semantics.json.
2
3
This is the fairness guarantee of the whole suite: which exact command each
4
subject kind runs per operation and track. It is data, not code, so outsiders
5
can audit it by reading one JSON file, and a fairness decision (for example
6
adopting `oak diff --stat` as agent-default once published agent docs do) is a
7
one-line data edit plus a version bump that every row records (ADR-0005).
8
"""
9
10
from __future__ import annotations
11
12
import json
13
from pathlib import Path
14
from typing import Any
15
16
ROOT = Path(__file__).resolve().parents[2]
17
DEFAULT_CONFIG = ROOT / "config" / "command_semantics.json"
18
TRACKS = ("agent-default", "core-equivalent")
19
20
21
class CommandSemantics:
22
    def __init__(self, config_path: Path = DEFAULT_CONFIG) -> None:
23
        data = json.loads(config_path.read_text())
24
        self.version: str = str(data["version"])
25
        self._tracks: dict[str, Any] = data["harness_tracks"]
26
        self._output_formats = data.get("output_formats", {})
27
        self._agent_native_json_probes: dict[str, Any] = data.get("agent_native_json_probes") or {}
28
29
    def _kind(self, kind: str) -> dict[str, Any]:
30
        if kind not in self._tracks:
31
            raise ValueError(f"unknown subject kind for command semantics: {kind!r}")
32
        return self._tracks[kind]
33
34
    def init_args(self, kind: str) -> list[str]:
35
        return list(self._kind(kind)["init"])
36
37
    def stage_args(self, kind: str) -> list[str] | None:
38
        value = self._kind(kind)["stage"]
39
        return list(value) if value is not None else None
40
41
    def commit_args(self, kind: str, message: str) -> list[str]:
42
        return [part.replace("{message}", message) for part in self._kind(kind)["commit"]]
43
44
    def branch_create_args(self, kind: str) -> list[str]:
45
        return list(self._kind(kind)["branch_create"])
46
47
    def status_args(self, kind: str, track: str) -> list[str]:
48
        return list(self._kind(kind)["status"][track])
49
50
    def diff_args(self, kind: str, track: str) -> list[str]:
51
        return list(self._kind(kind)["diff"][track])
52
53
    def output_format(self, kind: str, operation: str, track: str) -> str:
54
        family = "status" if operation.startswith("status.") else "diff"
55
        if operation == "diff.full":
56
            track = "core-equivalent"
57
        try:
58
            value = self._output_formats[kind][family][track]
59
        except (KeyError, TypeError) as exc:
60
            raise ValueError(
61
                f"missing output_formats entry for {kind!r}/{family!r}/{track!r}; "
62
                "update the command semantics config"
63
            ) from exc
64
        if value not in {"status", "stat", "diff", "porcelain-z", "oak-json"}:
65
            raise ValueError(f"unsupported configured output format: {value!r}")
66
        return value
67
68
    def agent_native_json_probes(self, kind: str, branch_name: str) -> dict[str, dict[str, Any]]:
69
        self._kind(kind)
70
        probes = self._agent_native_json_probes.get(kind) or {}
71
        result: dict[str, dict[str, Any]] = {}
72
        for operation, spec in probes.items():
73
            result[str(operation)] = {
74
                "output_format": str(spec.get("output_format", "json")),
75
                "args": [
76
                    str(part).replace("{branch}", branch_name)
77
                    for part in spec.get("args", [])
78
                ],
79
                "help_args": [str(part) for part in spec.get("help_args", [])],
80
                "required_fields": [str(part) for part in spec.get("required_fields", [])],
81
            }
82
            if spec.get("minimum_oak_version") is not None:
83
                result[str(operation)]["minimum_oak_version"] = str(spec["minimum_oak_version"])
84
        return result
85
86
    def snapshot_commands(self, bin_path: str, kind: str, message: str) -> list[list[str]]:
87
        """Full snapshot sequence: git pays stage+commit as two calls, oak one."""
88
        commands: list[list[str]] = []
89
        stage = self.stage_args(kind)
90
        if stage is not None:
91
            commands.append([bin_path, *stage])
92
        commands.append([bin_path, *self.commit_args(kind, message)])
93
        return commands
94
95
    def status_display(self, kind: str, track: str) -> str:
96
        return " ".join([kind, *self.status_args(kind, track)])
97
98
    def diff_display(self, kind: str, track: str) -> str:
99
        return " ".join([kind, *self.diff_args(kind, track)])
100
101
102
_semantics: CommandSemantics | None = None
103
104
105
def semantics() -> CommandSemantics:
106
    global _semantics
107
    if _semantics is None:
108
        _semantics = CommandSemantics()
109
    return _semantics