Skip to content

About

Shared wizard primitives and US household draft contract for PolicyEngine apps.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

policyengine-household-wizard

Shared wizard primitives and a US household draft contract for PolicyEngine apps (cliff-watch, Coverage Compass, app-v2, and standalone calculators).

The package ships two surfaces:

  • Wizard primitives — copy-neutral React primitives for step state, progress, navigation, option cards, and review screens. No household-specific code.
  • US household model — a draft type that starts blank for state, age, and marital status (issue #1044), validation rules, URL serialization, state-filtered counties, and adapters to the PolicyEngine API V1 household payload.

Install

bun add policyengine-household-wizard
# or
npm install policyengine-household-wizard

React 19 is a peer dependency.

If you want the default look, import the stylesheet once:

import 'policyengine-household-wizard/styles.css';

The CSS uses ui-kit CSS variables (--primary, --border, --accent, …) when they're present, with safe fallbacks otherwise. Apps already on @policyengine/ui-kit will inherit the teal theme automatically.

Quick start

import {
  useWizardSteps,
  WizardProgress,
  WizardNavigation,
  WizardOptionCard,
  WizardReviewList,
  createBlankDraft,
  applyMaritalStatusChange,
  validate,
  toV1HouseholdPayload,
  US_STATES,
  getCountiesByState,
  type USHouseholdDraft,
} from 'policyengine-household-wizard';

const steps = [
  { id: 'location', label: 'Location', isComplete: (d: USHouseholdDraft) => !!d.state },
  { id: 'marital', label: 'Marital status', isComplete: (d: USHouseholdDraft) => !!d.maritalStatus },
  { id: 'review', label: 'Review' },
];

function MyWizard() {
  const [draft, setDraft] = useState(createBlankDraft());
  const wizard = useWizardSteps({ steps, state: draft });

  // ...render WizardProgress, step bodies, WizardNavigation
}

See docs/api.md for wizard navigation and focus APIs and docs/migration.md for migrating from each app's local wizard.

Design decisions

  • Marital status, not filing status. The wizard asks users marital status in plain English; apps derive filing status (single, head of household, married filing jointly, …) in their own model adapters.
  • No defaults for state, age, or marital status. createBlankDraft() returns a draft with all three set to null/empty. The wizard explicitly surfaces those fields rather than picking a quiet default like California or age 30.
  • State-filtered counties. Counties come from a bundled JSON generated from PolicyEngine US metadata. Use getCountiesByState('CA') to get the options for the chosen state. Refresh with bun run regenerate-counties.
  • Adapters, not a UI shell. Apps render their own form layouts and pass the draft through validate() and toV1HouseholdPayload() (or their own app-specific adapter) at submit time.
  • No income field overrides another. An input on a PolicyEngine US total such as social_security replaces the sum of its components, so components sent alongside it are left out of the total. socialSecurityIncome therefore sets social_security_retirement and ssdiAmount sets social_security_disability; the model adds them. Pensions work the same way: publicPensionIncome and privatePensionIncome set the components of taxable_pension_income, which state rules read. A pension of unknown source (pensionIncome) is sent as private, and fields that share a variable are summed. Expenses set inputs too: mortgageInterest sets home_mortgage_interest, not mortgage_interest, a computed total the federal interest deduction ignores (the tests keep it as a guarded total that no field may set). Tips are taxed as wages in PolicyEngine US, so tipIncome sets tip_income and is also added to employment_income; with a listed tippedOccupationCode (TIPPED_OCCUPATIONS, from the final section 224 regulations) they can qualify for the 2025-2028 tip deduction. Property tests check the adapter against a snapshot of the PolicyEngine US variable graph (refresh it with bun run regenerate-variable-graph). Each total the adapter sets directly, and each variable that more than one field sets, is listed with its reason in tests/us-household/v1PayloadInvariants.test.ts.
  • Computed variables go to everyone or no one. PolicyEngine stores each person variable as one array covering everyone in the simulation, so an input for one person on a variable the model computes gives everyone else a stored default instead of the computed value: ssi sent for one adult zeroed the other adult's SSI. The adapter therefore sends computed variables for every person or no one, except employment income, self-employment income and long-term capital gains, which policyengine-us's Simulation moves onto input variables. kind is sent for everyone as is_tax_unit_dependent. isFullTimeStudent is sent as the input is_full_time_college_student, and only from age 18, because the model counts ages 5 to 17 as K-12 students. ssiAmount is sent as ssi only when every person has an amount; validate() asks for the missing ones, and an entered 0 is sent as takes_up_ssi_if_eligible: false. Property tests check this against the variable graph snapshot, which records which variables the model computes.

Repository structure

src/
  primitives/        useWizardSteps, WizardProgress, WizardNavigation, …
  us-household/      types, draft, validate, normalize, serialize, counties
    adapters/        toV1HouseholdPayload — PolicyEngine API V1 envelope
    data/            counties-by-state.json
tests/               vitest unit and round-trip tests
scripts/             generate-counties.ts, generate-variable-graph.ts

Development

bun install
bun run test
bun run typecheck
bun run build
bun run test:live   # adapter output against api.policyengine.org (network)

Changes follow the towncrier convention used by @policyengine/ui-kit. Add a fragment in changelog.d/ before opening a PR:

echo "Describe your change." > changelog.d/your-branch.added.md

Fragment types: added (minor), changed (patch), fixed (patch), removed (minor), breaking (major). The release workflow on main bumps the version, builds the changelog, publishes to npm, and commits the bump. See release setup and verification for npm trusted publishing.

About

Shared wizard primitives and US household draft contract for PolicyEngine apps.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages