217 lines
7.4 KiB
Python
217 lines
7.4 KiB
Python
"""ADR-0091 domain-pack contract validation.
|
|
|
|
This module is intentionally read-only. It validates optional manifest
|
|
metadata that links a language pack to capability-ledger evidence, but it
|
|
does not mutate packs and it does not promote capability status.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
_ALLOWED_SPLITS = {"dev", "public", "holdout"}
|
|
_KNOWN_DOMAIN_IDS = {
|
|
"systems_software",
|
|
"mathematics_logic",
|
|
"physics",
|
|
"hebrew_greek_textual_reasoning",
|
|
"philosophy_theology",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DomainEvalLane:
|
|
lane: str
|
|
version: str
|
|
splits: tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DomainPackContract:
|
|
domain_contract_version: int
|
|
domain_id: str
|
|
axioms: str | None
|
|
rules: str | None
|
|
teaching_chains: tuple[str, ...] = field(default_factory=tuple)
|
|
eval_lanes: tuple[DomainEvalLane, ...] = field(default_factory=tuple)
|
|
reviewers: tuple[str, ...] = field(default_factory=tuple)
|
|
known_gaps: tuple[str, ...] = field(default_factory=tuple)
|
|
provenance: str = ""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DomainContractValidation:
|
|
pack_id: str
|
|
present: bool
|
|
valid: bool
|
|
errors: tuple[str, ...] = field(default_factory=tuple)
|
|
contract: DomainPackContract | None = None
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"pack_id": self.pack_id,
|
|
"present": self.present,
|
|
"valid": self.valid,
|
|
"errors": list(self.errors),
|
|
"contract": _contract_as_dict(self.contract) if self.contract else None,
|
|
}
|
|
|
|
|
|
def _contract_as_dict(contract: DomainPackContract | None) -> dict[str, Any] | None:
|
|
if contract is None:
|
|
return None
|
|
return {
|
|
"domain_contract_version": contract.domain_contract_version,
|
|
"domain_id": contract.domain_id,
|
|
"axioms": contract.axioms,
|
|
"rules": contract.rules,
|
|
"teaching_chains": list(contract.teaching_chains),
|
|
"eval_lanes": [
|
|
{"lane": lane.lane, "version": lane.version, "splits": list(lane.splits)}
|
|
for lane in contract.eval_lanes
|
|
],
|
|
"reviewers": list(contract.reviewers),
|
|
"known_gaps": list(contract.known_gaps),
|
|
"provenance": contract.provenance,
|
|
}
|
|
|
|
|
|
def _as_string_tuple(value: object, *, field_name: str, errors: list[str]) -> tuple[str, ...]:
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, list):
|
|
errors.append(f"{field_name}:must_be_list")
|
|
return ()
|
|
out: list[str] = []
|
|
for idx, item in enumerate(value):
|
|
if not isinstance(item, str) or not item.strip():
|
|
errors.append(f"{field_name}[{idx}]:must_be_nonempty_string")
|
|
continue
|
|
out.append(item.strip())
|
|
return tuple(out)
|
|
|
|
|
|
def _optional_path(value: object, *, field_name: str, errors: list[str]) -> str | None:
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, str) or not value.strip():
|
|
errors.append(f"{field_name}:must_be_null_or_nonempty_string")
|
|
return None
|
|
if Path(value).is_absolute() or ".." in Path(value).parts:
|
|
errors.append(f"{field_name}:unsafe_path")
|
|
return None
|
|
return value.strip()
|
|
|
|
|
|
def parse_domain_contract(manifest: dict[str, Any], *, pack_id: str) -> DomainContractValidation:
|
|
if "domain_contract_version" not in manifest:
|
|
return DomainContractValidation(pack_id=pack_id, present=False, valid=True)
|
|
|
|
errors: list[str] = []
|
|
version_raw = manifest.get("domain_contract_version")
|
|
if version_raw != 1:
|
|
errors.append("domain_contract_version:unsupported")
|
|
version = 0
|
|
else:
|
|
version = 1
|
|
|
|
domain_id = manifest.get("domain_id")
|
|
if not isinstance(domain_id, str) or not domain_id.strip():
|
|
errors.append("domain_id:required")
|
|
domain_id_s = ""
|
|
else:
|
|
domain_id_s = domain_id.strip()
|
|
if domain_id_s not in _KNOWN_DOMAIN_IDS:
|
|
errors.append("domain_id:unknown")
|
|
|
|
axioms = _optional_path(manifest.get("axioms"), field_name="axioms", errors=errors)
|
|
rules = _optional_path(manifest.get("rules"), field_name="rules", errors=errors)
|
|
teaching_chains = _as_string_tuple(
|
|
manifest.get("teaching_chains"), field_name="teaching_chains", errors=errors
|
|
)
|
|
reviewers = _as_string_tuple(manifest.get("reviewers"), field_name="reviewers", errors=errors)
|
|
known_gaps = _as_string_tuple(manifest.get("known_gaps"), field_name="known_gaps", errors=errors)
|
|
|
|
eval_lanes: list[DomainEvalLane] = []
|
|
raw_lanes = manifest.get("eval_lanes")
|
|
if raw_lanes is None:
|
|
raw_lanes = []
|
|
if not isinstance(raw_lanes, list):
|
|
errors.append("eval_lanes:must_be_list")
|
|
else:
|
|
for idx, raw in enumerate(raw_lanes):
|
|
if not isinstance(raw, dict):
|
|
errors.append(f"eval_lanes[{idx}]:must_be_object")
|
|
continue
|
|
lane = raw.get("lane")
|
|
version_s = raw.get("version")
|
|
splits_raw = raw.get("splits")
|
|
if not isinstance(lane, str) or not lane.strip():
|
|
errors.append(f"eval_lanes[{idx}].lane:required")
|
|
continue
|
|
if not isinstance(version_s, str) or not version_s.strip():
|
|
errors.append(f"eval_lanes[{idx}].version:required")
|
|
continue
|
|
splits = _as_string_tuple(splits_raw, field_name=f"eval_lanes[{idx}].splits", errors=errors)
|
|
unknown = sorted(set(splits) - _ALLOWED_SPLITS)
|
|
if unknown:
|
|
errors.append(f"eval_lanes[{idx}].splits:unknown:{','.join(unknown)}")
|
|
eval_lanes.append(DomainEvalLane(lane=lane.strip(), version=version_s.strip(), splits=splits))
|
|
|
|
provenance = manifest.get("provenance")
|
|
if not isinstance(provenance, str) or not provenance.strip():
|
|
errors.append("provenance:required")
|
|
provenance_s = ""
|
|
else:
|
|
provenance_s = provenance.strip()
|
|
|
|
contract = DomainPackContract(
|
|
domain_contract_version=version,
|
|
domain_id=domain_id_s,
|
|
axioms=axioms,
|
|
rules=rules,
|
|
teaching_chains=teaching_chains,
|
|
eval_lanes=tuple(eval_lanes),
|
|
reviewers=reviewers,
|
|
known_gaps=known_gaps,
|
|
provenance=provenance_s,
|
|
)
|
|
return DomainContractValidation(
|
|
pack_id=pack_id,
|
|
present=True,
|
|
valid=not errors,
|
|
errors=tuple(errors),
|
|
contract=contract,
|
|
)
|
|
|
|
|
|
def validate_domain_contract_pack(pack_id: str, *, data_root: Path | None = None) -> DomainContractValidation:
|
|
root = data_root or Path(__file__).resolve().parent / "data"
|
|
manifest_path = root / pack_id / "manifest.json"
|
|
if not manifest_path.exists():
|
|
return DomainContractValidation(
|
|
pack_id=pack_id,
|
|
present=False,
|
|
valid=False,
|
|
errors=("manifest:not_found",),
|
|
)
|
|
try:
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return DomainContractValidation(
|
|
pack_id=pack_id,
|
|
present=False,
|
|
valid=False,
|
|
errors=("manifest:invalid_json",),
|
|
)
|
|
if not isinstance(manifest, dict):
|
|
return DomainContractValidation(
|
|
pack_id=pack_id,
|
|
present=False,
|
|
valid=False,
|
|
errors=("manifest:must_be_object",),
|
|
)
|
|
return parse_domain_contract(manifest, pack_id=pack_id)
|