Skip to content

Commit faa126e

Browse files
committed
Add CRFB TOB scenario contract
1 parent d30bb35 commit faa126e

4 files changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a named CRFB Post-OBBBA long-run TOB scenario contract for validating the target source, hash, and Trustees threshold mode.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from policyengine.scenarios.crfb import (
2+
CRFB_POST_OBBBA_TOB_SCENARIO_ID,
3+
CRFB_POST_OBBBA_TOB_TARGET_SHA256,
4+
CRFB_POST_OBBBA_TOB_TARGET_SOURCE,
5+
TRUSTEES_CORE_THRESHOLD_LAW_MODE,
6+
CRFBPostOBBBATOBContract,
7+
crfb_post_obbba_tob_contract,
8+
validate_crfb_post_obbba_tob_metadata,
9+
)
10+
11+
__all__ = [
12+
"CRFBPostOBBBATOBContract",
13+
"CRFB_POST_OBBBA_TOB_SCENARIO_ID",
14+
"CRFB_POST_OBBBA_TOB_TARGET_SHA256",
15+
"CRFB_POST_OBBBA_TOB_TARGET_SOURCE",
16+
"TRUSTEES_CORE_THRESHOLD_LAW_MODE",
17+
"crfb_post_obbba_tob_contract",
18+
"validate_crfb_post_obbba_tob_metadata",
19+
]

