rabbit-brain 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rabbit_brain/__init__.py +12 -0
- rabbit_brain/checks.py +107 -0
- rabbit_brain/cli.py +552 -0
- rabbit_brain/docs/AGENTS.md +139 -0
- rabbit_brain/errors.py +68 -0
- rabbit_brain/example.py +73 -0
- rabbit_brain/fmt.py +39 -0
- rabbit_brain/importer.py +162 -0
- rabbit_brain/models.py +390 -0
- rabbit_brain/prose.py +56 -0
- rabbit_brain/recorder.py +55 -0
- rabbit_brain/report.py +91 -0
- rabbit_brain/runs.py +204 -0
- rabbit_brain/stability.py +142 -0
- rabbit_brain-0.1.0.dist-info/METADATA +58 -0
- rabbit_brain-0.1.0.dist-info/RECORD +19 -0
- rabbit_brain-0.1.0.dist-info/WHEEL +4 -0
- rabbit_brain-0.1.0.dist-info/entry_points.txt +3 -0
- rabbit_brain-0.1.0.dist-info/licenses/LICENSE +202 -0
rabbit_brain/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Rabbit Brain: release review for iterative perception models.
|
|
2
|
+
|
|
3
|
+
Rank the cases that regressed or never settled, keep checks for the next checkpoint.
|
|
4
|
+
CLI: `rb`. Docs for agents and humans: `rb docs` (AGENTS.md).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .recorder import TrajectoryRecorder # noqa: E402
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__", "TrajectoryRecorder"]
|
rabbit_brain/checks.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Saved checks: absolute limits per case id for one project, kept in the repo (`checks.json`) and run against the next candidate."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from .errors import RBError
|
|
12
|
+
from .fmt import pct, to_fixed
|
|
13
|
+
from .models import Bundle, CheckResult, ChecksV1, ChecksV2, CheckV1, CheckV2, ComparisonV1
|
|
14
|
+
from .stability import EPS, trajectory_stats
|
|
15
|
+
|
|
16
|
+
DEFAULT_CHECKS_PATH = Path("checks.json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_checks(path: Path, project: Optional[str] = None, strict: bool = True) -> ChecksV2:
|
|
20
|
+
"""Read a version-1 (workspace / kit) or version-2 checks file. Returns version 2, filtered to `project` when given.
|
|
21
|
+
|
|
22
|
+
A file for another project raises E_CHECKS_PROJECT_MISMATCH when `strict`, otherwise returns no checks (the caller may warn)."""
|
|
23
|
+
if not path.exists():
|
|
24
|
+
return ChecksV2(version=2, project=project or "", checks=[])
|
|
25
|
+
try:
|
|
26
|
+
raw = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
raise RBError("E_CHECKS_INVALID", message=f"{path} is not valid JSON.")
|
|
29
|
+
if not isinstance(raw, dict):
|
|
30
|
+
raise RBError("E_CHECKS_INVALID")
|
|
31
|
+
try:
|
|
32
|
+
if raw.get("version") == 2:
|
|
33
|
+
v2 = ChecksV2.model_validate(raw)
|
|
34
|
+
if project and v2.checks and v2.project != project:
|
|
35
|
+
if strict:
|
|
36
|
+
raise RBError("E_CHECKS_PROJECT_MISMATCH", message=f"{path} holds checks for '{v2.project}', not '{project}'.")
|
|
37
|
+
return ChecksV2(version=2, project=project, checks=[])
|
|
38
|
+
return v2
|
|
39
|
+
if raw.get("version") == 1:
|
|
40
|
+
v1 = ChecksV1.model_validate(raw)
|
|
41
|
+
checks = [c for c in v1.checks if project is None or c.project == project]
|
|
42
|
+
if project and v1.checks and not checks:
|
|
43
|
+
if strict:
|
|
44
|
+
raise RBError("E_CHECKS_PROJECT_MISMATCH", message=f"No saved checks in {path} match project '{project}'.")
|
|
45
|
+
return ChecksV2(version=2, project=project, checks=[])
|
|
46
|
+
proj = project or (checks[0].project if checks else "")
|
|
47
|
+
return ChecksV2(version=2, project=proj, checks=[CheckV2(case_id=c.id, name=c.name, max_error=c.max_error, max_late_share=c.max_late_share) for c in checks])
|
|
48
|
+
except ValidationError as exc:
|
|
49
|
+
raise RBError("E_CHECKS_INVALID", message=f"{path}: {exc.errors()[0].get('msg', 'invalid')}")
|
|
50
|
+
raise RBError("E_CHECKS_INVALID", message=f"{path}: expected version 1 or 2.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_checks(path: Path, checks: ChecksV2) -> None:
|
|
54
|
+
try:
|
|
55
|
+
path.write_text(json.dumps(checks.model_dump(exclude_none=True), indent=2) + "\n", encoding="utf-8")
|
|
56
|
+
except OSError as exc:
|
|
57
|
+
raise RBError("E_WRITE_FAILED", message=f"Could not write {path}: {exc}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def evaluate_check(check: CheckV2, comparison: Union[ComparisonV1, Bundle], unit: str) -> CheckResult:
|
|
61
|
+
"""Same semantics as the workspace's checkResult() and the kit's check.py."""
|
|
62
|
+
found = next((c for c in comparison.cases if c.id == check.case_id), None)
|
|
63
|
+
base = dict(case_id=check.case_id, max_error=check.max_error, max_late_share=check.max_late_share, max_reversals=check.max_reversals)
|
|
64
|
+
if found is None:
|
|
65
|
+
return CheckResult(status="missing", reason="Case not present in this comparison.", **base)
|
|
66
|
+
base["candidate_error"] = found.candidate_error
|
|
67
|
+
if found.candidate_error > check.max_error + EPS:
|
|
68
|
+
return CheckResult(status="failing", reason=f"Candidate error {to_fixed(found.candidate_error)} {unit} exceeds the {to_fixed(check.max_error)} {unit} limit.", **base)
|
|
69
|
+
if check.max_late_share is not None or check.max_reversals is not None:
|
|
70
|
+
if not found.candidate_trajectory:
|
|
71
|
+
return CheckResult(status="failing", reason="This check requires a settled trajectory, but none was exported for the candidate.", **base)
|
|
72
|
+
stats = trajectory_stats(found.candidate_trajectory)
|
|
73
|
+
if check.max_late_share is not None and stats.late_share > check.max_late_share + EPS:
|
|
74
|
+
return CheckResult(status="failing", reason=f"Late revision {pct(stats.late_share)} exceeds the {pct(check.max_late_share)} limit.", **base)
|
|
75
|
+
if check.max_reversals is not None and stats.reversals > check.max_reversals:
|
|
76
|
+
return CheckResult(status="failing", reason=f"Reversals {stats.reversals} exceed the {check.max_reversals} limit.", **base)
|
|
77
|
+
return CheckResult(status="passing", reason="Within limits.", **base)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def evaluate_all(checks: ChecksV2, comparison: Union[ComparisonV1, Bundle], unit: str, case_id: Optional[str] = None) -> list[CheckResult]:
|
|
81
|
+
selected = [c for c in checks.checks if case_id is None or c.case_id == case_id]
|
|
82
|
+
return [evaluate_check(c, comparison, unit) for c in selected]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def upsert_check(checks: ChecksV2, new: CheckV2) -> ChecksV2:
|
|
86
|
+
others = [c for c in checks.checks if c.case_id != new.case_id]
|
|
87
|
+
return ChecksV2(version=2, project=checks.project, checks=[*others, new])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def default_check(bundle: Bundle, case_id: str, *, max_error: Optional[float], max_late_share: Optional[float], max_reversals: Optional[int], require_settled: bool, note: str) -> CheckV2:
|
|
91
|
+
case = next((c for c in bundle.cases if c.id == case_id), None)
|
|
92
|
+
if case is None:
|
|
93
|
+
raise RBError("E_CASE_NOT_FOUND", message=f"Case '{case_id}' is not in run {bundle.run_id}.")
|
|
94
|
+
if max_error is None:
|
|
95
|
+
max_error = float(to_fixed(case.baseline_error + bundle.limits.max_regression, 2))
|
|
96
|
+
if max_late_share is None and require_settled and case.candidate_trajectory:
|
|
97
|
+
max_late_share = bundle.limits.max_late_share
|
|
98
|
+
return CheckV2(case_id=case.id, name=case.name, max_error=max_error, max_late_share=max_late_share, max_reversals=max_reversals, from_run=bundle.run_id, created=date.today().isoformat(), note=note or "")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def describe_check(check: CheckV2, unit: str) -> str:
|
|
102
|
+
parts = [f"candidate error ≤ {to_fixed(check.max_error)} {unit}"]
|
|
103
|
+
if check.max_late_share is not None:
|
|
104
|
+
parts.append(f"late revision ≤ {pct(check.max_late_share)}")
|
|
105
|
+
if check.max_reversals is not None:
|
|
106
|
+
parts.append(f"reversals ≤ {check.max_reversals}")
|
|
107
|
+
return ", ".join(parts)
|