$npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-model-developmentLoad when editing a PolicyEngine country model repo (policyengine-us, policyengine-uk, policyengine-canada) — adding or changing variables, parameters, YAML tests, or contributed reforms. This is the engineering layer (how formulas, parameter YAML, and tests are written), distinc
| 1 | # PolicyEngine model development |
| 2 | |
| 3 | Engineering patterns for the country-model repos — `policyengine-us`, `policyengine-uk`, |
| 4 | `policyengine-canada`. These packages encode enacted law (and proposed reforms) as **variables** |
| 5 | (Python formulas) driven by **parameters** (YAML), validated by **YAML tests**, all running on |
| 6 | the vectorized policyengine-core engine. |
| 7 | |
| 8 | This skill is the *how-to-write-it* layer. To run calculations or score reforms, use the |
| 9 | `policyengine` skill instead. Verified against policyengine-us 1.764.x / policyengine-core 3.30.x |
| 10 | (2026-07). |
| 11 | |
| 12 | ## Mental model |
| 13 | |
| 14 | - **Parameters** are a YAML tree under `parameters/gov/...`. Each leaf is a dated value (or a |
| 15 | bracket/breakdown table) with required metadata. Accessed in formulas as |
| 16 | `parameters(period).gov.agency.program.thing`. Federal under `gov/{agency}/`, state under |
| 17 | `gov/states/{st}/`, contributed reforms under `gov/contrib/`. |
| 18 | - **Variables** live one-per-file under `variables/gov/...`, each a `Variable` subclass with |
| 19 | `value_type` / `entity` / `definition_period` / metadata and either a `formula` **or** an |
| 20 | `adds`/`subtracts` list (never both). Formulas run vectorized over the whole population. |
| 21 | - **Entities** nest: `Person` → `TaxUnit` / `SPMUnit` / `Family` / `MaritalUnit` / `Household` |
| 22 | (US); `Person` → `BenUnit` / `Household` (UK). Aggregation across entity levels is automatic |
| 23 | via `adds`/`add()`. |
| 24 | - **Tests** are YAML files mirroring the variable path under `tests/policy/baseline/...`, |
| 25 | asserting outputs for a described household at a period. |
| 26 | |
| 27 | ## The absolute musts |
| 28 | |
| 29 | 1. **Never hardcode a numeric policy value in a formula.** Every threshold, rate, and amount |
| 30 | comes from a parameter (`p.income_limit`, not `2000`). Bare `0`/`1`/`-1` and `MONTHS_IN_YEAR` |
| 31 | are the only acceptable literals. See references/variables.md. |
| 32 | 2. **Vectorize everything.** No `if`/`elif`/`else` or `and`/`or`/`not` on entity arrays — use |
| 33 | `where` / `select` / `&` / `|` / `~`. Python `if` is allowed **only** on scalar parameters |
| 34 | (`if p.flat_applies:`). See references/vectorization.md. |
| 35 | 3. **`adds`/`subtracts` XOR `formula` — never both** in one variable. A pure sum uses the `adds` |
| 36 | attribute with no formula; anything else uses a formula (with `add()` inside). See |
| 37 | references/periods-and-aggregation.md. |
| 38 | 4. **Get the period right.** From a MONTH formula, YEAR flow variables (income) use `period` |
| 39 | (auto ÷12); YEAR stocks/counts/ages/booleans use `period.this_year` (no division). See |
| 40 | references/periods-and-aggregation.md. |
| 41 | 5. **`uv run` for everything.** `uv run pytest ...`, `uv run policyengine-core test ...`. Never |
| 42 | bare `pytest`. Format with `uv run ruff` (line length **88**, the default — not 79). There is |
| 43 | **no `black`** in this toolchain. |
| 44 | 6. **Changelog = a towncrier fragment**, never an edit to `CHANGELOG.md`. Write |
| 45 | `changelog.d/{branch-name}.{added|fixed|changed}.md` with one line. |
| 46 | 7. **Branch from the PolicyEngine upstream repo, not a fork.** Encoding work targets |
| 47 | `origin/main` of `PolicyEngine/policyengine-us` (etc.). |
| 48 | |
| 49 | ## Routing |
| 50 | |
| 51 | | You are working on… | Read | |
| 52 | |---|---| |
| 53 | | A variable: `value_type`/`entity`/`defined_for`, `adds` vs formula, naming, gotchas (age float, `monthly_age`, `is_ssi_eligible`), federal aggregators | references/variables.md | |
| 54 | | A parameter: YAML structure, brackets (`-.inf`, `.inf`, `single_amount`), breakdowns, metadata, path syntax | references/parameters.md | |
| 55 | | Periods (`period` vs `period.this_year`, ÷12) or aggregation (`adds`/`add()`, `.any()`) | references/periods-and-aggregation.md | |
| 56 | | Vectorization: `where`/`select`, `max_(A-B,0)` floors, divide-by-zero, phantom values | references/vectorization.md | |
| 57 | | YAML tests: structure, period restrictions, error margins, enums, the CLI | references/tests.md | |
| 58 | | A contributed reform under `contrib/` / `reforms/`: factory, `in_effect`, registration | references/reforms.md | |
| 59 | | Code style, framework constants, folder layout, review response | references/style.md | |
| 60 | |
| 61 | ## Setup |
| 62 | |
| 63 | ```bash |
| 64 | c |