| 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
- Write tests first (TDD - see below)
- Format code:
make format or language-specific formatter
- Run tests:
make test to ensure all tests pass
- Check linting: Ensure no linting errors
- Use config files: Prefer config files over environment variables
- 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:
/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:
gh pr create --repo PolicyEngine/policyengine-us --draft --title "Title" --body "Body"
PR_NUMBER=$(gh pr view --json number --jq '.number')
POLL_INTERVAL=15
ELAPSED=0
while true; do
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
gh checks
ELAPSED=$((ELAPSED + POLL_INTERVAL))
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:
git push -u origin feature-branch
gh pr create --repo PolicyEngine/policyengine-us --draft --title "..." --body "..."
gh pr ready $PR_NUMBER
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):
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", 2026)[0]
assert ca_eitc == 3000, "CA EITC should be $3,000 for this household"
2. Implement feature (GREEN):
class ca_eitc(Variable):
value_type = float
entity = TaxUnit
definition_period = YEAR
def formula(tax_unit, period, parameters):
federal_eitc = tax_unit("eitc", period)
return federal_eitc * parameters(period).gov.states.ca.tax.eitc.match
3. Refactor (REFACTOR):
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:
- @document-collector gathers regulations
- @test-creator writes tests from regulations (isolated, no implementation access)
- @rules-engineer implements from regulations (isolated, no test access)
- Both work from same source → tests verify implementation accuracy
See policyengine-core-skill and country-models agents for details.
Test Examples
Python (pytest):
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", 2026)[0]
assert ctc == 4400, "CTC should be $2,200 per child"
React (Jest + RTL):
import { render, screen } from '@testing-library/react';
import TaxCalculator from './TaxCalculator';
test('displays calculated tax', () => {
render(<TaxCalculator income={50000} />);
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:
make test
uv run pytest tests/ -v
uv run pytest tests/test_credits.py::test_ctc_for_two_children -v
uv run pytest tests/ --cov=policyengine_us --cov-report=html
React:
make test
npm test -- --watch
npm test -- TaxCalculator.test.jsx
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
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", 2026)[0]
assert credit == 330, "Should receive $330 for child under 4"
Python Standards
Formatting
- Formatter: Black with 79-character line length
- Command:
make format or black . -l 79
- Check without changes:
black . -l 79 --check
make format
black . -l 79 --check
Code Style
import os
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from policyengine_us import Simulation
class TaxCalculator:
pass
def calculate_income_tax(income):
annual_income = income * 12
return annual_income
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
try:
result = simulation.calculate("income_tax", 2026)
except KeyError as e:
raise ValueError(f"Invalid variable name: {e}")
Testing
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", 2026)[0]
assert ctc == 4400, "CTC should be $2200 per child"
Run tests:
make test
uv run pytest tests/ -v
uv run pytest tests/test_tax.py::test_ctc_calculation -v
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
make format
npm run lint -- --fix
npx prettier --write .
npm run lint -- --max-warnings=0
Code Style
import { useState, useEffect } from "react";
function TaxCalculator({ income, state }) {
const [tax, setTax] = useState(0);
useEffect(() => {
calculateTax(income, state).then(setTax);
}, [income, state]);
return (
<div>
<p>Tax: ${tax.toLocaleString()}</p>
</div>
);
}
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.
Terminology Note:
When someone says "add a changelog entry" or "needs a changelog entry" in PolicyEngine context, they mean:
- ✅ Create/update
changelog_entry.yaml (the PR-level entry file)
- ❌ NOT editing
CHANGELOG.md (the main changelog file)
- ❌ NOT editing
changelog.yaml (the compiled changelog)
Correct Workflow:
-
Create changelog_entry.yaml at repository root:
- bump: patch
changes:
added:
- Description of new feature
fixed:
- Description of bug fix
changed:
- Description of change
-
Commit ONLY changelog_entry.yaml with your code changes
-
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
-
Create branches on PolicyEngine repos, NOT forks
- Forks cause CI failures due to missing secrets
- Request write access if needed
-
Branch naming: feature-name or fix-issue-123
-
Commit messages:
Add CTC reform analysis for CRFB report
- Implement household-level calculations
- Add state-by-state comparison
- Create visualizations
Fixes #123
-
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:
app_new.py
app_v2.py
component_refactored.jsx
✅ Correct:
app.py
2. Formatter Not Run
❌ Wrong: Committing without formatting (main cause of CI failures)
✅ Correct:
make format
black . -l 79
npm run lint -- --fix
npx prettier --write .
3. Environment Variables
❌ Wrong:
const API_URL = process.env.API_URL;
✅ Correct:
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:
make install
make test
make format
make changelog
make debug
make build
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
Repo rename checklist
When renaming a PolicyEngine repository, references to the old name are often hardcoded across the org. Follow this checklist to avoid broken links, builds, and embeds.
1. Search the org for all references
gh api "/search/code?q=org:PolicyEngine+OLD_REPO_NAME" --paginate | jq '.items[] | {repo: .repository.full_name, path: .path}'
Review every result -- some will be docs/changelogs (safe to update later), others will break builds if not updated before the rename.
2. Common places where repo names are hardcoded
| Location | What to look for | Example |
|---|
| GitHub Actions workflows | PUBLIC_URL, checkout paths, artifact names | PUBLIC_URL: https://policyengine.github.io/OLD_NAME |
| Iframe embeds in policyengine-app-v2 | src URLs in page components | app/src/pages/*.jsx referencing OLD_NAME.github.io |
| README badges and links | Shield.io badges, repo links |  |
| package.json / pyproject.toml | name, repository, homepage fields | "name": "old-name" |
| GitHub Pages URLs | Any URL containing policyengine.github.io/OLD_NAME | Links in docs, blog posts, other READMEs |
| CLAUDE.md | Repo-specific instructions that reference the old name | Paths, URLs, skill references |
| Import paths (Python) | Package name derived from repo name | from old_name import ... |
| Vercel / deployment configs | Project names, domain aliases | vercel.json, Vercel dashboard settings |
| policyengine-claude skills | Skill files that reference the repo | Links in SKILL.md files across this plugin |
3. Cross-repo coordination
If the renamed repo is embedded in another site (e.g., via iframe or GitHub Pages), both repos need updates:
- In the renamed repo: Update
PUBLIC_URL and any self-referencing URLs in workflows, configs, and docs.
- In the embedding repo: Update iframe
src URLs, links, and any CI that depends on the old name.
- Deploy order: Push the renamed repo's changes first (so the new URL is live), then update the embedding repo.
4. After renaming
Best Practices Checklist
Code Quality
Version Control
Testing
Documentation
Quick Reference
Format Commands by Language
Python:
make format
black . -l 79 --check
uv run pytest tests/ -v
React:
make format
npm run lint -- --max-warnings=0
npm test
Pre-Commit Checklist
make format
make test
git add .
git commit -m "Description
Fixes #123"
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