Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions backend/scripts/build_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,105 @@
_BASE_MODEL_DOC = inspect.getdoc(BaseModel) or ""


# ---------------------------------------------------------------------------
# Canonical reform recipes
# ---------------------------------------------------------------------------
# Copy-pasteable Python the agent can adapt verbatim inside `run_python`.
# Critical lesson from production: `Parameters(income_tax={"uk_brackets": [...]})`
# REPLACES the full schedule — passing a single-bracket list zeroes out the
# higher and additional rates and tanks revenue. To raise one rate you must
# pass the FULL bracket list with only that rate changed (recipe 1 below).
#
# Coverage: keys verified against backend/tests/test_agent_tools.py are marked
# VERIFIED. The remaining recipes use the conventional PolicyEngine UK naming
# from `_build_compiled_policy` (backend/agent_tools.py) — they should validate
# against `pe.Parameters.model_json_schema()` which is dumped further down in
# this reference. If a field name below is rejected, look it up in the JSON
# schema section and use the closest match.
REFORM_RECIPES = '''## Reform recipes

Canonical, copy-pasteable reforms. Each recipe constructs a *partial* override
on top of the current-law baseline — do not invent your own bracket lists from
scratch unless you really want to replace an entire schedule.

If a field name below is rejected by `Parameters(...)`, search the Parameters
JSON schema section of this reference for the closest match — programme keys
(`income_tax`, `national_insurance`, `child_benefit`, ...) and field names are
authoritative there.

### 1. Basic rate +1pp (20% -> 21%) — VERIFIED

`uk_brackets` replaces the full bracket list, so always pass every band with
just the one rate changed. This is the recipe the agent was failing on in
production.

```python
reform = Parameters(income_tax={"uk_brackets": [
{"rate": 0.21, "threshold": 0.0}, # basic (was 0.20)
{"rate": 0.40, "threshold": 37700.0}, # higher (unchanged)
{"rate": 0.45, "threshold": 125140.0}, # additional (unchanged)
]})
sim = Simulation(year=2025, dataset="efrs")
result = sim.run(policy=reform).budgetary_impact.model_dump()
```

### 2. Personal allowance £12,570 -> £15,000 — VERIFIED

```python
reform = Parameters(income_tax={"personal_allowance": 15000})
sim = Simulation(year=2025, dataset="efrs")
result = sim.run(policy=reform).budgetary_impact.model_dump()
```

### 3. NI primary threshold +£1,000

```python
# Verify field name against the Parameters JSON schema for `national_insurance`
# if this is rejected; primary-threshold-style names vary by release.
reform = Parameters(national_insurance={"primary_threshold": 13570})
sim = Simulation(year=2025, dataset="efrs")
result = sim.run(policy=reform).budgetary_impact.model_dump()
```

### 4. Child benefit +10% uprating

```python
# Verify the exact rate field name against the `child_benefit` schema;
# common names are `amount_for_first_child` / `amount_for_additional_child`.
reform = Parameters(child_benefit={
"amount_for_first_child": 26.95, # 24.50 * 1.10 (weekly £)
"amount_for_additional_child": 17.85, # 16.95 * 1.10 (weekly £)
})
sim = Simulation(year=2025, dataset="efrs")
result = sim.run(policy=reform).budgetary_impact.model_dump()
```

### 5. Marriage allowance — turn off

```python
# Marriage allowance lives inside `income_tax` in the compiled engine.
# Verify the exact field name (e.g. `marriage_allowance_fraction` or
# `marriage_allowance`) against the Parameters JSON schema before relying on
# this for production analysis.
reform = Parameters(income_tax={"marriage_allowance": 0.0})
sim = Simulation(year=2025, dataset="efrs")
result = sim.run(policy=reform).budgetary_impact.model_dump()
```

### Patterns to apply across all recipes

- `Parameters(programme={...})` is a *partial* override on the named programme:
fields you omit keep their current-law values. The one exception is list-
valued fields like `uk_brackets`, which are replaced wholesale — always
spell out the full list with only the rates/thresholds you intended to move.
- For distributional output, use `sim.run(policy=reform).decile_impacts` and
`.winners_losers`; for revenue, `.budgetary_impact`.
- `dataset="efrs"` is the Enhanced FRS — usually the default for distributional
work. Check `capabilities()` for the live list.
'''



def _own_doc(obj) -> str:
"""Return the object's own docstring, ignoring docs inherited from bases.

Expand Down Expand Up @@ -116,6 +215,10 @@ def render() -> str:
lines.append(f"```python\n{name} = {value_repr}\n```")
lines.append("")

# --- Reform recipes (canonical patterns) ---
lines.append(REFORM_RECIPES)
lines.append("")

# --- Parameters schema ---
lines.append("## Parameters JSON schema")
lines.append("")
Expand Down