Closes the load-bearing gap blocking every reasoning-capable claim
under ADR-0091: docs/reviewers.yaml was previously `reviewers: []` and
unparsed. Now schema-validated at v1, with a bootstrap shay-j entry
self-sealed via provenance.
- new core.capability.reviewers module: frozen Reviewer/ReviewerRegistry
dataclasses, strict load_reviewer_registry parser, ReviewerRegistryError
- enforces ADR-0092 schema rules: schema_version==1, no unknown
top-level keys, no unknown reviewer fields, role∈{primary,domain},
primary must claim ["*"], domain must NOT claim "*", review_scope
subset of {pack,proposal,chain,eval}, no duplicate reviewer_ids
- can_review(reviewer_id, domain_id, scope) helper implements
ADR-0092 rules 2-4 for downstream use by ADR-0093 validator
- docs/reviewers.yaml updated to v1 schema with shay-j bootstrap
- ledger_report() evidence_counts now exposes structured
reviewer_registry status (valid, schema_version, reviewer_count,
reviewer_ids, error) alongside the legacy reviewers_present bool
- new evals/reviewer_registry/ lane: 6 cases (2 positive + 4 negative)
covering empty-registry, wrong-version, domain-wildcard rejection,
and unknown-field rejection
- runner emits deterministic JSON report; two runs produce byte-identical
output (sha256 verified)
- 26 unit tests in tests/test_reviewer_registry.py
- capability ledger test extended to assert new reviewer_registry block
- smoke suite green (67/67); lane passes 6/6
The pre-existing test_flag_report_tracks_default_off_flags failure is
unrelated (discourse_planner flag default) and not introduced here.
187 lines
6.3 KiB
Python
187 lines
6.3 KiB
Python
"""Runner for evals/reviewer_registry/ (ADR-0092).
|
|
|
|
Iterates over the case fixtures under ``cases/`` and asserts each one's
|
|
load outcome matches its declared expectation. Emits a deterministic
|
|
JSON report to ``results/v1_dev.json``.
|
|
|
|
Exit code is non-zero on any divergence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from core.capability.reviewers import (
|
|
ReviewerRegistryError,
|
|
load_reviewer_registry,
|
|
)
|
|
|
|
|
|
CASE_EXPECTATIONS: dict[str, dict[str, Any]] = {
|
|
"positive_primary.yaml": {
|
|
"expected_outcome": "load_ok",
|
|
"expected_reviewer_count": 1,
|
|
"expected_reviewer_ids": ["shay-j"],
|
|
"purpose": "single primary reviewer loads cleanly",
|
|
},
|
|
"positive_domain.yaml": {
|
|
"expected_outcome": "load_ok",
|
|
"expected_reviewer_count": 1,
|
|
"expected_reviewer_ids": ["math-reviewer"],
|
|
"purpose": "single domain reviewer loads cleanly",
|
|
},
|
|
"negative_empty.yaml": {
|
|
"expected_outcome": "load_ok",
|
|
"expected_reviewer_count": 0,
|
|
"expected_reviewer_ids": [],
|
|
"purpose": "empty registry loads but blocks reasoning-capable claims",
|
|
},
|
|
"negative_wrong_version.yaml": {
|
|
"expected_outcome": "reject",
|
|
"expected_error_substring": "schema_version",
|
|
"purpose": "non-v1 schema rejected before any reviewer parsed",
|
|
},
|
|
"negative_domain_wildcard.yaml": {
|
|
"expected_outcome": "reject",
|
|
"expected_error_substring": "wildcard",
|
|
"purpose": "domain reviewer claiming '*' rejected",
|
|
},
|
|
"negative_unknown_field.yaml": {
|
|
"expected_outcome": "reject",
|
|
"expected_error_substring": "unknown fields",
|
|
"purpose": "unknown reviewer field rejected",
|
|
},
|
|
}
|
|
|
|
|
|
def _evaluate_case(path: Path, expectation: dict[str, Any]) -> dict[str, Any]:
|
|
actual: dict[str, Any] = {"case_id": path.name, "purpose": expectation["purpose"]}
|
|
|
|
try:
|
|
registry = load_reviewer_registry(path)
|
|
except ReviewerRegistryError as exc:
|
|
actual["actual_outcome"] = "reject"
|
|
actual["actual_error"] = str(exc)
|
|
actual["registry"] = None
|
|
else:
|
|
actual["actual_outcome"] = "load_ok"
|
|
actual["actual_error"] = None
|
|
actual["registry"] = {
|
|
"schema_version": registry.schema_version,
|
|
"reviewer_count": len(registry.reviewers),
|
|
"reviewer_ids": sorted(r.reviewer_id for r in registry.reviewers),
|
|
}
|
|
|
|
if actual["actual_outcome"] != expectation["expected_outcome"]:
|
|
actual["passed"] = False
|
|
actual["divergence"] = (
|
|
f"expected {expectation['expected_outcome']!r} got "
|
|
f"{actual['actual_outcome']!r}"
|
|
)
|
|
return actual
|
|
|
|
if expectation["expected_outcome"] == "load_ok":
|
|
registry_data = actual["registry"]
|
|
assert registry_data is not None # narrowed by branch
|
|
if registry_data["reviewer_count"] != expectation["expected_reviewer_count"]:
|
|
actual["passed"] = False
|
|
actual["divergence"] = (
|
|
f"expected reviewer_count={expectation['expected_reviewer_count']} got "
|
|
f"{registry_data['reviewer_count']}"
|
|
)
|
|
return actual
|
|
if registry_data["reviewer_ids"] != expectation["expected_reviewer_ids"]:
|
|
actual["passed"] = False
|
|
actual["divergence"] = (
|
|
f"expected reviewer_ids={expectation['expected_reviewer_ids']} got "
|
|
f"{registry_data['reviewer_ids']}"
|
|
)
|
|
return actual
|
|
else:
|
|
actual_error = actual["actual_error"] or ""
|
|
if expectation["expected_error_substring"] not in actual_error:
|
|
actual["passed"] = False
|
|
actual["divergence"] = (
|
|
f"expected error to contain "
|
|
f"{expectation['expected_error_substring']!r} got "
|
|
f"{actual_error!r}"
|
|
)
|
|
return actual
|
|
|
|
actual["passed"] = True
|
|
actual["divergence"] = None
|
|
return actual
|
|
|
|
|
|
def run(*, lane_dir: Path) -> dict[str, Any]:
|
|
cases_dir = lane_dir / "cases"
|
|
case_results: list[dict[str, Any]] = []
|
|
for case_name in sorted(CASE_EXPECTATIONS.keys()):
|
|
case_path = cases_dir / case_name
|
|
if not case_path.exists():
|
|
case_results.append(
|
|
{
|
|
"case_id": case_name,
|
|
"passed": False,
|
|
"divergence": f"case fixture missing: {case_path}",
|
|
}
|
|
)
|
|
continue
|
|
case_results.append(_evaluate_case(case_path, CASE_EXPECTATIONS[case_name]))
|
|
|
|
summary = {
|
|
"lane": "reviewer_registry",
|
|
"lane_version": "v1",
|
|
"split": "dev",
|
|
"adr": "ADR-0092",
|
|
"invariant": "reviewer_registry_schema_v1",
|
|
"total_cases": len(case_results),
|
|
"passed_cases": sum(1 for r in case_results if r["passed"]),
|
|
"failed_cases": sum(1 for r in case_results if not r["passed"]),
|
|
"all_passed": all(r["passed"] for r in case_results),
|
|
"cases": case_results,
|
|
}
|
|
return summary
|
|
|
|
|
|
def _canonical_json(payload: dict[str, Any]) -> bytes:
|
|
return json.dumps(payload, sort_keys=True, indent=2).encode("utf-8") + b"\n"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="reviewer_registry lane runner")
|
|
parser.add_argument(
|
|
"--lane-dir",
|
|
type=Path,
|
|
default=Path(__file__).resolve().parent,
|
|
help="lane root directory (defaults to this file's directory)",
|
|
)
|
|
parser.add_argument(
|
|
"--report",
|
|
type=Path,
|
|
default=None,
|
|
help="path to write JSON report (defaults to <lane>/results/v1_dev.json)",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
summary = run(lane_dir=args.lane_dir)
|
|
report_path = args.report or (args.lane_dir / "results" / "v1_dev.json")
|
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload_bytes = _canonical_json(summary)
|
|
report_path.write_bytes(payload_bytes)
|
|
|
|
sha = hashlib.sha256(payload_bytes).hexdigest()
|
|
print(f"report: {report_path}")
|
|
print(f"sha256: {sha}")
|
|
print(f"passed: {summary['passed_cases']}/{summary['total_cases']}")
|
|
|
|
return 0 if summary["all_passed"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|