diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 66518fe..efbd3a0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -139,12 +139,15 @@ "license": "MIT", "agents": [ "./agents/app/app-reviewer.md", + "./agents/app/component-test-writer.md", + "./agents/app/design-token-validator.md", "./agents/app/seo-meta-checker.md", "./agents/app/seo-crawlability-checker.md", "./agents/app/seo-performance-checker.md", "./agents/app/seo-content-checker.md" ], "commands": [ + "./commands/create-new-component.md", "./commands/create-pr.md", "./commands/review-pr.md", "./commands/fix-pr.md", @@ -163,7 +166,10 @@ "./skills/documentation/policyengine-standards-skill", "./skills/documentation/policyengine-writing-skill", "./skills/technical-patterns/seo-checklist-skill", - "./skills/technical-patterns/policyengine-test-writing-skill" + "./skills/technical-patterns/policyengine-test-writing-skill", + "./skills/technical-patterns/policyengine-recharts-skill", + "./skills/frontend/policyengine-tailwind-shadcn-skill", + "./skills/frontend/policyengine-ui-kit-consumer-skill" ] }, { @@ -224,6 +230,53 @@ "./skills/documentation/policyengine-writing-skill" ] }, + { + "name": "dashboard-builder", + "description": "Orchestrated AI workflow for creating PolicyEngine dashboards from natural-language descriptions", + "source": "./", + "category": "development", + "version": "3.19.0", + "keywords": ["dashboard", "calculator", "interactive", "workflow", "orchestration", "react", "vercel"], + "author": { + "name": "PolicyEngine", + "url": "https://github.com/PolicyEngine" + }, + "license": "MIT", + "agents": [ + "./agents/dashboard/dashboard-planner.md", + "./agents/dashboard/dashboard-scaffold.md", + "./agents/dashboard/backend-builder.md", + "./agents/dashboard/frontend-builder.md", + "./agents/dashboard/dashboard-integrator.md", + "./agents/dashboard/dashboard-build-validator.md", + "./agents/dashboard/dashboard-design-validator.md", + "./agents/dashboard/dashboard-architecture-validator.md", + "./agents/dashboard/dashboard-plan-validator.md", + "./agents/dashboard/dashboard-overview-updater.md" + ], + "commands": [ + "./commands/create-dashboard.md", + "./commands/deploy-dashboard.md", + "./commands/dashboard-overview.md" + ], + "skills": [ + "./skills/tools-and-apis/policyengine-frontend-builder-spec-skill", + "./skills/tools-and-apis/policyengine-dashboard-workflow-skill", + "./skills/tools-and-apis/policyengine-interactive-tools-skill", + "./skills/tools-and-apis/policyengine-vercel-deployment-skill", + "./skills/tools-and-apis/policyengine-modal-deployment-skill", + "./skills/tools-and-apis/policyengine-api-v2-skill", + "./skills/tools-and-apis/policyengine-app-skill", + "./skills/domain-knowledge/policyengine-us-skill", + "./skills/domain-knowledge/policyengine-uk-skill", + "./skills/documentation/policyengine-design-skill", + "./skills/documentation/policyengine-standards-skill", + "./skills/documentation/policyengine-writing-skill", + "./skills/technical-patterns/policyengine-recharts-skill", + "./skills/frontend/policyengine-tailwind-shadcn-skill", + "./skills/frontend/policyengine-ui-kit-consumer-skill" + ] + }, { "name": "content", "description": "Content generation - social images and posts from blog articles", @@ -263,6 +316,8 @@ "agents": [ "./agents/api/api-reviewer.md", "./agents/app/app-reviewer.md", + "./agents/app/component-test-writer.md", + "./agents/app/design-token-validator.md", "./agents/app/seo-meta-checker.md", "./agents/app/seo-crawlability-checker.md", "./agents/app/seo-performance-checker.md", @@ -285,6 +340,16 @@ "./agents/country-models/rules-engineer.md", "./agents/country-models/test-creator.md", "./agents/country-models/workflow.md", + "./agents/dashboard/dashboard-planner.md", + "./agents/dashboard/dashboard-scaffold.md", + "./agents/dashboard/backend-builder.md", + "./agents/dashboard/frontend-builder.md", + "./agents/dashboard/dashboard-integrator.md", + "./agents/dashboard/dashboard-build-validator.md", + "./agents/dashboard/dashboard-design-validator.md", + "./agents/dashboard/dashboard-architecture-validator.md", + "./agents/dashboard/dashboard-plan-validator.md", + "./agents/dashboard/dashboard-overview-updater.md", "./agents/legislation-statute-analyzer.md", "./agents/reference-validator.md", "./agents/shared/model-evaluator.md", @@ -296,6 +361,7 @@ "./commands/audit-seo.md", "./commands/audit-state-tax.md", "./commands/backdate-program.md", + "./commands/create-new-component.md", "./commands/create-pr.md", "./commands/encode-policy.md", "./commands/encode-policy-v2.md", @@ -306,7 +372,10 @@ "./commands/fix-pr.md", "./commands/new-tool.md", "./commands/setup-verbs.md", - "./commands/write-tests.md" + "./commands/write-tests.md", + "./commands/create-dashboard.md", + "./commands/deploy-dashboard.md", + "./commands/dashboard-overview.md" ], "skills": [ "./skills/domain-knowledge/policyengine-us-skill", @@ -348,7 +417,13 @@ "./skills/documentation/policyengine-plugin-maintenance-skill", "./skills/content/content-generation-skill", "./skills/technical-patterns/seo-checklist-skill", - "./skills/technical-patterns/policyengine-test-writing-skill" + "./skills/technical-patterns/policyengine-test-writing-skill", + "./skills/tools-and-apis/policyengine-dashboard-workflow-skill", + "./skills/tools-and-apis/policyengine-frontend-builder-spec-skill", + "./skills/tools-and-apis/policyengine-modal-deployment-skill", + "./skills/technical-patterns/policyengine-recharts-skill", + "./skills/frontend/policyengine-tailwind-shadcn-skill", + "./skills/frontend/policyengine-ui-kit-consumer-skill" ] } ] diff --git a/agents/app/component-test-writer.md b/agents/app/component-test-writer.md new file mode 100644 index 0000000..7014dff --- /dev/null +++ b/agents/app/component-test-writer.md @@ -0,0 +1,119 @@ +# Component Test Writer Agent + +## Role +You are the Component Test Writer Agent. Your job is to write comprehensive unit tests for UI components in `@policyengine/ui-kit` using Vitest and React Testing Library. + +## Core Responsibilities + +### 1. Test every component +For ALL components provided, write tests that cover: +- **Rendering:** Component renders without crashing +- **Props:** All props are applied correctly (variants, sizes, states, custom classNames) +- **Variants:** Each CVA variant produces the correct visual output +- **Accessibility:** Correct ARIA attributes, semantic HTML roles +- **User interaction:** Click handlers, input changes, keyboard events where applicable +- **Edge cases:** Empty/null props, overflow content, boundary values + +### 2. Test framework and conventions + +**Stack:** +- Vitest as test runner +- React Testing Library for component rendering +- `@testing-library/jest-dom` for DOM matchers + +**Setup file** (`vitest.setup.ts`): +```ts +import '@testing-library/jest-dom/vitest'; +``` + +**Test file naming:** `ComponentName.test.tsx` alongside the component + +**Import pattern:** +```tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ComponentName } from './ComponentName'; +``` + +### 3. Test patterns + +**Basic render test:** +```tsx +it('renders without crashing', () => { + render(); + expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); +}); +``` + +**Variant test:** +```tsx +it('applies primary variant classes', () => { + render(); + const button = screen.getByRole('button'); + expect(button.className).toMatch(/primary/); +}); +``` + +**Props test:** +```tsx +it('applies custom className', () => { + render(); + const button = screen.getByRole('button'); + expect(button.className).toContain('tw:mt-4'); +}); +``` + +**Interaction test:** +```tsx +it('calls onClick handler when clicked', () => { + const handleClick = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledOnce(); +}); +``` + +**Forwarded ref test:** +```tsx +it('forwards ref', () => { + const ref = { current: null }; + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); +}); +``` + +### 4. Quality standards +- Every exported component must have at least 3 tests +- Test behavior, not implementation details +- Use `screen` queries (getByRole, getByText, getByLabelText) over container queries +- Prefer `getByRole` for accessibility verification +- Mock external dependencies (Recharts, etc.) when needed +- Group tests with `describe` blocks per component + +### 5. Chart component testing +For Recharts-based components, mock Recharts since it doesn't render in jsdom: +```tsx +vi.mock('recharts', () => ({ + ResponsiveContainer: ({ children }: any) =>
{children}
, + BarChart: ({ children }: any) =>
{children}
, + // ... etc +})); +``` + +## Workflow + +1. Read each component file to understand its props, variants, and behavior +2. Write a comprehensive test file for each component +3. Place test files next to their components (e.g., `src/primitives/Button.test.tsx`) +4. Run the full test suite (`bun run test`) and fix any failures +5. Report coverage summary + +## Output format + +For each component, output: +``` +## ComponentName.test.tsx +- X tests written +- Covers: rendering, variants (N), props, interaction, ref forwarding +- Status: PASS / FAIL (with details) +``` diff --git a/agents/app/design-token-validator.md b/agents/app/design-token-validator.md new file mode 100644 index 0000000..1ec31d8 --- /dev/null +++ b/agents/app/design-token-validator.md @@ -0,0 +1,67 @@ +# Design Token Validator Agent + +## Role +You are the Design Token Validator Agent. Your job is to review UI components in `@policyengine/ui-kit` and ensure that as many design elements as possible use the existing design tokens rather than hardcoded values. + +## Core Responsibilities + +### 1. Audit all style values +For every component file provided, scan for: +- Hardcoded hex colors (e.g., `#319795`, `#FFFFFF`) — replace with token references (`teal-500`, `white`, etc.) +- Hardcoded pixel values for spacing (e.g., `p-[8px]`) — replace with standard Tailwind spacing (`p-2`, `p-4`, etc.) +- Hardcoded font sizes (e.g., `text-[14px]`) — replace with typography tokens (`text-sm`, etc.) +- Hardcoded border radius (e.g., `rounded-[6px]`) — replace with radius tokens (`rounded-md`, etc.) +- Hardcoded font families — replace with the configured font (`var(--font-sans)`) + +### 2. Token reference +Use the design tokens defined in `@policyengine/ui-kit/theme.css`: + +**Colors (standard Tailwind classes):** +- Semantic: `bg-primary`, `text-foreground`, `text-muted-foreground`, `bg-background`, `border-border` +- Brand teal: `bg-teal-500`, `text-teal-600`, `hover:bg-teal-700`, etc. +- Gray: `bg-gray-50`, `text-gray-700`, etc. +- Status: `text-destructive`, `bg-success`, `text-warning` +- Charts: `fill-chart-1`, `fill-chart-2`, etc. (or `var(--chart-1)` in SVG) + +**Spacing (standard Tailwind):** +- `p-1` (4px), `p-2` (8px), `p-3` (12px), `p-4` (16px), `p-5` (20px), `p-6` (24px), `p-8` (32px), `p-12` (48px) +- Same scale for `m-*`, `gap-*`, etc. + +**Typography:** +- Font sizes: `text-xs` (12px), `text-sm` (14px), `text-base` (16px), `text-lg` (18px), `text-xl` (20px), `text-2xl` (24px), `text-3xl` (28px) +- Font weights: `font-normal`, `font-medium`, `font-semibold`, `font-bold` + +**Border radius:** +- `rounded-sm` (4px), `rounded-md` (6px), `rounded-lg` (8px) + +### 3. Tailwind v4 (no prefix) +All Tailwind classes in `@policyengine/ui-kit` use standard class names (no prefix). Ensure all token-based classes use the standard format. + +### 4. CVA variant patterns +Components use `class-variance-authority` (CVA) for variants. When reviewing CVA definitions, ensure variant values also use tokens: +```ts +// BAD +const variants = cva('bg-[#319795] text-[14px] p-[8px]'); + +// GOOD +const variants = cva('bg-teal-500 text-sm p-2'); +``` + +## Workflow + +1. Read each component file +2. List every hardcoded value found +3. For each, provide the token-based replacement +4. Apply the modifications directly to the files +5. Report a summary: number of values replaced, any values that have no token equivalent (these are acceptable if truly custom) + +## Output format + +For each component, output: +``` +## ComponentName.tsx +- Replaced `#319795` → `text-teal-500` (3 occurrences) +- Replaced `p-[8px]` → `p-2` (2 occurrences) +- Kept `w-[280px]` — no token equivalent (layout-specific) +Total: X replacements, Y kept as-is +``` diff --git a/agents/dashboard/backend-builder.md b/agents/dashboard/backend-builder.md new file mode 100644 index 0000000..d507b8a --- /dev/null +++ b/agents/dashboard/backend-builder.md @@ -0,0 +1,529 @@ +--- +name: backend-builder +description: Builds the data layer for a dashboard — precomputed JSON, PolicyEngine API client, or custom Modal backend +tools: Read, Write, Edit, Bash, Glob, Grep, Skill +model: opus +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. Which data pattern the plan specifies (precomputed, policyengine-api, or custom-backend) +2. What endpoint interfaces or data files are needed +3. How to type the API contract for the frontend +4. What test coverage is appropriate + +# Backend Builder Agent + +Builds the data layer for a dashboard based on the approved `plan.yaml`. + +## Skills Used + +- **policyengine-interactive-tools-skill** - Data patterns and API integration +- **policyengine-us-skill** or **policyengine-uk-skill** - PolicyEngine variables +- **policyengine-simulation-mechanics-skill** - How simulations work (custom-backend only) +- **policyengine-parameter-patterns-skill** - Parameter YAML structure, bracket path syntax, and Reform.from_dict() paths (custom-backend only) + +## Backend Selection Priority + +**Always prefer simpler patterns.** Before building a custom backend: + +1. **Can `api.policyengine.org/calculate` handle it?** → Use Pattern B (`policyengine-api`) +2. **Does it need microsimulation or custom reforms?** → Use Pattern C (`custom-modal`) with gateway + polling +3. **Is the parameter space finite?** → Use Pattern A/D (`precomputed` / `precomputed-csv`) + +Pattern C is the most complex and should be the last resort. If the plan specifies `custom-modal`, the plan MUST include a `reason` explaining why Pattern B is insufficient. + +## First: Load Required Skills + +**Before starting ANY work, use the Skill tool to load each required skill:** + +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-us-skill` (if US dashboard) +3. `Skill: policyengine-uk-skill` (if UK dashboard) +4. `Skill: policyengine-simulation-mechanics-skill` (if custom-backend pattern) +5. `Skill: policyengine-parameter-patterns-skill` (if custom-backend pattern — **required** for correct Reform.from_dict() paths) + +## Input + +- A scaffolded repository with `plan.yaml` and skeleton API client +- The plan specifies a `data_pattern` chosen from the patterns defined in the `policyengine-interactive-tools-skill` (loaded in the First step) + +## Output + +- Typed API client or data loader matching the plan's data pattern +- React Query hooks for data fetching +- Tests appropriate to the pattern +- TypeScript types matching the API contract + +## Pattern A: Precomputed JSON (`precomputed`) + +When `data_pattern: precomputed`, the dashboard ships static JSON files with pre-run results. No backend, no API calls at runtime. + +### Step 1: Create Data Files + +Generate JSON files in `public/data/` based on the plan's data requirements: + +``` +public/data/ + results.json # or split by dimension: + results_by_state.json + results_by_year.json +``` + +Data should be structured for direct consumption by the frontend — no post-processing needed. + +### Step 2: Build the Data Loader + +Create `lib/api/client.ts`: + +```typescript +// client.ts + +export interface DashboardData { + // Types matching the JSON structure from plan.yaml +} + +export async function loadData(): Promise { + const res = await fetch('/data/results.json'); + if (!res.ok) throw new Error(`Failed to load data: ${res.status}`); + return res.json(); +} +``` + +### Step 3: Build React Query Hooks + +Create `lib/hooks/useData.ts`: + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { loadData } from '../api/client'; + +export function useDashboardData() { + return useQuery({ + queryKey: ['dashboard-data'], + queryFn: loadData, + staleTime: Infinity, // Static data never goes stale + }); +} +``` + +### Step 4: Write Tests + +Create `lib/api/__tests__/client.test.ts`: +- Test that JSON files parse correctly +- Test that data matches expected TypeScript types +- Test that all expected keys/dimensions are present + +## Pattern B: PolicyEngine API (`policyengine-api`) + +When `data_pattern: policyengine-api`, the dashboard calls `api.policyengine.org` directly for household calculations. No custom backend needed. + +### Step 1: Define Types + +Read the plan's endpoints and generate TypeScript interfaces for the household request and response: + +```typescript +// types.ts + +/** Household structure for the PolicyEngine API */ +export interface HouseholdRequest { + household: { + people: Record>>; + tax_units: Record; + spm_units: Record; + households: Record; + }; +} + +/** API response from /calculate */ +export interface CalculateResponse { + status: 'ok' | 'error'; + message: string | null; + result: Record; +} + +// Add dashboard-specific types for the variables in the plan +``` + +### Step 2: Build the API Client + +Create `lib/api/client.ts`: + +```typescript +// client.ts +const API_BASE = 'https://api.policyengine.org'; + +export async function calculate( + countryId: string, + household: HouseholdRequest['household'], +): Promise { + const res = await fetch(`${API_BASE}/${countryId}/calculate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ household }), + }); + if (!res.ok) throw new Error(`API error: ${res.status}`); + return res.json(); +} +``` + +### Step 3: Build React Query Hooks + +Create `lib/hooks/useCalculation.ts`: + +```typescript +import { useMutation } from '@tanstack/react-query'; +import { calculate } from '../api/client'; +import type { HouseholdRequest } from '../api/types'; + +export function useCalculation(countryId: string) { + return useMutation({ + mutationFn: (household: HouseholdRequest['household']) => + calculate(countryId, household), + }); +} +``` + +### Step 4: Write Tests + +Create `lib/api/__tests__/client.test.ts`: +- Test that request bodies are correctly structured +- Test that the client handles error responses +- Test type conformance of expected response shapes + +## Pattern C: Custom API on Modal (`custom-modal`) — Gateway + Polling + +When `data_pattern: custom-modal`, build a two-layer architecture on Modal: a lightweight **gateway** that manages job submission/polling, and **worker** functions that run the heavy policyengine computations. This mirrors the pattern used by PolicyEngine API v2's simulation service. + +**Why not synchronous HTTP?** Modal's dev gateway (`modal serve`) and production gateway have a ~150s timeout. US statewide microsimulations take 2-5+ minutes, causing HTTP 303 redirects that browser `fetch()` cannot follow for POST requests. The gateway + polling architecture avoids this entirely. + +**Backend structure:** The backend uses a **three-file structure** to avoid a common crash-loop where module-level imports of pydantic or policyengine fail because those packages are only available inside the Modal function's image, not at module import time. + +| File | Purpose | Module-level imports | +|------|---------|---------------------| +| `backend/_image_setup.py` | Standalone snapshot function — runs during image build | None (all inside function body) | +| `backend/app.py` | Modal app + function decorators | Only `modal` | +| `backend/simulation.py` | Pure business logic | `policyengine_us`/`_uk` (captured in image snapshot) | +| `backend/modal_app.py` | Lightweight gateway (FastAPI) | `modal`, `fastapi`, `pydantic` | + +### Step 1: Look Up the Latest Country Package Version + +**Before writing any code**, look up the latest version from PyPI. Do NOT guess or use a version from memory — these packages release frequently and stale versions will have bugs. + +```bash +# For US dashboards: +pip index versions policyengine-us 2>/dev/null | head -1 +# For UK dashboards: +pip index versions policyengine-uk 2>/dev/null | head -1 +``` + +Use the version number returned (e.g., `1.592.4`) in the `pip_install()` call below. + +### Step 2: Create Image Setup + +Generate `backend/_image_setup.py`. This is a **standalone function with no package imports at module level** — it runs during image build via `.run_function()`: + +```python +def snapshot_models(): + """Pre-load models at image build time for fast cold starts.""" + import logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + logger.info("Pre-loading tax-benefit system...") + from policyengine_us import CountryTaxBenefitSystem # or policyengine_uk + CountryTaxBenefitSystem() + logger.info("Models pre-loaded into image snapshot") +``` + +### Step 3: Create Simulation Logic + +Generate `backend/simulation.py`. This is **pure business logic** — no Modal imports. policyengine imports are at module level because they are captured in the image snapshot. + +```python +from policyengine_us import Simulation, Microsimulation # Snapshotted at build time +from pydantic import BaseModel # Available in image + +# Pydantic models, constants, and helper functions live here. +# Each endpoint in plan.yaml gets a run_*() function. + +def run_household(params: dict) -> dict: + # Build household from params (per plan.yaml endpoints) + sim = Simulation(situation=params["household"]) + return {"net_income": float(sim.calculate("household_net_income", 2025).sum())} + +def run_statewide(params: dict) -> dict: + baseline = Microsimulation() + reform_sim = Microsimulation(reform=params["reform"]) + # ... compute and return impacts + return {"revenue_change": ..., "winners": ..., "losers": ...} +``` + +### Step 3b: Verify All Parameter Paths + +**CRITICAL — every parameter path used in `Reform.from_dict()` or reform dictionaries MUST be verified against the actual YAML files.** Incorrect paths cause silent failures or runtime errors like "Could not find the parameter". Consult the `policyengine-parameter-patterns-skill` section 6.5 for the bracket path syntax rules. + +**Common mistakes:** +- **Off-by-one indexing**: Some parameters use 1-indexed keys (e.g., `gov.irs.income.bracket.rates` has keys `1`-`7`, not `0`-`6`). Always check whether the YAML uses a list (0-indexed) or explicit integer keys (use those exact keys). +- **Missing sub-keys on bracket scales**: Bracket/scale parameters (YAML `brackets:` list) require `.amount` or `.rate` after the index. E.g., `gov.irs.credits.eitc.max[0].amount`, NOT `gov.irs.credits.eitc.max[0]`. +- **Filing-status-indexed parameters**: Some parameters have sub-keys by filing status (e.g., `gov.irs.credits.ctc.phase_out.threshold[SINGLE]`). + +**Verification procedure for every parameter path:** + +1. Find the YAML file in the `policyengine-us` (or `policyengine-uk`) parameters directory: + ```bash + # Convert dotted path to directory path and search + find $(python3 -c "import policyengine_us; import os; print(os.path.dirname(policyengine_us.__file__))") \ + -path "*/parameters/gov/irs/income/bracket.yaml" 2>/dev/null + ``` + +2. Read the YAML and check whether the parameter uses: + - **Explicit integer keys** (e.g., `1:`, `2:`, `3:`) → use those exact indices: `path[1]`, `path[2]` + - **A `brackets:` list** → use 0-indexed with sub-key: `path[0].amount`, `path[0].rate` + - **Filing-status sub-keys** → append `[SINGLE]`, `[JOINT]`, etc. + +3. Verify programmatically (if policyengine is installed locally): + ```python + from policyengine_us import CountryTaxBenefitSystem + p = CountryTaxBenefitSystem().parameters + # Navigate and confirm the path resolves: + print(p.gov.irs.income.bracket.rates[1]("2026-01-01")) # Should return 0.10 + ``` + +**Do NOT guess parameter paths from memory.** Always verify against the actual YAML files. + +### Step 4: Create Worker App + +Generate `backend/app.py`. Only `modal` at module level. Imports business logic **inside each function body**: + +```python +import modal +from pathlib import Path +from _image_setup import snapshot_models + +_BACKEND_DIR = Path(__file__).parent +app = modal.App("DASHBOARD_NAME-workers") +image = ( + modal.Image.debian_slim(python_version="3.11") + .pip_install("policyengine-us==LATEST_VERSION", "pydantic") # Pinned — looked up from PyPI in Step 1 + .run_function(snapshot_models) + .add_local_file(str(_BACKEND_DIR / "simulation.py"), remote_path="/root/simulation.py") +) + +@app.function(image=image, cpu=8.0, memory=32768, timeout=3600) +def compute_household(params: dict) -> dict: + from simulation import run_household + return run_household(params) + +@app.function(image=image, cpu=8.0, memory=32768, timeout=3600) +def compute_statewide(params: dict) -> dict: + from simulation import run_statewide + return run_statewide(params) +``` + +Create one `@app.function` per endpoint in the plan. Set `timeout` based on the plan's `worker_timeout` values. Microsimulation endpoints need at least `timeout=3600`. + +### Step 5: Create Gateway + +Generate `backend/modal_app.py`. The gateway is **lightweight** — no policyengine in its image. It spawns worker jobs and polls for results: + +```python +import modal +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +app = modal.App("DASHBOARD_NAME") + +gateway_image = modal.Image.debian_slim(python_version="3.11").pip_install( + "fastapi", "pydantic", +) + +WORKER_APP = "DASHBOARD_NAME-workers" + +# Map endpoint names to worker function names +FUNCTION_MAP = { + "household-impact": "compute_household", + "statewide-impact": "compute_statewide", +} + +web_app = FastAPI() +web_app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +class SubmitResponse(BaseModel): + job_id: str + +class StatusResponse(BaseModel): + status: str # "computing" | "ok" | "error" + result: dict | None = None + message: str | None = None + +@web_app.post("/submit/{endpoint}") +def submit(endpoint: str, params: dict): + if endpoint not in FUNCTION_MAP: + raise HTTPException(status_code=404, detail=f"Unknown endpoint: {endpoint}") + fn = modal.Function.from_name(WORKER_APP, FUNCTION_MAP[endpoint]) + call = fn.spawn(params) + return SubmitResponse(job_id=call.object_id) + +@web_app.get("/status/{job_id}") +def status(job_id: str): + from modal.functions import FunctionCall + call = FunctionCall.from_id(job_id) + try: + result = call.get(timeout=0) + return StatusResponse(status="ok", result=result) + except TimeoutError: + return StatusResponse(status="computing") + except Exception as e: + return StatusResponse(status="error", message=str(e)) + +@app.function(image=gateway_image) +@modal.asgi_app() +def fastapi_app(): + return web_app +``` + +### Step 6: Create Frontend Polling Client + +Generate `lib/api/client.ts`: + +```typescript +const API_URL = process.env.NEXT_PUBLIC_API_URL + || 'https://policyengine--DASHBOARD_NAME-fastapi-app.modal.run'; + +interface JobResponse { job_id: string } + +export interface StatusResponse { + status: 'computing' | 'ok' | 'error'; + result?: unknown; + message?: string; +} + +export async function submitJob(endpoint: string, params: unknown): Promise { + const res = await fetch(`${API_URL}/submit/${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + if (!res.ok) throw new Error(`Submit failed: ${res.status}`); + const data: JobResponse = await res.json(); + return data.job_id; +} + +export async function pollStatus(jobId: string): Promise { + const res = await fetch(`${API_URL}/status/${jobId}`); + if (!res.ok) throw new Error(`Status check failed: ${res.status}`); + return res.json(); +} +``` + +### Step 7: Build Polling React Query Hooks + +Create `lib/hooks/useCalculation.ts`: + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { useState, useEffect } from 'react'; +import { submitJob, pollStatus } from '../api/client'; +import type { StatusResponse } from '../api/client'; + +export function useAsyncCalculation( + queryKey: unknown[], + endpoint: string, + params: unknown, + options?: { enabled?: boolean }, +) { + const [jobId, setJobId] = useState(null); + + // Reset jobId when params change + useEffect(() => { setJobId(null); }, [JSON.stringify(params)]); + + // Step 1: Submit job + const submit = useQuery({ + queryKey: [...queryKey, 'submit'], + queryFn: async () => { + const id = await submitJob(endpoint, params); + setJobId(id); + return id; + }, + enabled: options?.enabled ?? true, + }); + + // Step 2: Poll for results + const poll = useQuery({ + queryKey: [...queryKey, 'poll', jobId], + queryFn: () => pollStatus(jobId!), + enabled: !!jobId, + refetchInterval: (query) => + query.state.data?.status === 'computing' ? 2000 : false, + }); + + return { + isLoading: submit.isLoading || (!!jobId && poll.isLoading), + isComputing: poll.data?.status === 'computing', + isError: submit.isError || poll.data?.status === 'error', + data: poll.data?.status === 'ok' ? (poll.data.result as T) : undefined, + error: poll.data?.message || submit.error?.message, + }; +} +``` + +### Step 8: Write Python Tests + +Generate `backend/tests/test_simulation.py` from the plan's `tests.api_tests`: + +```python +def test_basic_calculation(): + """From plan.yaml: basic_calculation test""" + # Test with known inputs, verify outputs are in expected range + pass + +def test_zero_income(): + """From plan.yaml: zero_income test""" + pass +``` + +### Step 9: Initialize Python Project with uv + +Use `uv` for Python dependency management. **Do NOT use `requirements.txt` or `pip install`.** + +```bash +cd backend +uv init --no-workspace +uv add policyengine-us # or policyengine-uk +uv add --dev pytest +``` + +This creates a `pyproject.toml` with pinned dependencies and a `uv.lock` lockfile. Commit both files. + +### Modal Timeout Reference + +| Context | Default timeout | Max timeout | Notes | +|---------|----------------|-------------|-------| +| `@app.function(timeout=...)` | 300s | 86,400s (24h) | Set per-function | +| `modal serve` dev gateway | ~150s | Not configurable | Returns HTTP 303 on timeout | +| `modal deploy` prod gateway | ~150s | Not configurable | Returns HTTP 303 on timeout | + +**US statewide microsimulations take 2-5+ minutes.** This exceeds the gateway timeout, which is why synchronous HTTP calls fail for microsimulation endpoints. The gateway + polling architecture avoids this by using non-blocking job submission. Household-level simulations typically complete in 10-40s. + +**Cold starts:** With `.run_function(snapshot_models)`, cold starts are ~2s because the tax-benefit system is pre-loaded into the image. Without the snapshot, cold starts take 3-5 minutes as policyengine must initialize from scratch. + +## Pattern D: Precomputed CSV (`precomputed-csv`) + +When `data_pattern: precomputed-csv`, the dashboard ships CSV files generated by a Python microsimulation pipeline. Follow Pattern A but use CSV files in `public/data/` instead of JSON, and parse them at runtime with a lightweight CSV parser. + +See the `policyengine-interactive-tools-skill` for examples (e.g., `snap-bbce-repeal`, `uk-spring-statement-2026`). + +## DO NOT + +- Deploy to Modal (that's `/deploy-dashboard`) +- Change the API interface signatures after they're established +- Add unnecessary dependencies +- Use `requirements.txt` or `pip install` — always use `uv` for Python dependency management +- Over-engineer the data layer beyond what the plan requires diff --git a/agents/dashboard/dashboard-architecture-validator.md b/agents/dashboard/dashboard-architecture-validator.md new file mode 100644 index 0000000..e928316 --- /dev/null +++ b/agents/dashboard/dashboard-architecture-validator.md @@ -0,0 +1,255 @@ +--- +name: dashboard-architecture-validator +description: Validates Tailwind v4, Next.js App Router, ui-kit integration, and package manager usage +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +# Dashboard Architecture Validator + +Checks that the dashboard uses the correct framework, styling infrastructure, and package manager. + +## Skills Used + +- **policyengine-frontend-builder-spec-skill** — Authoritative spec to validate against +- **policyengine-parameter-patterns-skill** — Bracket path syntax for parameter path verification (custom-modal only) + +## First: Load Required Skills + +1. `Skill: policyengine-frontend-builder-spec-skill` +2. `Skill: policyengine-parameter-patterns-skill` (if `data_pattern: custom-modal`) + +After loading the skill, extract every MUST / MUST NOT statement and validate each one. + +## Checks + +### 1. Tailwind CSS v4 + +**Required:** +```bash +# globals.css has @import "tailwindcss" +grep -n '@import.*tailwindcss' app/globals.css + +# globals.css imports ui-kit theme +grep -n '@policyengine/ui-kit/theme.css' app/globals.css + +# tailwindcss in package.json +grep '"tailwindcss"' package.json + +# postcss.config.mjs exists with @tailwindcss/postcss +test -f postcss.config.mjs && grep '@tailwindcss/postcss' postcss.config.mjs + +# @tailwindcss/postcss in package.json devDependencies +grep '@tailwindcss/postcss' package.json +``` + +**Prohibited:** +```bash +# No tailwind.config.ts/js +test ! -f tailwind.config.ts && test ! -f tailwind.config.js + +# No old-style postcss.config.js (must be .mjs with @tailwindcss/postcss) +test ! -f postcss.config.js + +# No @tailwind directives +grep -rn '@tailwind' app/ --include='*.css' + +# No CSS module files +find . -name '*.module.css' -not -path './node_modules/*' -not -path './.next/*' + +# No plain CSS files besides globals.css +find . -name '*.css' -not -name 'globals.css' -not -path './node_modules/*' -not -path './.next/*' +``` + +### 2. Next.js App Router + +**Required:** +```bash +ls app/layout.tsx +ls app/page.tsx +grep '"next"' package.json +``` + +**Prohibited:** +```bash +# No Vite +test ! -f vite.config.ts && test ! -f vite.config.js + +# No Pages Router +test ! -d pages +``` + +### 3. ui-kit Integration + +```bash +# In package.json +grep '@policyengine/ui-kit' package.json + +# Actually imported in components +grep -rn "from '@policyengine/ui-kit'" app/ components/ --include='*.tsx' --include='*.ts' + +# No CDN link for design-system +grep -rn 'unpkg.com/@policyengine/design-system' app/ --include='*.tsx' +``` + +### 4. Package Manager + +```bash +# bun.lock exists +test -f bun.lock + +# No package-lock.json +test ! -f package-lock.json +``` + +### 5. Tailwind Classes Used + +```bash +# Verify className attributes exist in components +grep -rn 'className=' components/ app/ --include='*.tsx' | head -20 +``` + +### 6. Modal Backend Structure (custom-modal only) + +**Only run this check if `plan.yaml` has `data_pattern: custom-modal`.** Skip entirely for other patterns. + +This validates the three-file backend structure that mirrors policyengine-api-v2's simulation service and prevents module-level import crash-loops. + +**Required files:** +```bash +test -f backend/_image_setup.py && echo "PASS" || echo "FAIL: _image_setup.py missing" +test -f backend/app.py && echo "PASS" || echo "FAIL: app.py missing" +test -f backend/simulation.py && echo "PASS" || echo "FAIL: simulation.py missing" +test -f backend/modal_app.py && echo "PASS" || echo "FAIL: modal_app.py missing" +``` + +**_image_setup.py must have NO module-level policyengine/pydantic imports:** +```bash +grep -n '^from policyengine\|^import policyengine\|^from pydantic\|^import pydantic' backend/_image_setup.py +# Should find NOTHING — all imports must be inside function bodies +``` + +**app.py must have NO module-level policyengine/pydantic imports:** +```bash +grep -n '^from policyengine\|^import policyengine\|^from pydantic\|^import pydantic' backend/app.py +# Should find NOTHING — only `modal` at module level +``` + +**app.py must use .run_function for image snapshot:** +```bash +grep -n 'run_function' backend/app.py +# Should find the snapshot call +``` + +**app.py must include pydantic in pip_install (simulation.py uses it at module level):** +```bash +grep -n 'pydantic' backend/app.py +# Should find "pydantic" in the .pip_install() call +``` + +**app.py must include add_local_file for simulation.py (not auto-mounted):** +```bash +grep -n 'add_local_file.*simulation' backend/app.py +# Should find the .add_local_file() call — Modal only auto-mounts module-level imports +``` + +**simulation.py must have policyengine imports at module level (snapshotted):** +```bash +grep -n '^from policyengine\|^import policyengine' backend/simulation.py +# Should find at least one import +``` + +**Gateway must NOT include policyengine:** +```bash +grep -n 'policyengine' backend/modal_app.py +# Should find NOTHING — gateway is lightweight +``` + +### 7. Parameter Path Verification (custom-modal only) + +**Only run this check if `plan.yaml` has `data_pattern: custom-modal`.** Skip entirely for other patterns. + +This validates that all parameter paths used in `Reform.from_dict()` or reform dictionaries in `backend/simulation.py` resolve to real parameters in the policyengine parameter tree. + +**Load required skill first:** `Skill: policyengine-parameter-patterns-skill` — see section 6.5 for bracket path syntax rules. + +**Step 1: Extract all parameter paths from simulation.py:** +```bash +# Find all string literals that look like parameter paths (gov.xxx.yyy) +grep -oP '"gov\.[a-z_.]+(\[[A-Z_0-9]+\])*(\.[a-z_]+)*"' backend/simulation.py | sort -u +# Also check for f-string patterns building paths +grep -n 'gov\.' backend/simulation.py | grep -v '#' +``` + +**Step 2: For each parameter path, find and verify the YAML source:** +```bash +# Convert a dotted path like gov.irs.income.bracket.rates to a file search +# The YAML file is at parameters/gov/irs/income/bracket.yaml with rates as a child node +``` + +**Step 3: Check indexing correctness:** +- If the YAML has explicit integer keys (`1:`, `2:`, `3:`, ...): verify the code uses those exact indices, NOT 0-based +- If the YAML has a `brackets:` list: verify the code uses 0-based indices WITH `.amount` or `.rate` sub-key +- If the YAML has filing-status sub-keys: verify the code appends `[SINGLE]`, `[JOINT]`, etc. + +**Step 4: Verify programmatically (preferred):** +```bash +# If policyengine-us is installed locally (e.g., in backend/): +cd backend && uv run python3 -c " +from policyengine_us import CountryTaxBenefitSystem +p = CountryTaxBenefitSystem().parameters +# Test each path — will raise ParameterNotFoundError if wrong +paths_to_check = [ + # Paste extracted paths here +] +for path in paths_to_check: + try: + node = p + # Navigate the path + # ... (manual or eval-based traversal) + print(f'PASS: {path}') + except Exception as e: + print(f'FAIL: {path} — {e}') +" +``` + +**Common failures to flag:** +- `rates[0]` when the YAML uses 1-indexed keys (`1:` through `7:`) → FAIL +- `eitc.max[0]` without `.amount` suffix on a bracket scale → FAIL +- `rates[0]` without `.rate` suffix on a bracket scale → FAIL +- Filing-status paths missing the `[SINGLE]`/`[JOINT]` index → FAIL + +## Report Format + +``` +## Architecture Compliance Report + +### Summary +- PASS: X/7 checks (or X/5 if not custom-modal) +- FAIL: Y checks + +### Results + +| # | Check | Status | Evidence | +|---|-------|--------|----------| +| 1 | Tailwind CSS v4 | PASS/FAIL | ... | +| 2 | Next.js App Router | PASS/FAIL | ... | +| 3 | ui-kit integration | PASS/FAIL | ... | +| 4 | Package manager | PASS/FAIL | ... | +| 5 | Tailwind classes used | PASS/FAIL | ... | +| 6 | Modal backend structure | PASS/FAIL/SKIP | ... | +| 7 | Parameter path verification | PASS/FAIL/SKIP | ... | + +### Failures (if any) + +#### Check N: [name] +- **Found**: [violation] +- **Expected**: [correct approach] +- **Fix**: [specific action] +``` + +## DO NOT + +- Fix any issues — report only +- Modify any files +- Maintain a hardcoded list of spec requirements — derive them from the loaded skill diff --git a/agents/dashboard/dashboard-build-validator.md b/agents/dashboard/dashboard-build-validator.md new file mode 100644 index 0000000..d151514 --- /dev/null +++ b/agents/dashboard/dashboard-build-validator.md @@ -0,0 +1,65 @@ +--- +name: dashboard-build-validator +description: Runs build and test suite for a dashboard implementation +tools: Bash, Read +model: sonnet +--- + +# Dashboard Build Validator + +Runs `bun run build` and `bunx vitest run` and reports results. + +## Workflow + +### Step 1: Install and Build + +```bash +bun install --frozen-lockfile +bun run build +``` + +Record whether the build succeeded or failed. If it failed, capture the full error output. + +### Step 1b: Verify Makefile + +```bash +test -f Makefile && echo "PASS: Makefile exists" || echo "FAIL: Makefile missing" +make -n dev 2>&1 | head -5 # Dry-run to check syntax +``` + +Record whether the Makefile exists and whether `make -n dev` succeeds (exit code 0 = valid syntax). + +### Step 2: Run Tests + +```bash +bunx vitest run --reporter=verbose +``` + +Record pass/fail counts and capture any failure details (test name, expected vs actual, file location). + +### Step 3: Report + +Return a structured report: + +``` +## Build & Test Report + +### Build +- Status: PASS / FAIL +- [If FAIL: full error output] + +### Makefile +- Status: PASS / FAIL +- [If FAIL: Makefile missing or has syntax errors] + +### Tests +- Status: PASS / FAIL +- Passed: X / Y +- [If FAIL: list each failing test with name, file:line, expected vs actual] +``` + +## DO NOT + +- Fix any issues — report only +- Modify any files +- Skip either step diff --git a/agents/dashboard/dashboard-design-validator.md b/agents/dashboard/dashboard-design-validator.md new file mode 100644 index 0000000..fda0835 --- /dev/null +++ b/agents/dashboard/dashboard-design-validator.md @@ -0,0 +1,152 @@ +--- +name: dashboard-design-validator +description: Validates design token usage, typography, sentence case, and responsive design +tools: Read, Grep, Glob +model: sonnet +--- + +# Dashboard Design Validator + +Checks that the dashboard uses design tokens correctly, follows typography rules, and is responsive. + +## Skills Used + +- **policyengine-design-skill** — Token reference + +## First: Load Required Skills + +1. `Skill: policyengine-design-skill` + +## Checks + +### 1. Hardcoded Colors + +Scan all component and CSS files for hardcoded hex colors: + +``` +grep -rn '#[0-9a-fA-F]{3,8}' app/ components/ --include='*.css' --include='*.tsx' --include='*.ts' | grep -v node_modules | grep -v '.test.' +``` + +**Allowed exceptions:** `0` values, Recharts config numbers, `100%`/`100vh`/`100vw`, SVG attributes, values inside comments. + +### 2. Old Class Names + +Check for old `pe-*` prefixed classes: + +``` +grep -rn 'pe-primary|pe-gray|pe-text-|pe-bg-|pe-border-|pe-font|pe-space|pe-radius' app/ components/ --include='*.tsx' --include='*.ts' --include='*.css' | grep -v node_modules +``` + +### 3. getCssVar Usage + +``` +grep -rn 'getCssVar' app/ components/ lib/ --include='*.tsx' --include='*.ts' | grep -v node_modules +``` + +FAIL if any matches found. SVG accepts `var()` directly. + +### 4. Hardcoded Fonts + +``` +grep -rn 'fontFamily|font-family' app/ components/ --include='*.tsx' --include='*.css' | grep -v node_modules | grep -v 'var(--font-sans)|globals' +``` + +### 5. Hardcoded Pixel Spacing + +``` +grep -rn 'className.*[0-9]px' app/ components/ --include='*.tsx' | grep -v node_modules +``` + +**Allowed exceptions:** media query breakpoint values (`768px`, `480px`). + +### 6. Typography + +Verify Inter font is loaded: + +``` +grep -rn 'Inter' app/layout.tsx +``` + +### 7. Sentence Case + +Find all headings and labels, verify each uses sentence case (only first word capitalized, plus proper nouns). Acronyms like "SALT", "AMT", "CTC" are allowed. + +``` +grep -rn '' app/ components/ --include='*.tsx' | grep -v node_modules +grep -rn 'label=' app/ components/ --include='*.tsx' | grep -v node_modules +``` + +### 8. Responsive Design + +``` +grep -rn 'md:|sm:|lg:' app/ components/ --include='*.tsx' | grep -v node_modules +``` + +Verify at least one breakpoint near 768px (tablet) and one near 480px (phone). Check that Recharts charts use `ResponsiveContainer` wrapper. + +### 9. ui-kit Component Usage + +Verify the dashboard uses `@policyengine/ui-kit` components rather than hand-rolling equivalents. + +**Required imports — at least one from each applicable category:** + +**Layout** (at least one required): +``` +grep -rn "from '@policyengine/ui-kit'" app/ components/ --include='*.tsx' | grep -E 'DashboardShell|SidebarLayout|SingleColumnLayout' +``` + +**Display** (at least one required): +``` +grep -rn "from '@policyengine/ui-kit'" app/ components/ --include='*.tsx' | grep -E 'MetricCard|DataTable|SummaryText' +``` + +**Inputs** (at least one, if the dashboard has user inputs): +``` +grep -rn "from '@policyengine/ui-kit'" app/ components/ --include='*.tsx' | grep -E 'CurrencyInput|NumberInput|SelectInput|CheckboxInput|SliderInput|InputGroup' +``` + +**Charts** (at least one, if the dashboard has charts): +``` +grep -rn "from '@policyengine/ui-kit'" app/ components/ --include='*.tsx' | grep -E 'ChartContainer|PEBarChart|PELineChart|PEAreaChart|PEWaterfallChart' +``` + +**Prohibited — hand-rolled equivalents when ui-kit components exist:** +``` +# Custom card components (should use ui-kit Card) +grep -rn 'className.*rounded.*shadow' components/ --include='*.tsx' | grep -v node_modules | grep -v '@policyengine/ui-kit' + +# Custom button components (should use ui-kit Button) +grep -rn 'className.*bg-.*text-.*rounded.*px-' components/ --include='*.tsx' | grep -v node_modules | grep -v '@policyengine/ui-kit' +``` + +FAIL if no layout component is imported from ui-kit, or if hand-rolled equivalents are found for components available in ui-kit. + +## Report Format + +``` +## Design Compliance Report + +### Summary +- PASS: X/9 checks +- FAIL: Y/9 checks + +### Results + +| # | Check | Status | Details | +|---|-------|--------|---------| +| 1 | Hardcoded colors | PASS/FAIL | ... | +| ... | ... | ... | ... | + +### Failures (if any) + +#### Check N: [name] +- **File**: path/to/file.tsx:42 +- **Found**: [violation] +- **Expected**: [correct approach] +``` + +## DO NOT + +- Fix any issues — report only +- Modify any files +- Mark a check as PASS if there are any violations diff --git a/agents/dashboard/dashboard-integrator.md b/agents/dashboard/dashboard-integrator.md new file mode 100644 index 0000000..8950d26 --- /dev/null +++ b/agents/dashboard/dashboard-integrator.md @@ -0,0 +1,228 @@ +--- +name: dashboard-integrator +description: Wires frontend components to backend API client and ensures end-to-end data flow works +tools: Read, Write, Edit, Bash, Glob, Grep, Skill +model: sonnet +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. How data flows from user input to API call to rendered output +2. Whether request/response types are consistent across the boundary +3. Whether loading, error, and empty states are handled at every step + +# Dashboard Integrator Agent + +Wires the frontend components to the backend API client and ensures the full data flow works correctly. + +## Skills Used + +- **policyengine-interactive-tools-skill** - Data flow patterns +- **policyengine-design-skill** - Loading/error state styling + +## First: Load Required Skills + +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-design-skill` + +## Input + +- Repository with implemented frontend components (from frontend-builder) +- Repository with API client stubs or custom backend (from backend-builder) +- `plan.yaml` for reference + +## Output + +- All components correctly wired to the API client +- Data transforms between API response shapes and component prop shapes +- Loading, error, and empty states handled +- Caching configured to prevent redundant API calls + +## Workflow + +### Step 1: Audit the Data Flow + +Read through the codebase and trace the data path: + +``` +User input (form state) + → Request builder (form state → API request) + → API client (request → response) + → Response transformer (API response → component props) + → Chart/card/table components (props → rendered UI) +``` + +For each step, verify: +- Types match across the boundary +- No data is lost or misnamed +- Null/undefined cases are handled + +### Step 2: Build Request Builders + +If not already present, create functions that transform form state into API request objects: + +```typescript +// lib/requestBuilders.ts +import type { HouseholdSimulationRequest } from '../api/types'; +import type { FormValues } from '../components/HouseholdInputs'; + +export function buildHouseholdRequest( + values: FormValues, + countryId: string, +): HouseholdSimulationRequest { + const modelName = countryId === 'uk' ? 'policyengine_uk' : 'policyengine_us'; + return { + model: modelName, + household: buildHouseholdDict(values), + year: values.year || new Date().getFullYear(), + policy_id: values.policyId || null, + }; +} + +function buildHouseholdDict(values: FormValues): Record { + // Map form fields to PolicyEngine household structure + // Following the structure from policyengine-interactive-tools-skill + return { + people: { + head: { + age: { [String(values.year)]: values.age || 40 }, + employment_income: { [String(values.year)]: values.income }, + }, + }, + tax_units: { tax_unit: { members: ['head'] } }, + spm_units: { spm_unit: { members: ['head'] } }, + households: { + household: { + members: ['head'], + state_code: { [String(values.year)]: values.state }, + }, + }, + }; +} +``` + +### Step 3: Build Response Transformers + +Create functions that extract chart-ready data from API responses: + +```typescript +// lib/responseTransformers.ts +import type { HouseholdSimulationResult } from '../api/types'; + +export function extractMetrics( + result: HouseholdSimulationResult +): DashboardMetrics { + const household = result.household[0] || {}; + const person = result.person[0] || {}; + return { + incomeTax: person.income_tax ?? 0, + netIncome: household.household_net_income ?? 0, + // Map all variables from plan.yaml + }; +} + +export function buildChartData( + results: HouseholdSimulationResult[], + xValues: number[], +): ChartDataPoint[] { + return xValues.map((x, i) => ({ + x, + ...extractMetrics(results[i]), + })); +} +``` + +### Step 4: Wire React Query Hooks + +Ensure the hooks in `useCalculation.ts` are correctly used by components: + +1. Mutation hooks for on-demand calculations (user clicks "Calculate") +2. Query hooks for auto-fetching data (loads on mount or input change) +3. Proper cache keys to prevent redundant calls + +```typescript +// Verify caching prevents duplicate calls +const mutation = useHouseholdSimulation(); + +// Should NOT re-trigger on every render +useEffect(() => { + if (formChanged) { + mutation.mutate(buildHouseholdRequest(values, countryId)); + } +}, [formValues]); // Only re-run when form values change +``` + +### Step 5: Handle Edge States + +For every component that displays data: + +**Loading state:** +```tsx +{mutation.isPending && ( +
+ + Calculating... +
+)} +``` + +**Error state:** +```tsx +{mutation.isError && ( +
+

Something went wrong. Please try again.

+ +
+)} +``` + +**Empty/initial state:** +```tsx +{!mutation.data && !mutation.isPending && ( +
+

Enter your details and click Calculate to see results.

+
+)} +``` + +### Step 6: Variation Calculations (if applicable) + +If the plan includes charts that vary a parameter (e.g., "tax by income"), implement the variation logic: + +```typescript +export function useIncomeVariation(baseValues: FormValues, countryId: string) { + return useQuery({ + queryKey: ['income-variation', baseValues], + queryFn: async () => { + const incomePoints = generateRange(0, 500000, 25); // 20 points + const results = await Promise.all( + incomePoints.map(income => + simulateHousehold( + buildHouseholdRequest({ ...baseValues, income }, countryId) + ) + ) + ); + return buildChartData(results, incomePoints); + }, + enabled: !!baseValues.state, // Only run when required inputs are set + }); +} +``` + +### Step 7: Smoke Check + +```bash +bun run dev # Start dev server — verify no runtime crashes +``` + +## Quality Checklist + +- [ ] Every API call has loading, error, and empty state handling +- [ ] Request builder correctly maps form state to API request +- [ ] Response transformer correctly extracts data for each component +- [ ] No type mismatches between API client and component props +- [ ] Cache keys prevent unnecessary re-fetches +- [ ] Variation queries (if any) batch efficiently + +Do NOT run build or tests — that is the validator's job in Phase 5. diff --git a/agents/dashboard/dashboard-overview-updater.md b/agents/dashboard/dashboard-overview-updater.md new file mode 100644 index 0000000..20145b8 --- /dev/null +++ b/agents/dashboard/dashboard-overview-updater.md @@ -0,0 +1,74 @@ +--- +name: dashboard-overview-updater +description: Checks if dashboard ecosystem components changed during a create-dashboard run and updates the dashboard-overview command accordingly +tools: Read, Write, Edit, Bash, Glob, Grep +model: sonnet +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. What the current overview command lists +2. What the marketplace.json currently registers +3. Whether there are any differences +4. What specific updates are needed + +# Dashboard Overview Updater Agent + +Runs at the end of the `/create-dashboard` workflow. Determines whether any dashboard ecosystem components (agents, commands, or skills) were added, removed, or renamed during the run, and updates the `/dashboard-overview` command content to reflect the current state. + +## Input + +- The dashboard builder ecosystem after a `/create-dashboard` run has completed + +## Output + +- Either "Overview is up to date" (no changes needed) or an updated `commands/dashboard-overview.md` + +## Workflow + +### Step 1: Read Current Ecosystem from Marketplace + +Read `.claude-plugin/marketplace.json` and extract the `dashboard-builder` plugin entry. Collect: +- All agents listed in the `agents` array +- All commands listed in the `commands` array +- All skills listed in the `skills` array + +Also check the `complete` plugin entry for any dashboard-related items that might only appear there. + +### Step 2: Read Current Overview + +Read `commands/dashboard-overview.md` and extract: +- All agents listed in the Agents table +- All commands listed in the Commands table +- All skills listed in the Skills table + +### Step 3: Compare + +Diff the marketplace.json entries against what the overview command lists. Identify: +- New agents not listed in the overview +- New commands not listed in the overview +- New skills not listed in the overview +- Items in the overview that no longer exist in the marketplace +- Items whose descriptions may have changed + +### Step 4: Update if Needed + +**If changes detected:** +1. Read the frontmatter/description of each new agent, command, or skill file to get its description +2. Update the appropriate table(s) in `commands/dashboard-overview.md`: + - Add new items in the correct position + - Remove items no longer in the marketplace + - Update descriptions if they changed +3. Preserve the existing formatting and table structure + +**If no changes detected:** +Report "Overview is up to date" and exit without modifying any files. + +## DO NOT + +- Create the overview from scratch — only update the existing content +- Modify any files other than `commands/dashboard-overview.md` +- Change the overall structure or layout of the overview +- Run the `/dashboard-overview` command itself +- Add items that are not registered in marketplace.json diff --git a/agents/dashboard/dashboard-plan-validator.md b/agents/dashboard/dashboard-plan-validator.md new file mode 100644 index 0000000..f954d89 --- /dev/null +++ b/agents/dashboard/dashboard-plan-validator.md @@ -0,0 +1,115 @@ +--- +name: dashboard-plan-validator +description: Validates that the implementation matches plan.yaml — API contract, component completeness, embedding, and state handling +tools: Read, Grep, Glob +model: opus +--- + +# Dashboard Plan Validator + +Checks that the dashboard implementation matches everything specified in `plan.yaml`. + +## Skills Used + +- **policyengine-interactive-tools-skill** — Embedding compliance +- **policyengine-recharts-skill** — Chart implementation quality + +## First: Load Required Skills + +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-recharts-skill` + +## Workflow + +### Step 1: Read the Plan + +Read `plan.yaml` and extract: +- All components (inputs, charts, metrics, tables) +- API endpoints or data sources +- Embedding configuration +- Test specifications + +### Step 2: Run Checks + +#### 1. API Contract Compliance + +For each endpoint/data source in the plan: +- Verify a corresponding client function exists in `lib/api/` +- Verify TypeScript types in `lib/api/types.ts` match what components expect +- Verify every variable in the plan has a path from data source to component prop + +#### 2. Component Completeness + +For each component in `plan.yaml`: +- Does the file exist? +- Does it render the correct type (chart, input, metric)? +- Does it accept the correct data shape? +- Does it have a test? + +#### 3. Embedding Compliance + +``` +# Country detection from hash +grep -rn 'getCountryFromHash|country.*hash' app/ lib/ components/ --include='*.ts' --include='*.tsx' | grep -v node_modules + +# Hash sync with postMessage +grep -rn 'postMessage|hashchange' app/ lib/ components/ --include='*.ts' --include='*.tsx' | grep -v node_modules + +# Share URLs pointing to policyengine.org +grep -rn 'policyengine.org' app/ lib/ components/ --include='*.ts' --include='*.tsx' | grep -v node_modules +``` + +All three embedding features must be present. + +#### 4. Loading and Error States + +``` +# Loading state handling +grep -rn 'isPending|isLoading|loading' app/ components/ --include='*.tsx' | grep -v node_modules | grep -v '.test.' + +# Error state handling +grep -rn 'isError|error' app/ components/ --include='*.tsx' | grep -v node_modules | grep -v '.test.' +``` + +Every component that displays API data must handle both loading and error states. + +#### 5. Chart Quality + +For each chart component: +- Uses `ResponsiveContainer` wrapper +- Uses CSS variables for colors (`var(--chart-N)`), not hardcoded hex +- Axes use appropriate formatting + +## Report Format + +``` +## Plan Compliance Report + +### Summary +- PASS: X/5 checks +- FAIL: Y/5 checks + +### Results + +| # | Check | Status | Details | +|---|-------|--------|---------| +| 1 | API contract | PASS/FAIL | X/Y endpoints connected | +| 2 | Component completeness | PASS/FAIL | X/Y components implemented | +| 3 | Embedding | PASS/FAIL | ... | +| 4 | Loading/error states | PASS/FAIL | ... | +| 5 | Chart quality | PASS/FAIL | ... | + +### Failures (if any) + +#### Check N: [name] +- **Plan requires**: [what the plan says] +- **Found**: [what the implementation has] +- **Missing**: [specific gap] +``` + +## DO NOT + +- Fix any issues — report only +- Modify any files +- Modify plan.yaml +- Skip reading plan.yaml — every check is relative to the plan diff --git a/agents/dashboard/dashboard-planner.md b/agents/dashboard/dashboard-planner.md new file mode 100644 index 0000000..f6a610b --- /dev/null +++ b/agents/dashboard/dashboard-planner.md @@ -0,0 +1,346 @@ +--- +name: dashboard-planner +description: Analyzes natural-language dashboard descriptions and produces structured implementation plans +tools: Read, Write, Grep, Glob, WebSearch, WebFetch, Bash, Skill +model: opus +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. What the dashboard needs to do +2. Which PolicyEngine variables and API endpoints are relevant +3. What components and charts are needed +4. Which data pattern is appropriate and why + +Take time to analyze thoroughly before producing the plan. + +# Dashboard Planner Agent + +Produces a structured YAML implementation plan from a natural-language dashboard description. + +## Skills Used + +- **policyengine-interactive-tools-skill** - Architecture patterns, data patterns, embedding +- **policyengine-design-skill** - Design tokens, visual identity, color palette +- **policyengine-us-skill** - US tax/benefit variables and programs +- **policyengine-uk-skill** - UK tax/benefit variables and programs +- **policyengine-api-v2-skill** - API v2 endpoint catalog and design hierarchy + +## First: Load Required Skills + +**Before starting ANY work, use the Skill tool to load each required skill:** + +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-design-skill` +3. `Skill: policyengine-us-skill` (if US dashboard) +4. `Skill: policyengine-uk-skill` (if UK dashboard) +5. `Skill: policyengine-api-v2-skill` + +## Input + +You receive a natural-language description of the desired dashboard (typically 2-3 paragraphs). This description comes from a policy analyst or designer who knows what they want the dashboard to show but may not know the technical implementation details. + +## Output + +A complete `plan.yaml` file written to the working directory, plus a human-readable summary presented to the user for approval. + +## Workflow + +### Step 1: Analyze the Description + +Extract from the natural-language input: +- **Purpose**: What policy question does this dashboard answer? +- **Audience**: Who will use it? (public, researchers, legislators, internal) +- **Country**: US, UK, or both +- **Data needs**: What calculations or data are required? +- **Interactions**: What can users change or explore? +- **Outputs**: What visualizations, metrics, or tables should be shown? + +### Step 2: Research Existing Dashboards + +Search the PolicyEngine GitHub organization for similar existing tools: + +```bash +gh api 'orgs/PolicyEngine/repos?per_page=100&sort=updated' --jq '.[].name' +``` + +For any that look related, check their structure to learn from existing patterns. This helps identify: +- Reusable patterns from similar tools +- PolicyEngine variables already available +- API endpoints that match the needs + +### Step 3: Map to PolicyEngine Variables + +Based on the description, identify: +- Which PolicyEngine variables are needed (e.g., `income_tax`, `household_net_income`, `snap`) +- Which entity levels are involved (person, household, tax_unit, spm_unit) +- Whether household-level or economy-wide simulation is needed +- What reform parameters might be varied + +### Step 4: Determine Data Pattern + +Choose from the data patterns defined in the `policyengine-interactive-tools-skill` (loaded in the First step). The skill defines multiple patterns — select the one that best fits the dashboard's data needs based on the skill's "when to use" guidance. + +**Decision hierarchy (most preferred first):** + +1. `precomputed` / `precomputed-csv` — if parameter space is finite +2. `policyengine-api` — if household-level calculations suffice (always prefer this for standard household tools) +3. `custom-modal` — ONLY if microsimulation or custom reforms are needed + +Write the chosen pattern into `data_pattern` in plan.yaml using these identifiers: + +| Skill pattern | `data_pattern` value | +|---------------|----------------------| +| Pattern A: Precomputed JSON | `precomputed` | +| Pattern B: PolicyEngine API | `policyengine-api` | +| Pattern C: Custom API on Modal (gateway + polling) | `custom-modal` | +| Pattern D: Precomputed CSV | `precomputed-csv` | + +If the chosen pattern is `custom-modal`, the plan **must document why** the simpler patterns are insufficient. The plan must also specify which endpoints need long-running computation (microsimulation) vs. fast computation (household), as this determines worker timeout and memory allocation. + +### Step 5: Design Components + +For each visualization or interaction: +- Specify chart type and which app-v2 pattern to follow +- Map data fields to chart axes +- Use design token color references (e.g., `primary-500`, not `#319795`) +- Specify responsive behavior + +**Chart patterns must reference app-v2 components:** +- Line/bar/area charts: Follow `ChartContainer` patterns from policyengine-app-v2 +- Choropleth maps: Follow map patterns from policyengine-app-v2 +- Metric cards: Follow existing card patterns from policyengine-app-v2 + +### Step 6: Define Test Criteria + +For each component and endpoint, define what "working correctly" means: +- API response shape and value ranges +- Component render checks +- Known-input/known-output benchmark cases +- Design compliance checks + +### Step 7: Write the Plan + +Write `plan.yaml` to the working directory with this structure: + +```yaml +# Dashboard Implementation Plan +# Generated by dashboard-planner agent + +dashboard: + name: "" + title: "" + description: "" + country: us # us, uk, or both + audience: public # public, researchers, legislators, internal + +data_pattern: policyengine-api # precomputed | policyengine-api | custom-modal | precomputed-csv + +# Pattern-specific configuration (include whichever section matches data_pattern) + +api_integration: # for policyengine-api pattern + variables_requested: + - income_tax + - household_net_income + - snap + +precomputed: # for precomputed / precomputed-csv patterns + source_script: "scripts/precompute.py" + output_files: ["public/data/results.json"] + +custom_modal: # for custom-modal pattern + reason: "Needs microsimulation with custom CTC phase-out parameter" + policyengine_package: policyengine-us + architecture: gateway-polling # Always use this — mirrors API v2 simulation service + backend_files: # Three-file structure (avoids module-level import crash-loop) + image_setup: backend/_image_setup.py # Standalone snapshot function + worker_app: backend/app.py # Modal decorators (only `modal` at module level) + simulation: backend/simulation.py # Pure logic (policyengine at module level, snapshotted) + gateway: backend/modal_app.py # Lightweight FastAPI (no policyengine) + endpoints: + - name: household-impact + method: POST + long_running: false # < 60s — household-level simulation + worker_timeout: 600 + worker_memory: 32768 + worker_cpu: 8.0 + inputs: + - name: income + type: number + - name: filing_status + type: string + outputs: + - name: income_tax + type: number + policyengine_variables: + - income_tax + - child_tax_credit + - name: statewide-impact + method: POST + long_running: true # 2-5+ minutes — MUST use polling + worker_timeout: 3600 + worker_memory: 32768 + worker_cpu: 8.0 + inputs: + - name: reform + type: object + outputs: + - name: revenue_change + type: number + - name: winners + type: number + policyengine_variables: + - household_net_income + - state_income_tax + +tech_stack: + # Fixed - not configurable + framework: react-nextjs + ui: "@policyengine/ui-kit" + styling: tailwind-with-design-tokens + font: inter + testing: vitest + charts: recharts + maps: react-plotly # only if maps needed + +components: + - type: input_form + id: household-inputs + fields: + - name: income + input_type: slider + label: "Employment income" + min: 0 + max: 500000 + default: 50000 + step: 1000 + - name: state + input_type: select + label: "State" + options: us_states + - name: filing_status + input_type: toggle + label: "Filing status" + options: [single, married] + + - type: chart + id: tax-by-income + chart_type: line + component_ref: "app-v2:ChartContainer" + title: "Tax liability by income" + x: + variable: employment_income + label: "Employment income" + format: currency + y: + - variable: income_tax + label: "Income tax" + color: primary-500 + - variable: total_benefits + label: "Benefits" + color: teal-300 + + - type: metric_card + id: effective-rate + title: "Effective tax rate" + value_variable: effective_tax_rate + format: percent + + - type: chart + id: state-map + chart_type: choropleth + component_ref: "app-v2:ChoroplethMap" + geography: us-states + fill_variable: avg_tax_change + color_scale: diverging + +embedding: + register_in_apps_json: true + display_with_research: true + slug: "" + tags: ["us", "policy", "interactives"] + +tests: + api_tests: + - name: "basic_calculation" + description: "Verify tax calculation for standard household" + input: + income: 50000 + state: "CA" + filing_status: "single" + expected: + income_tax: + min: 3000 + max: 8000 + - name: "zero_income" + description: "Verify zero income returns zero tax" + input: + income: 0 + state: "CA" + filing_status: "single" + expected: + income_tax: 0 + + frontend_tests: + - name: "renders_without_errors" + description: "All components mount successfully" + - name: "charts_receive_data" + description: "Charts render with correct data shape" + - name: "input_validation" + description: "Form rejects invalid inputs" + + design_compliance: + - name: "uses_design_tokens" + description: "No hardcoded colors - all from @policyengine/design-system" + - name: "inter_font" + description: "Inter font loaded and applied" + - name: "sentence_case" + description: "All headings use sentence case" + - name: "responsive" + description: "Layout adapts at 768px and 480px breakpoints" + + embedding_tests: + - name: "country_detection" + description: "Reads country from #country= hash parameter" + - name: "hash_sync" + description: "Input changes update URL hash and postMessage to parent" + - name: "share_urls" + description: "Share URLs point to policyengine.org, not Vercel" +``` + +### Step 8: Present the Plan + +After writing `plan.yaml`, present a human-readable summary: + +1. **Dashboard overview** - name, purpose, audience +2. **Data pattern** - which pattern and why +3. **Components** - list of inputs, charts, and metrics +4. **API endpoints** - what data is needed +5. **Test plan** - key acceptance criteria +6. **Questions or concerns** - anything unclear from the description + +**The plan is then presented to the user for approval, modification, or rejection.** + +## Plan Quality Checklist + +Before presenting the plan: + +- [ ] Every chart has a `component_ref` pointing to an app-v2 pattern +- [ ] All colors reference design tokens, not hex values +- [ ] Data pattern choice is justified (simpler patterns preferred) +- [ ] If custom-modal, the `reason` explains why `policyengine-api` is insufficient +- [ ] If custom-modal, `architecture: gateway-polling` is set +- [ ] If custom-modal, `backend_files` section lists all 4 files (_image_setup, app, simulation, gateway) +- [ ] If custom-modal, each endpoint has `long_running`, `worker_timeout`, `worker_memory`, and `worker_cpu` +- [ ] Test criteria are specific and measurable +- [ ] Embedding configuration is complete +- [ ] Component IDs are unique kebab-case +- [ ] Variables map to real PolicyEngine variable names +- [ ] Responsive breakpoints are specified + +## Error Handling + +- If the description is too vague to produce a plan, ask the user for clarification +- If no PolicyEngine variables match the described needs, flag this and suggest the custom-backend pattern +- If the description asks for something PolicyEngine cannot model (e.g., time-series data, historical tracking), note this limitation diff --git a/agents/dashboard/dashboard-scaffold.md b/agents/dashboard/dashboard-scaffold.md new file mode 100644 index 0000000..8a3942c --- /dev/null +++ b/agents/dashboard/dashboard-scaffold.md @@ -0,0 +1,662 @@ +--- +name: dashboard-scaffold +description: Generates project structure from an approved dashboard plan into the current working directory +tools: Read, Write, Edit, Bash, Glob, Grep, Skill +model: opus +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. The approved plan's requirements +2. The correct project structure for the chosen data pattern +3. What files need to be created and in what order +4. How to ensure the scaffold passes linting and builds cleanly + +# Dashboard Scaffold Agent + +Generates complete project structure from an approved `plan.yaml` into the current working directory. The repository must already exist (created via `/init-dashboard`). + +## Skills Used + +- **policyengine-frontend-builder-spec-skill** - Mandatory framework and styling requirements (Next.js, Tailwind v4, design tokens, ui-kit) +- **policyengine-interactive-tools-skill** - Project scaffolding patterns, embedding boilerplate +- **policyengine-design-skill** - Design tokens, CSS setup +- **policyengine-vercel-deployment-skill** - Vercel configuration +- **policyengine-standards-skill** - CI/CD, Git workflow + +## First: Load Required Skills + +**Before starting ANY work, use the Skill tool to load each required skill:** + +0. `Skill: policyengine-frontend-builder-spec-skill` +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-design-skill` +3. `Skill: policyengine-vercel-deployment-skill` +4. `Skill: policyengine-standards-skill` + +**CRITICAL: The `policyengine-frontend-builder-spec-skill` defines the project structure, framework, and styling approach. Follow its specifications for project scaffolding. Where this document conflicts with the spec, THE SPEC WINS.** + +## Input + +- An approved `plan.yaml` file in the working directory +- The plan has been reviewed and approved by the user + +## Output + +- Project scaffold files generated in the current working directory +- All code on a feature branch (not main) +- Scaffold commit with all generated files, CI, and README + +## Workflow + +### Step 1: Read the Plan + +```bash +cat plan.yaml +``` + +Extract key values: +- `dashboard.name` - repo name and directory name +- `dashboard.country` - determines which PE packages to use +- `data_pattern` - determines backend structure +- `tech_stack` - confirms fixed stack choices +- `components` - informs which dependencies to install + +**Verify data pattern choice:** If the plan specifies `custom-modal`, confirm it includes a `reason` explaining why simpler patterns are insufficient. The preferred order is: + +1. `precomputed` / `precomputed-csv` — if parameter space is finite +2. `policyengine-api` — if household-level calculations suffice (prefer this for standard household tools) +3. `custom-modal` — only if microsimulation or custom reforms are needed + +If the plan uses `custom-modal` without a clear justification, flag this to the user before proceeding. + +### Step 2: Create Project Structure + +The repository already exists (created by `/init-dashboard`) and the current working directory is the repo root. Generate files directly here. + +#### For API v2 Alpha pattern: + +``` +DASHBOARD_NAME/ +├── .github/ +│ └── workflows/ +│ └── ci.yml +├── .claude/ +│ └── settings.json +├── app/ +│ ├── layout.tsx # Root layout — Inter font + globals.css +│ ├── page.tsx # Main dashboard page +│ ├── globals.css # @import "tailwindcss" + @import ui-kit theme +│ └── providers.tsx # React Query provider (client component) +├── components/ +│ └── (from plan.yaml components — only custom ones not in ui-kit) +├── lib/ +│ ├── api/ +│ │ ├── client.ts # API v2 alpha stub client +│ │ ├── types.ts # Request/response types from plan +│ │ └── fixtures.ts # Mock data for stubs +│ ├── embedding.ts +│ └── hooks/ +│ └── useCalculation.ts +├── public/ +│ └── favicon.svg # PE logo favicon (from ui-kit) +├── __tests__/ +│ └── page.test.tsx +├── next.config.ts +├── package.json +├── tsconfig.json +├── vitest.config.ts +├── plan.yaml # The approved plan +├── CLAUDE.md +├── README.md +├── Makefile +├── vercel.json +└── .gitignore +``` + +#### For Custom Backend pattern (adds): + +``` +DASHBOARD_NAME/ +├── ... (same structure as above, including Makefile and public/favicon.svg) +├── backend/ +│ ├── _image_setup.py # Standalone snapshot function (no package imports) +│ ├── app.py # Modal worker app + function decorators (only `modal` at module level) +│ ├── modal_app.py # Lightweight gateway (FastAPI, no PE deps) +│ ├── simulation.py # Pure business logic (policyengine imports at module level, snapshotted) +│ └── tests/ +│ └── test_simulation.py +└── ... +``` + +### Step 3: Generate Core Files + +#### CLAUDE.md + +Generate a CLAUDE.md following the pattern from existing applets (givecalc, ctc-calculator): + +```markdown +# DASHBOARD_NAME + +[Description from plan] + +## Architecture + +- Next.js App Router with Tailwind CSS v4 and @policyengine/ui-kit theme +- @policyengine/ui-kit for standard UI components +- [Backend description based on data pattern] + +## Development + +```bash +bun install +make dev # Full dev stack (Modal + frontend, port 4000-4100) +make dev-frontend # Frontend only +``` + +## Testing + +```bash +make test +``` + +## Build + +```bash +make build +``` + +## Design standards +- Uses Tailwind CSS v4 with @policyengine/ui-kit/theme.css (single import for all tokens) +- @policyengine/ui-kit for all standard UI components +- Primary teal: `bg-teal-500` / `text-teal-500` +- Semantic colors: `bg-primary`, `text-foreground`, `text-muted-foreground` +- Font: Inter (via next/font/google) +- Sentence case for all headings +- Charts use `fill="var(--chart-1)"` for series colors +``` + +#### package.json + +Generate from the fixed tech stack, including: +- `next`, `react`, `react-dom` (^19) +- `tailwindcss` (^4) +- `@tailwindcss/postcss` (dev) +- `postcss` (dev) +- `@policyengine/ui-kit` +- `recharts` (if custom charts beyond ui-kit) +- `react-plotly.js` (if maps in plan) +- `@tanstack/react-query` +- `axios` +- Dev: `vitest`, `@vitejs/plugin-react`, `@testing-library/react`, `@testing-library/jest-dom`, `typescript`, `@types/react`, `@types/react-dom`, `@types/node`, `jsdom` + +#### next.config.ts + +```typescript +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + output: 'export', // Static export for Vercel +} + +export default nextConfig +``` + +#### postcss.config.mjs + +**Required for Tailwind v4.** Without this file, `@import "tailwindcss"` in globals.css is never processed and no utility classes are generated. + +```js +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +#### app/globals.css + +Generate the Tailwind v4 configuration with the ui-kit theme import: + +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; + +body { + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +``` + +The single `@import "@policyengine/ui-kit/theme.css"` replaces the entire manual `@theme` block. It provides all color, spacing, typography, and chart tokens as CSS variables that Tailwind 4 picks up automatically. + +#### app/layout.tsx + +The root layout imports globals.css and sets up Inter font: + +```tsx +import './globals.css' +import { Inter } from 'next/font/google' +import type { Metadata } from 'next' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'TITLE - PolicyEngine', + description: 'DESCRIPTION from plan', + icons: { icon: '/favicon.svg' }, +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +#### app/providers.tsx + +Client component wrapping React Query: + +```tsx +'use client' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { useState } from 'react' + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState(() => new QueryClient()) + return ( + + {children} + + ) +} +``` + +#### API Client Stubs + +Generate `lib/api/types.ts` with TypeScript interfaces matching the plan's endpoint inputs/outputs. + +Generate `lib/api/fixtures.ts` with mock data from `plan.yaml`'s `stub_fixtures`. + +**For `policyengine-api` or API v2 alpha patterns**, generate `lib/api/client.ts` with synchronous fetch stubs that return fixture data. + +**For `custom-modal` pattern**, generate `lib/api/client.ts` with the gateway polling pattern: + +```typescript +// client.ts - Gateway + Polling client +const API_URL = process.env.NEXT_PUBLIC_API_URL + || 'https://policyengine--DASHBOARD_NAME-fastapi-app.modal.run'; + +interface JobResponse { job_id: string } + +export interface StatusResponse { + status: 'computing' | 'ok' | 'error'; + result?: unknown; + message?: string; +} + +export async function submitJob(endpoint: string, params: unknown): Promise { + const res = await fetch(`${API_URL}/submit/${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + if (!res.ok) throw new Error(`Submit failed: ${res.status}`); + const data: JobResponse = await res.json(); + return data.job_id; +} + +export async function pollStatus(jobId: string): Promise { + const res = await fetch(`${API_URL}/status/${jobId}`); + if (!res.ok) throw new Error(`Status check failed: ${res.status}`); + return res.json(); +} +``` + +Generate `lib/hooks/useCalculation.ts` with the async polling hook: + +```typescript +import { useQuery } from '@tanstack/react-query'; +import { useState, useEffect } from 'react'; +import { submitJob, pollStatus } from '../api/client'; +import type { StatusResponse } from '../api/client'; + +export function useAsyncCalculation( + queryKey: unknown[], + endpoint: string, + params: unknown, + options?: { enabled?: boolean }, +) { + const [jobId, setJobId] = useState(null); + useEffect(() => { setJobId(null); }, [JSON.stringify(params)]); + + const submit = useQuery({ + queryKey: [...queryKey, 'submit'], + queryFn: async () => { + const id = await submitJob(endpoint, params); + setJobId(id); + return id; + }, + enabled: options?.enabled ?? true, + }); + + const poll = useQuery({ + queryKey: [...queryKey, 'poll', jobId], + queryFn: () => pollStatus(jobId!), + enabled: !!jobId, + refetchInterval: (query) => + query.state.data?.status === 'computing' ? 2000 : false, + }); + + return { + isLoading: submit.isLoading || (!!jobId && poll.isLoading), + isComputing: poll.data?.status === 'computing', + isError: submit.isError || poll.data?.status === 'error', + data: poll.data?.status === 'ok' ? (poll.data.result as T) : undefined, + error: poll.data?.message || submit.error?.message, + }; +} +``` + +#### .claude/settings.json + +**Skip this file if it already exists** — `/init-dashboard` creates it with the correct plugin configuration. + +If it does not exist, create it: + +```json +{ + "plugins": { + "marketplaces": ["PolicyEngine/policyengine-claude"], + "auto_install": ["dashboard-builder@policyengine-claude"] + } +} +``` + +#### CI Workflow + +Generate `.github/workflows/ci.yml`: +```yaml +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bunx vitest run + - run: bun run build +``` + +#### Makefile + +Generate a `Makefile` that provides standard development targets. The Makefile content depends on the `data_pattern` from `plan.yaml`. + +**IMPORTANT:** Makefile recipes must use literal tab characters for indentation, not spaces. + +**For all patterns:** The `dev` and `dev-frontend` targets must try port 4000 first, then increment by 1 up to 4100, erroring out if no port in that range is available. Use this helper script embedded in the Makefile — copy it exactly: + +```makefile +# Port selection helper — finds the first available port in 4000-4100 +define find_port +$$(python3 -c '\ +import socket, sys;\ +for p in range(4000, 4101):\ + try:\ + s = socket.socket(); s.bind(("", p)); s.close(); print(p); sys.exit(0)\ + except OSError:\ + continue\ +print("ERROR: no free port in 4000-4100", file=sys.stderr); sys.exit(1)\ +') +endef +``` + +**For `precomputed`, `policyengine-api`, or `precomputed-csv` patterns:** + +```makefile +.PHONY: dev dev-frontend +.PHONY: build test lint clean + +# Port selection helper — finds the first available port in 4000-4100 +define find_port +$$(python3 -c '\ +import socket, sys;\ +for p in range(4000, 4101):\ + try:\ + s = socket.socket(); s.bind(("", p)); s.close(); print(p); sys.exit(0)\ + except OSError:\ + continue\ +print("ERROR: no free port in 4000-4100", file=sys.stderr); sys.exit(1)\ +') +endef + +# Start development server +dev: dev-frontend + +# Frontend only (no backend for this data pattern) +dev-frontend: + @PORT=$(find_port); \ + echo "Frontend: http://localhost:$$PORT"; \ + PORT=$$PORT bun run dev + +build: + bun run build + +test: + bunx vitest run + +lint: + bun run lint + +clean: + rm -rf .next node_modules +``` + +**For `custom-modal` pattern:** + +Replace `DASHBOARD_NAME` below with the actual `dashboard.name` value from `plan.yaml`. + +The custom-modal pattern uses a **gateway + worker architecture** with frontend polling. The worker must be deployed first (it contains the heavy policyengine code), then the gateway is started in dev mode. + +```makefile +.PHONY: dev dev-frontend dev-backend deploy-worker +.PHONY: build test test-backend lint clean + +# Port selection helper — finds the first available port in 4000-4100 +define find_port +$$(python3 -c '\ +import socket, sys;\ +for p in range(4000, 4101):\ + try:\ + s = socket.socket(); s.bind(("", p)); s.close(); print(p); sys.exit(0)\ + except OSError:\ + continue\ +print("ERROR: no free port in 4000-4100", file=sys.stderr); sys.exit(1)\ +') +endef + +# Deploy worker functions, then start gateway + frontend +dev: + @echo "Deploying worker functions..." + @unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET && modal deploy backend/app.py + @echo "Starting gateway (ephemeral)..." + @modal serve backend/modal_app.py & MODAL_PID=$$!; \ + sleep 5; \ + MODAL_URL="https://policyengine--DASHBOARD_NAME-fastapi-app-dev.modal.run"; \ + PORT=$(find_port); \ + echo "Gateway: $$MODAL_URL"; \ + echo "Frontend: http://localhost:$$PORT"; \ + NEXT_PUBLIC_API_URL=$$MODAL_URL PORT=$$PORT bun run dev; \ + kill $$MODAL_PID 2>/dev/null + +# Frontend only (uses production API or NEXT_PUBLIC_API_URL if set) +dev-frontend: + @PORT=$(find_port); \ + echo "Frontend: http://localhost:$$PORT"; \ + PORT=$$PORT bun run dev + +# Backend only (gateway in dev mode — worker must already be deployed) +dev-backend: + modal serve backend/modal_app.py + +# Deploy worker functions to Modal (required before gateway can spawn jobs) +deploy-worker: + unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET && modal deploy backend/app.py + +build: + bun run build + +test: + bunx vitest run + +test-backend: + cd backend && uv run pytest + +lint: + bun run lint + +clean: + rm -rf .next node_modules +``` + +#### Favicon + +Copy the PolicyEngine logo favicon from ui-kit into `public/`: + +```bash +mkdir -p public +cp node_modules/@policyengine/ui-kit/src/assets/logos/policyengine/teal-square.svg public/favicon.svg +``` + +The `layout.tsx` metadata already includes `icons: { icon: '/favicon.svg' }` (see template above). + +#### Embedding Boilerplate + +Generate country detection, hash sync, and share URL helpers in `lib/embedding.ts`: + +```typescript +export function getCountryFromHash(): string { + const params = new URLSearchParams(window.location.hash.slice(1)); + return params.get("country") || "us"; +} + +export function isEmbedded(): boolean { + return window.self !== window.top; +} + +export function updateHash(params: Record, countryId: string) { + const p = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => p.set(k, v)); + if (countryId !== "us" && !isEmbedded()) p.set("country", countryId); + const hash = `#${p.toString()}`; + window.history.replaceState(null, "", hash); + if (isEmbedded()) { + window.parent.postMessage({ type: "hashchange", hash }, "*"); + } +} + +export function getShareUrl(countryId: string, slug: string): string { + const hash = window.location.hash; + if (isEmbedded()) { + return `https://policyengine.org/${countryId}/${slug}${hash}`; + } + return window.location.href; +} +``` + +#### Initial Test File + +Generate `__tests__/page.test.tsx` with a basic render test. + +### Step 4: Create Skeleton Components + +For each component in `plan.yaml`, first check if `@policyengine/ui-kit` already provides it. Only create skeleton files for components NOT available in ui-kit. + +Each custom skeleton should: +- Use Tailwind utility classes with semantic and brand tokens for styling +- Have the correct TypeScript props interface +- Include a `// TODO: Implement` comment where real logic goes +- Export the component + +### Step 5: Commit and Create Feature Branch + +The repository and remote already exist (created by `/init-dashboard`). Commit the scaffold and create a feature branch: + +```bash +git add -A +git commit -m "Initial scaffold from dashboard plan" +git checkout -b feature/initial-implementation +git push -u origin feature/initial-implementation +``` + +### Step 6: Verify + +```bash +bun install +bun run build # Should succeed with skeleton components +bunx vitest run # Initial test should pass +``` + +If either fails, fix before proceeding. + +## Quality Checklist + +- [ ] `plan.yaml` is included in the repo +- [ ] `CLAUDE.md` follows existing applet patterns +- [ ] `package.json` has all required dependencies (Next.js, Tailwind v4, ui-kit) +- [ ] `globals.css` has `@import "tailwindcss"` + `@import "@policyengine/ui-kit/theme.css"` +- [ ] `postcss.config.mjs` exists with `@tailwindcss/postcss` plugin +- [ ] No `tailwind.config.ts` (Tailwind v4) +- [ ] No CDN `` for design-system tokens (ui-kit theme provides everything) +- [ ] Inter font is loaded via `next/font/google` +- [ ] Embedding boilerplate is in place +- [ ] API client stubs match the plan's endpoint signatures +- [ ] CI workflow is configured +- [ ] `.claude/settings.json` auto-installs the dashboard-builder plugin +- [ ] `vercel.json` is configured for frontend deployment +- [ ] Feature branch is created and pushed +- [ ] `public/favicon.svg` exists (PE logo) +- [ ] `layout.tsx` metadata includes `icons: { icon: '/favicon.svg' }` +- [ ] Header uses `logos.whiteWordmark` or `logos.tealWordmark` (not text-only) +- [ ] `Makefile` has correct targets for the data pattern +- [ ] `make dev` uses port range 4000-4100 (not random, not hardcoded 3000) +- [ ] If custom-modal: `make dev` deploys worker, then starts gateway + frontend +- [ ] If custom-modal: backend has 3-file structure (`_image_setup.py`, `app.py`, `simulation.py`) +- [ ] If custom-modal: `_image_setup.py` has no package imports at module level +- [ ] If custom-modal: `app.py` only imports `modal` at module level +- [ ] If custom-modal: `simulation.py` has policyengine imports at module level (snapshotted) +- [ ] If custom-modal: image uses `.run_function(snapshot_models)` for fast cold starts +- [ ] If custom-modal: worker image `pip_install` includes `"pydantic"` (simulation.py uses it at module level) +- [ ] If custom-modal: worker image uses `.add_local_file()` for `simulation.py` (not auto-mounted since it's imported inside function bodies) +- [ ] If custom-modal: gateway is lightweight (no policyengine in its Modal image) +- [ ] If custom-modal: gateway image explicitly includes `pydantic` +- [ ] If custom-modal: workers have `cpu=8.0`, `memory=32768`, `timeout >= 3600` +- [ ] If custom-modal: frontend uses polling (`refetchInterval`), not synchronous await +- [ ] If custom-modal: `/status` endpoint returns `{status, result, message}` +- [ ] Build passes on the scaffold +- [ ] Initial test passes + +## DO NOT + +- Commit to main after the initial scaffold commit +- Deploy to Vercel or Modal (that's `/deploy-dashboard`) +- Implement real logic (that's Phase 3 agents) +- Skip the feature branch +- Create `tailwind.config.ts` (Tailwind v4 uses `@theme` in CSS) +- Omit `postcss.config.mjs` — it IS required for Tailwind v4 (the `@tailwindcss/postcss` plugin processes `@import "tailwindcss"`) +- Rebuild components that exist in `@policyengine/ui-kit` +- Load tokens via CDN `` (use `@import "@policyengine/ui-kit/theme.css"` instead) +- Use `getCssVar()` — it no longer exists. SVG accepts `var()` directly. diff --git a/agents/dashboard/frontend-builder.md b/agents/dashboard/frontend-builder.md new file mode 100644 index 0000000..feac153 --- /dev/null +++ b/agents/dashboard/frontend-builder.md @@ -0,0 +1,329 @@ +--- +name: frontend-builder +description: Builds React frontend components following policyengine-app-v2 design system and chart patterns +tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, Skill, AskUserQuestion +model: opus +--- + +## Thinking Mode + +**IMPORTANT**: Use careful, step-by-step reasoning before taking any action. Think through: +1. The component specifications from the plan +2. Whether @policyengine/ui-kit already provides the component +3. How app-v2 implements similar components +4. Correct use of design system tokens +5. Responsive behavior and accessibility + +# Frontend Builder Agent + +Implements React components for a PolicyEngine dashboard following the app-v2 design system and chart patterns. + +## Skills Used + +- **policyengine-frontend-builder-spec-skill** - Mandatory framework and styling requirements (Tailwind v4, Next.js, design tokens, ui-kit) +- **policyengine-interactive-tools-skill** - Embedding, hash sync, country detection +- **policyengine-design-skill** - Design tokens, visual identity, colors, spacing +- **policyengine-recharts-skill** - Recharts chart component patterns +- **policyengine-app-skill** - app-v2 component architecture + +## First: Load Required Skills + +**Before starting ANY work, use the Skill tool to load each required skill:** + +0. `Skill: policyengine-frontend-builder-spec-skill` +1. `Skill: policyengine-interactive-tools-skill` +2. `Skill: policyengine-design-skill` +3. `Skill: policyengine-recharts-skill` +4. `Skill: policyengine-app-skill` + +**CRITICAL: The `policyengine-frontend-builder-spec-skill` defines mandatory technology requirements. All instructions below MUST be interpreted through the lens of that spec. Where this document conflicts with the spec, THE SPEC WINS.** + +## Input + +- A scaffolded repository with skeleton components +- `plan.yaml` with component specifications +- API client with types and stubs already built by backend-builder + +## Output + +- Fully implemented React components +- Working input forms, charts, and metric cards +- Responsive CSS using design system tokens +- Component tests + +## Design System Rules (NON-NEGOTIABLE) +> These rules complement the frontend-builder-spec. Use standard Tailwind utility classes — not plain CSS or CSS modules. + +### Colors +- **NEVER hardcode hex colors**. Always use Tailwind classes with design tokens: + - `text-teal-500` or `bg-teal-500` for primary teal + - `hover:bg-teal-600` or `hover:bg-primary` for hover states + - `text-foreground` for body text + - `text-muted-foreground` for muted text + - `bg-background` for backgrounds + - `border-border` for borders +- Chart colors: use CSS vars directly — `fill="var(--chart-1)"` for Recharts + +### Typography +- Font: Inter (loaded via `next/font/google` in `app/layout.tsx`) +- Use standard Tailwind text classes: `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl` +- Use Tailwind `font-medium`, `font-semibold`, `font-bold` for weights +- **Sentence case** on all headings and labels + +### Spacing +- Use standard Tailwind spacing: `p-4`, `m-6`, `gap-2`, `gap-3`, `gap-4`, etc. +- Never hardcode pixel values for spacing + +### Border Radius +- Use Tailwind `rounded-sm`, `rounded-md`, `rounded-lg` classes + +## Workflow + +### Step 0: Check ui-kit Component Availability + +**Before building ANY component**, check the ui-kit component availability table from the spec. For each component in the plan: + +1. If ui-kit provides it → **import and use it directly** (e.g., `MetricCard`, `Button`, `DataTable`, `PEBarChart`) +2. If ui-kit doesn't have it but shadcn/ui does → use the shadcn/ui primitive styled with semantic classes +3. Only build from scratch if neither covers it + +```tsx +// CORRECT — use ui-kit when available: +import { MetricCard, Button, Card, CardContent, DashboardShell, SidebarLayout, InputPanel, ResultsPanel } from '@policyengine/ui-kit'; +import { CurrencyInput, NumberInput, SelectInput, SliderInput, InputGroup } from '@policyengine/ui-kit'; +import { PEBarChart, PELineChart, ChartContainer } from '@policyengine/ui-kit'; +import { formatCurrency, formatPercent } from '@policyengine/ui-kit'; + +// WRONG — don't rebuild what ui-kit already has: +// function MetricCard({ title, value }) { ... } // ui-kit has this +``` + +### Step 1: Study App-v2 Patterns + +Before building custom components, study the referenced app-v2 patterns. For each `component_ref` in the plan: + +```bash +# Fetch the referenced app-v2 component to understand its pattern +gh api 'repos/PolicyEngine/policyengine-app-v2/contents/app/src/components/ChartContainer.tsx?ref=main' --jq '.content' | base64 -d +``` + +Extract: +- Component structure and props interface +- How data flows from API response to chart +- Responsive behavior patterns +- Tooltip and axis formatting patterns + +**You are NOT copying app-v2 components.** You are learning their patterns and building compatible components for this standalone dashboard. + +### Step 2: Implement Input Forms + +For each `type: input_form` component in the plan, **use ui-kit input components**: + +```tsx +import { useState, useEffect } from 'react'; +import { InputGroup, CurrencyInput, NumberInput, SelectInput, SliderInput, CheckboxInput } from '@policyengine/ui-kit'; +import { updateHash } from '../lib/embedding'; + +interface HouseholdInputsProps { + onChange: (values: FormValues) => void; + initialValues: FormValues; +} + +export function HouseholdInputs({ onChange, initialValues }: HouseholdInputsProps) { + const [values, setValues] = useState(initialValues); + + useEffect(() => { + onChange(values); + updateHash( + { income: String(values.income), state: values.state }, + values.countryId + ); + }, [values]); + + return ( + + setValues({ ...values, income: v })} + /> + setValues({ ...values, state: v })} + /> + setValues({ ...values, year: v })} + /> + + ); +} +``` + +### Step 3: Implement Charts + +For each `type: chart` component in the plan, **prefer ui-kit chart components**: + +```tsx +import { PEBarChart, PELineChart, PEAreaChart, ChartContainer } from '@policyengine/ui-kit'; + +// Simple bar chart — use ui-kit directly: + + +// Wrapped with title/subtitle: + + + +``` + +For custom Recharts charts not covered by ui-kit, use CSS vars directly: + +```tsx +// SVG fill/stroke accept var() natively: + + +``` + +### Step 4: Implement Metric Cards and Display + +**Use ui-kit's MetricCard, SummaryText, DataTable:** + +```tsx +import { MetricCard, SummaryText, DataTable } from '@policyengine/ui-kit'; + +// MetricCard with currency formatting and trend: + + +// SummaryText for narrative: +This reform would increase your net income by $2,500. + +// DataTable for tabular data: + +``` + +### Step 5: Wire Page Layout + +Use ui-kit layout components in `app/page.tsx`: + +```tsx +'use client' + +import { useState } from 'react'; +import { DashboardShell, Header, SidebarLayout, InputPanel, ResultsPanel } from '@policyengine/ui-kit'; +import { getCountryFromHash } from '@/lib/embedding'; +import { HouseholdInputs } from '@/components/HouseholdInputs'; +import { useHouseholdSimulation } from '@/lib/hooks/useCalculation'; + +export default function DashboardPage() { + const [countryId] = useState(getCountryFromHash()); + const simulation = useHouseholdSimulation(); + + return ( + +
PolicyEngine} variant="dark" /> + + simulation.mutate(buildRequest(values))} + initialValues={defaultValues} + /> + + } + > + + {simulation.isPending && } + {simulation.isError && } + {simulation.data && ( + <> + {/* Charts and metrics from plan, in order */} + + )} + + + + ); +} +``` + +### Step 6: Implement Responsive CSS + +Use Tailwind responsive prefixes instead of writing CSS media queries: + +- `md:flex-col` — stack layout at tablet (768px) +- `sm:px-4 sm:py-3` — tighter padding on mobile +- `sm:text-xl` — smaller headings on mobile + +### Step 7: Write Component Tests + +For each custom component (not ui-kit imports), create a Vitest test: + +```tsx +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect } from 'vitest'; +import { HouseholdInputs } from '../components/HouseholdInputs'; + +describe('HouseholdInputs', () => { + it('renders all fields from plan', () => { + render( {}} initialValues={defaults} />); + // Check each field from plan exists + }); + + it('calls onChange when input changes', async () => { + const onChange = vi.fn(); + render(); + // Interact with inputs, verify callback + }); +}); +``` + +### Step 8: Promote Custom Components to ui-kit + +After all custom components are built and tested, check if any would be useful additions to `@policyengine/ui-kit`. Use `AskUserQuestion` to ask: + +> "The following custom components were built for this dashboard: [list]. Would you like to open a PR to `@policyengine/ui-kit` to add any of these to the shared library?" + +If yes, invoke the `/create-new-component` command targeting the selected components. + +## Quality Checklist + +- [ ] Used ui-kit for all standard patterns (MetricCard, Button, Card, inputs, charts, layout) +- [ ] No hardcoded hex colors anywhere in TSX — use Tailwind classes or `var(--chart-N)` for Recharts +- [ ] All spacing uses standard Tailwind classes (`p-4`, `gap-3`, etc.) +- [ ] No plain CSS files other than `globals.css` (which imports ui-kit theme) +- [ ] Inter font loaded via `next/font/google` +- [ ] All headings and labels use sentence case +- [ ] Charts follow app-v2 patterns (ResponsiveContainer, consistent formatting) +- [ ] Loading states shown during API calls +- [ ] Error states show helpful messages +- [ ] Country detection works from hash +- [ ] Hash sync updates on input change +- [ ] Share URLs point to policyengine.org +- [ ] Responsive design uses Tailwind breakpoint prefixes +- [ ] All component tests pass +- [ ] TypeScript compiles without errors +- [ ] Next.js build succeeds +- [ ] Custom components offered for promotion to ui-kit + +## DO NOT + +- Use any styling framework OTHER than Tailwind (no Mantine, Chakra, etc.) +- Use plain CSS files or CSS modules for layout/styling — use Tailwind utility classes instead +- Hardcode any colors, spacing, or font values when a design token exists +- Copy app-v2 components directly — follow their patterns +- Skip responsive styles +- Leave `console.log` statements in production code +- Install dependencies not in the plan +- Use Vite — use Next.js as specified in the frontend-builder-spec skill +- Create `tailwind.config.ts` or `postcss.config.js` — Tailwind v4 uses `@theme` in CSS +- Rebuild components that exist in `@policyengine/ui-kit` +- Use `getCssVar()` — it no longer exists. SVG accepts `var()` directly. diff --git a/changelog.d/add-create-new-component-command.added.md b/changelog.d/add-create-new-component-command.added.md new file mode 100644 index 0000000..dabbbc6 --- /dev/null +++ b/changelog.d/add-create-new-component-command.added.md @@ -0,0 +1 @@ +Add /create-new-component command with design-token-validator and component-test-writer agents for building ui-kit components. diff --git a/changelog.d/align-tailwind-v4-bun-uikit.changed.md b/changelog.d/align-tailwind-v4-bun-uikit.changed.md new file mode 100644 index 0000000..57d2723 --- /dev/null +++ b/changelog.d/align-tailwind-v4-bun-uikit.changed.md @@ -0,0 +1 @@ +Update dashboard builder workflow to Tailwind v4, standardize on bun, and integrate @policyengine/ui-kit as the primary component library. diff --git a/changelog.d/gateway-polling-backend.changed.md b/changelog.d/gateway-polling-backend.changed.md new file mode 100644 index 0000000..afb6359 --- /dev/null +++ b/changelog.d/gateway-polling-backend.changed.md @@ -0,0 +1 @@ +Rewrite custom Modal backend pattern (Pattern C) to use gateway + worker + polling architecture, mirroring PolicyEngine API v1/v2. Adds API-first backend selection priority, Modal timeout warnings, and React Query polling hooks across all dashboard builder agents, skills, and commands. diff --git a/changelog.d/migrate-dashboard-to-command.changed.md b/changelog.d/migrate-dashboard-to-command.changed.md new file mode 100644 index 0000000..7441a8d --- /dev/null +++ b/changelog.d/migrate-dashboard-to-command.changed.md @@ -0,0 +1 @@ +Replace SDK-based dashboard builder with native /create-dashboard command for full user interactivity diff --git a/changelog.d/modal-three-file-backend.changed.md b/changelog.d/modal-three-file-backend.changed.md new file mode 100644 index 0000000..9509e28 --- /dev/null +++ b/changelog.d/modal-three-file-backend.changed.md @@ -0,0 +1 @@ +Pattern C (custom-modal) backend now uses three-file structure mirroring api-v2: _image_setup.py (snapshot), app.py (Modal decorators), simulation.py (pure logic). Adds .run_function() for fast cold starts, cpu=8.0/memory=32768 resource specs, explicit pydantic in worker pip_install, .add_local_file() for simulation.py mounting, and architecture validator check #6 for Modal backend structure. diff --git a/commands/create-dashboard.md b/commands/create-dashboard.md new file mode 100644 index 0000000..3d3b4bb --- /dev/null +++ b/commands/create-dashboard.md @@ -0,0 +1,575 @@ +--- +description: Orchestrates multi-agent workflow to create a PolicyEngine dashboard from a natural-language description +--- + +# Creating Dashboard: $ARGUMENTS + +Coordinate a multi-agent workflow to plan, scaffold, implement, validate, and commit a production-ready PolicyEngine dashboard application. + +## Arguments + +`$ARGUMENTS` should contain: +- **Dashboard description** (required) — a natural-language description of the desired dashboard +- **Options**: + - `--repo NAME_OR_PATH` — use an existing repo (searches sibling of cwd, `~/Documents/PolicyEngine/`, `~/PolicyEngine/`, `~/`) + - `--skip-init` — use current working directory as the repo (no repo creation or cloning) + - `--skip-validate` — skip the Phase 5 validation loop + +**Examples:** +``` +/create-dashboard A dashboard showing child poverty rates by state under different CTC reform scenarios +/create-dashboard --repo child-poverty-dashboard "Add a comparison mode showing baseline vs reform" +/create-dashboard --skip-init "Build a SNAP eligibility calculator for a single household" +/create-dashboard --skip-validate "A simple income tax calculator using the PolicyEngine API" +``` + +--- + +## Constants + +``` +DASHBOARD_NAME = derived from description or user input +REPO_PATH = absolute path to the dashboard repository +PREFIX = /tmp/dashboard-{DASHBOARD_NAME} +PLUGIN_ROOT = directory containing this plugin (use: dirname of agents/dashboard/*.md) +``` + +--- + +## YOUR ROLE: ORCHESTRATOR ONLY + +**CRITICAL — Context Window Protection:** +- You are an orchestrator. You do NOT write code, build components, or implement features. +- ALL implementation work is delegated to agents via the Task tool. +- You only read files marked "Short" in the handoff table (max 20 lines each). +- When spawning agents, point them to files on disk — do NOT paste file contents into prompts. + +**You DO:** +- Parse arguments +- Create repos and clone (Phase 0) +- Spawn agents (in parallel where possible) +- Run quality gates via Bash (build, test) +- Read SHORT summary files (≤20 lines) +- Present checkpoints to user via AskUserQuestion +- Commit and push (Phase 6) + +**You MUST NOT:** +- Write any code yourself +- Read full implementation files (plan.yaml, component files, etc.) +- Fix any issues manually — delegate to agents + +--- + +## Context Window Protection + +### Disk File Handoff Table + +| File | Writer | Reader | Size | +|------|--------|--------|------| +| `{REPO_PATH}/plan.yaml` | planner agent | All subsequent agents | Full | +| `{PREFIX}-plan-summary.md` | planner agent | Orchestrator | Short ≤20 | +| `{PREFIX}-build-report.md` | build-validator | Orchestrator | Short ≤15 | +| `{PREFIX}-design-report.md` | design-validator | Orchestrator | Short ≤15 | +| `{PREFIX}-arch-report.md` | arch-validator | Orchestrator | Short ≤15 | +| `{PREFIX}-plan-report.md` | plan-validator | Orchestrator | Short ≤15 | + +--- + +## Phase 0: Parse Arguments & Initialize Repository + +### Step 0A: Parse Arguments & Clean Up + +``` +Parse $ARGUMENTS: +- DESCRIPTION: the natural-language dashboard description +- OPTIONS: --repo, --skip-init, --skip-validate +``` + +Clean up leftover files from previous runs: +```bash +rm -f /tmp/dashboard-*-plan-summary.md /tmp/dashboard-*-build-report.md /tmp/dashboard-*-design-report.md /tmp/dashboard-*-arch-report.md /tmp/dashboard-*-plan-report.md +``` + +Resolve `PLUGIN_ROOT` — find the directory containing this plugin: +```bash +# Find the agents directory to determine plugin root +find ~ -maxdepth 6 -path '*/policyengine-claude/agents/dashboard/dashboard-planner.md' -not -path '*/node_modules/*' 2>/dev/null | head -1 | xargs dirname | xargs dirname | xargs dirname +``` + +### Step 0B: Determine Repository Path + +**If `--skip-init`**: Set `REPO_PATH` to the current working directory. Skip to Phase 1. + +**If `--repo NAME_OR_PATH`**: Search for the repo: +1. If it's an absolute path → use directly +2. If it contains slashes → resolve relative to cwd +3. Otherwise search: sibling of cwd → `~/Documents/PolicyEngine/{NAME}` → `~/PolicyEngine/{NAME}` → `~/{NAME}` +4. Set `REPO_PATH` to the found path. Skip to Phase 1. + +**Otherwise**: Create a new repository: + +1. Derive a dashboard name from the description (kebab-case, 2-4 words), then ask user: + +``` +AskUserQuestion: + Question: "What should the GitHub repository be named?" + Options: + - "{auto-derived-name}" (Recommended) + - "Let me type a name" +``` + +Set `DASHBOARD_NAME` from the answer. + +2. Check GitHub auth and org membership: +```bash +gh api user --jq '.login' +``` +If this fails, tell the user to run `gh auth login` and STOP. + +```bash +gh api orgs/PolicyEngine/memberships/{username} --jq '.role' +``` +If not admin or member, report and STOP. + +3. Create the repo: +```bash +gh repo create PolicyEngine/{DASHBOARD_NAME} --public --description "PolicyEngine {DASHBOARD_NAME} dashboard" +``` + +4. Ask where to clone: + +``` +AskUserQuestion: + Question: "Where should I clone the repository?" + Options: + - "{sibling-of-cwd}/{DASHBOARD_NAME}" (Recommended) + - "~/Documents/PolicyEngine/{DASHBOARD_NAME}" + - "Let me specify a path" +``` + +5. Clone and initial commit: +```bash +gh repo clone PolicyEngine/{DASHBOARD_NAME} "{CLONE_PATH}" +cd {CLONE_PATH} && git add -A && git commit --allow-empty -m "Initialize dashboard repository" && git push -u origin main +``` + +6. Set `REPO_PATH = {CLONE_PATH}`. + +Set `PREFIX = /tmp/dashboard-{DASHBOARD_NAME}`. + +--- + +## Phase 1: Plan (Human Gate) + +### Step 1A: Spawn planner agent + +Spawn a **general-purpose** agent ("dashboard-planner"): + +``` +subagent_type: "general-purpose" + +"You are the dashboard-planner agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +DASHBOARD DESCRIPTION: +{DESCRIPTION} + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/dashboard-planner.md + +ADDITIONAL REQUIREMENTS: +- Write plan.yaml to {REPO_PATH}/plan.yaml +- Also write a SHORT summary (≤20 lines) to {PREFIX}-plan-summary.md containing: + - Dashboard name and purpose + - Data pattern chosen and why + - Number and types of components + - Key API endpoints or data sources + - Any questions or concerns about the description" +``` + +### Step 1B: Plan approval + +Read `{PREFIX}-plan-summary.md` (SHORT file). Present the summary to the user: + +``` +AskUserQuestion: + Question: "Here's the dashboard plan. How should we proceed?" + Options: + - "Approve — start building" (Recommended) + - "Modify — I have feedback" + - "Reject — start over" +``` + +**If "Approve"**: Continue to Phase 2. + +**If "Modify"**: Ask follow-up question for feedback. Then re-spawn planner agent with extra context: + +``` +"User feedback on your previous plan: +{USER_FEEDBACK} + +Read the existing plan.yaml in {REPO_PATH} and update it based on this feedback. +Write updated plan.yaml and updated {PREFIX}-plan-summary.md." +``` + +Then repeat Step 1B (approval loop). + +**If "Reject"**: STOP. + +--- + +## Phase 2: Scaffold (Quality Gates) + +### Step 2A: Spawn scaffold agent + +Spawn a **general-purpose** agent ("dashboard-scaffold"): + +``` +subagent_type: "general-purpose" + +"You are the dashboard-scaffold agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/dashboard-scaffold.md + +The approved plan.yaml is at: {REPO_PATH}/plan.yaml" +``` + +### Step 2B: Quality gates + +After the agent completes, run quality gates: + +```bash +cd {REPO_PATH} && bun run build 2>&1 | tail -50 +``` + +```bash +cd {REPO_PATH} && bunx vitest run 2>&1 | tail -50 +``` + +**If both pass** (exit code 0): Continue to Phase 3. + +**If either fails**: Re-spawn scaffold agent with error context: + +``` +"Quality gate failed. Fix the issues: + +Command: {FAILED_COMMAND} +Exit code: {EXIT_CODE} +Output: +{STDERR_AND_STDOUT} + +Fix the issues in {REPO_PATH} and ensure both build and tests pass." +``` + +Max 2 retries. If still failing after retries: + +``` +AskUserQuestion: + Question: "Build/tests still failing after 2 fix attempts. What should we do?" + Options: + - "Continue anyway — I'll fix manually later" + - "Stop" +``` + +--- + +## Phase 3: Backend + Frontend (PARALLEL) + +Spawn **both agents in a single message** so they run concurrently: + +### Agent 1: backend-builder + +``` +subagent_type: "general-purpose" +run_in_background: true + +"You are the backend-builder agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/backend-builder.md + +The approved plan.yaml is at: {REPO_PATH}/plan.yaml" +``` + +### Agent 2: frontend-builder + +``` +subagent_type: "general-purpose" +run_in_background: true + +"You are the frontend-builder agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/frontend-builder.md + +The approved plan.yaml is at: {REPO_PATH}/plan.yaml" +``` + +Wait for both agents to complete before proceeding. + +--- + +## Phase 4: Integrate + +Spawn a **general-purpose** agent ("dashboard-integrator"): + +``` +subagent_type: "general-purpose" + +"You are the dashboard-integrator agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/dashboard-integrator.md + +The approved plan.yaml is at: {REPO_PATH}/plan.yaml" +``` + +--- + +## Phase 5: Validation (4 Parallel Validators + Fix Routing) + +**Skip this phase if `--skip-validate` flag is set.** + +### Validation Loop (max 3 cycles) + +``` +ROUND = 1 +MAX_ROUNDS = 3 +PENDING_VALIDATORS = [build, design, architecture, plan] # all 4 initially +``` + +### Step 5A: Run validators + +Spawn **all pending validators in a single message**, each with `run_in_background: true`. + +For each validator, use this prompt template: + +``` +subagent_type: "general-purpose" +run_in_background: true + +"You are the {VALIDATOR_NAME} agent for the PolicyEngine dashboard builder. + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && + +Read and follow the complete instructions in: +{PLUGIN_ROOT}/agents/dashboard/{VALIDATOR_FILE} + +The plan.yaml is at: {REPO_PATH}/plan.yaml + +ADDITIONAL: Write your report to {PREFIX}-{REPORT_FILE} in addition to returning it." +``` + +Validator details: + +| Validator | Agent file | Report file | +|-----------|-----------|-------------| +| build | `dashboard-build-validator.md` | `build-report.md` | +| design | `dashboard-design-validator.md` | `design-report.md` | +| architecture | `dashboard-architecture-validator.md` | `arch-report.md` | +| plan | `dashboard-plan-validator.md` | `plan-report.md` | + +Wait for all validators to complete. + +### Step 5B: Parse results + +Read each validator's SHORT report. Look for PASS or FAIL. + +**If all pass**: Exit validation loop, continue to Phase 6. + +**If any fail**: Collect the failure details from each failed validator. + +### Step 5C: Route failures to builders + +Use this routing table to determine which builder agent should fix each failure: + +| Failed Validator | Failure contains | Route to builder | +|------------------|-----------------|-----------------| +| build | (any failure) | `dashboard-scaffold.md` | +| design | (any failure) | `frontend-builder.md` | +| architecture | "tailwind", "next.js", "package manager" | `dashboard-scaffold.md` | +| architecture | "ui-kit", or other | `frontend-builder.md` | +| plan | "api contract" | `backend-builder.md` | +| plan | "component", "chart" | `frontend-builder.md` | +| plan | "embedding", "loading", "error" | `dashboard-integrator.md` | + +Group failures by target builder. For each builder that needs to fix issues, spawn it: + +``` +subagent_type: "general-purpose" + +"You are the {BUILDER_NAME} agent. Fix the following validation failures: + +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths. Prefix Bash with: cd {REPO_PATH} && + +Read the full instructions in: +{PLUGIN_ROOT}/agents/dashboard/{BUILDER_FILE} + +VALIDATION FAILURES TO FIX: +{LIST_OF_FAILURES_FROM_REPORTS} + +Fix each issue, ensuring the build and tests still pass. +Run: cd {REPO_PATH} && bun run build && bunx vitest run" +``` + +### Step 5D: Re-validate + +After fixes, set `PENDING_VALIDATORS` to only the validators that failed (don't re-run passed ones). + +Increment `ROUND`. If `ROUND <= MAX_ROUNDS`, go back to Step 5A. + +If `ROUND > MAX_ROUNDS` and failures remain: + +``` +AskUserQuestion: + Question: "Validation found {N} remaining issues after {MAX_ROUNDS} fix rounds. What should we do?" + Options: + - "Accept as-is — I'll fix manually" + - "Keep trying one more round" + - "Stop — don't commit" +``` + +**If "Accept"**: Continue to Phase 6. +**If "Keep trying"**: Increment MAX_ROUNDS by 1, go back to Step 5A. +**If "Stop"**: STOP. + +--- + +## Phase 6: Review & Commit (Human Gate) + +Present a summary to the user: + +``` +## Dashboard Build Complete + +**Repository**: {REPO_PATH} +**Phases completed**: plan → scaffold → backend + frontend → integrate → validate + +### How to run locally + +cd {REPO_PATH} +make dev # Start full dev stack +make dev-frontend # Frontend only +make test # Run tests +make build # Production build + +### Next steps + +After reviewing locally, commit and push. Then use /deploy-dashboard to deploy. +``` + +``` +AskUserQuestion: + Question: "Ready to commit and push to GitHub?" + Options: + - "Commit and push" (Recommended) + - "Stop — I want to review locally first" +``` + +**If "Commit and push"**: +```bash +cd {REPO_PATH} && git add -A && git commit -m "Implement dashboard from plan" && git push origin HEAD +``` + +Report the result to the user with the repo URL. + +**If "Stop"**: Report that code is ready locally at `{REPO_PATH}` and the user can commit when ready. + +--- + +## Phase 7: Overview Update (Silent) + +Spawn the overview updater in the background — no need to wait: + +``` +subagent_type: "general-purpose" +run_in_background: true + +"You are the dashboard-overview-updater agent. + +Read and follow: {PLUGIN_ROOT}/agents/dashboard/dashboard-overview-updater.md" +``` + +--- + +## Error Handling + +| Category | Example | Action | +|----------|---------|--------| +| **GitHub auth failure** | `gh api user` fails | Tell user to run `gh auth login`, STOP | +| **Repo already exists** | `gh repo create` says exists | Tell user to use `--repo NAME`, STOP | +| **Build failure** | `bun run build` fails | Re-run scaffold with error context (max 2 retries) | +| **Test failure** | `bunx vitest run` fails | Re-run scaffold with error context (max 2 retries) | +| **Validation failure** | Validator reports FAIL | Route to builder, re-validate (max 3 cycles) | +| **Agent failure** | Agent errors or doesn't complete | Report to user, suggest re-running that phase | +| **User rejects plan** | "Reject" at plan approval | STOP | + +### Escalation Path + +1. Agent encounters error → Attempt fix via re-spawning agent +2. Fix fails after max retries → Report to user with AskUserQuestion +3. User chooses to stop → STOP cleanly +4. Never proceed past a human gate without approval + +--- + +## Execution Instructions + +**YOUR ROLE**: You are an orchestrator ONLY. You must: +1. Invoke agents using the Task tool with `subagent_type: "general-purpose"` +2. Wait for agent completion (or use `run_in_background: true` for parallel) +3. Read ONLY short summary/report files (≤20 lines) +4. Run quality gates via Bash +5. Present checkpoints to user via AskUserQuestion +6. Proceed to the next phase after approval + +**YOU MUST NOT**: +- Write any code yourself +- Fix any issues manually +- Read full implementation files +- Skip human gates + +**IMPORTANT — Agent Working Directory**: +Every agent prompt MUST include: +``` +WORKING DIRECTORY: {REPO_PATH} +Use absolute paths for all Read/Write/Edit/Glob/Grep operations. +Prefix all Bash commands with: cd {REPO_PATH} && +``` +This ensures agents work in the correct directory regardless of Claude Code's cwd. + +**Execution Flow (CONTINUOUS)**: + +Execute all phases sequentially without stopping (unless a STOP condition is hit): + +0. **Phase 0**: Parse args, init repo (or use existing) +1. **Phase 1**: Plan → HUMAN APPROVAL (approve/modify/reject) +2. **Phase 2**: Scaffold → quality gates (build + test, max 2 retries) +3. **Phase 3**: Backend + Frontend (PARALLEL — spawn both in one message) +4. **Phase 4**: Integrate +5. **Phase 5**: Validate (4 validators parallel, fix routing, max 3 cycles) — skip if `--skip-validate` +6. **Phase 6**: Review → HUMAN APPROVAL → commit and push +7. **Phase 7**: Overview update (silent, background) diff --git a/commands/create-new-component.md b/commands/create-new-component.md new file mode 100644 index 0000000..69afede --- /dev/null +++ b/commands/create-new-component.md @@ -0,0 +1,260 @@ +--- +description: Build new UI components for @policyengine/ui-kit — with design token validation, tests, visual preview, and PR creation +--- + +# Create new component + +Builds one or more new components for the `@policyengine/ui-kit` library. Handles the full lifecycle: research existing patterns, build with design tokens, validate token usage, write tests, preview visually, and open a PR. + +## Prerequisites + +The `policyengine-ui-kit` repo must be cloned locally. If not found, clone it: +```bash +gh repo clone PolicyEngine/policyengine-ui-kit +``` + +All work happens in the `policyengine-ui-kit` repo directory. + +## Step 1: Gather requirements + +Use `AskUserQuestion` to ask the user: + +1. **Component description** — What component(s) do you need? Describe their purpose, behavior, and any variants. + +Parse the description to identify: +- Component names (PascalCase, e.g., `Tooltip`, `ProgressBar`, `Accordion`) +- Which category they belong to: `primitives`, `layout`, `inputs`, `display`, or `charts` +- Expected props, variants, and states + +## Step 2: Research app-v2 for similar components + +Before building from scratch, search `PolicyEngine/policyengine-app-v2` for similar implementations: + +```bash +# Search app-v2 for components with similar names or purposes +find /path/to/policyengine-app-v2/app/src -name "*.tsx" | xargs grep -li "COMPONENT_NAME_PATTERN" + +# Also check the design-system package +find /path/to/policyengine-app-v2/packages/design-system -name "*.tsx" | xargs grep -li "COMPONENT_NAME_PATTERN" + +# Check Mantine usage patterns for the same component type +grep -r "Mantine.*COMPONENT_TYPE" /path/to/policyengine-app-v2/app/src --include="*.tsx" -l +``` + +If similar components exist in app-v2: +- Study their props interface, variant patterns, and behavior +- Adapt the design and UX, but rebuild using the ui-kit's Tailwind v4 + CVA stack +- Do NOT copy Mantine-dependent code — translate it to Tailwind utility classes + +If nothing similar exists in app-v2: +- Research common patterns for that component type +- Design props and variants that are consistent with existing ui-kit components + +## Step 3: Build the component(s) + +**Tech stack (mandatory):** +- React 19 with `forwardRef` +- TypeScript with strict types +- Tailwind CSS v4 with standard class names (no prefix) +- CVA (`class-variance-authority`) for variant management +- `cn()` utility from `@/utils/cn` for class merging +- Design tokens from ui-kit's `theme.css` (colors, spacing, typography, radius) + +**File structure for each component:** +``` +src//ComponentName.tsx +``` + +**Component template:** +```tsx +import { cva, type VariantProps } from 'class-variance-authority'; +import { forwardRef, type HTMLAttributes } from 'react'; +import { cn } from '../utils/cn'; + +const componentVariants = cva( + 'base-classes-here', + { + variants: { + variant: { + default: 'variant-classes', + // ... more variants + }, + size: { + sm: 'size-classes', + md: 'size-classes', + lg: 'size-classes', + }, + }, + defaultVariants: { variant: 'default', size: 'md' }, + }, +); + +export interface ComponentNameProps + extends HTMLAttributes, + VariantProps { + // Additional props here +} + +export const ComponentName = forwardRef( + ({ variant, size, className, children, ...props }, ref) => ( +
+ {children} +
+ ), +); +ComponentName.displayName = 'ComponentName'; +``` + +**Rules:** +- ALL colors, spacing, font sizes, and radii must reference design tokens — no hardcoded hex values, no arbitrary pixel values +- Use semantic classes (`bg-primary`, `text-foreground`) or brand palette classes (`bg-teal-500`, `text-gray-600`) +- Export the component and its props interface from the category barrel (`src//index.ts`) +- Export from the main barrel (`src/index.ts`) +- Build ALL components the user requested — do not skip any + +## Step 4: Design token validation agent + +Launch the **Design Token Validator Agent** (`agents/app/design-token-validator.md`) to audit every component file created in Step 3. + +The agent will: +1. Scan all new component files for hardcoded values +2. Replace hardcoded colors, spacing, font sizes, and radii with token-based Tailwind classes +3. Report what was replaced and what was kept + +Review the agent's output. If it made changes, verify they look correct. + +## Step 5: Add components to a preview page + +Create or update `demo/Demo.tsx` to include a new section showcasing ALL new components with realistic example data. + +```tsx +// Add a new section for each component +
+

ComponentName

+
+ {/* Render every variant, size, and state */} + Example + Large primary + {/* ... all combinations */} +
+
+``` + +**Important:** Include ALL new components on the preview page, not just some. + +## Step 6: Launch preview and iterate with user + +Start the demo dev server: +```bash +cd /path/to/policyengine-ui-kit +bun run dev:demo +``` + +Tell the user: +> The demo server is running at http://localhost:5173/demo/index.html — scroll to the new component section to preview your components. + +Then use `AskUserQuestion` to ask: +> Do the components look correct? Please review and either approve or describe changes you'd like. + +**This is iterative.** If the user requests changes: +1. Apply the requested modifications +2. The dev server will hot-reload automatically +3. Ask the user to review again +4. Repeat until the user approves + +Do NOT proceed to Step 7 until the user explicitly approves the components. + +## Step 7: Component test writer agent + +After user approval, launch the **Component Test Writer Agent** (`agents/app/component-test-writer.md`) to write unit tests for ALL new components. + +The agent will: +1. Read each new component file +2. Write comprehensive test files (rendering, props, variants, interaction, ref forwarding) +3. Place tests next to components (`src//ComponentName.test.tsx`) +4. Run `bun run test` and fix any failures + +Verify all tests pass: +```bash +cd /path/to/policyengine-ui-kit +bun run test +``` + +If tests fail, fix them before proceeding. + +## Step 8: Create changelog fragment + +Add a towncrier changelog fragment: +```bash +echo "Add , , ... components." > changelog.d/add-COMPONENT_NAME.added.md +``` + +## Step 9: Commit, push, and open PR + +```bash +# Create feature branch +git checkout -b add-COMPONENT_NAME-component + +# Stage all new and modified files +git add src//ComponentName.tsx +git add src//ComponentName.test.tsx +git add src//index.ts +git add src/index.ts +git add demo/Demo.tsx +git add changelog.d/add-COMPONENT_NAME.added.md + +# Commit +git commit -m "Add ComponentName component with tests" + +# Push +git push -u origin add-COMPONENT_NAME-component +``` + +Create a PR with details about the new components and their test coverage: +```bash +gh pr create --repo PolicyEngine/policyengine-ui-kit \ + --title "Add ComponentName component" \ + --body "$(cat <<'EOF' +## Summary +- Add `ComponentName` component to `@policyengine/ui-kit` +- + +## Components added +- `ComponentName` — + - Variants: + - Props: + +## Testing +- Unit tests written with Vitest + React Testing Library +- Tests cover: rendering, all variants, props, interaction, ref forwarding +- All tests passing + +## Design token compliance +- All colors reference design tokens (no hardcoded hex) +- All spacing uses standard Tailwind classes +- All typography uses token scale +- Validated by design-token-validator agent + +## Test plan +- [ ] Run `bun run dev:demo` and verify components render correctly +- [ ] Run `bun run test` and verify all tests pass +- [ ] Review component API matches existing ui-kit conventions + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +Report the PR URL to the user. + +## Reference + +See these skills and agents for detailed guidance: +- `policyengine-design-skill` — Design token values and usage +- `policyengine-app-skill` — app-v2 component patterns to reference +- `agents/app/design-token-validator.md` — Automated design token compliance +- `agents/app/component-test-writer.md` — Automated test writing diff --git a/commands/dashboard-overview.md b/commands/dashboard-overview.md new file mode 100644 index 0000000..3c22539 --- /dev/null +++ b/commands/dashboard-overview.md @@ -0,0 +1,78 @@ +--- +description: Lists all available tools, commands, skills, and agents in the dashboard builder ecosystem +--- + +# Dashboard builder ecosystem overview + +Display a complete inventory of all tools, commands, skills, and agents available in the PolicyEngine dashboard builder workflow. + +## Commands + +| Command | Description | +|---------|-------------| +| `/create-dashboard` | Orchestrates multi-agent workflow: creates repo, plans, scaffolds, implements, validates, and commits a dashboard | +| `/deploy-dashboard` | Deploys a completed dashboard to Vercel (and optionally Modal) and registers it in the app | +| `/dashboard-overview` | This command — lists all dashboard builder ecosystem components | + +## Agents + +| Agent | Phase | Description | +|-------|-------|-------------| +| `dashboard-planner` | 1 — Plan | Analyzes natural-language descriptions and produces structured plan YAML | +| `dashboard-scaffold` | 2 — Scaffold | Generates Next.js + Tailwind project structure into the current repo | +| `backend-builder` | 3A — Implement | Builds API stubs for v2 alpha integration or custom Modal backends | +| `frontend-builder` | 3B — Implement | Builds React components with Tailwind + PE design tokens | +| `dashboard-integrator` | 4 — Integrate | Wires frontend components to backend API client, handles data flow | +| `dashboard-build-validator` | 5 — Validate | Runs build and test suite | +| `dashboard-design-validator` | 5 — Validate | Checks design tokens, typography, sentence case, responsive | +| `dashboard-architecture-validator` | 5 — Validate | Checks Tailwind v4, Next.js, ui-kit, package manager | +| `dashboard-plan-validator` | 5 — Validate | Checks API contract, components, embedding, states vs plan | +| `dashboard-overview-updater` | Post — Update | Updates this overview if ecosystem components changed | + +## Skills + +| Skill | Purpose | +|-------|---------| +| `policyengine-frontend-builder-spec-skill` | Mandatory frontend technology requirements (Next.js, Tailwind CSS, design tokens) | +| `policyengine-dashboard-workflow-skill` | Reference for the create/deploy dashboard workflow | +| `policyengine-interactive-tools-skill` | Embedding patterns, hash sync, country detection | +| `policyengine-design-skill` | Design tokens, visual identity, colors, typography, spacing | +| `policyengine-recharts-skill` | Recharts chart component patterns and styling | +| `policyengine-app-skill` | app-v2 component architecture reference | +| `policyengine-api-v2-skill` | API v2 endpoint catalog and async patterns | +| `policyengine-vercel-deployment-skill` | Vercel deployment configuration | +| `policyengine-modal-deployment-skill` | Modal deployment for custom dashboard backends | +| `policyengine-standards-skill` | Code quality and CI/CD standards | +| `policyengine-writing-skill` | PolicyEngine writing style for blog posts, documentation, and reports | +| `policyengine-us-skill` | US tax/benefit variables and programs | +| `policyengine-uk-skill` | UK tax/benefit variables and programs | +| `policyengine-tailwind-shadcn-skill` | Tailwind CSS v4 + shadcn/ui integration patterns and conventions | +| `policyengine-ui-kit-consumer-skill` | @policyengine/ui-kit consumer setup, CSS configuration, and troubleshooting | + +## Workflow phases + +``` +Phase 0: Init repo (creates GitHub repo + clones locally, or uses existing) +Phase 1: Plan (dashboard-planner) → HUMAN APPROVAL +Phase 2: Scaffold (dashboard-scaffold) → quality gates (build + test) +Phase 3: Backend + Frontend (PARALLEL) +Phase 4: Integrate (dashboard-integrator) +Phase 5: Validate (4 validators in parallel) ─┐ + build, design, architecture, plan │ ← max 3 fix cycles + Fix → re-validate ───────────────────┘ +Phase 6: Human review → commit and push +Phase 7: Update overview (dashboard-overview-updater, silent) + +Separately: /deploy-dashboard (after merge to main) +``` + +## Tech stack + +| Layer | Technology | +|-------|-----------| +| Framework | Next.js (App Router) + TypeScript | +| Styling | Tailwind CSS + `@policyengine/ui-kit` tokens | +| Charts | Recharts (line, bar, area) + Plotly (choropleths) | +| Data fetching | TanStack React Query | +| Testing | Vitest + React Testing Library | +| Deployment | Vercel (frontend) + Modal (backend, if custom) | diff --git a/commands/deploy-dashboard.md b/commands/deploy-dashboard.md new file mode 100644 index 0000000..6266444 --- /dev/null +++ b/commands/deploy-dashboard.md @@ -0,0 +1,281 @@ +--- +description: Deploys a PolicyEngine dashboard to Vercel (and optionally Modal) and registers it in the app +--- + +# Deploying dashboard: $ARGUMENTS + +Deploy a completed PolicyEngine dashboard to production. Run this AFTER merging your feature branch into `main`. + +**Precondition:** The user should be on the `main` branch with a clean working tree and the dashboard code merged. + +## Skills Used + +- **policyengine-vercel-deployment-skill** — Frontend deployment (all dashboards) +- **policyengine-modal-deployment-skill** — Backend deployment (only if `custom-backend` pattern) + +## Step 1: Verify Prerequisites + +```bash +# Check we're on main +git branch --show-current + +# Check for clean working tree +git status + +# Verify build passes +bun install --frozen-lockfile && bun run build && bunx vitest run +``` + +**If not on main:** Tell the user to merge their feature branch first: +> You're currently on branch `{branch}`. Please merge into `main` first: +> ```bash +> git checkout main +> git merge {branch} +> git push +> ``` +> Then run `/deploy-dashboard` again. + +**If build fails:** Report the error and STOP. Do not deploy broken code. + +## Step 2: Read the Plan + +```bash +cat plan.yaml +``` + +Extract: +- `dashboard.name` — for Vercel project and Modal app names +- `data_pattern` — determines if Modal deploy is needed (`custom-backend` vs `api-v2-alpha`) +- `tech_stack.framework` — should be `react-nextjs` (env var prefix: `NEXT_PUBLIC_*`) +- `embedding.register_in_apps_json` — determines if apps.json update is needed +- `embedding.slug` — the URL slug for policyengine.org + +## Step 3: Deploy Backend (if custom-backend) + +**Only if `data_pattern: custom-backend`.** If `api-v2-alpha`, skip to Step 4. + +See `policyengine-modal-deployment-skill` for the full Modal deployment reference. + +### 3a. Authentication check (human gate) + +```bash +modal token info +modal profile list +``` + +Present the output to the user. Verify: +- Active profile is `policyengine` +- Workspace is `policyengine` + +**If authentication fails or shows wrong workspace:** Stop and display instructions: + +> **Modal authentication required.** Your CLI is not configured for the `policyengine` workspace. +> +> Please run: +> ```bash +> modal token new --profile policyengine +> modal profile activate policyengine +> ``` +> +> If you don't have access, ask a PolicyEngine workspace owner for an invite. + +**Do NOT proceed until `modal token info` shows `Workspace: policyengine`.** + +**If authentication succeeds**, use `AskUserQuestion` to confirm before proceeding: + +``` +question: "Modal is authenticated to the policyengine workspace. Proceed with deployment?" +header: "Modal auth" +options: + - label: "Proceed" + description: "Continue to environment selection and deploy" + - label: "Cancel" + description: "Stop deployment" +``` + +### 3b. Environment selection (human gate) + +Use `AskUserQuestion` to select the Modal environment: + +``` +question: "Which Modal environment should this deploy to?" +header: "Environment" +options: + - label: "main (Recommended)" + description: "Production — policyengine--app-func.modal.run" + - label: "staging" + description: "Pre-production testing — policyengine-staging--app-func.modal.run" + - label: "testing" + description: "Development/CI — policyengine-testing--app-func.modal.run" +``` + +### 3c. Deploy + +```bash +# Guard against env var override +unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET + +# Deploy to the selected environment +modal deploy modal_app.py --env SELECTED_ENV +``` + +### 3d. Verify endpoint + +Construct the URL from the app name and function name in `modal_app.py`: + +- Pattern: `https://policyengine--APP_NAME-FUNCTION_NAME.modal.run` +- With non-main environment: `https://policyengine-ENV--APP_NAME-FUNCTION_NAME.modal.run` + +```bash +# Health check (if endpoint exists) +curl -s -w "\n%{http_code}" https://policyengine--DASHBOARD_NAME-health.modal.run + +# Test the calculation endpoint +curl -s -X POST https://policyengine--DASHBOARD_NAME-calculate.modal.run \ + -H "Content-Type: application/json" \ + -d '{"test": true}' +``` + +**If deploy fails:** Report error and STOP. See the `policyengine-modal-deployment-skill` troubleshooting table. + +### 3e. Set API URL in Vercel + +After successful Modal deploy, set the API URL as a Vercel environment variable. + +```bash +vercel env add NEXT_PUBLIC_API_URL production +# Enter: https://policyengine--DASHBOARD_NAME-calculate.modal.run +``` + +## Step 4: Deploy Frontend to Vercel + +See `policyengine-vercel-deployment-skill` for the full Vercel deployment reference. + +```bash +# Link to Vercel under PolicyEngine team (if not already linked) +vercel link --scope policy-engine + +# Deploy to production +vercel --prod --yes --scope policy-engine +``` + +If a Modal backend was deployed in Step 3, force-rebuild to pick up the new env var: +```bash +vercel --prod --force --yes --scope policy-engine +``` + +Capture the production URL from the output. + +Verify the deployment: +```bash +curl -s -o /dev/null -w "%{http_code}" https://VERCEL_PRODUCTION_URL/ +``` + +**IMPORTANT:** Use the auto-assigned Vercel production URL, not a custom alias. Custom aliases may have deployment protection issues. + +## Step 5: Register in apps.json (if applicable) + +**Only if `embedding.register_in_apps_json: true`:** + +This requires a PR to `PolicyEngine/policyengine-app-v2`. + +```bash +# Clone app-v2 if not already available +gh repo clone PolicyEngine/policyengine-app-v2 /tmp/policyengine-app-v2 + +cd /tmp/policyengine-app-v2 +git checkout main +git checkout -b add-DASHBOARD_NAME-tool +``` + +Add entry to `app/src/data/apps/apps.json`: + +```json +{ + "type": "iframe", + "slug": "SLUG", + "title": "TITLE", + "description": "DESCRIPTION", + "source": "VERCEL_PRODUCTION_URL", + "tags": ["COUNTRY", "policy", "interactives"], + "countryId": "COUNTRY", + "displayWithResearch": true, + "image": "SLUG-cover.png", + "date": "CURRENT_DATE 12:00:00", + "authors": ["AUTHOR_SLUG"] +} +``` + +Use `AskUserQuestion` to gather required metadata: + +``` +question: "What is the author slug for the apps.json entry? (Check existing entries in apps.json for format, e.g., 'max-ghenis')" +header: "Author" +options: [] (free text — let the user type via "Other") +``` + +If `displayWithResearch: true`, also ask: + +``` +question: "Do you have a cover image for the apps.json listing?" +header: "Cover image" +options: + - label: "I'll provide one" + description: "You'll give me the image file or path" + - label: "Skip for now" + description: "Use a placeholder — you can add a cover image later" +``` + +```bash +git add app/src/data/apps/apps.json +git commit -m "Register DASHBOARD_NAME interactive tool" +git push -u origin add-DASHBOARD_NAME-tool + +gh pr create --repo PolicyEngine/policyengine-app-v2 \ + --title "Register DASHBOARD_NAME tool" \ + --body "Adds DASHBOARD_NAME to the interactive tools listing. + +Source: VERCEL_PRODUCTION_URL +Slug: /COUNTRY/SLUG" +``` + +## Step 6: Smoke Test + +After deployment: + +1. **Direct URL:** Visit the Vercel production URL, verify the dashboard loads +2. **Embedded (if registered):** After apps.json PR merges, verify at `policyengine.org/COUNTRY/SLUG` +3. **Hash sync:** Test that URL parameters work (add `#income=50000` etc.) +4. **Country detection:** Test with `#country=uk` if the dashboard supports multiple countries + +## Step 7: Report + +Present deployment summary to the user: + +> ## Dashboard deployed +> +> - **Live URL:** VERCEL_PRODUCTION_URL +> - **Vercel project:** DASHBOARD_NAME +> [If custom backend:] +> - **API endpoint:** https://policyengine--DASHBOARD_NAME-calculate.modal.run +> - **Modal environment:** SELECTED_ENV +> [If registered:] +> - **apps.json PR:** PR_URL (will be available at policyengine.org/COUNTRY/SLUG after merge) +> +> ### Verify +> - [ ] Dashboard loads at the Vercel URL +> - [ ] Calculations work (or stubs respond correctly) +> - [ ] Hash parameters are preserved on refresh +> [If registered:] +> - [ ] After apps.json PR merges, dashboard embeds correctly in policyengine.org + +## Error Recovery + +| Issue | Fix | Reference | +|-------|-----|-----------| +| Vercel deploy fails | Check `vercel.json` config, ensure project builds | `policyengine-vercel-deployment-skill` | +| Modal deploy fails | Check Python deps, Modal auth, function timeouts | `policyengine-modal-deployment-skill` | +| Wrong Modal workspace | `modal profile activate policyengine` | `policyengine-modal-deployment-skill` | +| 404 on Vercel URL | Wait 30s for propagation, check Vercel dashboard | `policyengine-vercel-deployment-skill` | +| API returns errors | Check Modal logs: `modal app logs DASHBOARD_NAME` | `policyengine-modal-deployment-skill` | +| Hash sync broken | Check postMessage calls in embedding.ts | `policyengine-interactive-tools-skill` | diff --git a/commands/new-tool.md b/commands/new-tool.md index fa43230..9e983c8 100644 --- a/commands/new-tool.md +++ b/commands/new-tool.md @@ -1,5 +1,5 @@ --- -description: Scaffold a new PolicyEngine interactive tool (Next.js 14 + Tailwind 4 + design system + embedding boilerplate) +description: Scaffold a new PolicyEngine interactive tool (Next.js 14 + Tailwind 4 + ui-kit theme + embedding boilerplate) --- # New interactive tool scaffold @@ -25,10 +25,16 @@ bunx create-next-app@14 TOOL_NAME --js --app --tailwind --eslint --no-src-dir -- cd TOOL_NAME # Install dependencies -bun add @policyengine/design-system recharts +bun add @policyengine/ui-kit recharts bun add -D vitest ``` +Copy the favicon: +```bash +mkdir -p public +cp node_modules/@policyengine/ui-kit/src/assets/logos/policyengine/teal-square.svg public/favicon.svg +``` + If using code highlighting: ```bash bun add prism-react-renderer @@ -46,16 +52,13 @@ import "./globals.css"; export const metadata = { title: "TOOL_TITLE | PolicyEngine", description: "DESCRIPTION", + icons: { icon: "/favicon.svg" }, }; export default function RootLayout({ children }) { return ( - ` in ``. The `@import` from `node_modules` does not work with the Next.js CSS pipeline. - ### app/globals.css ```css @import "tailwindcss"; - -@theme { - --color-pe-primary-50: var(--pe-color-primary-50); - --color-pe-primary-500: var(--pe-color-primary-500); - --color-pe-primary-600: var(--pe-color-primary-600); - --color-pe-primary-700: var(--pe-color-primary-700); - - --color-pe-gray-50: var(--pe-color-gray-50); - --color-pe-gray-100: var(--pe-color-gray-100); - --color-pe-gray-200: var(--pe-color-gray-200); - - --color-pe-error: var(--pe-color-error); - - --color-pe-bg-primary: var(--pe-color-bg-primary); - --color-pe-text-primary: var(--pe-color-text-primary); - --color-pe-text-secondary: var(--pe-color-text-secondary); - --color-pe-text-tertiary: var(--pe-color-text-tertiary); - - --color-pe-border-light: var(--pe-color-border-light); -} +@import "@policyengine/ui-kit/theme.css"; body { - font-family: var(--pe-font-family-primary); - color: var(--pe-color-text-primary); - background: var(--pe-color-bg-primary); + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ``` +The single `@import "@policyengine/ui-kit/theme.css"` provides all design tokens (colors, spacing, typography, chart colors) as CSS variables that Tailwind 4 picks up automatically. No manual `@theme` block needed. + ### app/page.jsx ```jsx "use client"; import { useState } from "react"; - -const PE_LOGO_URL = - "https://raw.githubusercontent.com/PolicyEngine/policyengine-app-v2/main/app/public/assets/logos/policyengine/white.png"; +import { DashboardShell, Header, logos } from "@policyengine/ui-kit"; function getCountryFromHash() { if (typeof window === "undefined") return "us"; @@ -144,25 +126,17 @@ export default function Home() { } return ( -
-
+
} > -
-
- PolicyEngine -

