commit d698b2d1d1024c0b4f8dae9185c22fdf653d56fc Author: Zhongwei Li Date: Sun Nov 30 08:47:49 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..656bda9 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "api-development", + "description": "API development - Flask endpoints, caching, services, and integration patterns", + "version": "0.0.0-2025.11.28", + "author": { + "name": "PolicyEngine", + "email": "hello@policyengine.org" + }, + "skills": [ + "./skills/policyengine-api-skill", + "./skills/policyengine-core-skill", + "./skills/policyengine-us-skill", + "./skills/policyengine-uk-skill", + "./skills/policyengine-design-skill", + "./skills/policyengine-standards-skill", + "./skills/policyengine-writing-skill" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb6b7f3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# api-development + +API development - Flask endpoints, caching, services, and integration patterns diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..909d143 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,100 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:PolicyEngine/policyengine-claude:api-development", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "fc9b2ce7ec8753ea2421e3fab88e06f8229cd387", + "treeHash": "04bb71ef391bee5bc67d21d30c90dbcb03f391e635de60d70e9c6f5141612b1c", + "generatedAt": "2025-11-28T10:12:36.259353Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "api-development", + "description": "API development - Flask endpoints, caching, services, and integration patterns" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "10ac80e0b432371a1db39c245358469918e1aa3829bd6890d0266ea27f000e49" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "6bbcc684fe3787dabf68f8a886bcf9b9e62c2b21f3ea2c5c0f6914aacfbff9d7" + }, + { + "path": "skills/policyengine-writing-skill/SKILL.md", + "sha256": "7b1b6dfecb7db0cfde2cc87548e5292c4851c1d9062b549907cbcd9959d7fb19" + }, + { + "path": "skills/policyengine-us-skill/SKILL.md", + "sha256": "434d93548e3c792320c2ac4e736ec3327218829df30e3b7f06336bac833b2833" + }, + { + "path": "skills/policyengine-us-skill/examples/donation_sweep.yaml", + "sha256": "2d9294d931daa667c66c8a8a011524d15adbafb068f764b3d261086e0774ff7e" + }, + { + "path": "skills/policyengine-us-skill/examples/single_filer.yaml", + "sha256": "d14503e102c796e2141d56d85db4405f1681756d58158469f97ebc0a6c0f022f" + }, + { + "path": "skills/policyengine-us-skill/scripts/situation_helpers.py", + "sha256": "0fa2858e702ff64bbece06e1b383d9a07a034abb72f5467323aca8c8db40a97d" + }, + { + "path": "skills/policyengine-standards-skill/SKILL.md", + "sha256": "8d59f51c19d85bfd2d3cba44e51e33a2061e34bc7523a10a0811a0919e76aa5b" + }, + { + "path": "skills/policyengine-uk-skill/SKILL.md", + "sha256": "02cfc0e284c7d57be4cc265a6b57145f5485022a9b4b29a300e43c575a476890" + }, + { + "path": "skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml", + "sha256": "6d8577eded1f047b7c064b4344de3e1cdb85e410f6d3240986fe44f2efef4669" + }, + { + "path": "skills/policyengine-uk-skill/examples/couple.yaml", + "sha256": "e1d115fae8f72cea29c00fb2aeb712bff8e2f6ed29ddd6515692695a1a79ffc1" + }, + { + "path": "skills/policyengine-uk-skill/examples/family_with_children.yaml", + "sha256": "b6f69b29ae9827b20a2d01c8fc5daf614d1fe875d4d680b7a21273f707671c16" + }, + { + "path": "skills/policyengine-uk-skill/examples/single_person.yaml", + "sha256": "5f96d30e99f1e0f834cdcaf575bc260d681314d614f27d975f60d05202606995" + }, + { + "path": "skills/policyengine-uk-skill/scripts/situation_helpers.py", + "sha256": "8281fad497f12c98b82a10a5474e502a2e48dd4ece0bdc2ff6da846b9fc12049" + }, + { + "path": "skills/policyengine-design-skill/SKILL.md", + "sha256": "ca0653af9eda135b5f98c64446b35474ba7bd641dae111c666e6f6995e39c70c" + }, + { + "path": "skills/policyengine-api-skill/SKILL.md", + "sha256": "017c98bf207b8374ab6bb7d3ab2eaff3fc8e6358b61461d24e7a68093f8cf2b9" + }, + { + "path": "skills/policyengine-core-skill/SKILL.md", + "sha256": "c4b7c24d87a787c018a9595925f5dfa1eed52540ca804cff6d4706dfdca96b33" + } + ], + "dirSha256": "04bb71ef391bee5bc67d21d30c90dbcb03f391e635de60d70e9c6f5141612b1c" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/policyengine-api-skill/SKILL.md b/skills/policyengine-api-skill/SKILL.md new file mode 100644 index 0000000..0f01100 --- /dev/null +++ b/skills/policyengine-api-skill/SKILL.md @@ -0,0 +1,478 @@ +--- +name: policyengine-api +description: PolicyEngine API - Flask REST service powering policyengine.org and programmatic access +--- + +# PolicyEngine API + +The PolicyEngine API is a Flask-based REST service that provides tax and benefit calculations for the web app and programmatic users. + +## For Users 👥 + +### What is the API? + +When you use policyengine.org, the API processes your calculations on our servers. + +**API base:** https://api.policyengine.org + +**What it does:** +- Runs tax and benefit calculations +- Stores and retrieves policy reforms +- Computes population-wide impacts +- Serves parameter and variable metadata + +### Public Access + +The API is publicly accessible with rate limits: +- **Unauthenticated:** 100 requests/minute +- **Authenticated:** 1,000 requests/minute + +**Try it:** +```bash +curl https://api.policyengine.org/us/policy/2 +``` + +### API Documentation + +**OpenAPI spec:** https://api.policyengine.org/docs + +**Interactive docs:** Swagger UI at API docs endpoint + +## For Analysts 📊 + +### Using the API + +**Option 1: Python client (recommended)** +```python +# Use the policyengine package +# See policyengine-python-client-skill +``` + +**Option 2: Direct API calls** +```python +import requests + +# Calculate household impact +response = requests.post( + "https://api.policyengine.org/us/calculate", + json={ + "household": household_situation, + "policy_id": None # or reform_id + } +) +result = response.json() +``` + +### Key Endpoints + +**Household calculations:** +``` +POST /us/calculate +POST /uk/calculate +``` + +**Policy management:** +``` +GET /us/policy/{policy_id} +POST /us/policy +``` + +**Economy impacts:** +``` +GET /us/economy/{policy_id}/over/{baseline_policy_id} +``` + +**Metadata:** +``` +GET /us/parameters +GET /us/variables +GET /us/parameter/{parameter_name} +GET /us/variable/{variable_name} +``` + +### Rate Limits and Performance + +**Rate limits:** +- 100 req/min (unauthenticated) +- 1,000 req/min (authenticated - contact team) + +**Response times:** +- Household calculation: ~200-500ms +- Population impact: ~5-30 seconds +- Cached results: <100ms + +**Optimization:** +- Use the same policy_id for multiple requests (caching) +- Batch calculations when possible +- Use webhooks for long-running jobs (population impacts) + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-api + +**Clone:** +```bash +git clone https://github.com/PolicyEngine/policyengine-api +cd policyengine-api +``` + +### Current Architecture + +**To see current structure:** +```bash +tree policyengine_api/ + +# Key directories: +ls policyengine_api/ +# - endpoints/ - HTTP endpoint handlers +# - routes/ - Route registration +# - services/ - Business logic +# - compute_api/ - Calculation services +# - economy_api/ - Economy impact calculations +# - utils/ - Helpers (caching, validation) +# - data/ - Static data +``` + +### Current Implementation Patterns + +**Reference endpoint (read this first):** +```bash +cat policyengine_api/endpoints/economy.py +``` + +**This demonstrates:** +- Standard endpoint structure +- Request validation +- Caching pattern +- Error handling +- Response formatting + +**To find other endpoints:** +```bash +ls policyengine_api/endpoints/ +# - household.py +# - policy.py +# - economy.py +# - metadata.py +# - etc. +``` + +### Standard Endpoint Pattern (Stable) + +```python +from flask import Blueprint, request, jsonify +from policyengine_api.utils import cache + +blueprint = Blueprint("my_endpoint", __name__) + +@blueprint.route("/us/calculate", methods=["POST"]) +def calculate(): + """Standard pattern: validate, cache-check, compute, cache, return.""" + try: + # 1. Get and validate input + data = request.json + if not data: + return jsonify({"error": "No data provided"}), 400 + + # 2. Generate cache key + cache_key = f"calc_{hash(str(data))}" + + # 3. Check cache + cached = cache.get(cache_key) + if cached: + return jsonify(cached) + + # 4. Compute + result = perform_calculation(data) + + # 5. Cache result + cache.set(cache_key, result, expire=3600) + + # 6. Return + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e), "status": "error"}), 500 +``` + +**Current implementation details:** +```bash +# See actual endpoint for current pattern +cat policyengine_api/endpoints/household.py +``` + +### Caching Strategy + +**To see current caching implementation:** +```bash +# Redis configuration +cat policyengine_api/utils/cache.py + +# Find cache usage +grep -r "cache\." policyengine_api/endpoints/ +``` + +**Pattern:** +- Redis for caching +- Cache keys based on inputs +- TTL varies by endpoint (1 hour to 1 day) +- Clear cache on parameter changes + +### Background Jobs + +For long-running calculations (population impacts): + +**To see current implementation:** +```bash +# RQ (Redis Queue) usage +grep -r "@job" policyengine_api/ + +# Job patterns +cat policyengine_api/economy_api/ +``` + +**Pattern:** +- Use RQ for jobs > 5 seconds +- Return job_id immediately +- Poll for completion +- Cache results + +### Country Integration + +**How API loads country packages:** +```bash +cat policyengine_api/country.py +``` + +**Pattern:** +- Dynamically imports country packages +- Routes by country code (/us/, /uk/) +- Manages multiple model versions + +### Service Layer + +**Business logic separated from endpoints:** +```bash +ls policyengine_api/services/ +``` + +**Pattern:** +```python +# endpoints/household.py +from policyengine_api.services import household_service + +@app.route("/us/calculate", methods=["POST"]) +def calculate(): + result = household_service.calculate(data) + return jsonify(result) + +# services/household_service.py +def calculate(data): + # Business logic here + simulation = create_simulation(data) + return simulation.calculate(...) +``` + +### Testing + +**To see current test patterns:** +```bash +ls tests/ +cat tests/test_household.py +``` + +**Run tests:** +```bash +make test + +# Specific test +pytest tests/test_economy.py -v + +# With coverage +make test-coverage +``` + +### Development Server + +**Start locally:** +```bash +make debug +``` + +**Test endpoint:** +```bash +curl http://localhost:5000/us/policy/2 +``` + +### Deployment + +**To see deployment configuration:** +```bash +# Google Cloud Platform +cat app.yaml # App Engine config +cat cloudbuild.yaml # Cloud Build config + +# Environment variables +cat .env.example +``` + +**Current deployment:** +- Google App Engine +- Cloud SQL (PostgreSQL) +- Redis (caching) +- Cloud Build (CI/CD) + +### API Versions + +**To see versioning strategy:** +```bash +grep -r "version" policyengine_api/ +``` + +**Current approach:** +- API version in URLs (may add /v1/ prefix) +- Country package versions independent +- Breaking changes rare (backwards compatible) + +## Architecture Diagrams + +### Request Flow + +``` +User/App → API Gateway → Flask App → Country Package → Core Engine + ↓ + Redis Cache + ↓ + Background Job (if needed) + ↓ + PostgreSQL (storage) +``` + +### Dependencies + +``` +policyengine-core + ↓ +policyengine-us, policyengine-uk, etc. + ↓ +policyengine-api (you are here) + ↓ +policyengine-app (consumes API) +``` + +**To understand dependencies:** +- See `policyengine-core-skill` for engine patterns +- See `policyengine-us-skill` for country model usage +- See `policyengine-app-skill` for how app calls API + +## Common Development Tasks + +### Task 1: Add New Endpoint + +1. **Study reference implementation:** + ```bash + cat policyengine_api/endpoints/economy.py + ``` + +2. **Create new endpoint file:** + ```python + # policyengine_api/endpoints/my_endpoint.py + # Follow the pattern from economy.py + ``` + +3. **Register route:** + ```bash + # See route registration + cat policyengine_api/routes/__init__.py + ``` + +4. **Add tests:** + ```bash + # Follow test pattern + cat tests/test_economy.py + ``` + +### Task 2: Modify Caching Behavior + +**See current caching:** +```bash +cat policyengine_api/utils/cache.py +``` + +**Common changes:** +- Adjust TTL (time to live) +- Change cache key generation +- Add cache invalidation + +### Task 3: Update Country Package Version + +**To see how versions are managed:** +```bash +# Requirements +cat requirements.txt | grep policyengine- + +# Update and deploy +# See deployment docs in README +``` + +## Security and Best Practices + +### Input Validation + +**Always validate:** +- Country code (us, uk, ca) +- Policy ID format +- Household structure +- Parameter values + +**See validation examples:** +```bash +grep -r "validate" policyengine_api/endpoints/ +``` + +### Error Handling + +**Standard error response:** +```python +return jsonify({ + "error": "Error message", + "details": additional_context, + "status": "error" +}), status_code +``` + +**See error patterns:** +```bash +grep -A 5 "jsonify.*error" policyengine_api/endpoints/ +``` + +### Logging + +**To see logging configuration:** +```bash +cat policyengine_api/gcp_logging.py +``` + +**Pattern:** +- Google Cloud Logging +- Log all errors +- Log slow queries (>1s) +- Don't log sensitive data + +## Related Skills + +- **policyengine-python-client-skill** - Using the API +- **policyengine-core-skill** - Understanding the engine +- **policyengine-us-skill** - Country model integration +- **policyengine-app-skill** - How app consumes API +- **policyengine-standards-skill** - Code quality +- **policyengine-writing-skill** - API documentation style + +## Resources + +**Repository:** https://github.com/PolicyEngine/policyengine-api +**Live API:** https://api.policyengine.org +**Documentation:** https://api.policyengine.org/docs +**Status:** https://status.policyengine.org diff --git a/skills/policyengine-core-skill/SKILL.md b/skills/policyengine-core-skill/SKILL.md new file mode 100644 index 0000000..e49044d --- /dev/null +++ b/skills/policyengine-core-skill/SKILL.md @@ -0,0 +1,487 @@ +--- +name: policyengine-core +description: PolicyEngine Core simulation engine - the foundation powering all PolicyEngine calculations +--- + +# PolicyEngine Core + +PolicyEngine Core is the microsimulation engine that powers all PolicyEngine calculations. It's a fork of OpenFisca-Core adapted for PolicyEngine's needs. + +## For Users 👥 + +### What is Core? + +When you use policyengine.org to calculate taxes or benefits, PolicyEngine Core is the "calculator" running behind the scenes. + +**Core provides:** +- The simulation engine that processes tax rules +- Variable and parameter management +- Entity relationships (person → family → household) +- Period handling (2024, 2025, etc.) + +You don't interact with Core directly - you use it through: +- **Web app:** policyengine.org +- **Python packages:** policyengine-us, policyengine-uk +- **API:** api.policyengine.org + +### Why Core Matters + +Core ensures: +- ✅ **Accuracy** - Calculations follow official rules exactly +- ✅ **Consistency** - Same rules applied everywhere +- ✅ **Transparency** - All rules traceable to legislation +- ✅ **Performance** - Vectorized calculations for speed + +## For Analysts 📊 + +### Understanding Core Concepts + +When writing PolicyEngine code, you'll encounter Core concepts: + +**Variables:** +- Represent quantities (income_tax, ctc, snap, etc.) +- Defined for specific entities (person, household, tax_unit) +- Calculated from formulas or set directly + +**Parameters:** +- Policy rules that change over time (tax rates, benefit amounts) +- Organized hierarchically (gov.irs.credits.ctc.amount.base_amount) +- Stored in YAML files + +**Entities:** +- Person: Individual +- Family: Family unit +- Tax unit: Tax filing unit +- Household: Physical household +- Marital unit: Marital status grouping +- SPM unit: Supplemental Poverty Measure unit + +**Periods:** +- Year: 2024, 2025, etc. +- Month: 2024-01, 2024-02, etc. +- Specific dates: 2024-06-15 + +### Core in Action + +```python +from policyengine_us import Simulation + +# When you create a simulation +sim = Simulation(situation=household) + +# Core manages: +# - Entity relationships +# - Variable dependencies +# - Parameter lookups +# - Period conversions + +# When you calculate +result = sim.calculate("income_tax", 2024) + +# Core: +# 1. Checks if already calculated +# 2. Identifies dependencies (income → AGI → taxable income → tax) +# 3. Calculates dependencies first +# 4. Applies formulas +# 5. Returns result +``` + +### Core vs Country Packages + +**Core (policyengine-core):** +- Generic simulation engine +- No specific tax/benefit rules +- Variable and parameter infrastructure + +**Country packages (policyengine-us, etc.):** +- Built on Core +- Contain specific tax/benefit rules +- Define variables and parameters for that country + +**Relationship:** +``` +policyengine-core (engine) + ↓ powers +policyengine-us (US rules) + ↓ used by +policyengine-api (REST API) + ↓ serves +policyengine-app (web interface) +``` + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-core +**Origin:** Fork of OpenFisca-Core + +**Clone:** +```bash +git clone https://github.com/PolicyEngine/policyengine-core +``` + +### Current Architecture + +**To see current structure:** +```bash +tree policyengine_core/ + +# Key directories: +# - variables/ - Variable class and infrastructure +# - parameters/ - Parameter class and infrastructure +# - entities/ - Entity definitions +# - simulations/ - Simulation class +# - periods/ - Period handling +# - reforms/ - Reform application +``` + +**To understand a specific component:** +```bash +# Variable system +cat policyengine_core/variables/variable.py + +# Parameter system +cat policyengine_core/parameters/parameter.py + +# Simulation engine +cat policyengine_core/simulations/simulation.py + +# Entity system +cat policyengine_core/entities/entity.py +``` + +### Key Classes + +**Variable:** +```python +# To see Variable class implementation +cat policyengine_core/variables/variable.py + +# Variables in country packages inherit from this: +from policyengine_core.variables import Variable + +class income_tax(Variable): + value_type = float + entity = Person + label = "Income tax" + definition_period = YEAR + + def formula(person, period, parameters): + # Vectorized formula + return calculate_tax(...) +``` + +**Simulation:** +```python +# To see Simulation class implementation +cat policyengine_core/simulations/simulation.py + +# Manages calculation graph and caching +sim = Simulation(situation=situation) +sim.calculate("variable", period) +``` + +**Parameters:** +```python +# To see Parameter handling +cat policyengine_core/parameters/parameter_node.py + +# Access in formulas: +parameters(period).gov.irs.credits.ctc.amount.base_amount +``` + +### Vectorization (Critical!) + +Core requires vectorized operations - no if-elif-else with arrays: + +**❌ Wrong (scalar logic):** +```python +if age < 18: + eligible = True +else: + eligible = False +``` + +**✅ Correct (vectorized):** +```python +eligible = age < 18 # NumPy boolean array +``` + +**Why:** Core processes many households simultaneously for performance. + +**To see vectorization examples:** +```bash +# Search for where() usage (vectorized if-then-else) +grep -r "np.where" policyengine_core/ + +# Find select() usage (vectorized case statements) +grep -r "select" policyengine_core/ +``` + +### Formula Dependencies + +Core automatically resolves variable dependencies: + +```python +class taxable_income(Variable): + def formula(person, period, parameters): + # Core automatically calculates these first: + agi = person("adjusted_gross_income", period) + deduction = person("standard_deduction", period) + return agi - deduction + +class income_tax(Variable): + def formula(person, period, parameters): + # Core knows to calculate taxable_income first + taxable = person("taxable_income", period) + return apply_brackets(taxable, ...) +``` + +**To see dependency resolution:** +```bash +# Find trace functionality +grep -r "trace" policyengine_core/simulations/ + +# Enable in your code: +simulation.trace = True +simulation.calculate("income_tax", 2024) +``` + +### Period Handling + +**To see period implementation:** +```bash +cat policyengine_core/periods/period.py + +# Period types: +# - YEAR: 2024 +# - MONTH: 2024-01 +# - ETERNITY: permanent values +``` + +**Usage in variables:** +```python +# Annual variable +definition_period = YEAR # Called with 2024 + +# Monthly variable +definition_period = MONTH # Called with "2024-01" + +# Convert periods +yearly_value = person("monthly_income", period.this_year) * 12 +``` + +### Testing Core Changes + +**To run Core tests:** +```bash +cd policyengine-core +make test + +# Specific test +pytest tests/core/test_variables.py -v +``` + +**To test in country package:** +```bash +# Changes to Core affect all country packages +cd policyengine-us +pip install -e ../policyengine-core # Local development install +make test +``` + +### Key Differences from OpenFisca + +PolicyEngine Core differs from OpenFisca-Core: + +**To see PolicyEngine changes:** +```bash +# Compare to OpenFisca +# Core fork diverged to add: +# - Enhanced performance +# - Better error messages +# - PolicyEngine-specific features + +# See commit history for PolicyEngine changes +git log --oneline +``` + +## Core Development Workflow + +### Making Changes to Core + +1. **Clone repo:** + ```bash + git clone https://github.com/PolicyEngine/policyengine-core + ``` + +2. **Install for development:** + ```bash + make install + ``` + +3. **Make changes** to variable.py, simulation.py, etc. + +4. **Test locally:** + ```bash + make test + ``` + +5. **Test in country package:** + ```bash + cd ../policyengine-us + pip install -e ../policyengine-core + make test + ``` + +6. **Format and commit:** + ```bash + make format + git commit -m "Description" + ``` + +### Understanding Impact + +Changes to Core affect: +- ✅ All country packages (US, UK, Canada, IL, NG) +- ✅ The API +- ✅ The web app +- ✅ All analysis tools + +**Critical:** Always test in multiple country packages before merging. + +## Common Core Patterns + +### Pattern 1: Adding a New Variable Type + +**Current variable types:** +```bash +# See supported types +grep "value_type" policyengine_core/variables/variable.py +``` + +**Types:** int, float, bool, str, Enum, date + +### Pattern 2: Custom Formulas + +**Formula signature:** +```python +def formula(entity, period, parameters): + # entity: Person, TaxUnit, Household, etc. + # period: 2024, "2024-01", etc. + # parameters: Parameter tree for period + return calculated_value +``` + +**To see formula examples:** +```bash +# Search country packages for formulas +grep -A 10 "def formula" ../policyengine-us/policyengine_us/variables/ | head -50 +``` + +### Pattern 3: Parameter Access + +**Accessing parameters in formulas:** +```python +# Navigate parameter tree +param = parameters(period).gov.irs.credits.ctc.amount.base_amount + +# Parameters automatically valid for period +# No need to check dates manually +``` + +**To see parameter structure:** +```bash +# Example from country package +tree ../policyengine-us/policyengine_us/parameters/gov/ +``` + +## Advanced Topics + +### Formula Caching + +Core caches calculations automatically: +```python +# First call calculates +tax1 = sim.calculate("income_tax", 2024) + +# Second call returns cached value +tax2 = sim.calculate("income_tax", 2024) # Instant +``` + +### Neutralizing Variables + +```python +# Set variable to zero in reform +reform = { + "income_tax": { + "2024-01-01.2100-12-31": 0 + } +} +``` + +### Adding Variables + +Country packages add variables by inheriting from Core's Variable class. + +**See policyengine-us-skill for variable creation patterns.** + +## Resources + +**Repository:** https://github.com/PolicyEngine/policyengine-core + +**Documentation:** +- Core API docs (see README in repo) +- OpenFisca docs (original): https://openfisca.org/doc/ + +**Related skills:** +- **policyengine-us-skill** - Using Core through country packages +- **policyengine-standards-skill** - Code quality standards + +## Troubleshooting + +### Common Issues + +**Variable not found:** +```python +# Error: Variable 'income_tax' not found +# Solution: Variable is defined in country package, not Core +# Use policyengine-us, not policyengine-core directly +``` + +**Scalar vs array operations:** +```python +# Error: truth value of array is ambiguous +# Solution: Use np.where() instead of if-else +# See vectorization section above +``` + +**Period mismatch:** +```python +# Error: Cannot compute variable_name for period 2024-01 +# Solution: Check definition_period matches request +# YEAR variables need YEAR periods (2024, not "2024-01") +``` + +**To debug:** +```python +# Enable tracing +sim.trace = True +sim.calculate("variable", period) +# See calculation dependency tree +``` + +## Contributing to Core + +**Before contributing:** +1. Read Core README +2. Understand OpenFisca architecture +3. Test changes in multiple country packages +4. Follow policyengine-standards-skill + +**Development standards:** +- Python 3.10-3.13 +- Black formatting (79-char) +- Comprehensive tests +- No breaking changes without discussion diff --git a/skills/policyengine-design-skill/SKILL.md b/skills/policyengine-design-skill/SKILL.md new file mode 100644 index 0000000..d5526ca --- /dev/null +++ b/skills/policyengine-design-skill/SKILL.md @@ -0,0 +1,880 @@ +--- +name: policyengine-design +description: PolicyEngine visual identity - colors, fonts, logos, and branding for web apps, calculators, charts, and research +--- + +# PolicyEngine Design System + +PolicyEngine's visual identity and branding guidelines for creating consistent user experiences across web apps, calculators, charts, and research outputs. + +## For Users 👥 + +### PolicyEngine Visual Identity + +**Brand colors:** +- **Teal** (#39C6C0) - Primary accent color (buttons, highlights, interactive elements) +- **Blue** (#2C6496) - Secondary color (links, charts, headers) + +**Typography:** +- **Charts:** Roboto Serif +- **Web app:** System fonts (sans-serif) +- **Streamlit apps:** Default sans-serif + +**Logo:** +- Used in charts (bottom right) +- Blue version for light backgrounds +- White version for dark backgrounds + +### Recognizing PolicyEngine Content + +**You can identify PolicyEngine content by:** +- Teal accent color (#39C6C0) on buttons and interactive elements +- Blue (#2C6496) in charts and links +- Roboto Serif font in charts +- PolicyEngine logo in chart footer +- Clean, minimal white backgrounds +- Data-focused, quantitative presentation + +## For Analysts 📊 + +### Chart Branding + +When creating charts for PolicyEngine analysis, follow these guidelines: + +#### Color Palette + +**Primary colors:** +```python +TEAL_ACCENT = "#39C6C0" # Primary color (teal) +BLUE_PRIMARY = "#2C6496" # Secondary color (blue) +DARK_GRAY = "#616161" # Text color +``` + +**Extended palette:** +```python +# Blues +BLUE = "#2C6496" +BLUE_LIGHT = "#D8E6F3" +BLUE_PRESSED = "#17354F" +BLUE_98 = "#F7FAFD" +DARK_BLUE_HOVER = "#1d3e5e" +DARKEST_BLUE = "#0C1A27" + +# Teals +TEAL_ACCENT = "#39C6C0" +TEAL_LIGHT = "#F7FDFC" +TEAL_PRESSED = "#227773" + +# Grays +DARK_GRAY = "#616161" +GRAY = "#808080" +MEDIUM_LIGHT_GRAY = "#BDBDBD" +MEDIUM_DARK_GRAY = "#D2D2D2" +LIGHT_GRAY = "#F2F2F2" + +# Accents +WHITE = "#FFFFFF" +BLACK = "#000000" +DARK_RED = "#b50d0d" # For negative values +``` + +**See current colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +#### Plotly Chart Formatting + +**Standard PolicyEngine chart:** + +```python +import plotly.graph_objects as go + +def format_fig(fig): + """Format chart with PolicyEngine branding.""" + fig.update_layout( + # Typography + font=dict( + family="Roboto Serif", + color="black" + ), + + # Background + plot_bgcolor="white", + template="plotly_white", + + # Margins (leave room for logo) + margin=dict( + l=50, + r=100, + t=50, + b=120, + pad=4 + ), + + # Chart size + height=600, + width=800, + ) + + # Add PolicyEngine logo (bottom right) + fig.add_layout_image( + dict( + source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", + xref="paper", + yref="paper", + x=1.0, + y=-0.10, + sizex=0.10, + sizey=0.10, + xanchor="right", + yanchor="bottom" + ) + ) + + # Clean modebar + fig.update_layout( + modebar=dict( + bgcolor="rgba(0,0,0,0)", + color="rgba(0,0,0,0)" + ) + ) + + return fig + +# Usage +fig = go.Figure() +fig.add_trace(go.Scatter(x=x_data, y=y_data, line=dict(color=TEAL_ACCENT))) +fig = format_fig(fig) +``` + +**Current implementation:** +```bash +# See format_fig in action +cat givecalc/ui/visualization.py +cat policyengine-app/src/pages/policy/output/... +``` + +#### Chart Colors + +**For line charts:** +- Primary line: Teal (#39C6C0) or Blue (#2C6496) +- Background lines: Light gray (rgb(180, 180, 180)) +- Markers: Teal with 70% opacity + +**For bar charts:** +- Positive values: Teal (#39C6C0) +- Negative values: Dark red (#b50d0d) +- Neutral: Gray + +**For multiple series:** +Use variations of blue and teal, or discrete color scale: +```python +colors = ["#2C6496", "#39C6C0", "#17354F", "#227773"] +``` + +#### Typography + +**Charts:** +```python +font=dict(family="Roboto Serif", size=14, color="black") +``` + +**Axis labels:** +```python +xaxis=dict( + title=dict(text="Label", font=dict(size=14)), + tickfont=dict(size=12) +) +``` + +**Load Roboto font:** +```python +# In Streamlit apps +st.markdown(""" + + + +""", unsafe_allow_html=True) +``` + +### Streamlit App Branding + +**Streamlit configuration (.streamlit/config.toml):** + +```toml +[theme] +base = "light" +primaryColor = "#39C6C0" # Teal accent +backgroundColor = "#FFFFFF" # White background +secondaryBackgroundColor = "#F7FDFC" # Teal light +textColor = "#616161" # Dark gray + +[client] +toolbarMode = "minimal" +``` + +**Current implementation:** +```bash +cat givecalc/.streamlit/config.toml +cat salt-amt-calculator/.streamlit/config.toml # Other calculators +``` + +### Logo Usage + +**Logo URLs:** + +```python +# Blue logo (for light backgrounds) +LOGO_BLUE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png" + +# White logo (for dark backgrounds) +LOGO_WHITE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png" + +# SVG versions (scalable) +LOGO_BLUE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg" +LOGO_WHITE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg" +``` + +**Logo placement in charts:** +- Bottom right corner +- 10% of chart width +- Slightly below bottom edge (y=-0.10) + +**Current logos:** +```bash +ls policyengine-app/src/images/logos/policyengine/ +``` + +### Complete Example: Branded Chart + +```python +import plotly.graph_objects as go + +# PolicyEngine colors +TEAL_ACCENT = "#39C6C0" +BLUE_PRIMARY = "#2C6496" + +# Create chart +fig = go.Figure() + +# Add data +fig.add_trace(go.Scatter( + x=incomes, + y=taxes, + mode='lines', + name='Tax liability', + line=dict(color=TEAL_ACCENT, width=3) +)) + +# Apply PolicyEngine branding +fig.update_layout( + # Typography + font=dict(family="Roboto Serif", size=14, color="black"), + + # Title and labels + title="Tax liability by income", + xaxis_title="Income", + yaxis_title="Tax ($)", + + # Formatting + xaxis_tickformat="$,.0f", + yaxis_tickformat="$,.0f", + + # Appearance + plot_bgcolor="white", + template="plotly_white", + + # Size and margins + height=600, + width=800, + margin=dict(l=50, r=100, t=50, b=120, pad=4) +) + +# Add logo +fig.add_layout_image( + dict( + source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", + xref="paper", + yref="paper", + x=1.0, + y=-0.10, + sizex=0.10, + sizey=0.10, + xanchor="right", + yanchor="bottom" + ) +) + +# Show +fig.show() +``` + +## For Contributors 💻 + +### Brand Assets + +**Repository:** PolicyEngine/policyengine-app-v2 (current), PolicyEngine/policyengine-app (legacy) + +**Logo files:** +```bash +# Logos in app (both v1 and v2 use same logos) +ls policyengine-app/src/images/logos/policyengine/ +# - blue.png - For light backgrounds +# - white.png - For dark backgrounds +# - blue.svg - Scalable blue logo +# - white.svg - Scalable white logo +# - banners/ - Banner variations +# - profile/ - Profile/avatar versions +``` + +**Access logos:** +```bash +# View logo files (v1 repo has the assets) +cd policyengine-app/src/images/logos/policyengine/ +ls -la +``` + +### Color Definitions + +**⚠️ IMPORTANT: App V2 Transition** + +PolicyEngine is transitioning to policyengine-app-v2 with updated design tokens. Use app-v2 colors for new projects. + +**Current colors (policyengine-app-v2):** + +```typescript +// policyengine-app-v2/app/src/designTokens/colors.ts + +// Primary (teal) - 50 to 900 scale +primary[500]: "#319795" // Main teal +primary[400]: "#38B2AC" // Lighter teal +primary[600]: "#2C7A7B" // Darker teal + +// Blue scale +blue[700]: "#026AA2" // Primary blue +blue[500]: "#0EA5E9" // Lighter blue + +// Gray scale +gray[700]: "#344054" // Dark text +gray[100]: "#F2F4F7" // Light backgrounds + +// Semantic +success: "#22C55E" +warning: "#FEC601" +error: "#EF4444" + +// Background +background.primary: "#FFFFFF" +background.secondary: "#F5F9FF" + +// Text +text.primary: "#000000" +text.secondary: "#5A5A5A" +``` + +**To see current design tokens:** +```bash +cat policyengine-app-v2/app/src/designTokens/colors.ts +cat policyengine-app-v2/app/src/styles/colors.ts # Mantine integration +``` + +**Legacy colors (policyengine-app - still used in some projects):** + +```javascript +// policyengine-app/src/style/colors.js +TEAL_ACCENT = "#39C6C0" // Old teal (slightly different from v2) +BLUE = "#2C6496" // Old blue +DARK_GRAY = "#616161" // Old dark gray +``` + +**To see legacy colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +**Usage in React (app-v2):** +```typescript +import { colors } from 'designTokens'; + + + +// Links +Learn more + +// Text +

Description

+``` + +**Current colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +## Visual Guidelines + +### Chart Design Principles + +1. **Minimal decoration** - Let data speak +2. **White backgrounds** - Clean, print-friendly +3. **Clear axis labels** - Always include units +4. **Formatted numbers** - Currency ($), percentages (%), etc. +5. **Logo inclusion** - Bottom right, never intrusive +6. **Consistent sizing** - 800x600 standard +7. **Roboto Serif** - Professional, readable font + +### Color Usage Rules + +**Primary actions:** +- Use TEAL_ACCENT (#39C6C0) +- Buttons, highlights, current selection + +**Chart lines:** +- Primary data: TEAL_ACCENT or BLUE_PRIMARY +- Secondary data: BLUE_LIGHT or GRAY +- Negative values: DARK_RED (#b50d0d) + +**Backgrounds:** +- Main: WHITE (#FFFFFF) +- Secondary: TEAL_LIGHT (#F7FDFC) or BLUE_98 (#F7FAFD) +- Plot area: WHITE + +**Text:** +- Primary: BLACK (#000000) +- Secondary: DARK_GRAY (#616161) +- Muted: GRAY (#808080) + +### Accessibility + +**Color contrast requirements:** +- Text on background: 4.5:1 minimum (WCAG AA) +- DARK_GRAY on WHITE: ✅ Passes +- TEAL_ACCENT on WHITE: ✅ Passes for large text +- Use sufficient line weights for visibility + +**Don't rely on color alone:** +- Use patterns or labels for different data series +- Ensure charts work in grayscale + +## Common Branding Tasks + +### Task 1: Create Branded Plotly Chart + +1. **Define colors:** + ```python + TEAL_ACCENT = "#39C6C0" + BLUE_PRIMARY = "#2C6496" + ``` + +2. **Create chart:** + ```python + fig = go.Figure() + fig.add_trace(go.Scatter(x=x, y=y, line=dict(color=TEAL_ACCENT))) + ``` + +3. **Apply branding:** + ```python + fig = format_fig(fig) # See implementation above + ``` + +### Task 2: Setup Streamlit Branding + +1. **Create config directory:** + ```bash + mkdir .streamlit + ``` + +2. **Copy theme config:** + ```bash + cat givecalc/.streamlit/config.toml > .streamlit/config.toml + ``` + +3. **Verify in app:** + ```python + import streamlit as st + + st.button("Test", type="primary") # Should be teal + ``` + +### Task 3: Brand Consistency Check + +**Checklist:** +- [ ] Charts use Roboto Serif font +- [ ] Primary color is TEAL_ACCENT (#39C6C0) +- [ ] Secondary color is BLUE_PRIMARY (#2C6496) +- [ ] White backgrounds +- [ ] Logo in charts (bottom right) +- [ ] Currency formatted with $ and commas +- [ ] Percentages formatted with % +- [ ] Streamlit config.toml uses PolicyEngine theme + +## Reference Implementations + +### Excellent Examples + +**Streamlit calculators:** +```bash +# GiveCalc - Complete example +cat givecalc/ui/visualization.py +cat givecalc/.streamlit/config.toml + +# Other calculators +ls salt-amt-calculator/ +ls ctc-calculator/ +``` + +**Blog post charts:** +```bash +# Analysis with branded charts +cat policyengine-app/src/posts/articles/harris-eitc.md +cat policyengine-app/src/posts/articles/montana-tax-cuts-2026.md +``` + +**React app components:** +```bash +# Charts in app +cat policyengine-app/src/pages/policy/output/DistributionalImpact.jsx +``` + +### Don't Use These + +**❌ Wrong colors:** +```python +# Don't use random colors +color = "#FF5733" +color = "red" +color = "green" +``` + +**❌ Wrong fonts:** +```python +# Don't use other fonts for charts +font = dict(family="Arial") +font = dict(family="Times New Roman") +``` + +**❌ Missing logo:** +```python +# Don't skip the logo in charts for publication +# All published charts should include PolicyEngine logo +``` + +## Assets and Resources + +### Logo Files + +**In policyengine-app repository:** +```bash +policyengine-app/src/images/logos/policyengine/ +├── blue.png # Primary logo (light backgrounds) +├── white.png # Logo for dark backgrounds +├── blue.svg # Scalable blue logo +├── white.svg # Scalable white logo +├── banners/ # Banner variations +└── profile/ # Profile/avatar versions +``` + +**Raw URLs for direct use:** +```python +# Use these URLs in code +LOGO_URL = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png" +``` + +### Font Files + +**Roboto (charts):** +- Google Fonts: https://fonts.google.com/specimen/Roboto +- Family: Roboto Serif +- Weights: 300 (light), 400 (regular), 500 (medium), 700 (bold) + +**Loading:** +```html + +``` + +### Color Reference Files + +**JavaScript (React app):** +```bash +cat policyengine-app/src/style/colors.js +``` + +**Python (calculators, analysis):** +```python +# Define in constants.py or at top of file +TEAL_ACCENT = "#39C6C0" +BLUE_PRIMARY = "#2C6496" +DARK_GRAY = "#616161" +WHITE = "#FFFFFF" +``` + +## Brand Evolution + +**Current identity (2025):** +- Teal primary (#39C6C0) +- Blue secondary (#2C6496) +- Roboto Serif for charts +- Minimal, data-focused design + +**If brand evolves:** +- Colors defined in policyengine-app/src/style/colors.js are source of truth +- Update this skill to point to current definitions +- Never hardcode - always reference colors.js + +## Quick Reference + +### Color Codes + +| Color | Hex | Usage | +|-------|-----|-------| +| Teal Accent | #39C6C0 | Primary interactive elements | +| Blue Primary | #2C6496 | Secondary, links, charts | +| Dark Gray | #616161 | Body text | +| White | #FFFFFF | Backgrounds | +| Teal Light | #F7FDFC | Secondary backgrounds | +| Dark Red | #b50d0d | Negative values, errors | + +### Font Families + +| Context | Font | +|---------|------| +| Charts | Roboto Serif | +| Web app | System sans-serif | +| Streamlit | Default sans-serif | +| Code blocks | Monospace | + +### Logo URLs + +| Background | Format | URL | +|------------|--------|-----| +| Light | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png | +| Light | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg | +| Dark | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png | +| Dark | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg | + +## Related Skills + +- **policyengine-app-skill** - React component styling +- **policyengine-analysis-skill** - Chart creation patterns +- **policyengine-writing-skill** - Content style (complements visual style) + +## Resources + +**Brand assets:** PolicyEngine/policyengine-app/src/images/ +**Color definitions:** PolicyEngine/policyengine-app/src/style/colors.js +**Examples:** givecalc, salt-amt-calculator, crfb-tob-impacts diff --git a/skills/policyengine-standards-skill/SKILL.md b/skills/policyengine-standards-skill/SKILL.md new file mode 100644 index 0000000..2266849 --- /dev/null +++ b/skills/policyengine-standards-skill/SKILL.md @@ -0,0 +1,768 @@ +--- +name: policyengine-standards +description: PolicyEngine coding standards, formatters, CI requirements, and development best practices +--- + +# PolicyEngine Standards Skill + +Use this skill to ensure code meets PolicyEngine's development standards and passes CI checks. + +## When to Use This Skill + +- Before committing code to any PolicyEngine repository +- When CI checks fail with linting/formatting errors +- Setting up a new PolicyEngine repository +- Reviewing PRs for standard compliance +- When AI tools generate code that needs standardization + +## Critical Requirements + +### Python Version +⚠️ **MUST USE Python 3.13** - Do NOT downgrade to older versions +- Check version: `python --version` +- Use `pyproject.toml` to specify version requirements + +### Command Execution +⚠️ **ALWAYS use `uv run` for Python commands** - Never use bare `python` or `pytest` +- ✅ Correct: `uv run python script.py`, `uv run pytest tests/` +- ❌ Wrong: `python script.py`, `pytest tests/` +- This ensures correct virtual environment and dependencies + +### Documentation (Python Projects) +⚠️ **MUST USE Jupyter Book 2.0 (MyST-NB)** - NOT Jupyter Book 1.x +- Build docs: `myst build docs` (NOT `jb build`) +- Use MyST markdown syntax + +## Before Committing - Checklist + +1. **Write tests first** (TDD - see below) +2. **Format code**: `make format` or language-specific formatter +3. **Run tests**: `make test` to ensure all tests pass +4. **Check linting**: Ensure no linting errors +5. **Use config files**: Prefer config files over environment variables +6. **Reference issues**: Include "Fixes #123" in commit message + +## Creating Pull Requests + +### The CI Waiting Problem + +**Common failure pattern:** +``` +User: "Create a PR and mark it ready when CI passes" +Claude: "I've created the PR as draft. CI will take a while, I'll check back later..." +[Chat ends - Claude never checks back] +Result: PR stays in draft, user has to manually check CI and mark ready +``` + +### Solution: Use /create-pr Command + +**When creating PRs, use the /create-pr command:** + +```bash +/create-pr +``` + +**This command:** +- ✅ Creates PR as draft +- ✅ Actually waits for CI (polls every 15 seconds) +- ✅ Marks ready when CI passes +- ✅ Reports failures with details +- ✅ Handles timeouts gracefully + +**Why this works:** +The command contains explicit polling logic that Claude executes, so it actually waits instead of giving up. + +### If /create-pr is Not Available + +**If the command isn't installed, implement the pattern directly:** + +```bash +# 1. Create PR as draft +gh pr create --draft --title "Title" --body "Body" +PR_NUMBER=$(gh pr view --json number --jq '.number') + +# 2. Wait for CI (ACTUALLY WAIT - don't give up!) +POLL_INTERVAL=15 +ELAPSED=0 + +while true; do # No timeout - wait as long as needed + CHECKS=$(gh pr checks $PR_NUMBER --json status,conclusion) + TOTAL=$(echo "$CHECKS" | jq '. | length') + COMPLETED=$(echo "$CHECKS" | jq '[.[] | select(.status == "COMPLETED")] | length') + + echo "[$ELAPSED s] CI: $COMPLETED/$TOTAL completed" + + if [ "$COMPLETED" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then + FAILED=$(echo "$CHECKS" | jq '[.[] | select(.conclusion == "FAILURE")] | length') + if [ "$FAILED" -eq 0 ]; then + echo "✅ All CI passed! Marking ready..." + gh pr ready $PR_NUMBER + break + else + echo "❌ CI failed. PR remains draft." + gh pr checks $PR_NUMBER + break + fi + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) +done + +# Important: No timeout! Population simulations can take 30+ minutes. +``` + +### DO NOT Say "I'll Check Back Later" + +**❌ WRONG:** +``` +"I've created the PR as draft. CI checks will take a few minutes. +I'll check back later once they complete." +``` + +**Why wrong:** You cannot check back later. The chat session ends. + +**✅ CORRECT:** +``` +"I've created the PR as draft. Now polling CI status every 15 seconds..." +[Actually polls using while loop] +"CI checks completed. All passed! Marking PR as ready for review." +``` + +### When to Create Draft vs Ready + +**Always create as draft when:** +- CI checks are configured +- User asks to wait for CI +- Making automated changes +- Unsure if CI will pass + +**Create as ready only when:** +- User explicitly requests ready PR +- No CI configured +- CI already verified locally + +### PR Workflow Standards + +**Standard flow:** +```bash +# 1. Ensure branch is pushed +git push -u origin feature-branch + +# 2. Create PR as draft +gh pr create --draft --title "..." --body "..." + +# 3. Wait for CI (use polling loop - see pattern above) + +# 4. If CI passes: +gh pr ready $PR_NUMBER + +# 5. If CI fails: +echo "CI failed. PR remains draft. Fix issues and push again." +``` + +## Test-Driven Development (TDD) + +PolicyEngine follows Test-Driven Development practices across all repositories. + +### TDD Workflow + +**1. Write test first (RED):** +```python +# tests/test_new_feature.py +def test_california_eitc_calculation(): + """Test California EITC for family with 2 children earning $30,000.""" + situation = create_family(income=30000, num_children=2, state="CA") + sim = Simulation(situation=situation) + ca_eitc = sim.calculate("ca_eitc", 2024)[0] + + # Test fails initially (feature not implemented yet) + assert ca_eitc == 3000, "CA EITC should be $3,000 for this household" +``` + +**2. Implement feature (GREEN):** +```python +# policyengine_us/variables/gov/states/ca/tax/income/credits/ca_eitc.py +class ca_eitc(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + + def formula(tax_unit, period, parameters): + # Implementation to make test pass + federal_eitc = tax_unit("eitc", period) + return federal_eitc * parameters(period).gov.states.ca.tax.eitc.match +``` + +**3. Refactor (REFACTOR):** +```python +# Clean up, optimize, add documentation +# All while tests continue to pass +``` + +### TDD Benefits + +**Why PolicyEngine uses TDD:** +- ✅ **Accuracy** - Tests verify implementation matches regulations +- ✅ **Documentation** - Tests show expected behavior +- ✅ **Regression prevention** - Changes don't break existing features +- ✅ **Confidence** - Safe to refactor +- ✅ **Isolation** - Multi-agent workflow (test-creator and rules-engineer work separately) + +### TDD in Multi-Agent Workflow + +**Country model development:** +1. **@document-collector** gathers regulations +2. **@test-creator** writes tests from regulations (isolated, no implementation access) +3. **@rules-engineer** implements from regulations (isolated, no test access) +4. Both work from same source → tests verify implementation accuracy + +**See policyengine-core-skill and country-models agents for details.** + +### Test Examples + +**Python (pytest):** +```python +def test_ctc_for_two_children(): + """Test CTC calculation for married couple with 2 children.""" + situation = create_married_couple( + income_1=75000, + income_2=50000, + num_children=2, + child_ages=[5, 8] + ) + + sim = Simulation(situation=situation) + ctc = sim.calculate("ctc", 2024)[0] + + assert ctc == 4000, "CTC should be $2,000 per child" +``` + +**React (Jest + RTL):** +```javascript +import { render, screen } from '@testing-library/react'; +import TaxCalculator from './TaxCalculator'; + +test('displays calculated tax', () => { + render(); + + // Test what user sees, not implementation + expect(screen.getByText(/\$5,000/)).toBeInTheDocument(); +}); +``` + +### Test Organization + +**Python:** +``` +tests/ +├── test_variables/ +│ ├── test_income.py +│ ├── test_deductions.py +│ └── test_credits.py +├── test_parameters/ +└── test_simulations/ +``` + +**React:** +``` +src/ +├── components/ +│ └── TaxCalculator/ +│ ├── TaxCalculator.jsx +│ └── TaxCalculator.test.jsx +``` + +### Running Tests + +**Python:** +```bash +# All tests +make test + +# With uv +uv run pytest tests/ -v + +# Specific test +uv run pytest tests/test_credits.py::test_ctc_for_two_children -v + +# With coverage +uv run pytest tests/ --cov=policyengine_us --cov-report=html +``` + +**React:** +```bash +# All tests +make test + +# Watch mode +npm test -- --watch + +# Specific test +npm test -- TaxCalculator.test.jsx + +# Coverage +npm test -- --coverage +``` + +### Test Quality Standards + +**Good tests:** +- ✅ Test behavior, not implementation +- ✅ Clear, descriptive names +- ✅ Single assertion per test (when possible) +- ✅ Include documentation (docstrings) +- ✅ Based on official regulations with citations + +**Bad tests:** +- ❌ Testing private methods +- ❌ Mocking everything +- ❌ No assertion messages +- ❌ Magic numbers without explanation + +### Example: TDD for New Feature + +```python +# Step 1: Write test (RED) +def test_new_york_empire_state_child_credit(): + """Test NY Empire State Child Credit for family with 1 child. + + Based on NY Tax Law Section 606(c-1). + Family earning $50,000 with 1 child under 4 should receive $330. + """ + situation = create_family( + income=50000, + num_children=1, + child_ages=[2], + state="NY" + ) + + sim = Simulation(situation=situation) + credit = sim.calculate("ny_empire_state_child_credit", 2024)[0] + + assert credit == 330, "Should receive $330 for child under 4" + +# Test fails - feature doesn't exist yet + +# Step 2: Implement (GREEN) +# Create variable in policyengine_us/variables/gov/states/ny/... +# Test passes + +# Step 3: Refactor +# Optimize, add documentation, maintain passing tests +``` + +## Python Standards + +### Formatting +- **Formatter**: Black with 79-character line length +- **Command**: `make format` or `black . -l 79` +- **Check without changes**: `black . -l 79 --check` + +```bash +# Format all Python files +make format + +# Check if formatting is needed (CI-style) +black . -l 79 --check +``` + +### Code Style +```python +# Imports: Grouped and alphabetized +import os +import sys +from pathlib import Path # stdlib + +import numpy as np +import pandas as pd # third-party + +from policyengine_us import Simulation # local + +# Naming conventions +class TaxCalculator: # CamelCase for classes + pass + +def calculate_income_tax(income): # snake_case for functions + annual_income = income * 12 # snake_case for variables + return annual_income + +# Type hints (recommended) +def calculate_tax(income: float, state: str) -> float: + """Calculate state income tax. + + Args: + income: Annual income in dollars + state: Two-letter state code + + Returns: + Tax liability in dollars + """ + pass + +# Error handling - catch specific exceptions +try: + result = simulation.calculate("income_tax", 2024) +except KeyError as e: + raise ValueError(f"Invalid variable name: {e}") +``` + +### Testing +```python +import pytest + +def test_ctc_calculation(): + """Test Child Tax Credit calculation for family with 2 children.""" + situation = create_family(income=50000, num_children=2) + sim = Simulation(situation=situation) + ctc = sim.calculate("ctc", 2024)[0] + + assert ctc == 4000, "CTC should be $2000 per child" +``` + +**Run tests:** +```bash +# All tests +make test + +# Or with uv +uv run pytest tests/ -v + +# Specific test +uv run pytest tests/test_tax.py::test_ctc_calculation -v + +# With coverage +uv run pytest tests/ --cov=policyengine_us --cov-report=html +``` + +## JavaScript/React Standards + +### Formatting +- **Formatters**: Prettier + ESLint +- **Command**: `npm run lint -- --fix && npx prettier --write .` +- **CI Check**: `npm run lint -- --max-warnings=0` + +```bash +# Format all files +make format + +# Or manually +npm run lint -- --fix +npx prettier --write . + +# Check if formatting is needed (CI-style) +npm run lint -- --max-warnings=0 +``` + +### Code Style +```javascript +// Use functional components only (no class components) +import { useState, useEffect } from "react"; + +function TaxCalculator({ income, state }) { + const [tax, setTax] = useState(0); + + useEffect(() => { + // Calculate tax when inputs change + calculateTax(income, state).then(setTax); + }, [income, state]); + + return ( +
+

Tax: ${tax.toLocaleString()}

+
+ ); +} + +// File naming +// - Components: PascalCase.jsx (TaxCalculator.jsx) +// - Utilities: camelCase.js (formatCurrency.js) + +// Environment config - use config file pattern +// src/config/environment.js +const config = { + API_URL: process.env.NODE_ENV === 'production' + ? 'https://api.policyengine.org' + : 'http://localhost:5000' +}; +export default config; +``` + +### React Component Size +- Keep components under 150 lines after formatting +- Extract complex logic into custom hooks +- Split large components into smaller ones + +## Version Control Standards + +### Changelog Management + +**CRITICAL**: For PRs, ONLY modify `changelog_entry.yaml`. NEVER manually update `CHANGELOG.md` or `changelog.yaml`. + +**Correct Workflow:** +1. Create `changelog_entry.yaml` at repository root: + ```yaml + - bump: patch # or minor, major + changes: + added: + - Description of new feature + fixed: + - Description of bug fix + changed: + - Description of change + ``` + +2. Commit ONLY `changelog_entry.yaml` with your code changes + +3. GitHub Actions automatically updates `CHANGELOG.md` and `changelog.yaml` on merge + +**DO NOT:** +- ❌ Run `make changelog` manually during PR creation +- ❌ Commit `CHANGELOG.md` or `changelog.yaml` in your PR +- ❌ Modify main changelog files directly + +### Git Workflow + +1. **Create branches on PolicyEngine repos, NOT forks** + - Forks cause CI failures due to missing secrets + - Request write access if needed + +2. **Branch naming**: `feature-name` or `fix-issue-123` + +3. **Commit messages**: + ``` + Add CTC reform analysis for CRFB report + + - Implement household-level calculations + - Add state-by-state comparison + - Create visualizations + + Fixes #123 + ``` + +4. **PR description**: Include "Fixes #123" to auto-close issues + +### Common Git Pitfalls + +**Never do these:** +- ❌ Force push to main/master +- ❌ Commit secrets or `.env` files +- ❌ Skip hooks with `--no-verify` +- ❌ Create versioned files (app_v2.py, component_new.jsx) + +**Always do:** +- ✅ Fix original files in place +- ✅ Run formatters before pushing +- ✅ Reference issue numbers in commits +- ✅ Watch CI after filing PR + +## Common AI Pitfalls + +Since many PRs are AI-generated, watch for these common mistakes: + +### 1. File Versioning +**❌ Wrong:** +```bash +# Creating new versions instead of fixing originals +app_new.py +app_v2.py +component_refactored.jsx +``` + +**✅ Correct:** +```bash +# Always modify the original file +app.py # Fixed in place +``` + +### 2. Formatter Not Run +**❌ Wrong:** Committing without formatting (main cause of CI failures) + +**✅ Correct:** +```bash +# Python +make format +black . -l 79 + +# React +npm run lint -- --fix +npx prettier --write . +``` + +### 3. Environment Variables +**❌ Wrong:** +```javascript +// React env vars without REACT_APP_ prefix +const API_URL = process.env.API_URL; // Won't work! +``` + +**✅ Correct:** +```javascript +// Use config file pattern instead +import config from './config/environment'; +const API_URL = config.API_URL; +``` + +### 4. Using Wrong Python Version +**❌ Wrong:** Downgrading to Python 3.10 or older + +**✅ Correct:** Use Python 3.13 as specified in project requirements + +### 5. Manual Changelog Updates +**❌ Wrong:** Running `make changelog` and committing `CHANGELOG.md` + +**✅ Correct:** Only create `changelog_entry.yaml` in PR + +## Repository Setup Patterns + +### Python Package Structure +``` +policyengine-package/ +├── policyengine_package/ +│ ├── __init__.py +│ ├── core/ +│ ├── calculations/ +│ └── utils/ +├── tests/ +│ ├── test_calculations.py +│ └── test_core.py +├── pyproject.toml +├── Makefile +├── CLAUDE.md +├── CHANGELOG.md +└── README.md +``` + +### React App Structure +``` +policyengine-app/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ ├── config/ +│ │ └── environment.js +│ └── App.jsx +├── public/ +├── package.json +├── .eslintrc.json +├── .prettierrc +└── README.md +``` + +## Makefile Commands + +Standard commands across PolicyEngine repos: + +```bash +make install # Install dependencies +make test # Run tests +make format # Format code +make changelog # Update changelog (automation only, not manual) +make debug # Start dev server (apps) +make build # Production build (apps) +``` + +## CI Stability + +### Common CI Issues + +**1. Fork PRs Fail** +- **Problem**: PRs from forks don't have access to repository secrets +- **Solution**: Create branches directly on PolicyEngine repos + +**2. GitHub API Rate Limits** +- **Problem**: Smoke tests fail with 403 errors +- **Solution**: Re-run failed jobs (different runners have different limits) + +**3. Linting Failures** +- **Problem**: Code not formatted before commit +- **Solution**: Always run `make format` before committing + +**4. Test Failures in CI but Pass Locally** +- **Problem**: Missing `uv run` prefix +- **Solution**: Use `uv run pytest` instead of `pytest` + +## Best Practices Checklist + +### Code Quality +- [ ] Code formatted with Black (Python) or Prettier (JS) +- [ ] No linting errors +- [ ] All tests pass +- [ ] Type hints added (Python, where applicable) +- [ ] Docstrings for public functions/classes +- [ ] Error handling with specific exceptions + +### Version Control +- [ ] Only `changelog_entry.yaml` created (not CHANGELOG.md) +- [ ] Commit message references issue number +- [ ] Branch created on PolicyEngine repo (not fork) +- [ ] No secrets or .env files committed +- [ ] Original files modified (no _v2 or _new files) + +### Testing +- [ ] Tests written for new functionality +- [ ] Tests pass locally with `make test` +- [ ] Coverage maintained or improved +- [ ] Edge cases handled + +### Documentation +- [ ] README updated if needed +- [ ] Code comments for complex logic +- [ ] API documentation updated if needed +- [ ] Examples provided for new features + +## Quick Reference + +### Format Commands by Language + +**Python:** +```bash +make format # Format code +black . -l 79 --check # Check formatting +uv run pytest tests/ -v # Run tests +``` + +**React:** +```bash +make format # Format code +npm run lint -- --max-warnings=0 # Check linting +npm test # Run tests +``` + +### Pre-Commit Checklist +```bash +# 1. Format +make format + +# 2. Test +make test + +# 3. Check linting +# Python: black . -l 79 --check +# React: npm run lint -- --max-warnings=0 + +# 4. Stage and commit +git add . +git commit -m "Description + +Fixes #123" + +# 5. Push and watch CI +git push +``` + +## Resources + +- **Main CLAUDE.md**: `/PolicyEngine/CLAUDE.md` +- **Python Style**: PEP 8, Black documentation +- **React Style**: Airbnb React/JSX Style Guide +- **Testing**: pytest documentation, Jest/RTL documentation +- **Writing Style**: See policyengine-writing-skill for blog posts, PR descriptions, and documentation + +## Examples + +See PolicyEngine repositories for examples of standard-compliant code: +- **policyengine-us**: Python package standards +- **policyengine-app**: React app standards +- **givecalc**: Streamlit app standards +- **crfb-tob-impacts**: Analysis repository standards diff --git a/skills/policyengine-uk-skill/SKILL.md b/skills/policyengine-uk-skill/SKILL.md new file mode 100644 index 0000000..1ffd600 --- /dev/null +++ b/skills/policyengine-uk-skill/SKILL.md @@ -0,0 +1,660 @@ +--- +name: policyengine-uk +description: PolicyEngine-UK tax and benefit microsimulation patterns, situation creation, and common workflows +--- + +# PolicyEngine-UK + +PolicyEngine-UK models the UK tax and benefit system, including devolved variations for Scotland and Wales. + +## For Users 👥 + +### What is PolicyEngine-UK? + +PolicyEngine-UK is the "calculator" for UK taxes and benefits. When you use policyengine.org/uk, PolicyEngine-UK runs behind the scenes. + +**What it models:** + +**Direct taxes:** +- Income tax (UK-wide, Scottish, and Welsh variations) +- National Insurance (Classes 1, 2, 4) +- Capital gains tax +- Dividend tax + +**Property and transaction taxes:** +- Council Tax +- Stamp Duty Land Tax (England/NI) +- Land and Buildings Transaction Tax (Scotland) +- Land Transaction Tax (Wales) + +**Universal Credit:** +- Standard allowance +- Child elements +- Housing cost element +- Childcare costs element +- Carer element +- Work capability elements + +**Legacy benefits (being phased out):** +- Working Tax Credit +- Child Tax Credit +- Income Support +- Income-based JSA/ESA +- Housing Benefit + +**Other benefits:** +- Child Benefit +- Pension Credit +- Personal Independence Payment (PIP) +- Disability Living Allowance (DLA) +- Attendance Allowance +- State Pension + +**See full list:** https://policyengine.org/uk/parameters + +### Understanding Variables + +When you see results in PolicyEngine, these are variables: + +**Income variables:** +- `employment_income` - Gross employment earnings/salary +- `self_employment_income` - Self-employment profits +- `pension_income` - Private pension income +- `property_income` - Rental income +- `savings_interest_income` - Interest from savings +- `dividend_income` - Dividend income + +**Tax variables:** +- `income_tax` - Total income tax liability +- `national_insurance` - Total NI contributions +- `council_tax` - Council tax liability + +**Benefit variables:** +- `universal_credit` - Universal Credit amount +- `child_benefit` - Child Benefit amount +- `pension_credit` - Pension Credit amount +- `working_tax_credit` - Working Tax Credit (legacy) +- `child_tax_credit` - Child Tax Credit (legacy) + +**Summary variables:** +- `household_net_income` - Income after taxes and benefits +- `disposable_income` - Income after taxes +- `equivalised_household_net_income` - Adjusted for household size + +## For Analysts 📊 + +### Installation and Setup + +```bash +# Install PolicyEngine-UK +pip install policyengine-uk + +# Or with uv (recommended) +uv pip install policyengine-uk +``` + +### Quick Start + +```python +from policyengine_uk import Simulation + +# Create a household +situation = { + "people": { + "person": { + "age": {2025: 30}, + "employment_income": {2025: 30000} + } + }, + "benunits": { + "benunit": { + "members": ["person"] + } + }, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + } +} + +# Calculate taxes and benefits +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] + +print(f"Income tax: £{income_tax:,.0f}") +print(f"Universal Credit: £{universal_credit:,.0f}") +``` + +### Web App to Python + +**Web app URL:** +``` +policyengine.org/uk/household?household=12345 +``` + +**Equivalent Python (conceptually):** +The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation. + +### When to Use This Skill + +- Creating household situations for tax/benefit calculations +- Running microsimulations with PolicyEngine-UK +- Analyzing policy reforms and their impacts +- Building tools that use PolicyEngine-UK (calculators, analysis notebooks) +- Debugging PolicyEngine-UK calculations + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-uk + +**To see current implementation:** +```bash +git clone https://github.com/PolicyEngine/policyengine-uk +cd policyengine-uk + +# Explore structure +tree policyengine_uk/ +``` + +**Key directories:** +```bash +ls policyengine_uk/ +# - variables/ - Tax and benefit calculations +# - parameters/ - Policy rules (YAML) +# - reforms/ - Pre-defined reforms +# - tests/ - Test cases +``` + +## Core Concepts + +### 1. Situation Dictionary Structure + +PolicyEngine UK requires a nested dictionary defining household composition: + +```python +situation = { + "people": { + "person_id": { + "age": {2025: 35}, + "employment_income": {2025: 30000}, + # ... other person attributes + } + }, + "benunits": { + "benunit_id": { + "members": ["person_id", ...] + } + }, + "households": { + "household_id": { + "members": ["person_id", ...], + "region": {2025: "SOUTH_EAST"} + } + } +} +``` + +**Key Rules:** +- All entities must have consistent member lists +- Use year keys for all values: `{2025: value}` +- Region must be one of the ITL 1 regions (see below) +- All monetary values in pounds (not pence) +- UK tax year runs April 6 to April 5 (but use calendar year in code) + +**Important Entity Difference:** +- UK uses **benunits** (benefit units): a single adult OR couple + dependent children +- This is the assessment unit for most means-tested benefits +- Unlike US which uses families/marital_units/tax_units/spm_units + +### 2. Creating Simulations + +```python +from policyengine_uk import Simulation + +# Create simulation from situation +simulation = Simulation(situation=situation) + +# Calculate variables +income_tax = simulation.calculate("income_tax", 2025) +universal_credit = simulation.calculate("universal_credit", 2025) +household_net_income = simulation.calculate("household_net_income", 2025) +``` + +**Common Variables:** + +**Income:** +- `employment_income` - Gross employment earnings +- `self_employment_income` - Self-employment profits +- `pension_income` - Private pension income +- `property_income` - Rental income +- `savings_interest_income` - Interest income +- `dividend_income` - Dividend income +- `miscellaneous_income` - Other income sources + +**Tax Outputs:** +- `income_tax` - Total income tax liability +- `national_insurance` - Total NI contributions +- `council_tax` - Council tax liability +- `VAT` - Value Added Tax paid + +**Benefits:** +- `universal_credit` - Universal Credit +- `child_benefit` - Child Benefit +- `pension_credit` - Pension Credit +- `working_tax_credit` - Working Tax Credit (legacy) +- `child_tax_credit` - Child Tax Credit (legacy) +- `personal_independence_payment` - PIP +- `attendance_allowance` - Attendance Allowance +- `state_pension` - State Pension + +**Summary:** +- `household_net_income` - Income after taxes and benefits +- `disposable_income` - Income after taxes +- `equivalised_household_net_income` - Adjusted for household size + +### 3. Using Axes for Parameter Sweeps + +To vary a parameter across multiple values: + +```python +situation = { + # ... normal situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 100000, + "period": 2025 + }]] +} + +simulation = Simulation(situation=situation) +# Now calculate() returns arrays of 1001 values +incomes = simulation.calculate("employment_income", 2025) # Array of 1001 values +taxes = simulation.calculate("income_tax", 2025) # Array of 1001 values +``` + +**Important:** Remove axes before creating single-point simulations: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +simulation = Simulation(situation=situation_single) +``` + +### 4. Policy Reforms + +```python +from policyengine_uk import Simulation + +# Define a reform (modifies parameters) +reform = { + "gov.hmrc.income_tax.rates.uk.brackets[0].rate": { + "2025-01-01.2100-12-31": 0.25 # Increase basic rate to 25% + } +} + +# Create simulation with reform +simulation = Simulation(situation=situation, reform=reform) +``` + +## Common Patterns + +### Pattern 1: Single Person Household Calculation + +```python +from policyengine_uk import Simulation + +situation = { + "people": { + "person": { + "age": {2025: 30}, + "employment_income": {2025: 30000} + } + }, + "benunits": { + "benunit": { + "members": ["person"] + } + }, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + } +} + +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2025)[0] +national_insurance = sim.calculate("national_insurance", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] +``` + +### Pattern 2: Couple with Children + +```python +situation = { + "people": { + "parent_1": { + "age": {2025: 35}, + "employment_income": {2025: 35000} + }, + "parent_2": { + "age": {2025: 33}, + "employment_income": {2025: 25000} + }, + "child_1": { + "age": {2025: 8} + }, + "child_2": { + "age": {2025: 5} + } + }, + "benunits": { + "benunit": { + "members": ["parent_1", "parent_2", "child_1", "child_2"] + } + }, + "households": { + "household": { + "members": ["parent_1", "parent_2", "child_1", "child_2"], + "region": {2025: "NORTH_WEST"} + } + } +} + +sim = Simulation(situation=situation) +child_benefit = sim.calculate("child_benefit", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] +``` + +### Pattern 3: Marginal Tax Rate Analysis + +```python +# Create baseline with axes varying income +situation_with_axes = { + "people": { + "person": { + "age": {2025: 30} + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + }, + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 100000, + "period": 2025 + }]] +} + +sim = Simulation(situation=situation_with_axes) +incomes = sim.calculate("employment_income", 2025) +net_incomes = sim.calculate("household_net_income", 2025) + +# Calculate marginal tax rate +import numpy as np +mtr = 1 - (np.gradient(net_incomes) / np.gradient(incomes)) +``` + +### Pattern 4: Regional Comparison + +```python +regions = ["LONDON", "SCOTLAND", "WALES", "NORTH_EAST"] +results = {} + +for region in regions: + situation = create_situation(region=region, income=30000) + sim = Simulation(situation=situation) + results[region] = { + "income_tax": sim.calculate("income_tax", 2025)[0], + "national_insurance": sim.calculate("national_insurance", 2025)[0], + "total_tax": sim.calculate("income_tax", 2025)[0] + + sim.calculate("national_insurance", 2025)[0] + } +``` + +### Pattern 5: Policy Reform Impact + +```python +from policyengine_uk import Microsimulation, Reform + +# Define reform: Increase basic rate to 25% +class IncreaseBasicRate(Reform): + def apply(self): + def modify_parameters(parameters): + parameters.gov.hmrc.income_tax.rates.uk.brackets[0].rate.update( + period="year:2025:10", value=0.25 + ) + return parameters + self.modify_parameters(modify_parameters) + +# Run microsimulation +baseline = Microsimulation() +reformed = Microsimulation(reform=IncreaseBasicRate) + +# Calculate revenue impact +baseline_revenue = baseline.calc("income_tax", 2025).sum() +reformed_revenue = reformed.calc("income_tax", 2025).sum() +revenue_change = (reformed_revenue - baseline_revenue) / 1e9 # in billions + +# Calculate household impact +baseline_net_income = baseline.calc("household_net_income", 2025) +reformed_net_income = reformed.calc("household_net_income", 2025) +``` + +## Helper Scripts + +This skill includes helper scripts in the `scripts/` directory: + +```python +from policyengine_uk_skills.situation_helpers import ( + create_single_person, + create_couple, + create_family_with_children, + add_region +) + +# Quick situation creation +situation = create_single_person( + income=30000, + region="LONDON", + age=30 +) + +# Create couple +situation = create_couple( + income_1=35000, + income_2=25000, + region="SCOTLAND" +) +``` + +## Common Pitfalls and Solutions + +### Pitfall 1: Member Lists Out of Sync + +**Problem:** Different entities have different members +```python +# WRONG +"benunits": {"benunit": {"members": ["parent"]}}, +"households": {"household": {"members": ["parent", "child"]}} +``` + +**Solution:** Keep all entity member lists consistent: +```python +# CORRECT +all_members = ["parent", "child"] +"benunits": {"benunit": {"members": all_members}}, +"households": {"household": {"members": all_members}} +``` + +### Pitfall 2: Forgetting Year Keys + +**Problem:** `"age": 35` instead of `"age": {2025: 35}` + +**Solution:** Always use year dictionary: +```python +"age": {2025: 35}, +"employment_income": {2025: 30000} +``` + +### Pitfall 3: Wrong Region Format + +**Problem:** Using lowercase or incorrect region names + +**Solution:** Use uppercase ITL 1 region codes: +```python +# CORRECT regions: +"region": {2025: "LONDON"} +"region": {2025: "SCOTLAND"} +"region": {2025: "WALES"} +"region": {2025: "NORTH_EAST"} +"region": {2025: "SOUTH_EAST"} +``` + +### Pitfall 4: Axes Persistence + +**Problem:** Axes remain in situation when creating single-point simulation + +**Solution:** Remove axes before single-point simulation: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +``` + +### Pitfall 5: Missing Benunits + +**Problem:** Forgetting to include benunits (benefit units) + +**Solution:** Always include benunits in UK simulations: +```python +# UK requires benunits +situation = { + "people": {...}, + "benunits": {"benunit": {"members": [...]}}, # Required! + "households": {...} +} +``` + +## Regions in PolicyEngine UK + +UK uses ITL 1 (International Territorial Level 1, formerly NUTS 1) regions: + +**Regions:** +- `NORTH_EAST` - North East England +- `NORTH_WEST` - North West England +- `YORKSHIRE` - Yorkshire and the Humber +- `EAST_MIDLANDS` - East Midlands +- `WEST_MIDLANDS` - West Midlands +- `EAST_OF_ENGLAND` - East of England +- `LONDON` - London +- `SOUTH_EAST` - South East England +- `SOUTH_WEST` - South West England +- `WALES` - Wales +- `SCOTLAND` - Scotland +- `NORTHERN_IRELAND` - Northern Ireland + +**Regional Tax Variations:** + +**Scotland:** +- Has devolved income tax with 6 bands (starter 19%, basic 20%, intermediate 21%, higher 42%, advanced 45%, top 47%) +- Scottish residents automatically calculated with Scottish rates + +**Wales:** +- Has Welsh Rate of Income Tax (WRIT) +- Currently maintains parity with England/NI rates + +**England/Northern Ireland:** +- Standard UK rates: basic 20%, higher 40%, additional 45% + +## Key Parameters and Values (2025/26) + +### Income Tax +- **Personal Allowance:** £12,570 +- **Basic rate threshold:** £50,270 +- **Higher rate threshold:** £125,140 +- **Rates:** 20% (basic), 40% (higher), 45% (additional) +- **Personal allowance tapering:** £1 reduction for every £2 over £100,000 + +### National Insurance (Class 1) +- **Lower Earnings Limit:** £6,396/year +- **Primary Threshold:** £12,570/year +- **Upper Earnings Limit:** £50,270/year +- **Rates:** 12% (between primary and upper), 2% (above upper) + +### Universal Credit +- **Standard allowance:** Varies by single/couple and age +- **Taper rate:** 55% (rate at which UC reduced as income increases) +- **Work allowance:** Amount you can earn before UC reduced + +### Child Benefit +- **First child:** Higher rate +- **Subsequent children:** Lower rate +- **High Income Charge:** Tapered withdrawal starting at £60,000 + +## Version Compatibility + +- Use `policyengine-uk>=1.0.0` for 2025 calculations +- Check version: `import policyengine_uk; print(policyengine_uk.__version__)` +- Different years may require different package versions + +## Debugging Tips + +1. **Enable tracing:** + ```python + simulation.trace = True + result = simulation.calculate("variable_name", 2025) + ``` + +2. **Check intermediate calculations:** + ```python + gross_income = simulation.calculate("gross_income", 2025) + disposable_income = simulation.calculate("disposable_income", 2025) + ``` + +3. **Verify situation structure:** + ```python + import json + print(json.dumps(situation, indent=2)) + ``` + +4. **Test with PolicyEngine web app:** + - Go to policyengine.org/uk/household + - Enter same inputs + - Compare results + +## Additional Resources + +- **Documentation:** https://policyengine.org/uk/docs +- **API Reference:** https://github.com/PolicyEngine/policyengine-uk +- **Variable Explorer:** https://policyengine.org/uk/variables +- **Parameter Explorer:** https://policyengine.org/uk/parameters + +## Examples Directory + +See `examples/` for complete working examples: +- `single_person.yaml` - Single person household +- `couple.yaml` - Couple without children +- `family_with_children.yaml` - Family with dependents +- `universal_credit_sweep.yaml` - Analyzing UC with axes + +## Key Differences from US System + +1. **Benefit Units:** UK uses `benunits` (single/couple + children) instead of US multiple entity types +2. **Universal Credit:** Consolidated means-tested benefit (vs separate SNAP, TANF, etc. in US) +3. **National Insurance:** Separate from income tax with own thresholds (vs US Social Security tax) +4. **Devolved Taxes:** Scotland and Wales have different income tax rates +5. **Tax Year:** April 6 to April 5 (vs calendar year in US) +6. **No State Variation:** Council Tax is local, but most taxes/benefits are national (vs 50 US states) diff --git a/skills/policyengine-uk-skill/examples/couple.yaml b/skills/policyengine-uk-skill/examples/couple.yaml new file mode 100644 index 0000000..d2767d4 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/couple.yaml @@ -0,0 +1,29 @@ +# Example: Couple without children in Scotland +# Person 1: £35,000, Age: 35 +# Person 2: £25,000, Age: 33 + +people: + person_1: + age: + 2025: 35 + employment_income: + 2025: 35000 + person_2: + age: + 2025: 33 + employment_income: + 2025: 25000 + +benunits: + benunit: + members: + - person_1 + - person_2 + +households: + household: + members: + - person_1 + - person_2 + region: + 2025: SCOTLAND diff --git a/skills/policyengine-uk-skill/examples/family_with_children.yaml b/skills/policyengine-uk-skill/examples/family_with_children.yaml new file mode 100644 index 0000000..be02ac8 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/family_with_children.yaml @@ -0,0 +1,41 @@ +# Example: Family with children in Wales +# Parent 1: £35,000, Age: 35 +# Parent 2: £25,000, Age: 33 +# Child 1: Age 8 +# Child 2: Age 5 + +people: + parent_1: + age: + 2025: 35 + employment_income: + 2025: 35000 + parent_2: + age: + 2025: 33 + employment_income: + 2025: 25000 + child_1: + age: + 2025: 8 + child_2: + age: + 2025: 5 + +benunits: + benunit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +households: + household: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + region: + 2025: WALES diff --git a/skills/policyengine-uk-skill/examples/single_person.yaml b/skills/policyengine-uk-skill/examples/single_person.yaml new file mode 100644 index 0000000..33cd1e0 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/single_person.yaml @@ -0,0 +1,21 @@ +# Example: Single person household in London +# Income: £30,000, Age: 30 + +people: + person: + age: + 2025: 30 + employment_income: + 2025: 30000 + +benunits: + benunit: + members: + - person + +households: + household: + members: + - person + region: + 2025: LONDON diff --git a/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml b/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml new file mode 100644 index 0000000..b57492d --- /dev/null +++ b/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml @@ -0,0 +1,38 @@ +# Example: Analyzing Universal Credit with income variation +# Single parent with 2 children in North West +# Sweeps employment income from £0 to £50,000 + +people: + parent: + age: + 2025: 30 + child_1: + age: + 2025: 8 + child_2: + age: + 2025: 5 + +benunits: + benunit: + members: + - parent + - child_1 + - child_2 + +households: + household: + members: + - parent + - child_1 + - child_2 + region: + 2025: NORTH_WEST + +# Axes: Vary employment income from £0 to £50,000 +axes: + - - name: employment_income + count: 1001 + min: 0 + max: 50000 + period: 2025 diff --git a/skills/policyengine-uk-skill/scripts/situation_helpers.py b/skills/policyengine-uk-skill/scripts/situation_helpers.py new file mode 100644 index 0000000..4c1aadc --- /dev/null +++ b/skills/policyengine-uk-skill/scripts/situation_helpers.py @@ -0,0 +1,339 @@ +""" +Helper functions for creating PolicyEngine-UK situations. + +These utilities simplify the creation of situation dictionaries +for common household configurations. +""" + +CURRENT_YEAR = 2025 + +# UK ITL 1 regions +VALID_REGIONS = [ + "NORTH_EAST", + "NORTH_WEST", + "YORKSHIRE", + "EAST_MIDLANDS", + "WEST_MIDLANDS", + "EAST_OF_ENGLAND", + "LONDON", + "SOUTH_EAST", + "SOUTH_WEST", + "WALES", + "SCOTLAND", + "NORTHERN_IRELAND" +] + + +def create_single_person(income, region="LONDON", age=30, **kwargs): + """ + Create a situation for a single person household. + + Args: + income (float): Employment income + region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND") + age (int): Person's age + **kwargs: Additional person attributes (e.g., self_employment_income) + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + person_attrs = { + "age": {CURRENT_YEAR: age}, + "employment_income": {CURRENT_YEAR: income}, + } + person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": {"person": person_attrs}, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "region": {CURRENT_YEAR: region} + } + } + } + + +def create_couple( + income_1, income_2=0, region="LONDON", age_1=35, age_2=35, **kwargs +): + """ + Create a situation for a couple without children. + + Args: + income_1 (float): First person's employment income + income_2 (float): Second person's employment income + region (str): ITL 1 region + age_1 (int): First person's age + age_2 (int): Second person's age + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + members = ["person_1", "person_2"] + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": { + "person_1": { + "age": {CURRENT_YEAR: age_1}, + "employment_income": {CURRENT_YEAR: income_1} + }, + "person_2": { + "age": {CURRENT_YEAR: age_2}, + "employment_income": {CURRENT_YEAR: income_2} + } + }, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def create_family_with_children( + parent_income, + num_children=1, + child_ages=None, + region="LONDON", + parent_age=35, + couple=False, + partner_income=0, + **kwargs +): + """ + Create a situation for a family with children. + + Args: + parent_income (float): Primary parent's employment income + num_children (int): Number of children + child_ages (list): List of child ages (defaults to [5, 8, 12, ...]) + region (str): ITL 1 region + parent_age (int): Parent's age + couple (bool): Whether this is a couple household + partner_income (float): Partner's income if couple + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + if child_ages is None: + child_ages = [5 + i * 3 for i in range(num_children)] + elif len(child_ages) != num_children: + raise ValueError("Length of child_ages must match num_children") + + people = { + "parent": { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: parent_income} + } + } + + members = ["parent"] + + if couple: + people["partner"] = { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: partner_income} + } + members.append("partner") + + for i, age in enumerate(child_ages): + child_id = f"child_{i+1}" + people[child_id] = {"age": {CURRENT_YEAR: age}} + members.append(child_id) + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def add_income_sources( + situation, + person_id=None, + self_employment_income=0, + pension_income=0, + property_income=0, + savings_interest_income=0, + dividend_income=0, + miscellaneous_income=0 +): + """ + Add additional income sources to a person in an existing situation. + + Args: + situation (dict): Existing PolicyEngine situation + person_id (str): Person ID to add income to (defaults to first person) + self_employment_income (float): Self-employment income + pension_income (float): Private pension income + property_income (float): Rental income + savings_interest_income (float): Interest income + dividend_income (float): Dividend income + miscellaneous_income (float): Other income + + Returns: + dict: Updated situation with additional income + """ + # Get person ID + if person_id is None: + person_id = list(situation["people"].keys())[0] + + # Add income sources + if self_employment_income > 0: + situation["people"][person_id]["self_employment_income"] = { + CURRENT_YEAR: self_employment_income + } + + if pension_income > 0: + situation["people"][person_id]["pension_income"] = { + CURRENT_YEAR: pension_income + } + + if property_income > 0: + situation["people"][person_id]["property_income"] = { + CURRENT_YEAR: property_income + } + + if savings_interest_income > 0: + situation["people"][person_id]["savings_interest_income"] = { + CURRENT_YEAR: savings_interest_income + } + + if dividend_income > 0: + situation["people"][person_id]["dividend_income"] = { + CURRENT_YEAR: dividend_income + } + + if miscellaneous_income > 0: + situation["people"][person_id]["miscellaneous_income"] = { + CURRENT_YEAR: miscellaneous_income + } + + return situation + + +def add_axes(situation, variable_name, min_val, max_val, count=1001): + """ + Add axes to a situation for parameter sweeps. + + Args: + situation (dict): Existing PolicyEngine situation + variable_name (str): Variable to vary (e.g., "employment_income") + min_val (float): Minimum value + max_val (float): Maximum value + count (int): Number of points (default: 1001) + + Returns: + dict: Updated situation with axes + """ + situation["axes"] = [[{ + "name": variable_name, + "count": count, + "min": min_val, + "max": max_val, + "period": CURRENT_YEAR + }]] + + return situation + + +def set_region(situation, region): + """ + Set or change the region for a household. + + Args: + situation (dict): Existing PolicyEngine situation + region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND") + + Returns: + dict: Updated situation + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + household_id = list(situation["households"].keys())[0] + situation["households"][household_id]["region"] = {CURRENT_YEAR: region} + + return situation + + +def create_pensioner_household( + pension_income, + state_pension_income=0, + region="LONDON", + age=70, + couple=False, + partner_pension_income=0, + partner_age=68, + **kwargs +): + """ + Create a situation for a pensioner household. + + Args: + pension_income (float): Private pension income + state_pension_income (float): State pension income + region (str): ITL 1 region + age (int): Pensioner's age + couple (bool): Whether this is a couple household + partner_pension_income (float): Partner's pension income if couple + partner_age (int): Partner's age if couple + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + people = { + "pensioner": { + "age": {CURRENT_YEAR: age}, + "pension_income": {CURRENT_YEAR: pension_income}, + "state_pension": {CURRENT_YEAR: state_pension_income} + } + } + + members = ["pensioner"] + + if couple: + people["partner"] = { + "age": {CURRENT_YEAR: partner_age}, + "pension_income": {CURRENT_YEAR: partner_pension_income}, + "state_pension": {CURRENT_YEAR: 0} + } + members.append("partner") + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } diff --git a/skills/policyengine-us-skill/SKILL.md b/skills/policyengine-us-skill/SKILL.md new file mode 100644 index 0000000..c8f9359 --- /dev/null +++ b/skills/policyengine-us-skill/SKILL.md @@ -0,0 +1,524 @@ +--- +name: policyengine-us +description: PolicyEngine-US tax and benefit microsimulation patterns, situation creation, and common workflows +--- + +# PolicyEngine-US + +PolicyEngine-US models the US federal and state tax and benefit system. + +## For Users 👥 + +### What is PolicyEngine-US? + +PolicyEngine-US is the "calculator" for US taxes and benefits. When you use policyengine.org/us, PolicyEngine-US runs behind the scenes. + +**What it models:** + +**Federal taxes:** +- Income tax (with standard/itemized deductions) +- Payroll tax (Social Security, Medicare) +- Capital gains tax + +**Federal benefits:** +- Earned Income Tax Credit (EITC) +- Child Tax Credit (CTC) +- SNAP (food stamps) +- WIC, ACA premium tax credits +- Social Security, SSI, TANF + +**State programs (varies by state):** +- State income tax (all 50 states + DC) +- State EITC, CTC +- State-specific benefits + +**See full list:** https://policyengine.org/us/parameters + +### Understanding Variables + +When you see results in PolicyEngine, these are variables: + +**Income variables:** +- `employment_income` - W-2 wages +- `self_employment_income` - 1099 income +- `qualified_dividend_income` - Dividends +- `capital_gains` - Capital gains + +**Tax variables:** +- `income_tax` - Federal income tax +- `state_income_tax` - State income tax +- `payroll_tax` - FICA taxes + +**Benefit variables:** +- `eitc` - Earned Income Tax Credit +- `ctc` - Child Tax Credit +- `snap` - SNAP benefits + +**Summary variables:** +- `household_net_income` - Income after taxes and benefits +- `household_tax` - Total taxes +- `household_benefits` - Total benefits + +## For Analysts 📊 + +### Installation and Setup + +```bash +# Install PolicyEngine-US +pip install policyengine-us + +# Or with uv (recommended) +uv pip install policyengine-us +``` + +### Quick Start + +```python +from policyengine_us import Simulation + +# Create a household +situation = { + "people": { + "you": { + "age": {2024: 30}, + "employment_income": {2024: 50000} + } + }, + "families": {"family": {"members": ["you"]}}, + "marital_units": {"marital_unit": {"members": ["you"]}}, + "tax_units": {"tax_unit": {"members": ["you"]}}, + "spm_units": {"spm_unit": {"members": ["you"]}}, + "households": { + "household": { + "members": ["you"], + "state_name": {2024: "CA"} + } + } +} + +# Calculate taxes and benefits +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2024)[0] +eitc = sim.calculate("eitc", 2024)[0] + +print(f"Income tax: ${income_tax:,.0f}") +print(f"EITC: ${eitc:,.0f}") +``` + +### Web App to Python + +**Web app URL:** +``` +policyengine.org/us/household?household=12345 +``` + +**Equivalent Python (conceptually):** +The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation. + +### When to Use This Skill + +- Creating household situations for tax/benefit calculations +- Running microsimulations with PolicyEngine-US +- Analyzing policy reforms and their impacts +- Building tools that use PolicyEngine-US (calculators, analysis notebooks) +- Debugging PolicyEngine-US calculations + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-us + +**To see current implementation:** +```bash +git clone https://github.com/PolicyEngine/policyengine-us +cd policyengine-us + +# Explore structure +tree policyengine_us/ +``` + +**Key directories:** +```bash +ls policyengine_us/ +# - variables/ - Tax and benefit calculations +# - parameters/ - Policy rules (YAML) +# - reforms/ - Pre-defined reforms +# - tests/ - Test cases +``` + +## Core Concepts + +### 1. Situation Dictionary Structure + +PolicyEngine requires a nested dictionary defining household composition and characteristics: + +```python +situation = { + "people": { + "person_id": { + "age": {2024: 35}, + "employment_income": {2024: 50000}, + # ... other person attributes + } + }, + "families": { + "family_id": {"members": ["person_id", ...]} + }, + "marital_units": { + "marital_unit_id": {"members": ["person_id", ...]} + }, + "tax_units": { + "tax_unit_id": {"members": ["person_id", ...]} + }, + "spm_units": { + "spm_unit_id": {"members": ["person_id", ...]} + }, + "households": { + "household_id": { + "members": ["person_id", ...], + "state_name": {2024: "CA"} + } + } +} +``` + +**Key Rules:** +- All entities must have consistent member lists +- Use year keys for all values: `{2024: value}` +- State must be two-letter code (e.g., "CA", "NY", "TX") +- All monetary values in dollars (not cents) + +### 2. Creating Simulations + +```python +from policyengine_us import Simulation + +# Create simulation from situation +simulation = Simulation(situation=situation) + +# Calculate variables +income_tax = simulation.calculate("income_tax", 2024) +eitc = simulation.calculate("eitc", 2024) +household_net_income = simulation.calculate("household_net_income", 2024) +``` + +**Common Variables:** + +**Income:** +- `employment_income` - W-2 wages +- `self_employment_income` - 1099/business income +- `qualified_dividend_income` - Qualified dividends +- `capital_gains` - Capital gains +- `interest_income` - Interest income +- `social_security` - Social Security benefits +- `pension_income` - Pension/retirement income + +**Deductions:** +- `charitable_cash_donations` - Cash charitable giving +- `real_estate_taxes` - State and local property taxes +- `mortgage_interest` - Mortgage interest deduction +- `medical_expense` - Medical and dental expenses +- `casualty_loss` - Casualty and theft losses + +**Tax Outputs:** +- `income_tax` - Total federal income tax +- `payroll_tax` - FICA taxes +- `state_income_tax` - State income tax +- `household_tax` - Total taxes (federal + state + local) + +**Benefits:** +- `eitc` - Earned Income Tax Credit +- `ctc` - Child Tax Credit +- `snap` - SNAP benefits +- `household_benefits` - Total benefits + +**Summary:** +- `household_net_income` - Income minus taxes plus benefits + +### 3. Using Axes for Parameter Sweeps + +To vary a parameter across multiple values: + +```python +situation = { + # ... normal situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 200000, + "period": 2024 + }]] +} + +simulation = Simulation(situation=situation) +# Now calculate() returns arrays of 1001 values +incomes = simulation.calculate("employment_income", 2024) # Array of 1001 values +taxes = simulation.calculate("income_tax", 2024) # Array of 1001 values +``` + +**Important:** Remove axes before creating single-point simulations: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +simulation = Simulation(situation=situation_single) +``` + +### 4. Policy Reforms + +```python +from policyengine_us import Simulation + +# Define a reform (modifies parameters) +reform = { + "gov.irs.credits.ctc.amount.base_amount": { + "2024-01-01.2100-12-31": 5000 # Increase CTC to $5000 + } +} + +# Create simulation with reform +simulation = Simulation(situation=situation, reform=reform) +``` + +## Common Patterns + +### Pattern 1: Single Household Calculation + +```python +from policyengine_us import Simulation + +situation = { + "people": { + "parent": { + "age": {2024: 35}, + "employment_income": {2024: 60000} + }, + "child": { + "age": {2024: 5} + } + }, + "families": {"family": {"members": ["parent", "child"]}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "tax_units": {"tax_unit": {"members": ["parent", "child"]}}, + "spm_units": {"spm_unit": {"members": ["parent", "child"]}}, + "households": { + "household": { + "members": ["parent", "child"], + "state_name": {2024: "NY"} + } + } +} + +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2024)[0] +ctc = sim.calculate("ctc", 2024)[0] +``` + +### Pattern 2: Marginal Tax Rate Analysis + +```python +# Create baseline with axes varying income +situation_with_axes = { + # ... situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 200000, + "period": 2024 + }]] +} + +sim = Simulation(situation=situation_with_axes) +incomes = sim.calculate("employment_income", 2024) +taxes = sim.calculate("income_tax", 2024) + +# Calculate marginal tax rate +import numpy as np +mtr = np.gradient(taxes) / np.gradient(incomes) +``` + +### Pattern 3: Charitable Donation Impact + +```python +# Baseline (no donation) +situation_baseline = create_situation(income=100000, donation=0) +sim_baseline = Simulation(situation=situation_baseline) +tax_baseline = sim_baseline.calculate("income_tax", 2024)[0] + +# With donation +situation_donation = create_situation(income=100000, donation=5000) +sim_donation = Simulation(situation=situation_donation) +tax_donation = sim_donation.calculate("income_tax", 2024)[0] + +# Tax savings from donation +tax_savings = tax_baseline - tax_donation +effective_discount = tax_savings / 5000 # e.g., 0.24 = 24% discount +``` + +### Pattern 4: State Comparison + +```python +states = ["CA", "NY", "TX", "FL"] +results = {} + +for state in states: + situation = create_situation(state=state, income=75000) + sim = Simulation(situation=situation) + results[state] = { + "state_income_tax": sim.calculate("state_income_tax", 2024)[0], + "total_tax": sim.calculate("household_tax", 2024)[0] + } +``` + +## Helper Scripts + +This skill includes helper scripts in the `scripts/` directory: + +```python +from policyengine_skills.situation_helpers import ( + create_single_filer, + create_married_couple, + create_family_with_children, + add_itemized_deductions +) + +# Quick situation creation +situation = create_single_filer( + income=50000, + state="CA", + age=30 +) + +# Add deductions +situation = add_itemized_deductions( + situation, + charitable_donations=5000, + mortgage_interest=10000, + real_estate_taxes=8000 +) +``` + +## Common Pitfalls and Solutions + +### Pitfall 1: Member Lists Out of Sync +**Problem:** Different entities have different members +```python +# WRONG +"tax_units": {"tax_unit": {"members": ["parent"]}}, +"households": {"household": {"members": ["parent", "child"]}} +``` + +**Solution:** Keep all entity member lists consistent: +```python +# CORRECT +all_members = ["parent", "child"] +"families": {"family": {"members": all_members}}, +"tax_units": {"tax_unit": {"members": all_members}}, +"households": {"household": {"members": all_members}} +``` + +### Pitfall 2: Forgetting Year Keys +**Problem:** `"age": 35` instead of `"age": {2024: 35}` + +**Solution:** Always use year dictionary: +```python +"age": {2024: 35}, +"employment_income": {2024: 50000} +``` + +### Pitfall 3: Net Taxes vs Gross Taxes +**Problem:** Forgetting to subtract benefits from taxes + +**Solution:** Use proper calculation: +```python +# Net taxes (what household actually pays) +net_tax = sim.calculate("household_tax", 2024) - \ + sim.calculate("household_benefits", 2024) +``` + +### Pitfall 4: Axes Persistence +**Problem:** Axes remain in situation when creating single-point simulation + +**Solution:** Remove axes before single-point simulation: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +``` + +### Pitfall 5: State-Specific Variables +**Problem:** Using NYC-specific variables without `in_nyc: True` + +**Solution:** Set NYC flag for NY residents in NYC: +```python +"households": { + "household": { + "state_name": {2024: "NY"}, + "in_nyc": {2024: True} # Required for NYC taxes + } +} +``` + +## NYC Handling + +For New York City residents: +```python +situation = { + # ... people setup ... + "households": { + "household": { + "members": ["person"], + "state_name": {2024: "NY"}, + "in_nyc": {2024: True} # Enable NYC tax calculations + } + } +} +``` + +## Version Compatibility + +- Always use `policyengine-us>=1.155.0` for 2024 calculations +- Check version: `import policyengine_us; print(policyengine_us.__version__)` +- Different years may require different package versions + +## Debugging Tips + +1. **Enable tracing:** + ```python + simulation.trace = True + result = simulation.calculate("variable_name", 2024) + ``` + +2. **Check intermediate calculations:** + ```python + agi = simulation.calculate("adjusted_gross_income", 2024) + taxable_income = simulation.calculate("taxable_income", 2024) + ``` + +3. **Verify situation structure:** + ```python + import json + print(json.dumps(situation, indent=2)) + ``` + +4. **Test with PolicyEngine web app:** + - Go to policyengine.org/us/household + - Enter same inputs + - Compare results + +## Additional Resources + +- **Documentation:** https://policyengine.org/us/docs +- **API Reference:** https://github.com/PolicyEngine/policyengine-us +- **Example Notebooks:** https://github.com/PolicyEngine/analysis-notebooks +- **Variable Explorer:** https://policyengine.org/us/variables + +## Examples Directory + +See `examples/` for complete working examples: +- `single_filer.yaml` - Single person household +- `married_couple.yaml` - Married filing jointly +- `family_with_children.yaml` - Family with dependents +- `itemized_deductions.yaml` - Using itemized deductions +- `donation_sweep.yaml` - Analyzing donation impacts with axes diff --git a/skills/policyengine-us-skill/examples/donation_sweep.yaml b/skills/policyengine-us-skill/examples/donation_sweep.yaml new file mode 100644 index 0000000..6829690 --- /dev/null +++ b/skills/policyengine-us-skill/examples/donation_sweep.yaml @@ -0,0 +1,71 @@ +# Example: Analyzing charitable donation impacts using axes +# Married couple with 2 children in New York +# Sweeps charitable donations from $0 to $50,000 + +people: + parent_1: + age: + 2024: 35 + employment_income: + 2024: 100000 + parent_2: + age: + 2024: 35 + employment_income: + 2024: 50000 + child_1: + age: + 2024: 8 + child_2: + age: + 2024: 5 + +families: + family: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +marital_units: + marital_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +tax_units: + tax_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +spm_units: + spm_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +households: + household: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + state_name: + 2024: NY + +# Axes: Vary charitable donations from $0 to $50,000 +axes: + - - name: charitable_cash_donations + count: 1001 + min: 0 + max: 50000 + period: 2024 diff --git a/skills/policyengine-us-skill/examples/single_filer.yaml b/skills/policyengine-us-skill/examples/single_filer.yaml new file mode 100644 index 0000000..c51e04c --- /dev/null +++ b/skills/policyengine-us-skill/examples/single_filer.yaml @@ -0,0 +1,38 @@ +# Example: Single tax filer in California +# Income: $60,000, Age: 30, with charitable donations + +people: + person: + age: + 2024: 30 + employment_income: + 2024: 60000 + charitable_cash_donations: + 2024: 5000 + +families: + family: + members: + - person + +marital_units: + marital_unit: + members: + - person + +tax_units: + tax_unit: + members: + - person + +spm_units: + spm_unit: + members: + - person + +households: + household: + members: + - person + state_name: + 2024: CA diff --git a/skills/policyengine-us-skill/scripts/situation_helpers.py b/skills/policyengine-us-skill/scripts/situation_helpers.py new file mode 100644 index 0000000..1bac138 --- /dev/null +++ b/skills/policyengine-us-skill/scripts/situation_helpers.py @@ -0,0 +1,257 @@ +""" +Helper functions for creating PolicyEngine-US situations. + +These utilities simplify the creation of situation dictionaries +for common household configurations. +""" + +CURRENT_YEAR = 2024 + + +def create_single_filer(income, state="CA", age=35, **kwargs): + """ + Create a situation for a single tax filer. + + Args: + income (float): Employment income + state (str): Two-letter state code (e.g., "CA", "NY") + age (int): Person's age + **kwargs: Additional person attributes (e.g., self_employment_income) + + Returns: + dict: PolicyEngine situation dictionary + """ + person_attrs = { + "age": {CURRENT_YEAR: age}, + "employment_income": {CURRENT_YEAR: income}, + } + person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": {"person": person_attrs}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "state_name": {CURRENT_YEAR: state} + } + } + } + + +def create_married_couple( + income_1, income_2=0, state="CA", age_1=35, age_2=35, **kwargs +): + """ + Create a situation for a married couple filing jointly. + + Args: + income_1 (float): First spouse's employment income + income_2 (float): Second spouse's employment income + state (str): Two-letter state code + age_1 (int): First spouse's age + age_2 (int): Second spouse's age + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + members = ["spouse_1", "spouse_2"] + + household_attrs = { + "members": members, + "state_name": {CURRENT_YEAR: state} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": { + "spouse_1": { + "age": {CURRENT_YEAR: age_1}, + "employment_income": {CURRENT_YEAR: income_1} + }, + "spouse_2": { + "age": {CURRENT_YEAR: age_2}, + "employment_income": {CURRENT_YEAR: income_2} + } + }, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": members}}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def create_family_with_children( + parent_income, + num_children=1, + child_ages=None, + state="CA", + parent_age=35, + married=False, + spouse_income=0, + **kwargs +): + """ + Create a situation for a family with children. + + Args: + parent_income (float): Primary parent's employment income + num_children (int): Number of children + child_ages (list): List of child ages (defaults to [5, 8, 12, ...]) + state (str): Two-letter state code + parent_age (int): Parent's age + married (bool): Whether parents are married + spouse_income (float): Spouse's income if married + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if child_ages is None: + child_ages = [5 + i * 3 for i in range(num_children)] + elif len(child_ages) != num_children: + raise ValueError("Length of child_ages must match num_children") + + people = { + "parent": { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: parent_income} + } + } + + members = ["parent"] + + if married: + people["spouse"] = { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: spouse_income} + } + members.append("spouse") + + for i, age in enumerate(child_ages): + child_id = f"child_{i+1}" + people[child_id] = {"age": {CURRENT_YEAR: age}} + members.append(child_id) + + household_attrs = { + "members": members, + "state_name": {CURRENT_YEAR: state} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "families": {"family": {"members": members}}, + "marital_units": { + "marital_unit": { + "members": members if married else ["parent"] + } + }, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def add_itemized_deductions( + situation, + charitable_donations=0, + mortgage_interest=0, + real_estate_taxes=0, + medical_expenses=0, + casualty_losses=0 +): + """ + Add itemized deductions to an existing situation. + + Adds deductions to the first person in the situation. + + Args: + situation (dict): Existing PolicyEngine situation + charitable_donations (float): Cash charitable contributions + mortgage_interest (float): Mortgage interest paid + real_estate_taxes (float): State and local property taxes + medical_expenses (float): Medical and dental expenses + casualty_losses (float): Casualty and theft losses + + Returns: + dict: Updated situation with deductions + """ + # Get first person ID + first_person = list(situation["people"].keys())[0] + + # Add deductions + if charitable_donations > 0: + situation["people"][first_person]["charitable_cash_donations"] = { + CURRENT_YEAR: charitable_donations + } + + if mortgage_interest > 0: + situation["people"][first_person]["mortgage_interest"] = { + CURRENT_YEAR: mortgage_interest + } + + if real_estate_taxes > 0: + situation["people"][first_person]["real_estate_taxes"] = { + CURRENT_YEAR: real_estate_taxes + } + + if medical_expenses > 0: + situation["people"][first_person]["medical_expense"] = { + CURRENT_YEAR: medical_expenses + } + + if casualty_losses > 0: + situation["people"][first_person]["casualty_loss"] = { + CURRENT_YEAR: casualty_losses + } + + return situation + + +def add_axes(situation, variable_name, min_val, max_val, count=1001): + """ + Add axes to a situation for parameter sweeps. + + Args: + situation (dict): Existing PolicyEngine situation + variable_name (str): Variable to vary (e.g., "employment_income") + min_val (float): Minimum value + max_val (float): Maximum value + count (int): Number of points (default: 1001) + + Returns: + dict: Updated situation with axes + """ + situation["axes"] = [[{ + "name": variable_name, + "count": count, + "min": min_val, + "max": max_val, + "period": CURRENT_YEAR + }]] + + return situation + + +def set_state_nyc(situation, in_nyc=True): + """ + Set state to NY and configure NYC residence. + + Args: + situation (dict): Existing PolicyEngine situation + in_nyc (bool): Whether household is in NYC + + Returns: + dict: Updated situation + """ + household_id = list(situation["households"].keys())[0] + situation["households"][household_id]["state_name"] = {CURRENT_YEAR: "NY"} + situation["households"][household_id]["in_nyc"] = {CURRENT_YEAR: in_nyc} + + return situation diff --git a/skills/policyengine-writing-skill/SKILL.md b/skills/policyengine-writing-skill/SKILL.md new file mode 100644 index 0000000..9f3ed06 --- /dev/null +++ b/skills/policyengine-writing-skill/SKILL.md @@ -0,0 +1,526 @@ +--- +name: policyengine-writing +description: PolicyEngine writing style for blog posts, documentation, PR descriptions, and research reports - emphasizing active voice, quantitative precision, and neutral tone +--- + +# PolicyEngine Writing Skill + +Use this skill when writing blog posts, documentation, PR descriptions, research reports, or any public-facing PolicyEngine content. + +## When to Use This Skill + +- Writing blog posts about policy analysis +- Creating PR descriptions +- Drafting documentation +- Writing research reports +- Composing social media posts +- Creating newsletters +- Writing README files + +## Core Principles + +PolicyEngine's writing emphasizes clarity, precision, and objectivity. + +1. **Active voice** - Prefer active constructions over passive +2. **Direct and quantitative** - Use specific numbers, avoid vague adjectives/adverbs +3. **Sentence case** - Use sentence case for headings, not title case +4. **Neutral tone** - Describe what policies do, not whether they're good or bad +5. **Precise language** - Choose exact verbs over vague modifiers + +## Active Voice + +Active voice makes writing clearer and more direct. + +**✅ Correct (Active):** +``` +Harris proposes expanding the Earned Income Tax Credit +The reform reduces poverty by 3.2% +PolicyEngine projects higher costs than other organizations +We estimate the ten-year costs +The bill lowers the state's top income tax rate +Montana raises the EITC from 10% to 20% +``` + +**❌ Wrong (Passive):** +``` +The Earned Income Tax Credit is proposed to be expanded by Harris +Poverty is reduced by 3.2% by the reform +Higher costs are projected by PolicyEngine +The ten-year costs are estimated +The state's top income tax rate is lowered by the bill +The EITC is raised from 10% to 20% by Montana +``` + +## Quantitative and Precise + +Replace vague modifiers with specific numbers and measurements. + +**✅ Correct (Quantitative):** +``` +Costs the state $245 million +Benefits 77% of Montana residents +Lowers the Supplemental Poverty Measure by 0.8% +Raises net income by $252 in 2026 +The reform affects 14.3 million households +Hours worked falls by 0.27%, or 411,000 full-time equivalent jobs +The top decile receives an average benefit of $1,033 +PolicyEngine projects costs 40% higher than the Tax Foundation +``` + +**❌ Wrong (Vague adjectives/adverbs):** +``` +Significantly costs the state +Benefits most Montana residents +Greatly lowers poverty +Substantially raises net income +The reform affects many households +Hours worked falls considerably +High earners receive large benefits +PolicyEngine projects much higher costs +``` + +## Sentence Case for Headings + +Use sentence case (capitalize only the first word and proper nouns) for all headings. + +**✅ Correct (Sentence case):** +``` +## The proposal +## Nationwide impacts +## Household impacts +## Statewide impacts 2026 +## Case study: the End Child Poverty Act +## Key findings +``` + +**❌ Wrong (Title case):** +``` +## The Proposal +## Nationwide Impacts +## Household Impacts +## Statewide Impacts 2026 +## Case Study: The End Child Poverty Act +## Key Findings +``` + +## Neutral, Objective Tone + +Describe what policies do without value judgments. Let readers draw their own conclusions from the data. + +**✅ Correct (Neutral):** +``` +The reform reduces poverty by 3.2% and raises inequality by 0.16% +Single filers with earnings between $8,000 and $37,000 see their net incomes increase +The tax changes raise the net income of 75.9% of residents +PolicyEngine projects higher costs than other organizations +The top income decile receives 42% of total benefits +``` + +**❌ Wrong (Value judgments):** +``` +The reform successfully reduces poverty by 3.2% but unfortunately raises inequality +Low-income workers finally see their net incomes increase +The tax changes benefit most residents +PolicyEngine provides more accurate cost estimates +The wealthiest households receive a disproportionate share of benefits +``` + +## Precise Verbs Over Adverbs + +Choose specific verbs instead of generic verbs modified by adverbs. + +**✅ Correct (Precise verbs):** +``` +The bill lowers the top rate from 5.9% to 5.4% +The policy raises the maximum credit from $632 to $1,774 +The reform increases the phase-in rate from 7.65% to 15.3% +This doubles Montana's EITC from 10% to 20% +The change eliminates the age cap +``` + +**❌ Wrong (Vague verbs + adverbs):** +``` +The bill significantly changes the top rate +The policy substantially increases the maximum credit +The reform greatly boosts the phase-in rate +This dramatically expands Montana's EITC +The change completely removes the age cap +``` + +## Concrete Examples + +Always include specific household examples with precise numbers. + +**✅ Correct:** +``` +For a single adult with no children and $10,000 of earnings, the tax provisions +increase their net income by $69 in 2026 and $68 in 2027, solely from the +doubled EITC match. + +A single parent of two kids with an annual income of $50,000 will see a $252 +increase to their net income: $179 from the expanded EITC, and $73 from the +lower bracket threshold. + +A married couple with no dependents and $200,000 of earnings will see their +liability drop by $1,306 in 2027. +``` + +**❌ Wrong:** +``` +Low-income workers see modest increases to their net income from the +expanded EITC. + +Families with children benefit substantially from the tax changes. + +High earners also see significant reductions in their tax liability. +``` + +## Tables and Data + +Use tables liberally to present data clearly. Always include units and context. + +**Example 1: Tax parameters over time** + +| Year | Phase-in rate | Max credit | Phase-out start | Phase-out rate | +| ---- | ------------- | ---------- | --------------- | -------------- | +| 2025 | 15.3% | $1,774 | $13,706 | 15.3% | +| 2026 | 15.3% | $1,815 | $14,022 | 15.3% | +| 2027 | 15.3% | $1,852 | $14,306 | 15.3% | + +**Example 2: Household impacts** + +| Household composition | 2026 net income change | 2027 net income change | +| ------------------------------ | ---------------------- | ---------------------- | +| Single, no children, $10,000 | $66 | $68 | +| Single, two children, $50,000 | $252 | $266 | +| Married, no children, $200,000 | $853 | $1,306 | + +**Example 3: Ten-year costs** + +| Year | Federal cost ($ billions) | +| ------- | ------------------------- | +| 2025 | 14.3 | +| 2026 | 14.4 | +| 2027 | 14.7 | +| 2025-34 | 143.7 | + +## Avoid Superlatives + +Replace superlative claims with specific comparisons. + +**✅ Correct:** +``` +PolicyEngine projects costs 40% higher than the Tax Foundation +The top decile receives an average benefit of $1,033 +The reform reduces child poverty by 3.2 percentage points +This represents Montana's largest income tax cut since 2021 +``` + +**❌ Wrong:** +``` +PolicyEngine provides the most accurate cost projections +The wealthiest households receive massive benefits +The reform dramatically slashes child poverty +This is Montana's largest income tax cut in history +``` + +## Structure and Organization + +Follow a clear hierarchical structure with key findings up front. + +**Standard blog post structure:** + +```markdown +# Title (H1) + +Opening paragraph states what happened and when, with a link to PolicyEngine. + +Key results in [year]: +- Cost: $245 million +- Benefits: 77% of residents +- Poverty impact: Reduces SPM by 0.8% +- Inequality impact: Raises Gini by 0.16% + +## The proposal (H2) + +Detailed description of the policy changes, often with a table showing +the specific parameter values. + +## Household impacts (H2) + +Specific examples for representative household types. + +### Example 1: Single filer (H3) +Detailed calculation... + +### Example 2: Family with children (H3) +Detailed calculation... + +## Statewide impacts (H2) + +Population-level analysis with charts and tables. + +### Budgetary impact (H3) +Cost/revenue estimates... + +### Distributional impact (H3) +Winners/losers by income decile... + +### Poverty and inequality (H3) +Impact on poverty rates and inequality measures... + +## Methodology (H2) + +Explanation of data sources, modeling approach, and caveats. +``` + +## Common Patterns + +### Opening Paragraphs + +State the facts directly with dates, actors, and actions: + +``` +[On April 28, 2025], Governor Greg Gianforte (R-MT) signed House Bill 337, +a bill that amends Montana's individual income tax code. + +Vice President Harris proposes expanding the Earned Income Tax Credit (EITC) +for filers without qualifying dependents. + +In her economic plan, Harris proposes to restore the expanded Earned Income +Tax Credit for workers without children to its level under the American +Rescue Plan Act in 2021. +``` + +### Key Findings Format + +Lead with bullet points of quantitative results: + +``` +Key results in 2027: +- Costs the state $245 million +- Benefits 77% of Montana residents +- Lowers the Supplemental Poverty Measure by 0.8% +- Raises the Gini index by 0.16% +``` + +### Methodological Transparency + +Always specify the model, version, and assumptions: + +``` +Based on static microsimulation modeling with PolicyEngine US (version 1.103.0), +we project the following economic impacts for 2025. + +Assuming no behavioral responses, we project that the EITC expansion will cost +the federal government $14.3 billion in 2025. + +Incorporating elasticities of labor supply used by the Congressional Budget Office +increases the reform's cost. + +Over the ten-year budget window, this amounts to $143.7 billion. +``` + +### Household Examples + +Always include the household composition, income, and specific dollar impacts: + +``` +For a single adult with no children and $10,000 of earnings, the tax provisions +increase their net income by $69 in 2026 and $68 in 2027. + +A single parent of two kids with an annual income of $50,000 will see a $252 +increase to their net income due to House Bill 337: $179 from the expanded EITC, +and $73 from the lower bracket threshold. +``` + +## Examples in Context + +### Blog Post Opening + +**✅ Correct:** +``` +On April 28, 2025, Governor Gianforte signed House Bill 337, which lowers +Montana's top income tax rate from 5.9% to 5.4% and doubles the state EITC +from 10% to 20% of the federal credit. + +Key results in 2027: +- Costs the state $245 million +- Benefits 77% of Montana residents +- Lowers the Supplemental Poverty Measure by 0.8% +- Raises the Gini index by 0.16% + +Use PolicyEngine to view the full results or calculate the effect on your +household. +``` + +**❌ Wrong:** +``` +On April 28, 2025, Governor Gianforte made history by signing an amazing new +tax cut bill that will dramatically help Montana families. House Bill 337 +significantly reduces tax rates and greatly expands the EITC. + +This groundbreaking reform will: +- Cost the state money +- Help most residents +- Reduce poverty substantially +- Impact inequality + +Check out PolicyEngine to see how much you could save! +``` + +### PR Description + +**✅ Correct:** +``` +## Summary + +This PR adds Claude Code plugin configuration to enable automated installation +of agents and skills for PolicyEngine development. + +## Changes + +- Add plugin auto-install configuration in .claude/settings.json +- Configure auto-install of country-models plugin from PolicyEngine/policyengine-claude + +## Benefits + +- Access to 15 specialized agents +- 3 slash commands (/encode-policy, /review-pr, /fix-pr) +- 2 skills (policyengine-us-skill, policyengine-standards-skill) + +## Testing + +After merging, team members trust the repo and the plugin auto-installs. +``` + +**❌ Wrong:** +``` +## Summary + +This amazing PR adds incredible new Claude Code plugin support that will +revolutionize PolicyEngine development! + +## Changes + +- Adds some configuration files +- Sets up plugins and stuff + +## Benefits + +- Gets you lots of cool new features +- Makes development much easier +- Provides great new tools + +## Testing + +Should work great once merged! +``` + +### Documentation + +**✅ Correct:** +``` +## Installation + +Install PolicyEngine-US from PyPI: + +```bash +pip install policyengine-us +``` + +This installs version 1.103.0 or later, which includes support for 2025 +tax parameters. +``` + +**❌ Wrong:** +``` +## Installation + +Simply install PolicyEngine-US: + +```bash +pip install policyengine-us +``` + +This will install the latest version with all the newest features! +``` + +## Special Cases + +### Comparisons to Other Organizations + +State facts neutrally without claiming superiority: + +**✅ Correct:** +``` +PolicyEngine projects higher costs than other organizations when considering +behavioral responses. + +| Organization | Cost, 2025-2034 ($ billions) | +| ------------------------- | ---------------------------- | +| PolicyEngine (static) | 144 | +| PolicyEngine (dynamic) | 201 | +| Tax Foundation | 157 | +| Penn Wharton Budget Model | 135 | +``` + +**❌ Wrong:** +``` +PolicyEngine provides more accurate estimates than other organizations. + +Unlike other models that underestimate costs, PolicyEngine correctly accounts +for behavioral responses to project a more realistic $201 billion cost. +``` + +### Discussing Limitations + +Acknowledge limitations directly without hedging: + +**✅ Correct:** +``` +## Caveats + +The Current Population Survey has several limitations for tax microsimulation: + +- Truncates high incomes for privacy, underestimating tax impacts on high earners +- Underestimates benefit receipt compared to administrative totals +- Reflects 2020 data with 2025 policy parameters +- Lacks detail for specific income types (assumes all capital gains are long-term) +``` + +**❌ Wrong:** +``` +## Caveats + +While our model is highly sophisticated, like all models it has some potential +limitations that users should be aware of: + +- The data might not perfectly capture high incomes +- Benefits may be slightly underestimated +- We do our best to extrapolate older data to current years +``` + +## Writing Checklist + +Before publishing, verify: + +- [ ] Use active voice throughout +- [ ] Include specific numbers for all claims +- [ ] Use sentence case for all headings +- [ ] Maintain neutral, objective tone +- [ ] Choose precise verbs over vague adverbs +- [ ] Include concrete household examples +- [ ] Present data in tables +- [ ] Avoid all superlatives +- [ ] Structure with clear hierarchy +- [ ] Open with key quantitative findings +- [ ] Specify model version and assumptions +- [ ] Link to PolicyEngine when relevant +- [ ] Acknowledge limitations directly + +## Resources + +- **Example posts**: See `policyengine-app/src/posts/articles/` for reference implementations +- **PolicyEngine app**: https://policyengine.org for linking to analyses +- **Microsimulation docs**: https://policyengine.org/us/docs for methodology details