src/policyengine/scenarios/crfb.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Named CRFB long-run TOB scenario contracts."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any, Mapping
6+
7+
from pydantic import BaseModel, Field
8+
9+
CRFB_POST_OBBBA_TOB_SCENARIO_ID = "crfb_post_obbba_tob_75y"
10+
CRFB_POST_OBBBA_TOB_TARGET_ID = "post_obbba_calibrated_tob_75y"
11+
CRFB_POST_OBBBA_TOB_TARGET_SOURCE = "oact_2025_08_05_provisional"
12+
CRFB_POST_OBBBA_TOB_TARGET_SHA256 = (
13+
"75e9dbe6a30680670713089ceed3eb10d7ef597b88c4317d0b39571e25f381f3"
14+
)
15+
TRUSTEES_CORE_THRESHOLD_LAW_MODE = "trustees-2025-core-thresholds-v1"
16+
17+
18+
def _expected_artifact_contract() -> dict[str, Any]:
19+
return {
20+
"must_consume_baseline_sha256": CRFB_POST_OBBBA_TOB_TARGET_SHA256,
21+
"must_expose_scenario_id": CRFB_POST_OBBBA_TOB_SCENARIO_ID,
22+
"reject_raw_current_law_substitution": True,
23+
}
24+
25+
26+
class CRFBPostOBBBATOBContract(BaseModel):
27+
"""Reproducibility contract for the CRFB Post-OBBBA TOB target source."""
28+
29+
scenario_id: str = CRFB_POST_OBBBA_TOB_SCENARIO_ID
30+
calibration_target_id: str = CRFB_POST_OBBBA_TOB_TARGET_ID
31+
target_source: str = CRFB_POST_OBBBA_TOB_TARGET_SOURCE
32+
target_sha256: str = CRFB_POST_OBBBA_TOB_TARGET_SHA256
33+
baseline_kind: str = "calibration_target"
34+
not_law: bool = True
35+
law_mode: str = TRUSTEES_CORE_THRESHOLD_LAW_MODE
36+
artifact_contract: dict[str, Any] = Field(
37+
default_factory=_expected_artifact_contract
38+
)
39+
40+
def validate_metadata(self, metadata: Mapping[str, Any]) -> dict[str, Any]:
41+
errors = []
42+
checks = {
43+
"name": self.target_source,
44+
"scenario_id": self.scenario_id,
45+
"calibration_target_id": self.calibration_target_id,
46+
"baseline_kind": self.baseline_kind,
47+
"not_law": self.not_law,
48+
"law_mode": self.law_mode,
49+
"sha256": self.target_sha256,
50+
}
51+
for key, expected in checks.items():
52+
actual = metadata.get(key)
53+
if actual != expected:
54+
errors.append(f"{key}={actual!r}, expected {expected!r}")
55+
56+
artifact_contract = metadata.get("artifact_contract")
57+
if artifact_contract != self.artifact_contract:
58+
errors.append(
59+
"artifact_contract does not match the CRFB Post-OBBBA TOB contract"
60+
)
61+
62+
if errors:
63+
raise ValueError(
64+
"Invalid CRFB Post-OBBBA TOB scenario metadata: " + "; ".join(errors)
65+
)
66+
return dict(metadata)
67+
68+
69+
def crfb_post_obbba_tob_contract() -> CRFBPostOBBBATOBContract:
70+
return CRFBPostOBBBATOBContract()
71+
72+
73+
def validate_crfb_post_obbba_tob_metadata(
74+
metadata: Mapping[str, Any],
75+
) -> dict[str, Any]:
76+
return crfb_post_obbba_tob_contract().validate_metadata(metadata)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pytest
2+
3+
from policyengine.scenarios import (
4+
CRFB_POST_OBBBA_TOB_SCENARIO_ID,
5+
CRFB_POST_OBBBA_TOB_TARGET_SHA256,
6+
CRFB_POST_OBBBA_TOB_TARGET_SOURCE,
7+
TRUSTEES_CORE_THRESHOLD_LAW_MODE,
8+
crfb_post_obbba_tob_contract,
9+
validate_crfb_post_obbba_tob_metadata,
10+
)
11+
12+
13+
def _valid_metadata(**overrides):
14+
metadata = {
15+
"name": CRFB_POST_OBBBA_TOB_TARGET_SOURCE,
16+
"scenario_id": CRFB_POST_OBBBA_TOB_SCENARIO_ID,
17+
"calibration_target_id": "post_obbba_calibrated_tob_75y",
18+
"baseline_kind": "calibration_target",
19+
"not_law": True,
20+
"law_mode": TRUSTEES_CORE_THRESHOLD_LAW_MODE,
21+
"sha256": CRFB_POST_OBBBA_TOB_TARGET_SHA256,
22+
"artifact_contract": {
23+
"must_consume_baseline_sha256": CRFB_POST_OBBBA_TOB_TARGET_SHA256,
24+
"must_expose_scenario_id": CRFB_POST_OBBBA_TOB_SCENARIO_ID,
25+
"reject_raw_current_law_substitution": True,
26+
},
27+
}
28+
metadata.update(overrides)
29+
return metadata
30+
31+
32+
def test_crfb_post_obbba_tob_contract_names_target_and_law_mode():
33+
contract = crfb_post_obbba_tob_contract()
34+
35+
assert contract.scenario_id == CRFB_POST_OBBBA_TOB_SCENARIO_ID
36+
assert contract.target_source == CRFB_POST_OBBBA_TOB_TARGET_SOURCE
37+
assert contract.target_sha256 == CRFB_POST_OBBBA_TOB_TARGET_SHA256
38+
assert contract.law_mode == TRUSTEES_CORE_THRESHOLD_LAW_MODE
39+
assert contract.not_law is True
40+
41+
42+
def test_validate_crfb_post_obbba_tob_metadata_accepts_exact_contract():
43+
metadata = _valid_metadata()
44+
45+
assert validate_crfb_post_obbba_tob_metadata(metadata) == metadata
46+
47+
48+
def test_validate_crfb_post_obbba_tob_metadata_rejects_current_law_substitution():
49+
metadata = _valid_metadata(
50+
name="trustees_2025_current_law",
51+
baseline_kind="current_law_comparator",
52+
not_law=False,
53+
sha256="e059aa9fba806b260a399b8a6a18b892a6363ba12ee00fe21ab109d09dff0ec4",
54+
)
55+
56+
with pytest.raises(ValueError, match="trustees_2025_current_law"):
57+
validate_crfb_post_obbba_tob_metadata(metadata)
58+
59+
60+
def test_validate_crfb_post_obbba_tob_metadata_rejects_hash_mismatch():
61+
metadata = _valid_metadata(sha256="0" * 64)
62+
63+
with pytest.raises(ValueError, match="sha256"):
64+
validate_crfb_post_obbba_tob_metadata(metadata)

0 commit comments

Comments
 (0)