TOOL_TITLE

-
-
-
+ TOOL_TITLE +
{/* Your tool UI goes here */}
-
+ ); } ``` @@ -177,6 +151,10 @@ export default function Home() { ## Step 4: Data pattern boilerplate +> **Prefer Pattern B** (PolicyEngine API) unless the tool needs microsimulation, +> custom reforms, or variables not in the API. Pattern C is significantly more complex +> and should be the last resort. + Based on the user's choice, add the appropriate data fetching code. **For Pattern B (PolicyEngine API):** Create `lib/api.js`: @@ -195,38 +173,127 @@ export async function calculate(countryId, household) { } ``` -**For Pattern C (Modal API):** Create `modal_app.py`: +**For Pattern C (Custom Modal API — gateway + polling):** + +Pattern C uses a **three-file backend structure** mirroring policyengine-api-v2's simulation service: a standalone image setup, a worker app, and pure simulation logic. A lightweight gateway manages job submission/polling. This avoids Modal's ~150s gateway timeout and a common crash-loop where module-level imports fail. + +First, look up the latest package version from PyPI. Do NOT guess or use a version from memory: + +```bash +pip index versions policyengine-us 2>/dev/null | head -1 +# or for UK: pip index versions policyengine-uk 2>/dev/null | head -1 +``` + +Create `backend/_image_setup.py` (standalone snapshot function, no package imports at module level): + +```python +def snapshot_models(): + """Pre-load models at image build time for fast cold starts.""" + import logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + logger.info("Pre-loading tax-benefit system...") + from policyengine_us import CountryTaxBenefitSystem # or policyengine_uk + CountryTaxBenefitSystem() + logger.info("Models pre-loaded into image snapshot") +``` + +Create `backend/simulation.py` (pure logic, policyengine at module level — captured in snapshot): + +```python +from policyengine_us import Simulation # Snapshotted at build time + +def run_compute(params: dict) -> dict: + sim = Simulation(situation=params["household"]) + return {"result": float(sim.calculate("variable_name", 2025).sum())} +``` + +Create `backend/app.py` (worker app, only `modal` at module level): ```python import modal +from pathlib import Path +from _image_setup import snapshot_models + +_BACKEND_DIR = Path(__file__).parent +app = modal.App("TOOL_NAME-workers") +image = ( + modal.Image.debian_slim(python_version="3.11") + .pip_install("policyengine-us==LATEST_VERSION", "pydantic") + .run_function(snapshot_models) + .add_local_file(str(_BACKEND_DIR / "simulation.py"), remote_path="/root/simulation.py") +) + +@app.function(image=image, cpu=8.0, memory=32768, timeout=3600) +def compute(params: dict) -> dict: + from simulation import run_compute + return run_compute(params) +``` + +Create `backend/modal_app.py` (lightweight gateway, no policyengine): + +```python +import modal +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel app = modal.App("TOOL_NAME") -image = modal.Image.debian_slim().pip_install("policyengine-us==X.Y.Z") - -@app.function(image=image, timeout=300) -@modal.web_endpoint(method="POST") -def calculate(params: dict): - from policyengine_us import Simulation - # Build household from params - sim = Simulation(situation=household) - return {"result": float(sim.calculate("variable_name", 2025).sum())} +gateway_image = modal.Image.debian_slim(python_version="3.11").pip_install("fastapi", "pydantic") +WORKER_APP = "TOOL_NAME-workers" +FUNCTION_MAP = {"calculate": "compute"} + +web_app = FastAPI() +web_app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) + +@web_app.post("/submit/{endpoint}") +def submit(endpoint: str, params: dict): + if endpoint not in FUNCTION_MAP: + raise HTTPException(status_code=404, detail=f"Unknown endpoint: {endpoint}") + fn = modal.Function.from_name(WORKER_APP, FUNCTION_MAP[endpoint]) + call = fn.spawn(params) + return {"job_id": call.object_id} + +@web_app.get("/status/{job_id}") +def status(job_id: str): + from modal.functions import FunctionCall + call = FunctionCall.from_id(job_id) + try: + result = call.get(timeout=0) + return {"status": "ok", "result": result} + except TimeoutError: + return {"status": "computing"} + except Exception as e: + return {"status": "error", "message": str(e)} + +@app.function(image=gateway_image) +@modal.asgi_app() +def fastapi_app(): + return web_app ``` -And `lib/api.js`: +And `lib/api.js` (polling client): ```js const API_URL = process.env.NEXT_PUBLIC_API_URL || - "https://policyengine--TOOL_NAME-calculate.modal.run"; + "https://policyengine--TOOL_NAME-fastapi-app.modal.run"; -export async function calculate(params) { - const res = await fetch(API_URL, { +export async function submitJob(endpoint, params) { + const res = await fetch(`${API_URL}/submit/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); - if (!res.ok) throw new Error(`API error: ${res.status}`); - return res.json(); + if (!res.ok) throw new Error(`Submit failed: ${res.status}`); + const data = await res.json(); + return data.job_id; +} + +export async function pollStatus(jobId) { + const res = await fetch(`${API_URL}/status/${jobId}`); + if (!res.ok) throw new Error(`Status check failed: ${res.status}`); + return res.json(); // { status: "computing" | "ok" | "error", result?, message? } } ``` @@ -247,12 +314,15 @@ vercel link --scope policy-engine vercel --prod --yes ``` -If using Pattern C (Modal): +If using Pattern C (Modal gateway + worker): ```bash unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET -modal deploy modal_app.py +# Deploy worker first (includes image snapshot — first build takes ~5 min) +modal deploy backend/app.py +# Deploy gateway (lightweight job submission/polling) +modal deploy backend/modal_app.py vercel env add NEXT_PUBLIC_API_URL production -# Enter the Modal URL +# Enter: https://policyengine--TOOL_NAME-fastapi-app.modal.run vercel --prod --force --yes --scope policy-engine ``` diff --git a/skills/documentation/policyengine-design-skill/SKILL.md b/skills/documentation/policyengine-design-skill/SKILL.md index d59b34b..26d558f 100644 --- a/skills/documentation/policyengine-design-skill/SKILL.md +++ b/skills/documentation/policyengine-design-skill/SKILL.md @@ -2,114 +2,104 @@ name: policyengine-design description: | PolicyEngine design system — tokens, typography, colors, charts, and branding for all project types. - Triggers: "brand colors", "design tokens", "PolicyEngine colors", "typography", "font", "color palette", "CSS variables", "pe-color", "design system", "branding guidelines" + Triggers: "brand colors", "design tokens", "PolicyEngine colors", "typography", "font", "color palette", "CSS variables", "design system", "branding guidelines" --- # PolicyEngine design system -Single source of truth for PolicyEngine's visual identity. All tokens live in `@policyengine/design-system`. Every project — app-v2, standalone tools, charts — should reference these tokens, never hardcode hex values. +Single source of truth for PolicyEngine's visual identity. Design tokens are defined as CSS custom properties in `@policyengine/ui-kit/theme.css`. Every frontend project imports this single CSS file. **When to use which format:** | Context | Approach | Example | |---------|----------|---------| -| **React/Next.js components** | CSS vars or TS imports | `style={{ color: "var(--pe-color-primary-500)" }}` or `colors.primary[500]` | -| **Recharts (SVG)** | Resolve CSS vars at render time via `getCssVar()` | `const teal = getCssVar("--pe-color-primary-500")` | -| **Tailwind** | Use `theme.extend.colors` mapped to CSS vars | `className="text-pe-primary-500"` | -| **Python charts (Plotly/matplotlib)** | Hex values with CSS var name in comment | `TEAL = "#319795" # --pe-color-primary-500` | +| **React components** | Tailwind semantic classes | `className="bg-primary text-foreground"` | +| **Brand palette** | Tailwind direct classes | `className="bg-teal-500 text-gray-600"` | +| **Recharts (SVG)** | CSS vars directly in fill/stroke | `fill="var(--chart-1)"` | +| **Inline styles** | CSS vars | `style={{ color: "var(--primary)" }}` | +| **Python (Plotly)** | Hex with CSS var comment | `TEAL = "#319795" # --chart-1` | | **`` tags, static HTML** | Hex values with CSS var name in comment | `content="#319795"` | Python has no CSS runtime, so hex values are acceptable — but always comment with the CSS var name so values stay traceable to the design system. -## The design system package +## The ui-kit theme **Install:** ```bash -bun install @policyengine/design-system +bun install @policyengine/ui-kit ``` -**Three export formats:** +**Import the theme CSS in your `globals.css`:** +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +``` -| Format | Import | Use case | -|--------|--------|----------| -| TypeScript | `import { colors } from '@policyengine/design-system/tokens'` | app-v2, any TS/JS project | -| JSON | `import tokens from '@policyengine/design-system/tokens.json'` | Python scripts, config files | -| CSS | `import '@policyengine/design-system/tokens.css'` | Standalone tools, plain HTML | +The first line enables Tailwind v4 utilities. The second provides all PE design tokens, `@theme` configuration, and base styles. Both are required — see `policyengine-ui-kit-consumer-skill` for details. -**CDN (no install):** -```html - -``` +**Source:** `PolicyEngine/policyengine-ui-kit/src/theme/tokens.css` -**Source:** `PolicyEngine/policyengine-app-v2/packages/design-system/` +The theme CSS has three layers: +1. **`:root`** — shadcn/ui semantic variables (`--primary`, `--background`, `--chart-1`, etc.) +2. **`@theme inline`** — Bridges `:root` vars to Tailwind utilities (`bg-primary`, `text-foreground`) +3. **`@theme`** — Brand palette (`bg-teal-500`, `text-gray-600`), font sizes, spacing, breakpoints ## Colors ### Primary — teal -| Token | Hex | Usage | -|-------|-----|-------| -| `primary.500` | `#319795` | **Main brand color** — buttons, links, active states | -| `primary.400` | `#38B2AC` | Lighter interactive elements | -| `primary.600` | `#2C7A7B` | Hover state | -| `primary.700` | `#285E61` | Active/pressed state | -| `primary.50` | `#E6FFFA` | Tinted backgrounds | -| `primary.800` | `#234E52` | Dark text on light teal | - -CSS: `var(--pe-color-primary-500)` through `var(--pe-color-primary-900)` - -### Gray - -| Token | Hex | Usage | -|-------|-----|-------| -| `gray.50` | `#F9FAFB` | Subtle backgrounds | -| `gray.100` | `#F2F4F7` | Card backgrounds | -| `gray.200` | `#E2E8F0` | Borders, dividers | -| `gray.500` | `#6B7280` | Secondary text | -| `gray.600` | `#4B5563` | Chart secondary series | -| `gray.700` | `#344054` | Dark UI text | - -### Blue (accent) - -| Token | Hex | Usage | -|-------|-----|-------| -| `blue.500` | `#0EA5E9` | Informational highlights | -| `blue.700` | `#026AA2` | Chart secondary series | - -### Semantic - -| Color | Hex | CSS variable | Usage | -|-------|-----|-------------|-------| -| Success | `#22C55E` | `--pe-color-success` | Positive changes, gains | -| Error | `#EF4444` | `--pe-color-error` | Negative changes, losses | -| Warning | `#FEC601` | `--pe-color-warning` | Cautions, alerts | -| Info | `#1890FF` | `--pe-color-info` | Informational | - -### Text - -| Token | Hex | CSS variable | -|-------|-----|-------------| -| `text.primary` | `#000000` | `--pe-color-text-primary` | -| `text.secondary` | `#5A5A5A` | `--pe-color-text-secondary` | -| `text.tertiary` | `#9CA3AF` | `--pe-color-text-tertiary` | - -### Background - -| Token | Hex | CSS variable | -|-------|-----|-------------| -| `background.primary` | `#FFFFFF` | `--pe-color-bg-primary` | -| `background.secondary` | `#F5F9FF` | `--pe-color-bg-secondary` | -| `background.tertiary` | `#F1F5F9` | `--pe-color-bg-tertiary` | - -### Legacy colors (deprecated) - -These appear in older projects. Migrate to the values above. - -| Old | Hex | Replacement | -|-----|-----|-------------| -| `TEAL_ACCENT` | `#319795` | `primary.500` (`#319795`) | -| `BLUE_PRIMARY` | `#026AA2` | `blue.700` (`#026AA2`) | -| `DARK_GRAY` | `#5A5A5A` | `text.secondary` (`#5A5A5A`) | +| Token | Hex | Tailwind class | Usage | +|-------|-----|---------------|-------| +| `teal-500` | `#319795` | `bg-teal-500` | **Main brand color** — charts, highlights | +| `teal-400` | `#38B2AC` | `bg-teal-400` | Lighter interactive elements | +| `teal-600` | `#2C7A7B` | `bg-teal-600` / `bg-primary` | Hover state, buttons | +| `teal-700` | `#285E61` | `bg-teal-700` | Active/pressed state | +| `teal-50` | `#E6FFFA` | `bg-teal-50` | Tinted backgrounds | +| `teal-800` | `#234E52` | `bg-teal-800` | Dark text on light teal | + +### Semantic (shadcn/ui) + +| Role | CSS variable | Tailwind class | Hex | +|------|-------------|---------------|-----| +| Primary | `--primary` | `bg-primary` | `#2C7A7B` | +| Background | `--background` | `bg-background` | `#FFFFFF` | +| Foreground | `--foreground` | `text-foreground` | `#000000` | +| Muted | `--muted` | `bg-muted` | `#F2F4F7` | +| Muted foreground | `--muted-foreground` | `text-muted-foreground` | `#6B7280` | +| Border | `--border` | `border-border` | `#E2E8F0` | +| Destructive | `--destructive` | `bg-destructive` | `#EF4444` | +| Card | `--card` | `bg-card` | `#FFFFFF` | +| Ring | `--ring` | `ring-ring` | `#319795` | + +### Charts + +| CSS variable | Tailwind class | Hex | Usage | +|-------------|---------------|-----|-------| +| `--chart-1` | `fill-chart-1` | `#319795` | Primary series (teal) | +| `--chart-2` | `fill-chart-2` | `#0EA5E9` | Secondary series (blue) | +| `--chart-3` | `fill-chart-3` | `#285E61` | Tertiary series (dark teal) | +| `--chart-4` | `fill-chart-4` | `#026AA2` | Quaternary series (dark blue) | +| `--chart-5` | `fill-chart-5` | `#6B7280` | Quinary series (gray) | + +### Additional semantic colors + +| Color | Hex | Tailwind class | +|-------|-----|---------------| +| Success | `#22C55E` | `text-success` / `bg-success` | +| Error | `#EF4444` | `text-destructive` / `bg-destructive` | +| Warning | `#FEC601` | `text-warning` / `bg-warning` | +| Info | `#1890FF` | `text-info` / `bg-info` | + +### Gray scale + +| Token | Hex | Tailwind class | +|-------|-----|---------------| +| `gray-50` | `#F9FAFB` | `bg-gray-50` | +| `gray-100` | `#F2F4F7` | `bg-gray-100` | +| `gray-200` | `#E2E8F0` | `bg-gray-200` | +| `gray-500` | `#6B7280` | `text-gray-500` | +| `gray-600` | `#4B5563` | `text-gray-600` | +| `gray-700` | `#344054` | `text-gray-700` | ## Typography @@ -117,12 +107,10 @@ These appear in older projects. Migrate to the values above. **Two font families only: Inter + JetBrains Mono.** No serif fonts, no Roboto, no Public Sans. -| Context | Font | CSS variable | -|---------|------|-------------| -| **Everything** (UI, charts, blog, tools) | Inter | `--pe-font-family-primary` | -| **Code** | JetBrains Mono | `--pe-font-family-mono` | - -Legacy aliases (`--pe-font-family-chart`, `--pe-font-family-body`, `--pe-font-family-prose`, `--pe-font-family-secondary`) all resolve to Inter for backward compatibility. +| Context | Font | CSS variable | Tailwind | +|---------|------|-------------|----------| +| **Everything** (UI, charts, blog, tools) | Inter | `--font-sans` | `font-sans` | +| **Code** | JetBrains Mono | `--font-mono` | `font-mono` | **Loading Inter:** ```html @@ -131,15 +119,15 @@ Legacy aliases (`--pe-font-family-chart`, `--pe-font-family-body`, `--pe-font-fa ### Font sizes -| Token | Size | Usage | -|-------|------|-------| -| `xs` | 12px | Small labels, captions | -| `sm` | 14px | Body text, form labels | -| `base` | 16px | Large body text | -| `lg` | 18px | Subheadings | -| `xl` | 20px | Section titles | -| `2xl` | 24px | Page titles | -| `3xl` | 28px | Large headings | +| Tailwind class | Size | Usage | +|---------------|------|-------| +| `text-xs` | 12px | Small labels, captions | +| `text-sm` | 14px | Body text, form labels | +| `text-base` | 16px | Large body text | +| `text-lg` | 18px | Subheadings | +| `text-xl` | 20px | Section titles | +| `text-2xl` | 24px | Page titles | +| `text-3xl` | 28px | Large headings | ### Sentence case @@ -151,34 +139,45 @@ All UI text uses sentence case — capitalize only the first word and proper nou ## Spacing -| Token | Value | CSS variable | -|-------|-------|-------------| -| `xs` | 4px | `--pe-space-xs` | -| `sm` | 8px | `--pe-space-sm` | -| `md` | 12px | `--pe-space-md` | -| `lg` | 16px | `--pe-space-lg` | -| `xl` | 20px | `--pe-space-xl` | -| `2xl` | 24px | `--pe-space-2xl` | -| `3xl` | 32px | `--pe-space-3xl` | -| `4xl` | 48px | `--pe-space-4xl` | +Standard Tailwind spacing classes (`p-4`, `gap-2`, `m-6`) use the default Tailwind scale. Named spacing tokens: + +| Token | Value | Tailwind class | +|-------|-------|---------------| +| Header | 58px | `h-header` | +| Sidebar | 280px | `w-sidebar` | +| Content | 976px | `max-w-content` | ### Border radius -| Token | Value | CSS variable | -|-------|-------|-------------| -| `sm` | 4px | `--pe-radius-sm` | -| `md` | 6px | `--pe-radius-md` | -| `lg` | 8px | `--pe-radius-lg` | +| Tailwind class | Value | +|---------------|-------| +| `rounded-sm` | 4px | +| `rounded-md` | 6px | +| `rounded-lg` | 8px | ## Chart branding +### Recharts (React tools) + +```tsx +import { BarChart, Bar, XAxis, YAxis, Tooltip } from "recharts"; + + + + + + + +``` + +SVG `fill` and `stroke` attributes accept `var()` directly — no helper function needed. + ### Plotly (Python) ```python import plotly.graph_objects as go -# Import from tokens.json or hardcode -TEAL = "#319795" +TEAL = "#319795" # --chart-1 CHART_FONT = "Inter" LOGO_URL = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app-v2/main/app/public/assets/logos/policyengine/teal.png" @@ -203,35 +202,37 @@ def format_fig(fig): return fig ``` -### Recharts (React standalone tools) - -```jsx -import { BarChart, Bar, XAxis, YAxis, Tooltip } from "recharts"; - - - - - - - -``` - ### Chart color conventions -| Meaning | Color | Hex | -|---------|-------|-----| -| Positive / bonus / gains | Teal | `#319795` | -| Negative / penalty / losses | Gray or red | `#4B5563` or `#EF4444` | -| Neutral / baseline | Light gray | `#E2E8F0` | -| Multi-series | Series array | `#319795`, `#0EA5E9`, `#285E61`, `#026AA2`, `#6B7280` | +| Meaning | CSS variable | Hex | +|---------|-------------|-----| +| Positive / bonus / gains | `--chart-1` | `#319795` | +| Negative / penalty / losses | `--chart-5` or `--destructive` | `#6B7280` or `#EF4444` | +| Neutral / baseline | `--border` | `#E2E8F0` | +| Multi-series | `--chart-1` through `--chart-5` | See chart table above | **Inverted metrics (taxes):** When a positive delta means bad (higher taxes), use `invertDelta` logic to show "Penalty" label and swap colors. ### Chart typography -- **Axis labels and titles:** Inter, 14px -- **Tick labels:** Inter, 12px -- **Legend:** Inter, horizontal, above chart +- **Axis labels and titles:** `var(--font-sans)`, 14px +- **Tick labels:** `var(--font-sans)`, 12px +- **Legend:** `var(--font-sans)`, horizontal, above chart + +## Favicon + +Every PolicyEngine dashboard must include a favicon. The ui-kit exports the logo as a favicon-ready SVG: + +1. Copy: `cp node_modules/@policyengine/ui-kit/src/assets/logos/policyengine/teal-square.svg public/favicon.svg` +2. Add to `layout.tsx` metadata: + ```tsx + export const metadata: Metadata = { + // ... + icons: { icon: '/favicon.svg' }, + }; + ``` + +The ui-kit also exports `logos.favicon` (SVG) and `logos.faviconPng` (PNG fallback) for programmatic use. ## Logos @@ -253,16 +254,16 @@ https://raw.githubusercontent.com/PolicyEngine/policyengine-app-v2/main/app/publ | Project type | Token source | Font setup | |-------------|-------------|------------| +| **Standalone tool** | `@import "@policyengine/ui-kit/theme.css"` | Google Fonts: Inter | | **app-v2** | `import { colors } from '@/designTokens'` | Built-in (Mantine + Inter) | -| **Standalone tool** | `@import tokens.css` or CDN link | Google Fonts: Inter | -| **Python chart** | Hardcode or load `tokens.json` | Inter for Plotly | +| **Python chart** | Hardcode or load `tokens.json` from `@policyengine/design-system` | Inter for Plotly | | **Blog HTML** | Hardcode from token values | Google Fonts: Inter | ## Accessibility - Teal `#319795` on white passes WCAG AA for large text (3.8:1) -- `text.primary` (`#000000`) on white passes AAA (21:1) -- `text.secondary` (`#5A5A5A`) on white passes AA (7.4:1) +- `text-foreground` (`#000000`) on white passes AAA (21:1) +- `text-muted-foreground` (`#6B7280`) on white passes AA (4.6:1) - Never rely on color alone — use labels, patterns, or position to convey meaning - Ensure chart data series are distinguishable in grayscale diff --git a/skills/frontend/policyengine-tailwind-shadcn-skill/SKILL.md b/skills/frontend/policyengine-tailwind-shadcn-skill/SKILL.md new file mode 100644 index 0000000..8051cc6 --- /dev/null +++ b/skills/frontend/policyengine-tailwind-shadcn-skill/SKILL.md @@ -0,0 +1,223 @@ +--- +name: policyengine-tailwind-shadcn +description: | + Tailwind CSS v4 + shadcn/ui integration patterns for PolicyEngine frontend projects. + Covers @theme namespaces, CSS variable conventions, SVG var() usage, and common mistakes. + Triggers: "Tailwind v4", "@theme", "shadcn", "CSS variables", "design tokens CSS", "theme.css", "@theme inline" +--- + +# Tailwind CSS v4 + shadcn/ui integration + +Technical reference for how PolicyEngine's CSS-first design token architecture works. This skill explains the **mechanism** — how Tailwind v4 and shadcn/ui consume CSS variables. For the actual **token values** (colors, fonts, spacing), see `policyengine-design-skill`. + +## Architecture overview + +PolicyEngine uses a single CSS file (`@policyengine/ui-kit/theme.css`) as the source of truth for all design tokens. This file has three layers: + +``` +Layer 1: :root { --primary: #2C7A7B; } ← Raw values (shadcn/ui convention) +Layer 2: @theme inline { --color-primary: var(--primary); } ← Bridge to Tailwind +Layer 3: @theme { --color-teal-500: #319795; } ← Brand palette + fonts + sizes +``` + +Consumers import it in their `globals.css`: +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +``` + +## Tailwind v4 `@theme` namespaces + +Tailwind v4 uses CSS custom properties in `@theme` blocks to generate utility classes. The **namespace prefix** determines which utilities are created: + +| CSS variable prefix | Tailwind utility | Example variable | Example class | +|---|---|---|---| +| `--color-*` | `bg-*`, `text-*`, `border-*`, `fill-*` | `--color-primary: #2C7A7B` | `bg-primary`, `text-primary` | +| `--color-teal-*` | `bg-teal-*`, `text-teal-*` | `--color-teal-500: #319795` | `bg-teal-500` | +| `--text-*` | `text-*` (font size) | `--text-sm: 14px` | `text-sm` | +| `--font-*` | `font-*` | `--font-sans: Inter, ...` | `font-sans` | +| `--radius-*` | `rounded-*` | `--radius-lg: 8px` | `rounded-lg` | +| `--spacing-*` | `p-*`, `m-*`, `gap-*`, `w-*`, `h-*` | `--spacing-header: 58px` | `h-header` | +| `--breakpoint-*` | `sm:`, `md:`, `lg:` | `--breakpoint-md: 62rem` | `md:flex-col` | + +**Source:** [Tailwind v4 Theme docs](https://tailwindcss.com/docs/theme) + +## `@theme` vs `@theme inline` + +```css +/* @theme — bakes values directly into generated CSS */ +@theme { + --color-teal-500: #319795; /* Resolved at build time */ +} + +/* @theme inline — preserves var() for runtime resolution */ +@theme inline { + --color-primary: var(--primary); /* Resolved at runtime via :root */ +} +``` + +**When to use `@theme inline`:** +- When the value references a `:root` CSS variable (`var(--something)`) +- Required for dark mode (`:root` values change, Tailwind must re-resolve) +- Used for all shadcn/ui semantic tokens (primary, background, foreground, etc.) + +**When to use `@theme`:** +- When the value is a static literal (`#319795`, `14px`, `Inter`) +- Used for brand palette colors, font sizes, spacing, breakpoints + +**Source:** [Tailwind v4 Functions and Directives](https://tailwindcss.com/docs/functions-and-directives) + +## shadcn/ui CSS variable convention + +shadcn/ui defines semantic tokens as **unprefixed** CSS variables in `:root`: + +```css +:root { + --primary: #2C7A7B; + --background: #FFFFFF; + --foreground: #000000; + --muted: #F2F4F7; + --muted-foreground: #6B7280; + --border: #E2E8F0; + --chart-1: #319795; + --chart-2: #0EA5E9; + /* ... */ +} +``` + +These are then bridged to Tailwind via `@theme inline`: + +```css +@theme inline { + --color-primary: var(--primary); /* → bg-primary, text-primary */ + --color-background: var(--background); /* → bg-background */ + --color-foreground: var(--foreground); /* → text-foreground */ + --color-chart-1: var(--chart-1); /* → fill-chart-1 */ +} +``` + +**Source:** [shadcn/ui Theming](https://ui.shadcn.com/docs/theming), [shadcn/ui Tailwind v4 guide](https://ui.shadcn.com/docs/tailwind-v4) + +## SVG `var()` in Recharts + +Modern browsers resolve CSS custom properties in SVG presentation attributes. Recharts `fill` and `stroke` props accept `var()` directly: + +```tsx + + + +``` + +No helper function needed. The old `getCssVar()` / `getComputedStyle()` pattern is unnecessary. + +**Source:** [shadcn/ui Charts](https://ui.shadcn.com/docs/components/chart) + +## Complete namespace reference + +### Layer 1: `:root` (shadcn/ui semantic) + +| Variable | Hex | Usage | +|----------|-----|-------| +| `--primary` | `#2C7A7B` | Primary actions, buttons | +| `--primary-foreground` | `#FFFFFF` | Text on primary | +| `--background` | `#FFFFFF` | Page background | +| `--foreground` | `#000000` | Body text | +| `--muted` | `#F2F4F7` | Muted backgrounds | +| `--muted-foreground` | `#6B7280` | Secondary text | +| `--border` | `#E2E8F0` | Borders, dividers | +| `--card` | `#FFFFFF` | Card backgrounds | +| `--destructive` | `#EF4444` | Error states | +| `--ring` | `#319795` | Focus rings | +| `--chart-1` through `--chart-5` | Teal→Gray | Chart series | + +### Layer 2: `@theme inline` (bridges) + +Maps each `:root` var to Tailwind's `--color-*` namespace. Example: +- `--color-primary: var(--primary)` → enables `bg-primary`, `text-primary` +- `--color-chart-1: var(--chart-1)` → enables `fill-chart-1` + +### Layer 3: `@theme` (brand palette) + +| Namespace | Example | Tailwind class | +|-----------|---------|---------------| +| `--color-teal-*` | `--color-teal-500: #319795` | `bg-teal-500` | +| `--color-gray-*` | `--color-gray-600: #4B5563` | `text-gray-600` | +| `--color-blue-*` | `--color-blue-500: #0EA5E9` | `bg-blue-500` | +| `--text-*` | `--text-sm: 14px` | `text-sm` | +| `--font-*` | `--font-sans: Inter, ...` | `font-sans` | +| `--spacing-*` | `--spacing-header: 58px` | `h-header` | + +## Common mistakes + +### 1. Using `@theme` instead of `@theme inline` for var() references + +```css +/* WRONG — var() won't resolve, Tailwind bakes the literal string */ +@theme { + --color-primary: var(--primary); +} + +/* CORRECT — var() resolves at runtime */ +@theme inline { + --color-primary: var(--primary); +} +``` + +### 2. Wrong namespace prefix + +```css +/* WRONG — creates a utility called "primary", not a color */ +@theme { + --primary: #2C7A7B; +} + +/* CORRECT — --color-* prefix creates bg-primary, text-primary */ +@theme inline { + --color-primary: var(--primary); +} +``` + +### 3. Using getCssVar() for Recharts + +```tsx +// WRONG — unnecessary helper +const color = getCssVar('--chart-1'); + + +// CORRECT — SVG accepts var() directly + +``` + +### 4. Hardcoding hex in components + +```tsx +// WRONG +
+ +// CORRECT — use Tailwind class +
+ +// CORRECT — use CSS var for inline styles +
+``` + +### 5. Creating tailwind.config.ts + +Tailwind v4 does NOT use `tailwind.config.ts`. All configuration is in CSS via `@theme` blocks. The ui-kit theme CSS file handles this. + +### 6. Using old pe-* prefixed classes + +```tsx +// WRONG — old convention +
+ +// CORRECT — standard Tailwind/shadcn classes +
+``` + +## Related skills + +- `policyengine-design-skill` — Token values, color tables, chart branding +- `policyengine-frontend-builder-spec-skill` — Mandatory technology requirements +- `policyengine-recharts-skill` — Chart-specific patterns +- `policyengine-interactive-tools-skill` — Standalone tool scaffolding diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md new file mode 100644 index 0000000..8e7021b --- /dev/null +++ b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md @@ -0,0 +1,246 @@ +--- +name: policyengine-ui-kit-consumer +description: | + This skill should be used when setting up a new project that uses @policyengine/ui-kit, + debugging CSS or styling issues in a consumer app, or when Tailwind utility classes are not + being generated. Also use when creating globals.css, configuring PostCSS, or troubleshooting + "no styles", "no spacing", or "no layout" problems. + Triggers: "ui-kit import", "globals.css setup", "Tailwind not working", "styles not applying", + "utility classes missing", "setup ui-kit", "PostCSS config", "no styling", "CSS broken", + "import ui-kit", "theme.css", "no layout", "no spacing", "@tailwindcss/postcss" +--- + +# Consuming @policyengine/ui-kit + +How to correctly import and use the PolicyEngine UI kit's design system in any consumer application. This skill covers the required setup, the correct import order, and common mistakes that cause styling to break. + +## Required Consumer Setup + +Every app using `@policyengine/ui-kit` needs exactly three things: + +### 1. Install dependencies + +```bash +bun add @policyengine/ui-kit +bun add -D @tailwindcss/postcss postcss +``` + +### 2. Create `postcss.config.mjs` + +```js +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +No other PostCSS plugins needed — `@tailwindcss/postcss` handles imports, vendor prefixes, and nesting internally. + +### 3. Create `app/globals.css` with two imports + +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +``` + +**Both lines are required. The order matters.** Tailwind must come first because the ui-kit's `@theme` blocks extend it. + +This provides: +- All Tailwind v4 utility classes (`flex`, `grid`, `p-4`, `text-sm`, etc.) +- All PolicyEngine design tokens (colors, fonts, spacing, breakpoints) +- shadcn/ui semantic tokens (`bg-primary`, `text-foreground`, `border-border`) +- Brand palette (`bg-teal-500`, `text-gray-600`, `bg-blue-500`) +- Base element styles (body font, border defaults, slider styling) + +## How It Works + +Understanding the flow prevents debugging confusion: + +1. The consumer's build tool (Next.js/Vite) processes `globals.css` through `@tailwindcss/postcss` +2. `@import "tailwindcss"` establishes the cascade layers and enables utility class generation +3. Tailwind's automatic source detection scans from `process.cwd()` (the consumer's project root) — this is why the consumer's utility classes get generated +4. `@import "@policyengine/ui-kit/theme.css"` is inlined by Tailwind's import bundler +5. The ui-kit's `@theme` and `@theme inline` blocks merge into the consumer's Tailwind build +6. The ui-kit's `@source` directive tells Tailwind to also scan the ui-kit's own component files +7. The ui-kit's `@layer base` styles apply within the existing cascade + +## What NOT to Do + +### Do NOT skip the Tailwind import + +```css +/* WRONG — utility classes will not be generated */ +@import "@policyengine/ui-kit/theme.css"; +``` + +```css +/* CORRECT */ +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +``` + +Without `@import "tailwindcss"`, there is no Tailwind build. The ui-kit's `@theme` blocks have nothing to extend. No utility classes (`flex`, `p-4`, `grid`) will exist. + +### Do NOT add a duplicate Tailwind import + +```css +/* WRONG — double Tailwind causes conflicting resets and broken styles */ +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +@import "tailwindcss"; +``` + +The ui-kit does NOT contain `@import "tailwindcss"` inside it. One import at the top of `globals.css` is all that's needed. + +### Do NOT create tailwind.config.ts + +``` +/* WRONG — Tailwind v4 does not use JavaScript config */ +tailwind.config.ts ← DELETE THIS +``` + +Tailwind v4 is CSS-first. All configuration comes from `@theme` blocks in the ui-kit's theme CSS. There is no `content` array, no `theme.extend`, no JavaScript config. + +### Do NOT add postcss-import or autoprefixer + +```js +/* WRONG — these conflict with @tailwindcss/postcss */ +export default { + plugins: { + "postcss-import": {}, + "@tailwindcss/postcss": {}, + "autoprefixer": {}, + }, +}; +``` + +```js +/* CORRECT — @tailwindcss/postcss handles both internally */ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +### Do NOT put `@import "tailwindcss"` inside the ui-kit package + +If working on the ui-kit itself, never add `@import "tailwindcss"` to `tokens.css`. The consumer owns that import. See `tailwind-design-system-authoring` skill for details. + +### Do NOT hardcode hex colors or font names + +```tsx +/* WRONG */ +
+ +/* CORRECT — use Tailwind classes */ +
+ +/* CORRECT — use CSS variables for inline styles */ +
+``` + +## Troubleshooting + +### "No styles at all" — page is unstyled + +1. Verify `globals.css` has `@import "tailwindcss"` as the first line +2. Verify `postcss.config.mjs` exists with `@tailwindcss/postcss` +3. Verify `@tailwindcss/postcss` and `postcss` are installed as devDependencies +4. Verify `globals.css` is imported in `app/layout.tsx` (or `pages/_app.tsx`) + +### "Tokens load but no utility classes" — colors work but no flex/grid/padding + +This means `@theme` tokens are being processed but Tailwind's utility generation isn't scanning files correctly. + +**If missing classes are from the consumer's own components** (`app/`, `components/`): +1. Verify `@import "tailwindcss"` comes BEFORE the ui-kit import (order matters) +2. Check that `process.cwd()` is the project root when the build runs +3. If in a monorepo, add `source()` to the import: `@import "tailwindcss" source("./src")` + +**If missing classes are from ui-kit components** (`DashboardShell`, `Header`, `InputPanel`, etc.): +The ui-kit's `@source` directive in `tokens.css` may not match the actual directory structure. This is a ui-kit-side fix — the `@source` glob must cover all directories containing `.tsx` files with `className=` attributes. See the `tailwind-design-system-authoring` skill for the verification procedure. + +### "Double styling / Tailwind defaults override tokens" + +This means Tailwind is being imported twice. + +1. Check that the ui-kit's `tokens.css` does NOT contain `@import "tailwindcss"` +2. Check that `globals.css` has only ONE `@import "tailwindcss"` line +3. Check for other CSS files that might import Tailwind + +### "Utility classes from ui-kit components missing" + +The ui-kit ships `@source` directives to tell Tailwind to scan its components. If this fails: + +1. Add a manual `@source` in `globals.css`: + ```css + @import "tailwindcss"; + @import "@policyengine/ui-kit/theme.css"; + @source "../node_modules/@policyengine/ui-kit/src"; + ``` +2. If using `bun link` (symlinked package), the path resolves differently — check the actual resolved path + +## Framework-Specific Notes + +### Next.js 14 (App Router) + +Standard setup. Requires `@tailwindcss/postcss` in PostCSS config. + +``` +app/ + globals.css ← @import "tailwindcss"; @import ui-kit theme + layout.tsx ← import "./globals.css"; +postcss.config.mjs +``` + +### Next.js 15+ / Next.js 16 + +Same setup. Turbopack processes PostCSS normally. No changes needed. + +### Vite (non-Next.js) + +Use `@tailwindcss/vite` instead of `@tailwindcss/postcss`: + +```ts +// vite.config.ts +import tailwindcss from '@tailwindcss/vite' +export default defineConfig({ plugins: [tailwindcss()] }) +``` + +No `postcss.config.mjs` needed — the Vite plugin handles everything. + +`globals.css` is the same two imports. + +## Quick Reference + +| What | Where | Content | +|------|-------|---------| +| PostCSS config | `postcss.config.mjs` | `{ plugins: { "@tailwindcss/postcss": {} } }` | +| Entry CSS | `app/globals.css` | `@import "tailwindcss"; @import "@policyengine/ui-kit/theme.css";` | +| Dependencies | `package.json` devDeps | `@tailwindcss/postcss`, `postcss` | +| Dependencies | `package.json` deps | `@policyengine/ui-kit` | + +## What the Theme Provides + +After the two-line import, these are available: + +| Category | Examples | Source | +|----------|---------|--------| +| Semantic colors | `bg-primary`, `text-foreground`, `border-border` | `:root` + `@theme inline` | +| Brand palette | `bg-teal-500`, `text-gray-600`, `bg-blue-500` | `@theme` | +| Status colors | `text-success`, `bg-warning`, `text-error` | `@theme` | +| Chart colors | `fill-chart-1` through `fill-chart-5` | `:root` + `@theme inline` | +| Typography | `text-sm` (14px), `text-base` (16px), `font-sans` | `@theme` | +| Spacing | `h-header` (58px), `w-sidebar` (280px), `max-w-content` (976px) | `@theme` | +| Breakpoints | `xs:`, `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | `@theme` | +| Radius | `rounded-sm` (4px), `rounded-md` (6px), `rounded-lg` (8px) | `@theme inline` | +| All Tailwind utilities | `flex`, `grid`, `p-4`, `gap-2`, `hidden`, etc. | `@import "tailwindcss"` | + +## Related Skills + +- `policyengine-design-skill` — Full token reference (hex values, usage guidelines) +- `policyengine-tailwind-shadcn-skill` — `@theme` namespace mechanics, SVG var() usage +- `policyengine-interactive-tools-skill` — Full tool scaffolding checklist +- `policyengine-vercel-deployment-skill` — Deploying consumer apps diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/references/migration-from-broken-setup.md b/skills/frontend/policyengine-ui-kit-consumer-skill/references/migration-from-broken-setup.md new file mode 100644 index 0000000..79ed348 --- /dev/null +++ b/skills/frontend/policyengine-ui-kit-consumer-skill/references/migration-from-broken-setup.md @@ -0,0 +1,127 @@ +# Migrating from a Broken Tailwind + ui-kit Setup + +Step-by-step guide for fixing projects where the ui-kit was imported incorrectly and styles are broken. + +## Symptoms of a Broken Setup + +| Symptom | Likely Cause | +|---------|-------------| +| No styles at all (unstyled HTML) | Missing `@import "tailwindcss"` or missing PostCSS config | +| Colors load but no layout (no flex/grid/padding) | `@import "tailwindcss"` is inside the ui-kit, not in consumer's CSS | +| Tailwind defaults override PE tokens | Double `@import "tailwindcss"` (once in consumer, once in ui-kit) | +| Correct on first load, wrong after hot reload | PostCSS processing order issue — check import order | +| Works with Tailwind CLI but not with Next.js | Missing `@tailwindcss/postcss` in PostCSS config | + +## Fix Procedure + +### Step 1: Verify the ui-kit's tokens.css + +Read `node_modules/@policyengine/ui-kit/src/theme/tokens.css`. It must NOT contain `@import "tailwindcss"`. + +If it does, the ui-kit itself needs to be fixed (remove that line). If using a local/linked version, fix it there. + +### Step 2: Clean globals.css + +Replace the entire contents of `app/globals.css` with exactly: + +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +``` + +Remove any: +- Additional `@import "tailwindcss"` lines +- Manual `@theme` blocks that duplicate ui-kit tokens +- Manual `:root` blocks with PE colors +- `@tailwind base; @tailwind components; @tailwind utilities;` (v3 syntax) +- `@config` directives +- `postcss-import` related imports + +### Step 3: Verify postcss.config.mjs + +Replace with exactly: + +```js +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +Remove any: +- `postcss-import` (Tailwind handles imports) +- `autoprefixer` (Tailwind handles prefixing) +- `tailwindcss` (v3 plugin name — v4 uses `@tailwindcss/postcss`) +- `postcss-nested` (Tailwind uses Lightning CSS for nesting) + +### Step 4: Delete stale config files + +Remove if present: +- `tailwind.config.ts` / `tailwind.config.js` (v4 is CSS-first) +- `tailwind.config.cjs` / `tailwind.config.mjs` + +### Step 5: Verify dependencies + +```bash +# Required +bun add @policyengine/ui-kit +bun add -D @tailwindcss/postcss postcss + +# Remove stale deps if present +bun remove tailwindcss-animate # replaced by tw-animate-css (bundled in ui-kit) +bun remove postcss-import # handled by @tailwindcss/postcss +bun remove autoprefixer # handled by @tailwindcss/postcss +``` + +Note: `tailwindcss` itself should still be installed (it's a peer dep of `@tailwindcss/postcss`). + +### Step 6: Clear caches and restart + +```bash +rm -rf .next node_modules/.cache +bun dev +``` + +### Step 7: Verify + +Open the app in a browser. Check that: +1. Page has the correct background color (white, `--background`) +2. Text is in Inter font +3. Teal brand colors are present +4. Layout utilities work (`flex`, `grid`, `p-4`, `gap-2`) +5. No Tailwind default blue/indigo colors leaking through + +## If `@source` Scanning Fails + +If ui-kit component styles are missing (components render but with wrong styling), the `@source` directive inside the ui-kit's CSS may not be reaching the consumer's build. + +Add a manual fallback in `globals.css`: + +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; +@source "../node_modules/@policyengine/ui-kit/src"; +``` + +For `bun link`'d packages, find the real path: + +```bash +readlink -f node_modules/@policyengine/ui-kit +``` + +And use that absolute path or adjust the relative path accordingly. + +## History of This Issue + +The original problem: `tokens.css` in the ui-kit contained `@import "tailwindcss"`. This caused Tailwind's source detection to scan from the ui-kit's directory inside `node_modules` instead of the consumer's project. Design tokens loaded (because `@theme` blocks are processed regardless of scan directory), but utility classes for the consumer's components were not generated (because the scanner only found files inside the ui-kit package). + +Multiple fix attempts failed: +- Adding `@source` directives in the consumer — path resolution was fragile +- Inlining all tokens in the consumer's CSS — user rejected (ui-kit should own styling) +- Removing `@import "tailwindcss"` from tokens.css but not adding it to the consumer — no Tailwind at all + +The correct fix (confirmed by Tailwind maintainers and every major Tailwind-based library): +1. Remove `@import "tailwindcss"` from the library +2. Consumer writes `@import "tailwindcss"` first, then imports the library's theme +3. Library includes `@source` for its own components diff --git a/skills/technical-patterns/policyengine-recharts-skill/SKILL.md b/skills/technical-patterns/policyengine-recharts-skill/SKILL.md index 7b76e9c..62ec443 100644 --- a/skills/technical-patterns/policyengine-recharts-skill/SKILL.md +++ b/skills/technical-patterns/policyengine-recharts-skill/SKILL.md @@ -100,15 +100,13 @@ Recharts default tooltip separator is ` : ` (with leading space). Always set `se ## Standard chart template -Recharts renders to SVG, which cannot read CSS custom properties. Resolve design tokens at render time with a helper (see `policyengine-interactive-tools-skill` for the `getCssVar` utility): +SVG `fill` and `stroke` attributes accept `var()` directly -- no helper function is needed to resolve CSS custom properties. Use the shadcn/ui chart color variables (`--chart-1` through `--chart-5`) for series colors and standard semantic variables for UI elements: ```tsx -import { useMemo } from "react"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Label, ReferenceDot, } from "recharts"; -import getCssVar from "@/lib/getCssVar"; // reads CSS custom properties interface DataPoint { x: number; y: number; } @@ -116,14 +114,6 @@ export default function MyChart({ data, highlightX }: { data: DataPoint[]; highlightX?: number; }) { - // Resolve design tokens for SVG (never hardcode hex values) - const { primaryColor, darkTeal, gridColor, fontFamily } = useMemo(() => ({ - primaryColor: getCssVar("--pe-color-primary-500"), - darkTeal: getCssVar("--pe-color-primary-900"), - gridColor: getCssVar("--pe-color-gray-200"), - fontFamily: getCssVar("--pe-font-family-primary") || "Inter, sans-serif", - }), []); - const fmt = (v: number) => v.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }); @@ -141,27 +131,27 @@ export default function MyChart({ data, highlightX }: { return ( - + [fmt(v), "Value"]} /> - + {highlightPoint && ( + fill="var(--chart-3)" stroke="var(--chart-3)" /> )} @@ -182,28 +172,40 @@ export default function MyChart({ data, highlightX }: { ## PolicyEngine styling -Never hardcode hex colors in frontend chart code. Resolve from CSS custom properties at render time: +Never hardcode hex colors in frontend chart code. Use CSS custom properties directly via `var()` in SVG attributes: ```typescript -import getCssVar from "@/lib/getCssVar"; - -// Resolve design tokens once per render (useMemo in components) -const primaryColor = getCssVar("--pe-color-primary-500"); // Primary series -const darkTeal = getCssVar("--pe-color-primary-900"); // Reference dots -const gridColor = getCssVar("--pe-color-gray-200"); // Grid lines -const lightFill = getCssVar("--pe-color-primary-alpha-40"); // Light fill -const fontFamily = getCssVar("--pe-font-family-primary"); // Font - -// Tooltip +// Chart series colors (shadcn/ui chart palette) +// Use these for data series — lines, areas, bars, dots +// --chart-1 Primary series (first line/bar) +// --chart-2 Secondary series +// --chart-3 Tertiary series / reference dots +// --chart-4 Fourth series +// --chart-5 Fifth series + +// Semantic UI colors — use for chart chrome (grids, borders, backgrounds) +// --border Grid lines, axis lines +// --background Tooltip background +// --foreground Axis labels, tick text +// --primary Interactive UI elements (buttons, links) +// --font-sans Font family + +// Usage in JSX — pass var() directly to SVG attributes: + + + + + +// Tooltip style object const TOOLTIP_STYLE = { - background: "var(--pe-color-bg-primary)", - border: "1px solid var(--pe-color-border-light)", + background: "var(--background)", + border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", }; ``` -See `policyengine-design-skill` for the full token reference. The `getCssVar` helper is documented in `policyengine-interactive-tools-skill`. +See `policyengine-design-skill` for the full token reference. ## Key rules @@ -214,7 +216,7 @@ See `policyengine-design-skill` for the full token reference. The `getCssVar` he 5. **Always wrap in `ResponsiveContainer`** with explicit height 6. **Use `dot={false}`** on Line components for clean curves with many data points 7. **Use `ReferenceDot`** to highlight the user's current selection -8. **Use design tokens for chart colors** — resolve `--pe-color-primary-500` (teal) via `getCssVar`, never hardcode hex values +8. **Use CSS variables for chart colors** -- pass `var(--chart-1)` through `var(--chart-5)` directly to SVG `fill`/`stroke` attributes; never hardcode hex values 9. **Negative currency: sign before symbol** - Always format as `-$31`, never `$-31` ## Currency formatting @@ -231,4 +233,4 @@ const fmt = (v: number) => v.toLocaleString("en-US", { }); ``` -In policyengine-app-v2, use `formatParameterValue()` from `@/utils/chartValueUtils` or `formatCurrency()` from `@/utils/formatters` — both use `Intl.NumberFormat` internally. +In policyengine-app-v2, use `formatParameterValue()` from `@/utils/chartValueUtils` or `formatCurrency()` from `@/utils/formatters` -- both use `Intl.NumberFormat` internally. diff --git a/skills/tools-and-apis/policyengine-dashboard-workflow-skill/SKILL.md b/skills/tools-and-apis/policyengine-dashboard-workflow-skill/SKILL.md new file mode 100644 index 0000000..1aa9356 --- /dev/null +++ b/skills/tools-and-apis/policyengine-dashboard-workflow-skill/SKILL.md @@ -0,0 +1,243 @@ +--- +name: policyengine-dashboard-workflow +description: Reference for the /create-dashboard and /deploy-dashboard orchestrated AI workflow +--- + +# PolicyEngine Dashboard Workflow + +How to use the orchestrated AI workflow for creating PolicyEngine dashboards from natural-language descriptions. + +## Overview + +The dashboard workflow is a multi-agent pipeline that takes a few paragraphs describing a desired dashboard and produces a working, deployable application in a new GitHub repository. + +### Commands + +| Command | Purpose | +|---------|---------| +| `/create-dashboard` | Full pipeline: init repo → plan → scaffold → implement → validate → review → commit | +| `/deploy-dashboard` | Deploy a completed dashboard to Vercel (and optionally Modal) | +| `/dashboard-overview` | List all dashboard builder ecosystem components | + +### Agents + +| Agent | Phase | Role | +|-------|-------|------| +| `dashboard-planner` | 1 | Produces structured plan YAML from description | +| `dashboard-scaffold` | 2 | Generates Next.js + Tailwind project structure into the current repo | +| `backend-builder` | 3 | Builds API stubs or custom Modal backend | +| `frontend-builder` | 3 | Builds React components with Tailwind + ui-kit design tokens | +| `dashboard-integrator` | 4 | Wires frontend to backend, handles data flow | +| `dashboard-build-validator` | 5 | Runs build and test suite | +| `dashboard-design-validator` | 5 | Checks design tokens, typography, sentence case, responsive | +| `dashboard-architecture-validator` | 5 | Checks Tailwind v4, Next.js, ui-kit, package manager | +| `dashboard-plan-validator` | 5 | Checks API contract, components, embedding, states vs plan | +| `dashboard-overview-updater` | Post | Updates dashboard-overview command if ecosystem changed | + +## Workflow Phases + +``` +Phase 0: Init repo (or use existing with --repo/--skip-init) +Phase 1: Plan ──→ [HUMAN APPROVAL] ──→ Phase 2: Scaffold + quality gates + ──→ Phase 3: Implement (backend + frontend IN PARALLEL) + ──→ Phase 4: Integrate + ──→ Phase 5: Validate (4 validators in parallel) ──→ [fix loop, max 3 cycles] + ──→ Phase 6: [HUMAN REVIEW] ──→ commit and push + ──→ Phase 7: Update overview (silent) + +Separately: /deploy-dashboard (after user merges to main) +``` + +## Data Patterns + +### API v2 Alpha (default) + +The dashboard is built against the PolicyEngine API v2 alpha interface. During development, the backend-builder creates **typed stubs** that return fixture data matching the v2 alpha response shapes. + +**When v2 alpha alignment agent is built (future):** Stubs will be replaced with real API calls using the async job pattern: +1. `POST /endpoint` → returns `{ job_id, status }` +2. `GET /endpoint/{job_id}` → poll until `status: COMPLETED` +3. Extract `result` from completed response + +**Available v2 alpha endpoints (from DESIGN.md):** + +| Endpoint | Purpose | +|----------|---------| +| `POST /simulate/household` | Single household calculation | +| `POST /simulate/economy` | Population simulation | +| `POST /analysis/decile-impact/economy` | Income decile breakdown | +| `POST /analysis/budget-impact/economy` | Tax/benefit programme totals | +| `POST /analysis/winners-losers/economy` | Who gains and loses | +| `POST /analysis/compare/economy` | Multi-scenario comparison | +| `POST /analysis/compare/household` | Household scenario comparison | + +**Switching from stubs to real API:** Set `NEXT_PUBLIC_API_V2_URL` environment variable. The client code checks this and switches from fixture returns to real HTTP calls. + +### Custom Backend (escape hatch) + +Use only when the dashboard needs something v2 alpha cannot provide: +- Custom reform parameters not exposed by the API +- Non-standard entity structures +- Combining PolicyEngine with external models +- Microsimulation with custom reform configurations + +**Pattern:** FastAPI on Modal with `policyengine-us` or `policyengine-uk` packages. + +**The plan MUST document why v2 alpha is insufficient** before selecting this pattern. + +## Tech Stack (fixed) + +| Layer | Technology | Source | +|-------|-----------|--------| +| Framework | Next.js (App Router) + React 19 + TypeScript | Fixed | +| UI tokens | `@policyengine/ui-kit/theme.css` | Single CSS import | +| Styling | Tailwind CSS v4 with ui-kit theme | Fixed | +| Font | Inter (via `next/font/google`) | Fixed | +| Charts | Recharts | Following app-v2 patterns | +| Maps | react-plotly.js | Following app-v2 patterns | +| Data fetching | TanStack React Query | Fixed | +| Testing | Vitest + React Testing Library | Fixed | +| Deployment | Vercel (frontend) + Modal (backend) | Fixed | + +See `policyengine-frontend-builder-spec-skill` for the full mandatory technology specification. + +### Design Token Usage + +All visual values come from `@policyengine/ui-kit/theme.css`, accessed via Tailwind utility classes: + +```tsx +// Colors — use Tailwind semantic classes from ui-kit theme +
{/* Primary teal */} +
{/* Hover state */} + {/* Body text */} + {/* Muted text */} +
{/* Backgrounds */} +
{/* Borders */} + +// Spacing — standard Tailwind classes +
+ +// Typography (font sizes from ui-kit theme — use standard text-xs, text-sm, etc.) + + +// Border radius +
+``` + +**Never hardcode hex colors, pixel spacing, or font values.** The Phase 5 validators check for violations. + +### Chart Patterns + +Charts use CSS variables directly from the ui-kit theme: + +```tsx +// Standard Recharts pattern +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + + + + + + + + + + +``` + +## Plan YAML Schema + +The plan is the contract between the planner and all other agents: + +```yaml +dashboard: + name: string # kebab-case, becomes repo name + title: string # Human-readable title + description: string # One paragraph + country: string # us, uk, or both + audience: string # public, researchers, legislators, internal + +data_pattern: string # api-v2-alpha or custom-backend + +api_v2_integration: # Only if api-v2-alpha + endpoints_needed: [{ endpoint, purpose, variables_requested }] + stub_fixtures: [{ name, description, expected_outputs }] + +custom_backend: # Only if custom-backend + reason: string # WHY v2 alpha is insufficient + framework: string + policyengine_package: string + endpoints: [{ name, method, inputs, outputs, policyengine_variables }] + +tech_stack: # Fixed values, included for documentation + framework: react-nextjs + ui: "@policyengine/ui-kit" + styling: tailwind-with-ui-kit-theme + charts: recharts + testing: vitest + +components: # What to build + - type: input_form | chart | metric_card | data_table + id: string + # Type-specific fields... + +embedding: + register_in_apps_json: boolean + display_with_research: boolean + slug: string + tags: string[] + +tests: + api_tests: [{ name, description, input, expected }] + frontend_tests: [{ name, description }] + design_compliance: [{ name, description }] + embedding_tests: [{ name, description }] +``` + +## Embedding + +All dashboards are built to embed in policyengine.org via iframe: + +1. **Country detection:** Read `#country=` from URL hash +2. **Hash sync:** Update hash on input change, `postMessage` to parent +3. **Share URLs:** Point to `policyengine.org/{country}/{slug}`, not Vercel URL +4. **Country toggle:** Hidden when embedded (country comes from route) + +See `policyengine-interactive-tools-skill` for full embedding documentation. + +## Validation Checklist + +Phase 5 runs four validators in parallel: + +**Build validator:** Build compiles, all tests pass. + +**Design validator:** No hardcoded colors/spacing/fonts, no `pe-*` classes, no `getCssVar`, Inter font loaded, sentence case headings, responsive at 768px and 480px, chart `ResponsiveContainer` wrappers. + +**Architecture validator:** Tailwind CSS v4 (`@import "tailwindcss"`, no config files), Next.js App Router (no Vite, no Pages Router), `@policyengine/ui-kit` installed and imported, bun as package manager. + +**Plan validator:** API contract matches plan, all plan components implemented, embedding features (country detection, hash sync, share URLs), loading and error states handled. + +## Deployment + +After the user merges the feature branch to `main`: + +```bash +/deploy-dashboard +``` + +This handles: +- Vercel frontend deployment +- Modal backend deployment (if custom-backend) +- Registration in policyengine-app-v2's apps.json (via PR) +- Smoke testing + +## Future: API v2 Alpha Alignment + +When the API v2 alpha is production-ready, an alignment agent will: +1. Read the plan's `api_v2_integration` section +2. Replace stub functions in `client.ts` with real v2 alpha HTTP calls +3. Implement the async job polling pattern +4. Run the full validation suite against live API responses +5. Update fixture data with real response shapes + +This is a planned future addition, not yet implemented. diff --git a/skills/tools-and-apis/policyengine-frontend-builder-spec-skill/SKILL.md b/skills/tools-and-apis/policyengine-frontend-builder-spec-skill/SKILL.md new file mode 100644 index 0000000..21ae0b1 --- /dev/null +++ b/skills/tools-and-apis/policyengine-frontend-builder-spec-skill/SKILL.md @@ -0,0 +1,311 @@ +--- +name: policyengine-frontend-builder-spec +description: Mandatory frontend technology requirements for PolicyEngine dashboards and interactive tools — Tailwind CSS v4, Next.js (App Router), @policyengine/ui-kit theme, Vercel deployment +--- + +# Frontend builder spec + +Authoritative specification for all PolicyEngine frontend projects (dashboards and interactive tools). Any agent building or validating a frontend MUST load this skill and follow every requirement below. Where another agent's instructions conflict with this spec, **this spec wins**. + +## Mandatory requirements + +### 1. Tailwind CSS (v4+) + +The application MUST use **Tailwind CSS v4** for all styling. Tailwind utility classes are the primary styling mechanism. + +- MUST install `tailwindcss` (v4+) +- MUST have a `globals.css` containing: + ```css + @import "tailwindcss"; + @import "@policyengine/ui-kit/theme.css"; + ``` +- MUST NOT have a `tailwind.config.ts` or `tailwind.config.js` — Tailwind v4 uses `@theme` in CSS instead +- MUST NOT have a `postcss.config.js` or `postcss.config.mjs` — Tailwind v4 does not require PostCSS +- MUST NOT use `@tailwind base; @tailwind components; @tailwind utilities;` — use `@import "tailwindcss"` instead +- MUST NOT use plain CSS files or CSS modules (`*.module.css`) for layout or styling +- MUST NOT use other CSS-in-JS libraries (styled-components, emotion, vanilla-extract) +- MUST NOT use other component frameworks for styling (Mantine, Chakra UI, Material UI) +- The only CSS files allowed are `globals.css` (which imports ui-kit theme) + +### 2. @policyengine/ui-kit (component library + theme) + +The application MUST install `@policyengine/ui-kit` and use it as the primary component library and design token source. **MUST use ui-kit components when an equivalent exists** — do NOT rebuild components that ui-kit already provides. + +- MUST install: `bun add @policyengine/ui-kit` +- MUST import theme in `globals.css`: `@import "@policyengine/ui-kit/theme.css";` +- MUST use ui-kit components for all standard UI patterns (see availability table below) +- MAY build custom components only when no ui-kit equivalent exists + +**Component availability table:** + +| Dashboard need | ui-kit component | +|---|---| +| Page shell | `DashboardShell` | +| Header with logo + nav | `Header` (light/dark variants, `navLinks` prop) | +| Two-column layout | `SidebarLayout` + `InputPanel` + `ResultsPanel` | +| Single-column narrative | `SingleColumnLayout` | +| Buttons | `Button` (4 variants, 3 sizes) | +| Cards | `Card`, `CardHeader`, `CardTitle`, `CardContent`, `CardFooter` | +| Badges | `Badge` (6 variants) | +| Tab navigation | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | +| Currency input | `CurrencyInput` | +| Number input | `NumberInput` | +| Select dropdown | `SelectInput` | +| Checkbox | `CheckboxInput` | +| Slider | `SliderInput` | +| Input grouping | `InputGroup` | +| KPI display | `MetricCard` (currency/percent, trends) | +| Summary text | `SummaryText` | +| Data tables | `DataTable` | +| Charts | `ChartContainer`, `PEBarChart`, `PELineChart`, `PEAreaChart`, `PEWaterfallChart` | +| Branding | `PolicyEngineWatermark`, `logos.*` | +| Utilities | `formatCurrency`, `formatPercent`, `formatNumber` | + +**Component precedence rule:** When building UI: +1. **First**: Use `@policyengine/ui-kit` if it has the component +2. **Second**: Use [shadcn/ui](https://ui.shadcn.com) primitives (Dialog, Popover, Tooltip, Select, DropdownMenu, etc.) styled with Tailwind semantic classes +3. **Third**: Build custom from scratch with Tailwind utility classes + +### 3. Design tokens via ui-kit theme + +The application MUST load design tokens from `@policyengine/ui-kit/theme.css`. This single CSS import provides all colors, spacing, typography, and chart tokens. + +- MUST import theme in `globals.css`: `@import "@policyengine/ui-kit/theme.css";` +- MUST NOT load tokens via CDN `` — the theme is bundled with ui-kit +- MUST NOT hardcode hex color values when a design token exists +- MUST NOT hardcode pixel spacing values when a Tailwind spacing class exists +- MUST NOT hardcode font-family values — use `var(--font-sans)` +- MAY use custom values when no token covers the need (e.g., chart-specific dimensions, animation durations) + +**Token usage patterns:** + +| Context | Approach | Example | +|---------|----------|---------| +| React components | Tailwind semantic classes | `className="bg-primary text-foreground"` | +| Brand palette | Tailwind direct classes | `className="bg-teal-500 text-gray-600"` | +| Recharts (SVG) | CSS vars directly in fill/stroke | `fill="var(--chart-1)"` | +| Inline styles | CSS vars | `style={{ color: "var(--primary)" }}` | + +### 4. Framework: Next.js (App Router) + +The application MUST use **Next.js with the App Router**. + +- MUST use `create-next-app` or equivalent to scaffold with App Router +- MUST have `next.config.ts` at the project root +- MUST have an `app/` directory with `layout.tsx` and `page.tsx` +- MUST use TypeScript (`.ts`/`.tsx` files, `tsconfig.json`) +- MUST NOT use the Pages Router (`pages/` directory) +- MUST NOT use Vite as the application bundler (Vite is only used by Vitest for testing) +- MUST NOT use other bundlers (Webpack, Parcel, esbuild, etc.) +- MUST NOT use other meta-frameworks (Remix, Gatsby, Astro, etc.) + +### 5. Package manager: bun + +The application MUST use **bun** as the package manager. + +- MUST use `bun install` instead of `npm install` +- MUST use `bun run dev`, `bun run build` instead of `npm run dev`, `npm run build` +- MUST use `bunx vitest run` instead of `npx vitest run` +- MUST have a `bun.lock` lockfile (not `package-lock.json`) +- MUST NOT use npm, yarn, or pnpm + +### 6. Vercel deployment + +The application MUST be deployed using **Vercel**. + +- MUST have a `vercel.json` at the project root with appropriate configuration +- MUST use `output: 'export'` in `next.config.ts` for static export, unless the dashboard requires server-side rendering +- MUST configure the Vercel project to build from the repository root (not a subdirectory) +- MUST set any required environment variables in the Vercel project settings using the `NEXT_PUBLIC_*` prefix +- MUST deploy under the `policy-engine` Vercel scope +- MUST NOT deploy using other hosting platforms (Netlify, AWS Amplify, GitHub Pages, etc.) for the frontend + +### 7. shadcn/ui for custom components + +When building custom components not available in `@policyengine/ui-kit`, the application SHOULD use [shadcn/ui](https://ui.shadcn.com) primitives as the base layer. + +- SHOULD initialize shadcn/ui: `bunx shadcn@latest init` +- SHOULD use shadcn/ui for: Dialog, Popover, Tooltip, Select, DropdownMenu, Accordion, Sheet, and other interaction primitives +- MUST style shadcn/ui components with Tailwind semantic classes (the ui-kit theme already defines shadcn/ui semantic tokens like `background`, `foreground`, `primary`, `muted`) +- MUST NOT use shadcn/ui when an equivalent `@policyengine/ui-kit` component exists + +## Tailwind v4 + ui-kit theme integration pattern + +### globals.css + +```css +@import "tailwindcss"; +@import "@policyengine/ui-kit/theme.css"; + +body { + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +``` + +The single `@import "@policyengine/ui-kit/theme.css"` provides: +1. **`:root` variables** — shadcn/ui semantic tokens (`--primary`, `--background`, `--chart-1`, etc.) +2. **`@theme inline`** — Bridges `:root` vars to Tailwind utilities (`bg-primary`, `text-foreground`) +3. **`@theme`** — Brand palette (`bg-teal-500`, `text-gray-600`), font sizes, spacing, breakpoints + +### Next.js: app/layout.tsx + +```tsx +import './globals.css' +import { Inter } from 'next/font/google' +import type { Metadata } from 'next' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'DASHBOARD_TITLE - PolicyEngine', + description: 'DASHBOARD_DESCRIPTION', + icons: { icon: '/favicon.svg' }, +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} +``` + +### Usage in components + +```tsx +// Prefer ui-kit components: +import { MetricCard, Button, Card, CardContent } from '@policyengine/ui-kit'; +import { formatCurrency } from '@policyengine/ui-kit'; + +// Use Tailwind classes with semantic and brand tokens for custom layouts: +
+ Metric title + {formatCurrency(1234)} +
+ +// Brand colors when needed: +
Primary teal
+ +// Responsive design uses Tailwind breakpoint prefixes: +
+
+ {/* sidebar collapses at md breakpoint */} +
+
+ +// Recharts uses CSS vars directly: + + + + +// Header with logo and structured nav links: +import { Header, logos } from '@policyengine/ui-kit'; + +
} + navLinks={[ + { slug: 'research', text: 'Research', href: 'https://policyengine.org/research' }, + ]} +> + Dashboard Title +
+``` + +## Project structure + +``` +DASHBOARD_NAME/ +├── app/ +│ ├── layout.tsx # Root layout — Inter font + globals.css +│ ├── page.tsx # Main dashboard page +│ ├── globals.css # @import "tailwindcss" + @import ui-kit theme +│ └── providers.tsx # React Query provider (client component) +├── components/ +│ └── (from plan.yaml) # Custom dashboard components (only if not in ui-kit) +├── lib/ +│ ├── api/ +│ │ ├── client.ts # API client (stubs or real) +│ │ ├── types.ts # Request/response TypeScript types +│ │ └── fixtures.ts # Mock data for stubs +│ ├── embedding.ts # Country detection, hash sync, share URLs +│ └── hooks/ +│ └── useCalculation.ts # React Query hooks +├── public/ +├── next.config.ts +├── tsconfig.json +├── package.json +├── vitest.config.ts +├── plan.yaml +├── CLAUDE.md +├── README.md +├── vercel.json +└── .gitignore +``` + +## Package dependencies + +**Production:** +- `next` +- `react`, `react-dom` +- `tailwindcss` (v4+) +- `@policyengine/ui-kit` +- `@tanstack/react-query` +- `recharts` (if custom charts beyond ui-kit) +- `react-plotly.js`, `plotly.js-dist-min` (if maps) +- `axios` + +**Development:** +- `typescript`, `@types/react`, `@types/react-dom`, `@types/node` +- `vitest`, `@vitejs/plugin-react`, `jsdom` +- `@testing-library/react`, `@testing-library/jest-dom` + +## Testing + +Vitest is the test runner. Configure `vitest.config.ts`: + +```ts +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./vitest.setup.ts'], + }, +}) +``` + +Note: `@vitejs/plugin-react` is only used by Vitest for JSX transform during testing — Vite is NOT used as the application bundler. + +## What NOT to do + +- MUST NOT use Vite as the application bundler — only Next.js is allowed (Vite is used only by Vitest for testing) +- MUST NOT use other bundlers (Webpack, Parcel, esbuild) +- MUST NOT use other meta-frameworks (Remix, Gatsby, Astro) +- MUST NOT use the Next.js Pages Router — use App Router only +- MUST NOT have `tailwind.config.ts` or `postcss.config.js` — Tailwind v4 uses `@theme` in CSS +- MUST NOT use `@tailwind base; @tailwind components; @tailwind utilities;` — use `@import "tailwindcss"` +- MUST NOT use plain CSS files or CSS modules for layout/styling +- MUST NOT use styled-components, emotion, or vanilla-extract +- MUST NOT use Mantine, Chakra UI, Material UI, or other component frameworks for styling +- MUST NOT hardcode hex color values when a design token exists +- MUST NOT hardcode pixel spacing values when a Tailwind spacing class exists +- MUST NOT hardcode font-family values — use `var(--font-sans)` via Tailwind +- MUST NOT deploy to platforms other than Vercel +- MUST NOT use npm, yarn, or pnpm — use bun +- MUST NOT rebuild components that exist in `@policyengine/ui-kit` +- MUST NOT use `getCssVar()` — it no longer exists. SVG accepts `var()` directly. + +## Related skills + +- **policyengine-design-skill** — Complete design token reference (colors, typography, spacing, chart branding) +- **policyengine-interactive-tools-skill** — Embedding, hash sync, country detection patterns +- **policyengine-app-skill** — app-v2 component architecture reference diff --git a/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md b/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md index 1658744..fc8c3fd 100644 --- a/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md +++ b/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md @@ -23,7 +23,7 @@ How to build standalone React apps (calculators, dashboards, visualizations) tha | Component | Choice | |-----------|--------| | Framework | Next.js 14 (App Router) | -| CSS | Tailwind 4 with `@theme` mapping PE tokens | +| CSS | Tailwind 4 with `@policyengine/ui-kit` theme | | Charts | Recharts | | Code highlighting | Prism React Renderer | | Testing | Vitest | @@ -31,10 +31,10 @@ How to build standalone React apps (calculators, dashboards, visualizations) tha | Package manager | `bun` (not npm) | **Requirements:** -- `@policyengine/design-system` tokens (CDN link in `layout.jsx`) +- `@policyengine/ui-kit` theme (installed via `bun add @policyengine/ui-kit`) - Inter font via Google Fonts CDN - Recharts for charts -- **NEVER hardcode hex colors or font names** — always use `var(--pe-color-*)` and `var(--pe-font-family-primary)` +- **NEVER hardcode hex colors or font names** — always use CSS variables from the ui-kit theme (e.g., `var(--primary)`, `var(--chart-1)`, `var(--font-sans)`) - **PolicyEngine logo** — always use the actual logo image, never styled text. Files at `policyengine-app-v2/app/public/assets/logos/policyengine/` (white.png for dark backgrounds, teal.png for light) - Sentence case on all UI text @@ -152,73 +152,266 @@ export async function calculateHousehold(countryId, household) { **Pros:** Always up-to-date with latest policy rules, handles arbitrary inputs. **Cons:** Network latency (1-5s per call), rate limits, limited to variables the API supports. -### Pattern C: Custom API on Modal +### Pattern C: Custom API on Modal (gateway + polling) Best when you need variables or calculations not in the main PolicyEngine API — custom reform parameters, non-standard entity structures, or computations that combine PolicyEngine with other models. +> **Decision rule:** Before choosing Pattern C, verify that the PolicyEngine API +> (`api.policyengine.org`) cannot handle the computation. Pattern C is only needed when: +> - You need microsimulation (society-wide) results +> - You need custom reform parameters not exposed by the API +> - You need variables or entity structures not supported by the API +> +> If the tool only needs household-level calculations, Pattern B (PolicyEngine API) is +> always preferred — it's faster, always up-to-date, and requires no backend maintenance. + **When to use:** Tools that vary parameters not exposed by the main API (e.g., varying UBI amounts, custom phase-outs), or tools that need microsimulation (society-wide) results for arbitrary reforms. +**Architecture:** Two-layer gateway + worker with frontend polling. This mirrors the pattern used by PolicyEngine API v1 and API v2. + ``` -┌───────────┐ ┌──────────────────┐ ┌──────────────┐ -│ Next.js │───>│ Modal serverless │───>│ policyengine │ -│ (browser) │<───│ Python function │<───│ -us (local) │ -└───────────┘ └──────────────────┘ └──────────────┘ +┌───────────┐ POST /submit ┌──────────────────┐ spawn() ┌──────────────┐ +│ Next.js │──────────────>│ Gateway (FastAPI) │─────────>│ Worker │ +│ (browser) │ │ (lightweight) │ │ (policyengine)│ +│ │ GET /status │ │ poll │ │ +│ │<──────────────│ │<─────────│ │ +└───────────┘ {status,data} └──────────────────┘ └──────────────┘ ``` -**Example:** GiveCalc lets users specify custom giving amounts and calculates the tax benefit using policyengine-us with custom reform parameters. +**Why not synchronous HTTP?** Modal's dev gateway (`modal serve`) and production gateway have a ~150s timeout. Long-running requests (like US statewide microsimulations, which take 2-5+ minutes) get an HTTP 303 redirect that browser `fetch()` cannot follow for POST requests. The gateway + polling architecture avoids this entirely. + +#### Why three files? + +The backend uses a **three-file structure** mirroring policyengine-api-v2's simulation service. This prevents a common crash-loop where module-level imports of pydantic or policyengine fail because those packages are only available inside the Modal function's image, not at module import time. + +| File | Purpose | Module-level imports | +|------|---------|---------------------| +| `backend/_image_setup.py` | Standalone snapshot function — runs during image build | None (all inside function body) | +| `backend/app.py` | Modal app + function decorators | Only `modal` | +| `backend/simulation.py` | Pure business logic | `policyengine_us`/`_uk` (captured in image snapshot) | +| `backend/modal_app.py` | Lightweight gateway (FastAPI) | `modal`, `fastapi`, `pydantic` | + +#### Image setup (`backend/_image_setup.py`) + +Standalone function with **no package imports at module level** — executed during image build via `.run_function()`: + +```python +def snapshot_models(): + """Pre-load models at image build time for fast cold starts.""" + import logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + logger.info("Pre-loading tax-benefit system...") + from policyengine_us import CountryTaxBenefitSystem # or policyengine_uk + CountryTaxBenefitSystem() + logger.info("Models pre-loaded into image snapshot") +``` + +#### Worker app (`backend/app.py`) + +Only `modal` at module level. Imports business logic **inside each function body**: ```python -# modal_app.py import modal +from pathlib import Path +from _image_setup import snapshot_models -app = modal.App("my-tool") -image = modal.Image.debian_slim().pip_install("policyengine-us==1.x.x") - -@app.function(image=image, timeout=300) -@modal.web_endpoint(method="POST") -def calculate(params: dict): - from policyengine_us import Simulation - household = params["household"] - sim = Simulation(situation=household) +app = modal.App("my-tool-workers") +_BACKEND_DIR = Path(__file__).parent +image = ( + modal.Image.debian_slim(python_version="3.11") + .pip_install("policyengine-us==X.Y.Z", "pydantic") # Pin to latest — look up from PyPI + .run_function(snapshot_models) + .add_local_file(str(_BACKEND_DIR / "simulation.py"), remote_path="/root/simulation.py") +) + +@app.function(image=image, cpu=8.0, memory=32768, timeout=3600) +def compute_household(params: dict) -> dict: + from simulation import run_household + return run_household(params) + +@app.function(image=image, cpu=8.0, memory=32768, timeout=3600) +def compute_statewide(params: dict) -> dict: + from simulation import run_statewide + return run_statewide(params) +``` + +#### Simulation logic (`backend/simulation.py`) + +Pure business logic — **policyengine imports at module level** (captured in the image snapshot via `.run_function()`). No Modal imports here. + +```python +from policyengine_us import Simulation, Microsimulation # Snapshotted at build time + +def run_household(params: dict) -> dict: + sim = Simulation(situation=params["household"]) return { "net_income": float(sim.calculate("household_net_income", 2025).sum()), } -``` -**Deploy:** -```bash -# MUST unset env vars — keychain tokens override modal profile -unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET -modal deploy modal_app.py +def run_statewide(params: dict) -> dict: + baseline = Microsimulation() + reform = Microsimulation(reform=params["reform"]) + # ... compute impacts + return {"revenue_change": ..., "winners": ..., "losers": ...} ``` -**URL pattern:** `https://policyengine--my-tool-calculate.modal.run` +#### Gateway (`backend/modal_app.py`) -**Frontend:** -```js -const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://policyengine--my-tool-calculate.modal.run"; +The gateway is lightweight — no policyengine dependency. It spawns worker jobs and polls for results: + +```python +import modal +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel -async function calculate(params) { - const res = await fetch(API_URL, { +app = modal.App("my-tool") + +gateway_image = modal.Image.debian_slim(python_version="3.11").pip_install( + "fastapi", "pydantic", +) + +WORKER_APP = "my-tool-workers" + +FUNCTION_MAP = { + "household-impact": "compute_household", + "statewide-impact": "compute_statewide", +} + +web_app = FastAPI() +web_app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) + +class SubmitResponse(BaseModel): + job_id: str + +class StatusResponse(BaseModel): + status: str # "computing" | "ok" | "error" + result: dict | None = None + message: str | None = None + +@web_app.post("/submit/{endpoint}") +def submit(endpoint: str, params: dict): + if endpoint not in FUNCTION_MAP: + raise HTTPException(status_code=404, detail=f"Unknown endpoint: {endpoint}") + fn = modal.Function.from_name(WORKER_APP, FUNCTION_MAP[endpoint]) + call = fn.spawn(params) + return SubmitResponse(job_id=call.object_id) + +@web_app.get("/status/{job_id}") +def status(job_id: str): + from modal.functions import FunctionCall + call = FunctionCall.from_id(job_id) + try: + result = call.get(timeout=0) + return StatusResponse(status="ok", result=result) + except TimeoutError: + return StatusResponse(status="computing") + except Exception as e: + return StatusResponse(status="error", message=str(e)) + +@app.function(image=gateway_image) +@modal.asgi_app() +def fastapi_app(): + return web_app +``` + +#### Frontend polling client + +```typescript +const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://policyengine--my-tool-fastapi-app.modal.run"; + +export async function submitJob(endpoint: string, params: unknown): Promise { + const res = await fetch(`${API_URL}/submit/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); - return res.json(); + if (!res.ok) throw new Error(`Submit failed: ${res.status}`); + const data = await res.json(); + return data.job_id; +} + +export async function pollStatus(jobId: string) { + const res = await fetch(`${API_URL}/status/${jobId}`); + if (!res.ok) throw new Error(`Status check failed: ${res.status}`); + return res.json(); // { status: "computing" | "ok" | "error", result?, message? } } ``` +#### React Query polling hook + +```typescript +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { submitJob, pollStatus } from "../api/client"; + +export function useAsyncCalculation(queryKey: unknown[], endpoint: string, params: unknown, enabled = true) { + const [jobId, setJobId] = useState(null); + + // Step 1: Submit job when params change + const submit = useQuery({ + queryKey: [...queryKey, "submit"], + queryFn: async () => { + const id = await submitJob(endpoint, params); + setJobId(id); + return id; + }, + enabled, + }); + + // Step 2: Poll for results + const poll = useQuery({ + queryKey: [...queryKey, "poll", jobId], + queryFn: () => pollStatus(jobId!), + enabled: !!jobId, + refetchInterval: (query) => + query.state.data?.status === "computing" ? 2000 : false, + }); + + return { + isLoading: submit.isLoading || (!!jobId && poll.isLoading), + isComputing: poll.data?.status === "computing", + isError: submit.isError || poll.data?.status === "error", + data: poll.data?.status === "ok" ? poll.data.result : undefined, + error: poll.data?.message || submit.error?.message, + }; +} +``` + +**Deploy:** +```bash +# Deploy the worker functions first (includes image snapshot — first build takes ~5 min) +unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET +modal deploy backend/app.py + +# Deploy the gateway +modal deploy backend/modal_app.py +``` + +**URL pattern:** `https://policyengine--my-tool-fastapi-app.modal.run` + **Set Vercel env var:** ```bash vercel env add NEXT_PUBLIC_API_URL production -# Enter: https://policyengine--my-tool-calculate.modal.run +# Enter: https://policyengine--my-tool-fastapi-app.modal.run vercel --prod --force --yes --scope policy-engine ``` -**Pros:** Full control over calculations, can use any policyengine variables/reforms, can do microsimulation. **Cons:** Cold starts (5-15s first call), Modal costs, must pin policyengine version, must redeploy when policy rules update. +**Pros:** Full control over calculations, can use any policyengine variables/reforms, can do microsimulation, no timeout issues. **Cons:** Fast cold starts (~2s thanks to model pre-loading via `.run_function()`; without snapshot, cold starts take 3-5 minutes), Modal costs, must pin policyengine version, must redeploy when policy rules update, more complex architecture (four files). **Failure mode:** Modal apps can silently disappear. If frontend gets network errors, `curl` the Modal URL — if 404, redeploy. +#### Modal timeout reference + +| Context | Default timeout | Max timeout | Notes | +|---------|----------------|-------------|-------| +| `@app.function(timeout=...)` | 300s | 86,400s (24h) | Set per-function | +| `modal serve` dev gateway | ~150s | Not configurable | Returns HTTP 303 on timeout | +| `modal deploy` prod gateway | ~150s | Not configurable | Returns HTTP 303 on timeout | + +**US statewide microsimulations take 2-5+ minutes.** This exceeds the gateway timeout, which is why synchronous HTTP calls fail for microsimulation endpoints. The gateway + polling architecture avoids this by using non-blocking job submission. Household-level simulations typically complete in 10-40s, within the gateway timeout, but polling is still recommended for consistency. + ### Pattern D: Precomputed CSV dashboard For analysis repos that precompute data with Python microsimulation pipelines: @@ -240,7 +433,7 @@ For analysis repos that precompute data with Python microsimulation pipelines: ```bash bunx create-next-app@14 my-tool --js --app --tailwind --eslint --no-src-dir --import-alias "@/*" cd my-tool -bun add @policyengine/design-system recharts +bun add @policyengine/ui-kit recharts bun add -D vitest ``` @@ -258,10 +451,6 @@ export default function RootLayout({ children }) { return ( - ` in ``. The `@import` from `node_modules` does not work with the Next.js CSS pipeline. - -### app/globals.css — map PE tokens into Tailwind `@theme` +### app/globals.css — import ui-kit theme ```css @import "tailwindcss"; - -@theme { - --color-pe-primary-50: var(--pe-color-primary-50); - --color-pe-primary-500: var(--pe-color-primary-500); - --color-pe-primary-600: var(--pe-color-primary-600); - --color-pe-primary-700: var(--pe-color-primary-700); - - --color-pe-gray-50: var(--pe-color-gray-50); - --color-pe-gray-100: var(--pe-color-gray-100); - --color-pe-gray-200: var(--pe-color-gray-200); - - --color-pe-error: var(--pe-color-error); - - --color-pe-bg-primary: var(--pe-color-bg-primary); - --color-pe-text-primary: var(--pe-color-text-primary); - --color-pe-text-secondary: var(--pe-color-text-secondary); - --color-pe-text-tertiary: var(--pe-color-text-tertiary); - - --color-pe-border-light: var(--pe-color-border-light); -} +@import "@policyengine/ui-kit/theme.css"; body { - font-family: var(--pe-font-family-primary); - color: var(--pe-color-text-primary); - background: var(--pe-color-bg-primary); + font-family: var(--font-sans); + color: var(--foreground); + background: var(--background); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ``` -### Using PE tokens in components +The single `@import "@policyengine/ui-kit/theme.css"` replaces the entire manual `@theme` block. It provides all color, spacing, and typography tokens as CSS variables that Tailwind 4 picks up automatically. + +### Using tokens in components -Use `style=` with `var()` for dynamic PE token values: +Use Tailwind classes from the ui-kit theme: ```jsx -
+
``` -Or use Tailwind classes that reference the `@theme` mappings: +Or use `style=` with `var()` for inline styles: ```jsx -
+
``` ## Embedding in policyengine.org @@ -429,36 +599,22 @@ bun add recharts **For simple visualizations:** Use SVG directly. The marriage calculator uses hand-rolled SVG heatmaps. **Color conventions:** -- Positive/bonus: `var(--pe-color-primary-500)` -- Negative/penalty: `var(--pe-color-gray-600)` or `var(--pe-color-error)` -- Neutral: `var(--pe-color-gray-200)` +- Positive/bonus: `var(--chart-1)` +- Negative/penalty: `var(--chart-3)` or `var(--destructive)` +- Neutral: `var(--border)` **Inverted metrics (taxes):** When positive delta means bad (more taxes), pass `invertDelta` to your chart component to flip labels and colors. -### Recharts + PE tokens +### Recharts + ui-kit tokens -Recharts renders SVG, which **cannot inherit CSS custom properties** via `style=`. You must resolve token values at render time: +Recharts accepts CSS variables directly via `fill` and `stroke` props: ```jsx -/* Helper to read PE tokens for Recharts SVG props */ -function getCssVar(name) { - if (typeof window === "undefined") return ""; - return getComputedStyle(document.documentElement) - .getPropertyValue(name) - .trim(); -} - -// In your chart component: -const primaryColor = getCssVar("--pe-color-primary-500"); -const errorColor = getCssVar("--pe-color-error"); -const gridColor = getCssVar("--pe-color-border-light"); -const fontFamily = getCssVar("--pe-font-family-primary"); - - - - - + + + + ``` @@ -471,7 +627,7 @@ const fontFamily = getCssVar("--pe-font-family-primary"); tickFormatter={(v) => v < 0 ? `-$${Math.abs(v)}` : `$${v}`} ``` -**Never pass hardcoded hex values** like `fill="#319795"` to Recharts — always resolve from CSS variables. +**Never pass hardcoded hex values** like `fill="#319795"` to Recharts — always use CSS variables (e.g., `fill="var(--chart-1)"`). ## Code highlighting @@ -513,12 +669,12 @@ Test API responses against Python fixtures for numerical accuracy. See `PolicyEn ## Checklist for new tools - [ ] Next.js 14 + Tailwind 4 scaffold -- [ ] `@policyengine/design-system` tokens loaded via CDN `` in layout.jsx -- [ ] PE tokens mapped in `globals.css` `@theme` block +- [ ] `@policyengine/ui-kit` installed (`bun add @policyengine/ui-kit`) +- [ ] `@import "@policyengine/ui-kit/theme.css"` in `globals.css` - [ ] Inter font loaded via Google Fonts CDN -- [ ] **Zero hardcoded hex colors** — all colors via `var(--pe-color-*)` -- [ ] **Zero hardcoded font names** — all fonts via `var(--pe-font-family-primary)` -- [ ] Recharts charts use `getCssVar()` helper for SVG props (font, colors) +- [ ] **Use Tailwind classes from ui-kit theme** — no hardcoded hex colors +- [ ] **Zero hardcoded font names** — all fonts via `var(--font-sans)` +- [ ] Recharts charts use `fill="var(--chart-1)"` pattern for SVG props (font, colors) - [ ] Recharts axes use `niceTicks` with `domain={["auto", "auto"]}` for human-friendly tick values - [ ] Negative dollar values formatted as `-$100` not `$-100` - [ ] PE logo is an actual image, not styled text diff --git a/skills/tools-and-apis/policyengine-modal-deployment-skill/SKILL.md b/skills/tools-and-apis/policyengine-modal-deployment-skill/SKILL.md new file mode 100644 index 0000000..2208326 --- /dev/null +++ b/skills/tools-and-apis/policyengine-modal-deployment-skill/SKILL.md @@ -0,0 +1,320 @@ +--- +name: policyengine-modal-deployment +description: Deploying PolicyEngine backend APIs to Modal — workspace setup, authentication, deployment commands, environments, and troubleshooting +--- + +# PolicyEngine Modal deployment + +How to deploy PolicyEngine backend APIs (custom Modal backends for dashboards and interactive tools) to Modal under the PolicyEngine organizational workspace. + +**This skill applies only when a dashboard or tool uses the `custom-backend` data pattern.** If the project uses `api-v2-alpha` (stub data or direct API calls), no Modal deployment is needed. + +## Workspace + +PolicyEngine uses a shared Modal workspace called `policyengine`. All backend deployments MUST target this workspace — never a personal workspace. + +### Environments + +The `policyengine` workspace has three environments: + +| Environment | Web suffix | URL pattern | Purpose | +|-------------|-----------|-------------|---------| +| `main` | _(empty)_ | `policyengine---.modal.run` | Production | +| `staging` | `staging` | `policyengine-staging---.modal.run` | Pre-production testing | +| `testing` | `testing` | `policyengine-testing---.modal.run` | Development/CI | + +Default to `main` for production deployments. + +## Authentication + +### Prerequisites + +1. A Modal account linked to the PolicyEngine workspace (ask a workspace owner for an invite) +2. The Modal CLI installed: `pip install modal` +3. A token for the `policyengine` workspace stored in a local profile + +### Setting up authentication + +```bash +# Create a token for the policyengine workspace (opens browser) +modal token new --profile policyengine + +# Activate the profile +modal profile activate policyengine + +# Verify — must show "Workspace: policyengine" +modal token info +``` + +### Verifying authentication before deploy + +**HUMAN GATE:** Before any deployment, verify the active workspace: + +```bash +modal token info +modal profile list +``` + +Expected output from `modal profile list`: +``` +┏━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓ +┃ ┃ Profile ┃ Workspace ┃ +┡━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩ +│ • │ policyengine │ policyengine │ +└───┴──────────────┴──────────────┘ +``` + +The `•` indicates the active profile. If `policyengine` is not active or not present: + +> **Authentication required.** Your Modal CLI is not configured for the `policyengine` workspace. +> +> Run: +> ```bash +> modal token new --profile policyengine +> modal profile activate policyengine +> ``` +> +> If you don't have access to the PolicyEngine workspace, ask a workspace owner to invite you at https://modal.com/settings/policyengine + +**Do NOT proceed with deployment until `modal token info` shows `Workspace: policyengine`.** + +### Critical: environment variable override + +Modal CLI respects `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` environment variables, which **override** the profile. If these are set (e.g., from a CI environment), the CLI will deploy to whatever workspace those tokens belong to — potentially a personal workspace. + +**Always unset before deploying:** +```bash +unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET +``` + +## App structure + +### Naming convention + +The Modal app name MUST match the dashboard/tool repo name in kebab-case: + +```python +app = modal.App("my-dashboard-name") +``` + +This keeps the app name consistent with the GitHub repo and Vercel project. + +### Image and dependencies + +Build a container image with the required Python packages: + +```python +image = ( + modal.Image.debian_slim(python_version="3.12") + .pip_install( + "policyengine-us>=1.155.0", # Pin minimum version + "fastapi[standard]", + "numpy", + "pandas", + ) + .env({"NUMEXPR_MAX_THREADS": "4"}) + .add_local_dir("api", "/root/api") # Local source code + .add_local_file("config.yaml", "/root/config.yaml") # Config files +) +``` + +**Guidelines:** +- Use `debian_slim` with Python 3.12 or 3.13 +- Pin minimum versions for `policyengine-us` / `policyengine-uk` +- Use `.add_local_dir()` / `.add_local_file()` for project source code +- Use `.env()` for non-secret environment variables +- Set memory and timeout on the function, not the image + +### Web endpoint patterns + +#### Simple endpoint (single function) + +For tools with one calculation endpoint: + +```python +@app.function(image=image, timeout=300, memory=2048) +@modal.web_endpoint(method="POST") +def calculate(data: dict) -> dict: + from policyengine_us import Simulation + # Build simulation from data, return results + return {"result": value} +``` + +URL: `https://policyengine---calculate.modal.run` + +#### Full FastAPI app (multiple endpoints) + +For dashboards with multiple API routes: + +```python +@app.function(image=image, timeout=300, memory=2048) +@modal.concurrent(max_inputs=100) +@modal.asgi_app() +def fastapi_app(): + from api.main import app as api + return api +``` + +URL: `https://policyengine---fastapi-app.modal.run` + +#### Health check endpoint + +Every Modal backend SHOULD include a health check: + +```python +@app.function(image=image) +@modal.web_endpoint(method="GET") +def health(): + return {"status": "ok"} +``` + +### Function configuration + +| Parameter | Default | Recommended for PE | Purpose | +|-----------|---------|-------------------|---------| +| `timeout` | 60s | 300 | PolicyEngine simulations can take minutes | +| `memory` | 128MB | 2048 | PE models are memory-intensive | +| `@modal.concurrent(max_inputs=N)` | 1 | 100 | Handle concurrent requests without cold starts | + +### Secrets + +Store sensitive values as Modal Secrets (not in code or `.env` files): + +```bash +# Create a secret +modal secret create my-secret API_KEY=abc123 + +# List secrets +modal secret list +``` + +Reference in code: +```python +@app.function( + image=image, + secrets=[modal.Secret.from_name("my-secret")], +) +def my_function(): + import os + api_key = os.environ["API_KEY"] # Injected by Modal +``` + +Existing secrets in the `policyengine` workspace: +- `policyengine-logfire` — logging/observability +- `gcp-credentials` — Google Cloud access +- `huggingface-token` — HuggingFace model access +- `anthropic-api-key` — Anthropic API access + +## Deployment + +### Deploy command + +```bash +# 1. Ensure correct workspace +unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET +modal token info # Verify "Workspace: policyengine" + +# 2. Deploy to production +modal deploy modal_app.py --env main + +# 3. Deploy to staging (for testing) +modal deploy modal_app.py --env staging +``` + +**Flags:** +- `--env main` / `--env staging` / `--env testing` — target environment +- `--name TEXT` — override the deployment name (rarely needed) +- `--tag TEXT` — tag the deployment with a version string +- `--stream-logs` — stream container logs during deployment + +### Verify deployment + +```bash +# List deployed apps +modal app list --env main + +# Health check +curl -s -w "\n%{http_code}" https://policyengine--DASHBOARD_NAME-health.modal.run + +# Test the endpoint +curl -X POST https://policyengine--DASHBOARD_NAME-calculate.modal.run \ + -H "Content-Type: application/json" \ + -d '{"test": true}' +``` + +### Connecting to Vercel frontend + +After Modal deployment, set the API URL as an environment variable in the Vercel project: + +```bash +vercel env add NEXT_PUBLIC_API_URL production +# Enter: https://policyengine--DASHBOARD_NAME-calculate.modal.run +vercel --prod --force --yes --scope policy-engine +``` + +The `--force` flag is required to rebuild with the new environment variable. + +### Redeployment + +Redeploying an existing app is the same command — Modal handles zero-downtime transitions: +1. New containers build while old ones handle requests +2. New containers start accepting requests +3. Old containers finish in-flight requests then terminate + +```bash +modal deploy modal_app.py --env main +``` + +### Stopping an app + +**WARNING: This is destructive and irreversible.** A stopped app cannot be restarted; you must redeploy. + +```bash +modal app stop +``` + +## Monitoring + +```bash +# View logs for a deployed app +modal app logs + +# List all apps and their status +modal app list --env main +``` + +App states: +- `deployed` — running and accepting requests +- `ephemeral` — temporary (from `modal serve`) +- `stopped` — permanently stopped + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| `modal token info` shows wrong workspace | Wrong profile active | `modal profile activate policyengine` | +| Deploy goes to personal workspace | `MODAL_TOKEN_ID` env var set | `unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET` | +| 404 on endpoint URL | App stopped or never deployed | `modal deploy modal_app.py --env main` | +| Cold start latency (5-15s) | No warm containers | Add `@modal.concurrent(max_inputs=100)` | +| `MemoryError` during simulation | Container memory too low | Increase `memory=` (try 4096) | +| Timeout error | Simulation exceeds limit | Increase `timeout=` (try 600) | +| Python dependency conflict | Version mismatch | Pin exact versions in `.pip_install()` | +| Missing local files in container | Forgot `.add_local_dir()` | Add source dirs to image definition | +| Modal app silently disappeared | Unknown — can happen | `curl` the URL; if 404, redeploy | + +## What NOT to do + +- MUST NOT deploy to a personal workspace — always use the `policyengine` workspace +- MUST NOT leave `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET` env vars set when deploying via profile +- MUST NOT use `modal run` for production deployments — use `modal deploy` +- MUST NOT use `modal serve` for production — it creates ephemeral apps that stop when you close your terminal +- MUST NOT skip health check verification after deployment +- MUST NOT use generic app names — always match the dashboard/tool repo name +- MUST NOT hardcode secrets in Python code — use Modal Secrets +- MUST NOT deploy without confirming the target workspace with `modal token info` + +## Related skills + +- **policyengine-vercel-deployment-skill** — Frontend deployment to Vercel +- **policyengine-interactive-tools-skill** — Pattern C (custom Modal API) for interactive tools +- **policyengine-dashboard-workflow-skill** — Full dashboard creation workflow diff --git a/skills/tools-and-apis/policyengine-vercel-deployment-skill/SKILL.md b/skills/tools-and-apis/policyengine-vercel-deployment-skill/SKILL.md index 16ef0e1..c4484bb 100644 --- a/skills/tools-and-apis/policyengine-vercel-deployment-skill/SKILL.md +++ b/skills/tools-and-apis/policyengine-vercel-deployment-skill/SKILL.md @@ -53,14 +53,14 @@ vercel --prod --yes --scope policy-engine For apps with API backends (e.g., Modal): ```bash -# Set env var -vercel env add VITE_API_URL production +# Set env var (Next.js uses NEXT_PUBLIC_* prefix) +vercel env add NEXT_PUBLIC_API_URL production # Must force-redeploy after changing env vars vercel --prod --force --yes --scope policy-engine ``` -Vite apps access env vars via `import.meta.env.VITE_API_URL`. +Next.js apps access env vars via `process.env.NEXT_PUBLIC_API_URL`. ### Verify deployment @@ -82,13 +82,9 @@ vercel --prod --yes **Generic project names:** Never use generic names like `app` or `site` — they can steal domains from other projects. Always use descriptive names. -### Vite base path +### vercel.json -For Vercel, always use `base: "/"` in vite.config.js (unlike GitHub Pages which needs a subpath). - -### vercel.json (optional) - -Must be at repo root. For SPAs: +Must be at repo root. For Next.js static exports, configure rewrites as needed: ```json { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]