diff --git a/bun.lock b/bun.lock
index 335d7de..53e0b9b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -29,6 +29,7 @@
"eslint": "^9.39.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^7.0.1",
+ "fast-check": "^4.10.2",
"globals": "^16.5.0",
"jsdom": "^28.1.0",
"typescript": "~5.9.3",
@@ -706,6 +707,8 @@
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+ "fast-check": ["fast-check@4.10.2", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -976,6 +979,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
+
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
diff --git a/package.json b/package.json
index ec6b659..767dded 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"eslint": "^9.39.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^7.0.1",
+ "fast-check": "^4.10.2",
"globals": "^16.5.0",
"jsdom": "^28.1.0",
"typescript": "~5.9.3",
diff --git a/src/components/microsim/ThreeIngredients.tsx b/src/components/microsim/ThreeIngredients.tsx
index e3ad1cd..8899878 100644
--- a/src/components/microsim/ThreeIngredients.tsx
+++ b/src/components/microsim/ThreeIngredients.tsx
@@ -1,12 +1,17 @@
-import { type ReactNode } from 'react';
+import { type ReactNode, useEffect, useState } from 'react';
+import Link from 'next/link';
+import { useSearchParams } from 'next/navigation';
import { motion } from 'framer-motion';
import { colors } from '../../designTokens';
import { IconFileText, IconUsers, IconTrendingUp } from '@tabler/icons-react';
import type { Country } from '../../hooks/useCountry';
+import { usePublicBasePrefix } from '../../hooks/usePublicBasePrefix';
+import { fetchProgramsWithSource } from '../../data/fetchPrograms';
+import { summarizeRegistry, type RegistrySummary } from '../../data/programStatus';
interface IngredientCardProps {
title: string;
- description: string;
+ description: ReactNode;
icon: ReactNode;
delay: number;
isActive?: boolean;
@@ -42,12 +47,49 @@ interface ThreeIngredientsProps {
country?: Country;
}
+/** Cites the coverage registry's counts once they load, never a hardcoded figure. */
+function UsPolicyRulesDescription({ registry }: { registry: RegistrySummary | null }) {
+ const basePrefix = usePublicBasePrefix();
+ const search = useSearchParams().toString();
+ const trackerPath = `${basePrefix}/rules/coverage`;
+ const tracker = (
+
+ coverage tracker
+
+ );
+ return (
+ <>
+ Tax codes, benefit formulas, and eligibility rules encoded as executable logic. The{' '}
+ {tracker}{' '}
+ {registry
+ ? `lists ${registry.total.toLocaleString('en-US')} federal, state, and local programs, ${registry.complete.toLocaleString('en-US')} of them marked complete.`
+ : 'lists federal, state, and local programs and the status of each.'}
+ >
+ );
+}
+
export default function ThreeIngredients({ activeIngredient, country = 'us' }: ThreeIngredientsProps) {
+ const [registry, setRegistry] = useState(null);
+
+ useEffect(() => {
+ if (country !== 'us') return;
+ let active = true;
+ fetchProgramsWithSource(country).then(result => {
+ if (active) setRegistry(summarizeRegistry(result));
+ });
+ return () => { active = false; };
+ }, [country]);
+
const ingredients = [
{
title: 'Policy rules',
description:
- 'Tax codes, benefit formulas, and eligibility rules encoded as executable logic. Over 55 federal, state, and local programs.',
+ country === 'uk'
+ ? 'Tax codes, benefit formulas, and eligibility rules encoded as executable logic. Over 55 federal, state, and local programs.'
+ : ,
icon: ,
key: 'policies' as const,
},
diff --git a/src/data/microsimSteps.ts b/src/data/microsimSteps.ts
index 49c9f12..6e2ae46 100644
--- a/src/data/microsimSteps.ts
+++ b/src/data/microsimSteps.ts
@@ -168,7 +168,7 @@ export function getMicrosimSteps(country: Country): MicrosimStep[] {
title: country === 'uk' ? 'Model behavioural responses' : 'Model behavioral responses',
description: country === 'uk'
? 'People change their behaviour in response to tax changes. Using elasticity of taxable income, we estimate how earnings shift.'
- : 'People change their behavior in response to tax changes. Using elasticity of taxable income, we estimate how earnings shift.',
+ : 'People may work more or less when taxes and benefits change. Earnings stay fixed by default; once labor supply elasticities are set, workers\' earnings respond to changes in their marginal tax rates and in their households\' net income.',
ingredient: 'dynamics',
},
{
diff --git a/src/data/programStatus.ts b/src/data/programStatus.ts
index 2dd6c80..980fe83 100644
--- a/src/data/programStatus.ts
+++ b/src/data/programStatus.ts
@@ -1,4 +1,5 @@
import type { CoverageStatus, Program } from '../types/Program';
+import type { ProgramsWithSource } from './fetchPrograms';
export const ALL_STATES = [
'AL','AK','AZ','AR','CA','CO','CT','DE','DC','FL','GA','HI','ID','IL','IN','IA',
@@ -170,3 +171,19 @@ export function computeStatusCount(programList: Program[]): StatusCounts {
});
return counts;
}
+
+export interface RegistrySummary {
+ total: number;
+ complete: number;
+}
+
+/**
+ * Headline counts for citing the coverage registry elsewhere on the site.
+ * They match the coverage tracker's "Total programs" and "Complete" cards.
+ * Returns null when there is no registry to cite: the bundled fallback list
+ * is a stale hand-kept copy, not the registry.
+ */
+export function summarizeRegistry({ programs, source }: ProgramsWithSource): RegistrySummary | null {
+ if (source.kind === 'fallback' || programs.length === 0) return null;
+ return { total: programs.length, complete: computeStatusCount(programs).complete };
+}
diff --git a/src/test/overviewClaims.test.tsx b/src/test/overviewClaims.test.tsx
new file mode 100644
index 0000000..985d609
--- /dev/null
+++ b/src/test/overviewClaims.test.tsx
@@ -0,0 +1,195 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, render, screen, waitFor, within } from '@testing-library/react';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import fc from 'fast-check';
+import type { CoverageStatus, Program } from '../types/Program';
+import type { ProgramsSource } from '../data/fetchPrograms';
+import { computeStatusCount, summarizeRegistry } from '../data/programStatus';
+import { getMicrosimSteps } from '../data/microsimSteps';
+
+const searchParams = vi.hoisted(() => ({ current: new URLSearchParams() }));
+vi.mock('next/navigation', () => ({
+ usePathname: () => '/',
+ useSearchParams: () => searchParams.current,
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }),
+}));
+
+// A pinned copy of the registry snapshot (public/programs-us.json is generated at build time and not committed).
+const usSnapshot = JSON.parse(readFileSync(resolve(__dirname, 'fixtures/programs-us.snapshot.json'), 'utf8'));
+
+function response(data: unknown, status = 200) {
+ return { ok: status >= 200 && status < 300, status, json: async () => data } as Response;
+}
+
+const policyRulesCard = () => screen.getByRole('heading', { name: 'Policy rules' }).parentElement!;
+
+describe('US overview policy-rules card', () => {
+ beforeEach(() => {
+ // fetchPrograms caches per module instance; start every test from an empty cache.
+ vi.resetModules();
+ searchParams.current = new URLSearchParams();
+ window.history.replaceState({}, '', '/us/model');
+ });
+
+ afterEach(() => {
+ vi.doUnmock('../data/fetchPrograms');
+ vi.unstubAllGlobals();
+ window.history.replaceState({}, '', '/');
+ });
+
+ it('cites the same total and complete counts the coverage tracker shows', async () => {
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response(usSnapshot)));
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+ const { default: CoverageTrackerPage } = await import('../views/rules/CoverageTrackerPage');
+
+ const tracker = render();
+ // A summary stat card is a label whose previous sibling holds the number.
+ const statValue = (label: string) => {
+ const card = within(tracker.container).getAllByText(label)
+ .find(el => /^\d+$/.test(el.previousElementSibling?.textContent ?? ''));
+ return Number(card!.previousElementSibling!.textContent);
+ };
+ await within(tracker.container).findByText(/Federal programs \(\d+\)/);
+ const total = statValue('Total programs');
+ const complete = statValue('Complete');
+ tracker.unmount();
+
+ render();
+ expect(total).toBe(usSnapshot.programs.length);
+ expect(await within(policyRulesCard()).findByText(
+ `lists ${total} federal, state, and local programs, ${complete} of them marked complete.`,
+ { exact: false },
+ )).toBeInTheDocument();
+ expect(policyRulesCard()).not.toHaveTextContent(/Over 55/);
+ });
+
+ it('links to the coverage tracker under the public mount and keeps the query string', async () => {
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response(usSnapshot)));
+ searchParams.current = new URLSearchParams('embed=true&country=us');
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+
+ render();
+ const link = within(policyRulesCard()).getByRole('link', { name: 'coverage tracker' });
+ expect(link).toHaveAttribute('href', '/us/model/rules/coverage?embed=true&country=us');
+ await within(policyRulesCard()).findByText(/marked complete/);
+ });
+
+ it('shows no number while the registry is loading', async () => {
+ vi.doMock('../data/fetchPrograms', () => ({ fetchProgramsWithSource: () => new Promise(() => {}) }));
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+
+ render();
+ expect(policyRulesCard()).toHaveTextContent(
+ 'The coverage tracker lists federal, state, and local programs and the status of each.',
+ );
+ expect(policyRulesCard()).not.toHaveTextContent(/\d/);
+ });
+
+ it('shows no number when only the bundled fallback list is available', async () => {
+ const fallbackPrograms: Program[] = [
+ { id: 'a', name: 'A', fullName: 'A', status: 'complete', githubLinks: {} },
+ ];
+ const fetchProgramsWithSource = vi.fn().mockResolvedValue({
+ programs: fallbackPrograms,
+ source: { kind: 'fallback' },
+ });
+ vi.doMock('../data/fetchPrograms', () => ({ fetchProgramsWithSource }));
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+
+ render();
+ await waitFor(() => expect(fetchProgramsWithSource).toHaveBeenCalledWith('us'));
+ await act(async () => {});
+ expect(policyRulesCard()).toHaveTextContent('lists federal, state, and local programs and the status of each.');
+ expect(policyRulesCard()).not.toHaveTextContent(/\d/);
+ });
+
+ it('cites the production API registry when the snapshot is unavailable', async () => {
+ const programs: Program[] = [
+ { id: 'a', name: 'A', fullName: 'A', agency: 'IRS', status: 'complete', githubLinks: {} },
+ { id: 'b', name: 'B', fullName: 'B', agency: 'State', status: 'partial', githubLinks: {} },
+ ];
+ vi.doMock('../data/fetchPrograms', () => ({
+ fetchProgramsWithSource: vi.fn().mockResolvedValue({
+ programs,
+ source: { kind: 'api', repo: 'PolicyEngine/policyengine-us', apiVersion: '2.2.1' },
+ }),
+ }));
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+
+ render();
+ expect(await within(policyRulesCard()).findByText(
+ 'lists 2 federal, state, and local programs, 1 of them marked complete.',
+ { exact: false },
+ )).toBeInTheDocument();
+ });
+
+ it('does not load the US registry for the UK overview', async () => {
+ const fetchProgramsWithSource = vi.fn();
+ vi.doMock('../data/fetchPrograms', () => ({ fetchProgramsWithSource }));
+ const { default: ThreeIngredients } = await import('../components/microsim/ThreeIngredients');
+
+ render();
+ await act(async () => {});
+ expect(fetchProgramsWithSource).not.toHaveBeenCalled();
+ expect(within(policyRulesCard()).queryByRole('link')).not.toBeInTheDocument();
+ });
+});
+
+const STATUSES: CoverageStatus[] = ['complete', 'partial', 'inProgress', 'notStarted'];
+const status = fc.constantFrom(...STATUSES);
+const program: fc.Arbitrary = fc.record({
+ id: fc.string(),
+ agency: fc.option(fc.constantFrom('IRS', 'HHS', 'SSA', 'State', 'Local' as const), { nil: undefined }),
+ coverage: fc.option(fc.constantFrom('US', 'CA', 'NY', 'Los Angeles County, CA', ''), { nil: undefined }),
+ status,
+ hasStateVariation: fc.option(fc.boolean(), { nil: undefined }),
+ stateImplementations: fc.option(
+ fc.array(fc.record({ state: fc.constantFrom('CA', 'NY', 'TX', 'WA'), status })),
+ { nil: undefined },
+ ),
+}).map(p => ({ ...p, name: p.id, fullName: p.id, githubLinks: {} }));
+const source: fc.Arbitrary = fc.constantFrom(
+ { kind: 'fallback' },
+ { kind: 'api', repo: 'PolicyEngine/policyengine-us' },
+ {
+ kind: 'snapshot', repo: 'PolicyEngine/policyengine-us', branch: 'main',
+ commit: 'abcdef1', fetchedAt: '2026-10-14T00:00:00.000Z', version: '2.2.1',
+ },
+);
+
+describe('summarizeRegistry invariants', () => {
+ it('never cites the bundled fallback list or an empty registry', () => {
+ fc.assert(fc.property(fc.array(program), programs => {
+ expect(summarizeRegistry({ programs, source: { kind: 'fallback' } })).toBeNull();
+ }));
+ expect(summarizeRegistry({ programs: [], source: { kind: 'api', repo: 'PolicyEngine/policyengine-us' } })).toBeNull();
+ });
+
+ it('counts every registry program once and never claims more complete programs than the registry marks complete', () => {
+ fc.assert(fc.property(fc.array(program, { minLength: 1 }), source, (programs, src) => {
+ const summary = summarizeRegistry({ programs, source: src });
+ if (src.kind === 'fallback') {
+ expect(summary).toBeNull();
+ return;
+ }
+ const counts = computeStatusCount(programs);
+ expect(summary).toEqual({ total: programs.length, complete: counts.complete });
+ expect(Object.values(counts).reduce((a, b) => a + b, 0)).toBe(programs.length);
+ expect(summary!.complete).toBeGreaterThanOrEqual(0);
+ expect(summary!.complete).toBeLessThanOrEqual(programs.filter(p => p.status === 'complete').length);
+ }));
+ });
+});
+
+describe('US walkthrough behavioral step', () => {
+ const step = getMicrosimSteps('us')[3];
+
+ it('describes the labor supply mechanism policyengine-us implements, not the elasticity of taxable income', () => {
+ expect(step.description).not.toMatch(/taxable income/i);
+ expect(step.description).toMatch(/fixed by default/);
+ expect(step.description).toMatch(/labor supply elasticities/);
+ expect(step.description).toMatch(/marginal tax rates/);
+ expect(step.description).toMatch(/net income/);
+ });
+});