From c49ca7bd97127f09f51320e72782fef816ad4ab7 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Mon, 12 Jan 2026 10:28:41 -0800 Subject: [PATCH 01/15] feat: add user-policy associations --- src/policyengine_api/api/__init__.py | 2 + src/policyengine_api/api/user_policies.py | 138 +++++++++ src/policyengine_api/models/__init__.py | 10 + src/policyengine_api/models/user_policy.py | 52 ++++ tests/test_user_policies.py | 317 +++++++++++++++++++++ 5 files changed, 519 insertions(+) create mode 100644 src/policyengine_api/api/user_policies.py create mode 100644 src/policyengine_api/models/user_policy.py create mode 100644 tests/test_user_policies.py diff --git a/src/policyengine_api/api/__init__.py b/src/policyengine_api/api/__init__.py index f135b14..7e94d29 100644 --- a/src/policyengine_api/api/__init__.py +++ b/src/policyengine_api/api/__init__.py @@ -20,6 +20,7 @@ tax_benefit_model_versions, tax_benefit_models, user_household_associations, + user_policies, variables, ) @@ -43,5 +44,6 @@ api_router.include_router(analysis.router) api_router.include_router(agent.router) api_router.include_router(user_household_associations.router) +api_router.include_router(user_policies.router) __all__ = ["api_router"] diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py new file mode 100644 index 0000000..d2326c2 --- /dev/null +++ b/src/policyengine_api/api/user_policies.py @@ -0,0 +1,138 @@ +"""User-policy association endpoints. + +Associates users with policies they've saved/created. This enables users to +maintain a list of their policies across sessions without duplicating the +underlying policy data. +""" + +from datetime import datetime, timezone +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlmodel import Session, select + +from policyengine_api.models import ( + Policy, + User, + UserPolicy, + UserPolicyCreate, + UserPolicyRead, + UserPolicyUpdate, +) +from policyengine_api.services.database import get_session + +router = APIRouter(prefix="/user-policies", tags=["user-policies"]) + + +@router.post("/", response_model=UserPolicyRead) +def create_user_policy( + user_policy: UserPolicyCreate, + session: Session = Depends(get_session), +): + """Create a new user-policy association. + + Associates a user with a policy, allowing them to save it to their list. + A user can only have one association per policy (duplicates are rejected). + """ + # Validate user exists + user = session.get(User, user_policy.user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Validate policy exists + policy = session.get(Policy, user_policy.policy_id) + if not policy: + raise HTTPException(status_code=404, detail="Policy not found") + + # Check for duplicate (same user_id + policy_id) + existing = session.exec( + select(UserPolicy).where( + UserPolicy.user_id == user_policy.user_id, + UserPolicy.policy_id == user_policy.policy_id, + ) + ).first() + if existing: + raise HTTPException( + status_code=409, + detail="User already has an association with this policy", + ) + + # Create the association + db_user_policy = UserPolicy.model_validate(user_policy) + session.add(db_user_policy) + session.commit() + session.refresh(db_user_policy) + return db_user_policy + + +@router.get("/", response_model=list[UserPolicyRead]) +def list_user_policies( + user_id: UUID = Query(..., description="User ID to filter by"), + country_id: str | None = Query(None, description="Country filter (us/uk)"), + session: Session = Depends(get_session), +): + """List all policy associations for a user. + + Returns all policies saved by the specified user. Optionally filter by country. + """ + query = select(UserPolicy).where(UserPolicy.user_id == user_id) + + if country_id: + query = query.where(UserPolicy.country_id == country_id) + + user_policies = session.exec(query).all() + return user_policies + + +@router.get("/{user_policy_id}", response_model=UserPolicyRead) +def get_user_policy( + user_policy_id: UUID, + session: Session = Depends(get_session), +): + """Get a specific user-policy association by ID.""" + user_policy = session.get(UserPolicy, user_policy_id) + if not user_policy: + raise HTTPException(status_code=404, detail="User-policy association not found") + return user_policy + + +@router.patch("/{user_policy_id}", response_model=UserPolicyRead) +def update_user_policy( + user_policy_id: UUID, + updates: UserPolicyUpdate, + session: Session = Depends(get_session), +): + """Update a user-policy association (e.g., rename label).""" + user_policy = session.get(UserPolicy, user_policy_id) + if not user_policy: + raise HTTPException(status_code=404, detail="User-policy association not found") + + # Apply updates + update_data = updates.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(user_policy, key, value) + + # Update timestamp + user_policy.updated_at = datetime.now(timezone.utc) + + session.add(user_policy) + session.commit() + session.refresh(user_policy) + return user_policy + + +@router.delete("/{user_policy_id}", status_code=204) +def delete_user_policy( + user_policy_id: UUID, + session: Session = Depends(get_session), +): + """Delete a user-policy association. + + This only removes the association, not the underlying policy. + """ + user_policy = session.get(UserPolicy, user_policy_id) + if not user_policy: + raise HTTPException(status_code=404, detail="User-policy association not found") + + session.delete(user_policy) + session.commit() diff --git a/src/policyengine_api/models/__init__.py b/src/policyengine_api/models/__init__.py index 7361979..e7b386a 100644 --- a/src/policyengine_api/models/__init__.py +++ b/src/policyengine_api/models/__init__.py @@ -61,6 +61,12 @@ UserHouseholdAssociationRead, UserHouseholdAssociationUpdate, ) +from .user_policy import ( + UserPolicy, + UserPolicyCreate, + UserPolicyRead, + UserPolicyUpdate, +) from .variable import Variable, VariableCreate, VariableRead __all__ = [ @@ -136,6 +142,10 @@ "UserHouseholdAssociationRead", "UserHouseholdAssociationUpdate", "UserRead", + "UserPolicy", + "UserPolicyCreate", + "UserPolicyRead", + "UserPolicyUpdate", "Variable", "VariableCreate", "VariableRead", diff --git a/src/policyengine_api/models/user_policy.py b/src/policyengine_api/models/user_policy.py new file mode 100644 index 0000000..7eb7353 --- /dev/null +++ b/src/policyengine_api/models/user_policy.py @@ -0,0 +1,52 @@ +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from uuid import UUID, uuid4 + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .policy import Policy + from .user import User + + +class UserPolicyBase(SQLModel): + """Base user-policy association fields.""" + + user_id: UUID = Field(foreign_key="users.id", index=True) + policy_id: UUID = Field(foreign_key="policies.id", index=True) + country_id: str # "us" or "uk" + label: str | None = None + + +class UserPolicy(UserPolicyBase, table=True): + """User-policy association database model.""" + + __tablename__ = "user_policies" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + # Relationships + user: "User" = Relationship() + policy: "Policy" = Relationship() + + +class UserPolicyCreate(UserPolicyBase): + """Schema for creating user-policy associations.""" + + pass + + +class UserPolicyRead(UserPolicyBase): + """Schema for reading user-policy associations.""" + + id: UUID + created_at: datetime + updated_at: datetime + + +class UserPolicyUpdate(SQLModel): + """Schema for updating user-policy associations.""" + + label: str | None = None diff --git a/tests/test_user_policies.py b/tests/test_user_policies.py new file mode 100644 index 0000000..5c4e327 --- /dev/null +++ b/tests/test_user_policies.py @@ -0,0 +1,317 @@ +"""Tests for user-policy association endpoints.""" + +from uuid import uuid4 + +from policyengine_api.models import Policy, User, UserPolicy + + +def test_list_user_policies_empty(client, session): + """List user policies returns empty list when user has no associations.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + session.add(user) + session.commit() + + response = client.get(f"/user-policies?user_id={user.id}") + assert response.status_code == 200 + assert response.json() == [] + + +def test_create_user_policy(client, session): + """Create a new user-policy association.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + response = client.post( + "/user-policies", + json={ + "user_id": str(user.id), + "policy_id": str(policy.id), + "country_id": "us", + "label": "My test policy", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["user_id"] == str(user.id) + assert data["policy_id"] == str(policy.id) + assert data["country_id"] == "us" + assert data["label"] == "My test policy" + assert "id" in data + assert "created_at" in data + assert "updated_at" in data + + +def test_create_user_policy_without_label(client, session): + """Create a user-policy association without a label.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + response = client.post( + "/user-policies", + json={ + "user_id": str(user.id), + "policy_id": str(policy.id), + "country_id": "uk", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["label"] is None + + +def test_create_user_policy_user_not_found(client): + """Create user-policy association with non-existent user returns 404.""" + fake_user_id = uuid4() + fake_policy_id = uuid4() + + response = client.post( + "/user-policies", + json={ + "user_id": str(fake_user_id), + "policy_id": str(fake_policy_id), + "country_id": "us", + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "User not found" + + +def test_create_user_policy_policy_not_found(client, session): + """Create user-policy association with non-existent policy returns 404.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + session.add(user) + session.commit() + session.refresh(user) + + fake_policy_id = uuid4() + response = client.post( + "/user-policies", + json={ + "user_id": str(user.id), + "policy_id": str(fake_policy_id), + "country_id": "us", + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Policy not found" + + +def test_create_user_policy_duplicate(client, session): + """Creating duplicate user-policy association returns 409.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + # Create first association + user_policy = UserPolicy( + user_id=user.id, + policy_id=policy.id, + country_id="us", + ) + session.add(user_policy) + session.commit() + + # Try to create duplicate + response = client.post( + "/user-policies", + json={ + "user_id": str(user.id), + "policy_id": str(policy.id), + "country_id": "us", + }, + ) + assert response.status_code == 409 + assert "already has an association" in response.json()["detail"] + + +def test_list_user_policies_with_data(client, session): + """List user policies returns all associations for a user.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy1 = Policy(name="Policy 1", description="First policy") + policy2 = Policy(name="Policy 2", description="Second policy") + session.add(user) + session.add(policy1) + session.add(policy2) + session.commit() + session.refresh(user) + session.refresh(policy1) + session.refresh(policy2) + + user_policy1 = UserPolicy( + user_id=user.id, + policy_id=policy1.id, + country_id="us", + label="US policy", + ) + user_policy2 = UserPolicy( + user_id=user.id, + policy_id=policy2.id, + country_id="uk", + label="UK policy", + ) + session.add(user_policy1) + session.add(user_policy2) + session.commit() + + response = client.get(f"/user-policies?user_id={user.id}") + assert response.status_code == 200 + data = response.json() + assert len(data) == 2 + + +def test_list_user_policies_filter_by_country(client, session): + """List user policies with country filter.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy1 = Policy(name="Policy 1", description="First policy") + policy2 = Policy(name="Policy 2", description="Second policy") + session.add(user) + session.add(policy1) + session.add(policy2) + session.commit() + session.refresh(user) + session.refresh(policy1) + session.refresh(policy2) + + user_policy1 = UserPolicy( + user_id=user.id, + policy_id=policy1.id, + country_id="us", + ) + user_policy2 = UserPolicy( + user_id=user.id, + policy_id=policy2.id, + country_id="uk", + ) + session.add(user_policy1) + session.add(user_policy2) + session.commit() + + response = client.get(f"/user-policies?user_id={user.id}&country_id=us") + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["country_id"] == "us" + + +def test_get_user_policy(client, session): + """Get a specific user-policy association by ID.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + user_policy = UserPolicy( + user_id=user.id, + policy_id=policy.id, + country_id="us", + label="My policy", + ) + session.add(user_policy) + session.commit() + session.refresh(user_policy) + + response = client.get(f"/user-policies/{user_policy.id}") + assert response.status_code == 200 + data = response.json() + assert data["id"] == str(user_policy.id) + assert data["label"] == "My policy" + + +def test_get_user_policy_not_found(client): + """Get a non-existent user-policy association returns 404.""" + fake_id = uuid4() + response = client.get(f"/user-policies/{fake_id}") + assert response.status_code == 404 + assert response.json()["detail"] == "User-policy association not found" + + +def test_update_user_policy(client, session): + """Update a user-policy association label.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + user_policy = UserPolicy( + user_id=user.id, + policy_id=policy.id, + country_id="us", + label="Old label", + ) + session.add(user_policy) + session.commit() + session.refresh(user_policy) + + response = client.patch( + f"/user-policies/{user_policy.id}", + json={"label": "New label"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["label"] == "New label" + + +def test_update_user_policy_not_found(client): + """Update a non-existent user-policy association returns 404.""" + fake_id = uuid4() + response = client.patch( + f"/user-policies/{fake_id}", + json={"label": "New label"}, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "User-policy association not found" + + +def test_delete_user_policy(client, session): + """Delete a user-policy association.""" + user = User(first_name="Test", last_name="User", email="test@example.com") + policy = Policy(name="Test policy", description="A test policy") + session.add(user) + session.add(policy) + session.commit() + session.refresh(user) + session.refresh(policy) + + user_policy = UserPolicy( + user_id=user.id, + policy_id=policy.id, + country_id="us", + ) + session.add(user_policy) + session.commit() + session.refresh(user_policy) + + response = client.delete(f"/user-policies/{user_policy.id}") + assert response.status_code == 204 + + # Verify it's deleted + response = client.get(f"/user-policies/{user_policy.id}") + assert response.status_code == 404 + + +def test_delete_user_policy_not_found(client): + """Delete a non-existent user-policy association returns 404.""" + fake_id = uuid4() + response = client.delete(f"/user-policies/{fake_id}") + assert response.status_code == 404 + assert response.json()["detail"] == "User-policy association not found" From 264966f07aaef6cfd0a5d5e64aa14a39f87ac92d Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Fri, 23 Jan 2026 00:51:19 -0800 Subject: [PATCH 02/15] feat: Add tax_benefit_model_id to Policy, remove country_id from UserPolicy --- src/policyengine_api/api/policies.py | 27 ++++-- src/policyengine_api/api/user_policies.py | 12 ++- src/policyengine_api/models/policy.py | 3 + src/policyengine_api/models/user_policy.py | 1 - tests/conftest.py | 20 +++++ tests/test_policies.py | 63 +++++++++++-- tests/test_user_policies.py | 100 +++++++++++++-------- 7 files changed, 174 insertions(+), 52 deletions(-) diff --git a/src/policyengine_api/api/policies.py b/src/policyengine_api/api/policies.py index d0e2ca5..1f4c744 100644 --- a/src/policyengine_api/api/policies.py +++ b/src/policyengine_api/api/policies.py @@ -31,7 +31,7 @@ from typing import List from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, select from policyengine_api.models import ( @@ -40,6 +40,7 @@ Policy, PolicyCreate, PolicyRead, + TaxBenefitModel, ) from policyengine_api.services.database import get_session @@ -67,8 +68,17 @@ def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)) ] } """ + # Validate tax_benefit_model exists + tax_model = session.get(TaxBenefitModel, policy.tax_benefit_model_id) + if not tax_model: + raise HTTPException(status_code=404, detail="Tax benefit model not found") + # Create the policy - db_policy = Policy(name=policy.name, description=policy.description) + db_policy = Policy( + name=policy.name, + description=policy.description, + tax_benefit_model_id=policy.tax_benefit_model_id, + ) session.add(db_policy) session.flush() # Get the policy ID before adding parameter values @@ -112,10 +122,15 @@ def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)) @router.get("/", response_model=List[PolicyRead]) -def list_policies(session: Session = Depends(get_session)): - """List all policies.""" - policies = session.exec(select(Policy)).all() - return policies +def list_policies( + tax_benefit_model_id: UUID | None = Query(None, description="Filter by tax benefit model"), + session: Session = Depends(get_session), +): + """List all policies, optionally filtered by tax benefit model.""" + query = select(Policy) + if tax_benefit_model_id: + query = query.where(Policy.tax_benefit_model_id == tax_benefit_model_id) + return session.exec(query).all() @router.get("/{policy_id}", response_model=PolicyRead) diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py index d2326c2..7898f15 100644 --- a/src/policyengine_api/api/user_policies.py +++ b/src/policyengine_api/api/user_policies.py @@ -68,17 +68,21 @@ def create_user_policy( @router.get("/", response_model=list[UserPolicyRead]) def list_user_policies( user_id: UUID = Query(..., description="User ID to filter by"), - country_id: str | None = Query(None, description="Country filter (us/uk)"), + tax_benefit_model_id: UUID | None = Query(None, description="Filter by tax benefit model"), session: Session = Depends(get_session), ): """List all policy associations for a user. - Returns all policies saved by the specified user. Optionally filter by country. + Returns all policies saved by the specified user. Optionally filter by tax benefit model. """ query = select(UserPolicy).where(UserPolicy.user_id == user_id) - if country_id: - query = query.where(UserPolicy.country_id == country_id) + if tax_benefit_model_id: + query = ( + query + .join(Policy, UserPolicy.policy_id == Policy.id) + .where(Policy.tax_benefit_model_id == tax_benefit_model_id) + ) user_policies = session.exec(query).all() return user_policies diff --git a/src/policyengine_api/models/policy.py b/src/policyengine_api/models/policy.py index 570320b..69eeecf 100644 --- a/src/policyengine_api/models/policy.py +++ b/src/policyengine_api/models/policy.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: from .parameter_value import ParameterValue + from .tax_benefit_model import TaxBenefitModel class PolicyBase(SQLModel): @@ -13,6 +14,7 @@ class PolicyBase(SQLModel): name: str description: str | None = None + tax_benefit_model_id: UUID = Field(foreign_key="tax_benefit_models.id", index=True) class Policy(PolicyBase, table=True): @@ -26,6 +28,7 @@ class Policy(PolicyBase, table=True): # Relationships parameter_values: list["ParameterValue"] = Relationship(back_populates="policy") + tax_benefit_model: "TaxBenefitModel" = Relationship() class PolicyCreate(PolicyBase): diff --git a/src/policyengine_api/models/user_policy.py b/src/policyengine_api/models/user_policy.py index 7eb7353..6bdf9ff 100644 --- a/src/policyengine_api/models/user_policy.py +++ b/src/policyengine_api/models/user_policy.py @@ -14,7 +14,6 @@ class UserPolicyBase(SQLModel): user_id: UUID = Field(foreign_key="users.id", index=True) policy_id: UUID = Field(foreign_key="policies.id", index=True) - country_id: str # "us" or "uk" label: str | None = None diff --git a/tests/conftest.py b/tests/conftest.py index 8be9b3f..fe5c04a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,6 +48,26 @@ def get_session_override(): app.dependency_overrides.clear() +@pytest.fixture(name="tax_benefit_model") +def tax_benefit_model_fixture(session: Session): + """Create a TaxBenefitModel for tests.""" + model = TaxBenefitModel(name="policyengine-us", description="US model") + session.add(model) + session.commit() + session.refresh(model) + return model + + +@pytest.fixture(name="uk_tax_benefit_model") +def uk_tax_benefit_model_fixture(session: Session): + """Create a UK TaxBenefitModel for tests.""" + model = TaxBenefitModel(name="policyengine-uk", description="UK model") + session.add(model) + session.commit() + session.refresh(model) + return model + + @pytest.fixture(name="simulation_id") def simulation_fixture(session: Session): """Create a test simulation with required dependencies.""" diff --git a/tests/test_policies.py b/tests/test_policies.py index f48730b..9c85f96 100644 --- a/tests/test_policies.py +++ b/tests/test_policies.py @@ -2,7 +2,7 @@ from uuid import uuid4 -from policyengine_api.models import Policy +from policyengine_api.models import Policy, TaxBenefitModel def test_list_policies_empty(client): @@ -12,25 +12,46 @@ def test_list_policies_empty(client): assert response.json() == [] -def test_create_policy(client): +def test_create_policy(client, tax_benefit_model): """Create a new policy.""" response = client.post( "/policies", json={ "name": "Test policy", "description": "A test policy", + "tax_benefit_model_id": str(tax_benefit_model.id), }, ) assert response.status_code == 200 data = response.json() assert data["name"] == "Test policy" assert data["description"] == "A test policy" + assert data["tax_benefit_model_id"] == str(tax_benefit_model.id) assert "id" in data -def test_list_policies_with_data(client, session): +def test_create_policy_invalid_tax_benefit_model(client): + """Create policy with non-existent tax_benefit_model returns 404.""" + fake_id = uuid4() + response = client.post( + "/policies", + json={ + "name": "Test policy", + "description": "A test policy", + "tax_benefit_model_id": str(fake_id), + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Tax benefit model not found" + + +def test_list_policies_with_data(client, session, tax_benefit_model): """List policies returns all policies.""" - policy = Policy(name="test-policy", description="Test") + policy = Policy( + name="test-policy", + description="Test", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(policy) session.commit() @@ -41,9 +62,39 @@ def test_list_policies_with_data(client, session): assert data[0]["name"] == "test-policy" -def test_get_policy(client, session): +def test_list_policies_filter_by_tax_benefit_model( + client, session, tax_benefit_model, uk_tax_benefit_model +): + """List policies with tax_benefit_model_id filter.""" + policy1 = Policy( + name="US policy", + description="US", + tax_benefit_model_id=tax_benefit_model.id, + ) + policy2 = Policy( + name="UK policy", + description="UK", + tax_benefit_model_id=uk_tax_benefit_model.id, + ) + session.add(policy1) + session.add(policy2) + session.commit() + + # Filter by US model + response = client.get(f"/policies?tax_benefit_model_id={tax_benefit_model.id}") + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["name"] == "US policy" + + +def test_get_policy(client, session, tax_benefit_model): """Get a specific policy by ID.""" - policy = Policy(name="test-policy", description="Test") + policy = Policy( + name="test-policy", + description="Test", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(policy) session.commit() session.refresh(policy) diff --git a/tests/test_user_policies.py b/tests/test_user_policies.py index 5c4e327..0db9367 100644 --- a/tests/test_user_policies.py +++ b/tests/test_user_policies.py @@ -16,10 +16,14 @@ def test_list_user_policies_empty(client, session): assert response.json() == [] -def test_create_user_policy(client, session): +def test_create_user_policy(client, session, tax_benefit_model): """Create a new user-policy association.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -31,7 +35,6 @@ def test_create_user_policy(client, session): json={ "user_id": str(user.id), "policy_id": str(policy.id), - "country_id": "us", "label": "My test policy", }, ) @@ -39,17 +42,20 @@ def test_create_user_policy(client, session): data = response.json() assert data["user_id"] == str(user.id) assert data["policy_id"] == str(policy.id) - assert data["country_id"] == "us" assert data["label"] == "My test policy" assert "id" in data assert "created_at" in data assert "updated_at" in data -def test_create_user_policy_without_label(client, session): +def test_create_user_policy_without_label(client, session, tax_benefit_model): """Create a user-policy association without a label.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -61,7 +67,6 @@ def test_create_user_policy_without_label(client, session): json={ "user_id": str(user.id), "policy_id": str(policy.id), - "country_id": "uk", }, ) assert response.status_code == 200 @@ -79,7 +84,6 @@ def test_create_user_policy_user_not_found(client): json={ "user_id": str(fake_user_id), "policy_id": str(fake_policy_id), - "country_id": "us", }, ) assert response.status_code == 404 @@ -99,17 +103,20 @@ def test_create_user_policy_policy_not_found(client, session): json={ "user_id": str(user.id), "policy_id": str(fake_policy_id), - "country_id": "us", }, ) assert response.status_code == 404 assert response.json()["detail"] == "Policy not found" -def test_create_user_policy_duplicate(client, session): +def test_create_user_policy_duplicate(client, session, tax_benefit_model): """Creating duplicate user-policy association returns 409.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -120,7 +127,6 @@ def test_create_user_policy_duplicate(client, session): user_policy = UserPolicy( user_id=user.id, policy_id=policy.id, - country_id="us", ) session.add(user_policy) session.commit() @@ -131,18 +137,25 @@ def test_create_user_policy_duplicate(client, session): json={ "user_id": str(user.id), "policy_id": str(policy.id), - "country_id": "us", }, ) assert response.status_code == 409 assert "already has an association" in response.json()["detail"] -def test_list_user_policies_with_data(client, session): +def test_list_user_policies_with_data(client, session, tax_benefit_model, uk_tax_benefit_model): """List user policies returns all associations for a user.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy1 = Policy(name="Policy 1", description="First policy") - policy2 = Policy(name="Policy 2", description="Second policy") + policy1 = Policy( + name="Policy 1", + description="First policy", + tax_benefit_model_id=tax_benefit_model.id, + ) + policy2 = Policy( + name="Policy 2", + description="Second policy", + tax_benefit_model_id=uk_tax_benefit_model.id, + ) session.add(user) session.add(policy1) session.add(policy2) @@ -154,13 +167,11 @@ def test_list_user_policies_with_data(client, session): user_policy1 = UserPolicy( user_id=user.id, policy_id=policy1.id, - country_id="us", label="US policy", ) user_policy2 = UserPolicy( user_id=user.id, policy_id=policy2.id, - country_id="uk", label="UK policy", ) session.add(user_policy1) @@ -173,11 +184,21 @@ def test_list_user_policies_with_data(client, session): assert len(data) == 2 -def test_list_user_policies_filter_by_country(client, session): - """List user policies with country filter.""" +def test_list_user_policies_filter_by_tax_benefit_model( + client, session, tax_benefit_model, uk_tax_benefit_model +): + """List user policies filtered by tax_benefit_model_id via Policy join.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy1 = Policy(name="Policy 1", description="First policy") - policy2 = Policy(name="Policy 2", description="Second policy") + policy1 = Policy( + name="Policy 1", + description="First policy", + tax_benefit_model_id=tax_benefit_model.id, + ) + policy2 = Policy( + name="Policy 2", + description="Second policy", + tax_benefit_model_id=uk_tax_benefit_model.id, + ) session.add(user) session.add(policy1) session.add(policy2) @@ -189,28 +210,32 @@ def test_list_user_policies_filter_by_country(client, session): user_policy1 = UserPolicy( user_id=user.id, policy_id=policy1.id, - country_id="us", ) user_policy2 = UserPolicy( user_id=user.id, policy_id=policy2.id, - country_id="uk", ) session.add(user_policy1) session.add(user_policy2) session.commit() - response = client.get(f"/user-policies?user_id={user.id}&country_id=us") + response = client.get( + f"/user-policies?user_id={user.id}&tax_benefit_model_id={tax_benefit_model.id}" + ) assert response.status_code == 200 data = response.json() assert len(data) == 1 - assert data[0]["country_id"] == "us" + assert data[0]["policy_id"] == str(policy1.id) -def test_get_user_policy(client, session): +def test_get_user_policy(client, session, tax_benefit_model): """Get a specific user-policy association by ID.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -220,7 +245,6 @@ def test_get_user_policy(client, session): user_policy = UserPolicy( user_id=user.id, policy_id=policy.id, - country_id="us", label="My policy", ) session.add(user_policy) @@ -242,10 +266,14 @@ def test_get_user_policy_not_found(client): assert response.json()["detail"] == "User-policy association not found" -def test_update_user_policy(client, session): +def test_update_user_policy(client, session, tax_benefit_model): """Update a user-policy association label.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -255,7 +283,6 @@ def test_update_user_policy(client, session): user_policy = UserPolicy( user_id=user.id, policy_id=policy.id, - country_id="us", label="Old label", ) session.add(user_policy) @@ -282,10 +309,14 @@ def test_update_user_policy_not_found(client): assert response.json()["detail"] == "User-policy association not found" -def test_delete_user_policy(client, session): +def test_delete_user_policy(client, session, tax_benefit_model): """Delete a user-policy association.""" user = User(first_name="Test", last_name="User", email="test@example.com") - policy = Policy(name="Test policy", description="A test policy") + policy = Policy( + name="Test policy", + description="A test policy", + tax_benefit_model_id=tax_benefit_model.id, + ) session.add(user) session.add(policy) session.commit() @@ -295,7 +326,6 @@ def test_delete_user_policy(client, session): user_policy = UserPolicy( user_id=user.id, policy_id=policy.id, - country_id="us", ) session.add(user_policy) session.commit() From da3a8ad48bf94be75e964820203f0f37b99cbfa6 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Tue, 3 Feb 2026 00:37:15 -0800 Subject: [PATCH 03/15] feat: Add base schema migration and allow duplicate user-policy saves --- src/policyengine_api/api/user_policies.py | 18 +- .../migrations/20241115000000_base_schema.sql | 273 ++++++++++++++++++ 2 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 supabase/migrations/20241115000000_base_schema.sql diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py index 7898f15..3ac2786 100644 --- a/src/policyengine_api/api/user_policies.py +++ b/src/policyengine_api/api/user_policies.py @@ -32,7 +32,8 @@ def create_user_policy( """Create a new user-policy association. Associates a user with a policy, allowing them to save it to their list. - A user can only have one association per policy (duplicates are rejected). + Duplicates are allowed - users can save the same policy multiple times + with different labels (matching FE localStorage behavior). """ # Validate user exists user = session.get(User, user_policy.user_id) @@ -44,20 +45,7 @@ def create_user_policy( if not policy: raise HTTPException(status_code=404, detail="Policy not found") - # Check for duplicate (same user_id + policy_id) - existing = session.exec( - select(UserPolicy).where( - UserPolicy.user_id == user_policy.user_id, - UserPolicy.policy_id == user_policy.policy_id, - ) - ).first() - if existing: - raise HTTPException( - status_code=409, - detail="User already has an association with this policy", - ) - - # Create the association + # Create the association (duplicates allowed) db_user_policy = UserPolicy.model_validate(user_policy) session.add(db_user_policy) session.commit() diff --git a/supabase/migrations/20241115000000_base_schema.sql b/supabase/migrations/20241115000000_base_schema.sql new file mode 100644 index 0000000..1b1a0ce --- /dev/null +++ b/supabase/migrations/20241115000000_base_schema.sql @@ -0,0 +1,273 @@ +-- Base schema for PolicyEngine API v2 +-- This migration creates all tables required by SQLModel definitions + +-- ============================================================================ +-- ENUM TYPES +-- ============================================================================ + +CREATE TYPE householdjobstatus AS ENUM ('pending', 'running', 'completed', 'failed'); +CREATE TYPE simulationstatus AS ENUM ('pending', 'running', 'completed', 'failed'); +CREATE TYPE reportstatus AS ENUM ('pending', 'running', 'completed', 'failed'); +CREATE TYPE aggregatetype AS ENUM ('sum', 'mean', 'count'); +CREATE TYPE aggregatestatus AS ENUM ('pending', 'running', 'completed', 'failed'); +CREATE TYPE changeaggregatetype AS ENUM ('sum', 'mean', 'count'); +CREATE TYPE changeaggregatestatus AS ENUM ('pending', 'running', 'completed', 'failed'); + +-- ============================================================================ +-- TABLES (in dependency order - parents before children) +-- ============================================================================ + +-- Independent tables (no foreign keys) + +CREATE TABLE dynamics ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE tax_benefit_models ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE users ( + id UUID PRIMARY KEY, + first_name VARCHAR NOT NULL, + last_name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE UNIQUE INDEX ix_users_email ON users (email); + +-- Tables with single foreign key dependency + +CREATE TABLE datasets ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR, + filepath VARCHAR NOT NULL, + year INTEGER NOT NULL, + is_output_dataset BOOLEAN NOT NULL, + tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE policies ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR, + tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE INDEX ix_policies_tax_benefit_model_id ON policies (tax_benefit_model_id); + +CREATE TABLE tax_benefit_model_versions ( + id UUID PRIMARY KEY, + model_id UUID NOT NULL REFERENCES tax_benefit_models(id), + version VARCHAR NOT NULL, + description VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +-- Tables with multiple foreign key dependencies + +CREATE TABLE dataset_versions ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + description VARCHAR NOT NULL, + dataset_id UUID NOT NULL REFERENCES datasets(id), + tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE household_jobs ( + id UUID PRIMARY KEY, + tax_benefit_model_name VARCHAR NOT NULL, + request_data JSON, + policy_id UUID REFERENCES policies(id), + dynamic_id UUID REFERENCES dynamics(id), + status householdjobstatus NOT NULL, + error_message VARCHAR, + result JSON, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + started_at TIMESTAMP WITHOUT TIME ZONE, + completed_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE parameters ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + label VARCHAR, + description VARCHAR, + data_type VARCHAR, + unit VARCHAR, + tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE simulations ( + id UUID PRIMARY KEY, + dataset_id UUID NOT NULL REFERENCES datasets(id), + policy_id UUID REFERENCES policies(id), + dynamic_id UUID REFERENCES dynamics(id), + tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), + output_dataset_id UUID REFERENCES datasets(id), + status simulationstatus NOT NULL, + error_message VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + started_at TIMESTAMP WITHOUT TIME ZONE, + completed_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE user_policies ( + id UUID PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id), + policy_id UUID NOT NULL REFERENCES policies(id), + label VARCHAR, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE INDEX ix_user_policies_user_id ON user_policies (user_id); +CREATE INDEX ix_user_policies_policy_id ON user_policies (policy_id); + +CREATE TABLE variables ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + entity VARCHAR NOT NULL, + description VARCHAR, + data_type VARCHAR, + possible_values JSON, + tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE parameter_values ( + id UUID PRIMARY KEY, + parameter_id UUID NOT NULL REFERENCES parameters(id), + value_json JSON, + start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_date TIMESTAMP WITHOUT TIME ZONE, + policy_id UUID REFERENCES policies(id), + dynamic_id UUID REFERENCES dynamics(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE reports ( + id UUID PRIMARY KEY, + label VARCHAR NOT NULL, + description VARCHAR, + user_id UUID REFERENCES users(id), + markdown TEXT, + parent_report_id UUID REFERENCES reports(id), + status reportstatus NOT NULL, + error_message VARCHAR, + baseline_simulation_id UUID REFERENCES simulations(id), + reform_simulation_id UUID REFERENCES simulations(id), + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE aggregates ( + id UUID PRIMARY KEY, + simulation_id UUID NOT NULL REFERENCES simulations(id), + user_id UUID REFERENCES users(id), + report_id UUID REFERENCES reports(id), + variable VARCHAR NOT NULL, + aggregate_type aggregatetype NOT NULL, + entity VARCHAR, + filter_config JSON, + status aggregatestatus NOT NULL, + error_message VARCHAR, + result FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE change_aggregates ( + id UUID PRIMARY KEY, + baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), + reform_simulation_id UUID NOT NULL REFERENCES simulations(id), + user_id UUID REFERENCES users(id), + report_id UUID REFERENCES reports(id), + variable VARCHAR NOT NULL, + aggregate_type changeaggregatetype NOT NULL, + entity VARCHAR, + filter_config JSON, + change_geq FLOAT, + change_leq FLOAT, + status changeaggregatestatus NOT NULL, + error_message VARCHAR, + result FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE decile_impacts ( + id UUID PRIMARY KEY, + baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), + reform_simulation_id UUID NOT NULL REFERENCES simulations(id), + report_id UUID REFERENCES reports(id), + income_variable VARCHAR NOT NULL, + entity VARCHAR, + decile INTEGER NOT NULL, + quantiles INTEGER NOT NULL, + baseline_mean FLOAT, + reform_mean FLOAT, + absolute_change FLOAT, + relative_change FLOAT, + count_better_off FLOAT, + count_worse_off FLOAT, + count_no_change FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE inequality ( + id UUID PRIMARY KEY, + simulation_id UUID NOT NULL REFERENCES simulations(id), + report_id UUID REFERENCES reports(id), + income_variable VARCHAR NOT NULL, + entity VARCHAR NOT NULL, + gini FLOAT, + top_10_share FLOAT, + top_1_share FLOAT, + bottom_50_share FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE poverty ( + id UUID PRIMARY KEY, + simulation_id UUID NOT NULL REFERENCES simulations(id), + report_id UUID REFERENCES reports(id), + poverty_type VARCHAR NOT NULL, + entity VARCHAR NOT NULL, + filter_variable VARCHAR, + headcount FLOAT, + total_population FLOAT, + rate FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE TABLE program_statistics ( + id UUID PRIMARY KEY, + baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), + reform_simulation_id UUID NOT NULL REFERENCES simulations(id), + report_id UUID REFERENCES reports(id), + program_name VARCHAR NOT NULL, + entity VARCHAR NOT NULL, + is_tax BOOLEAN NOT NULL, + baseline_total FLOAT, + reform_total FLOAT, + change FLOAT, + baseline_count FLOAT, + reform_count FLOAT, + winners FLOAT, + losers FLOAT, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); From 47d13f11012d7aeda890ea4eec82fa8a9ebfd3c8 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Tue, 3 Feb 2026 11:10:46 -0800 Subject: [PATCH 04/15] fix: Update tests for tax_benefit_model_id and duplicate policy behavior --- test_fixtures/fixtures_parameters.py | 10 ++++++++-- tests/conftest.py | 2 -- tests/test_parameters.py | 4 ++-- tests/test_policies.py | 2 +- tests/test_user_policies.py | 17 +++++++++++------ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/test_fixtures/fixtures_parameters.py b/test_fixtures/fixtures_parameters.py index ff69b0e..0df134c 100644 --- a/test_fixtures/fixtures_parameters.py +++ b/test_fixtures/fixtures_parameters.py @@ -54,9 +54,15 @@ def create_parameter(session, model_version, name: str, label: str) -> Parameter return param -def create_policy(session, name: str, description: str = "A test policy") -> Policy: +def create_policy( + session, name: str, model_version, description: str = "A test policy" +) -> Policy: """Create and persist a Policy.""" - policy = Policy(name=name, description=description) + policy = Policy( + name=name, + description=description, + tax_benefit_model_id=model_version.model_id, + ) session.add(policy) session.commit() session.refresh(policy) diff --git a/tests/conftest.py b/tests/conftest.py index fe5c04a..77c29ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,5 @@ """Pytest fixtures for tests.""" -from uuid import uuid4 - import pytest from fastapi.testclient import TestClient from fastapi_cache import FastAPICache diff --git a/tests/test_parameters.py b/tests/test_parameters.py index f95016b..50bb213 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -107,7 +107,7 @@ def test__given_policy_id_filter__then_returns_only_matching_values( """GET /parameter-values?policy_id=X returns only values for that policy.""" # Given param = create_parameter(session, model_version, "test.param", "Test Param") - policy = create_policy(session, "Test Policy") + policy = create_policy(session, "Test Policy", model_version) create_parameter_value(session, param.id, 100, policy_id=None) # baseline create_parameter_value(session, param.id, 150, policy_id=policy.id) # reform @@ -135,7 +135,7 @@ def test__given_both_parameter_and_policy_filters__then_returns_matching_interse param2 = create_parameter( session, model_version, "test.both.param2", "Test Both Param 2" ) - policy = create_policy(session, "Test Both Policy") + policy = create_policy(session, "Test Both Policy", model_version) create_parameter_value(session, param1.id, 100, policy_id=None) # baseline create_parameter_value(session, param1.id, 150, policy_id=policy.id) # target diff --git a/tests/test_policies.py b/tests/test_policies.py index 9c85f96..b4ac25f 100644 --- a/tests/test_policies.py +++ b/tests/test_policies.py @@ -2,7 +2,7 @@ from uuid import uuid4 -from policyengine_api.models import Policy, TaxBenefitModel +from policyengine_api.models import Policy def test_list_policies_empty(client): diff --git a/tests/test_user_policies.py b/tests/test_user_policies.py index 0db9367..f4d1910 100644 --- a/tests/test_user_policies.py +++ b/tests/test_user_policies.py @@ -109,8 +109,8 @@ def test_create_user_policy_policy_not_found(client, session): assert response.json()["detail"] == "Policy not found" -def test_create_user_policy_duplicate(client, session, tax_benefit_model): - """Creating duplicate user-policy association returns 409.""" +def test_create_user_policy_duplicate_allowed(client, session, tax_benefit_model): + """Creating duplicate user-policy association is allowed (matches FE localStorage behavior).""" user = User(first_name="Test", last_name="User", email="test@example.com") policy = Policy( name="Test policy", @@ -131,7 +131,7 @@ def test_create_user_policy_duplicate(client, session, tax_benefit_model): session.add(user_policy) session.commit() - # Try to create duplicate + # Create duplicate - should succeed with a new ID response = client.post( "/user-policies", json={ @@ -139,11 +139,16 @@ def test_create_user_policy_duplicate(client, session, tax_benefit_model): "policy_id": str(policy.id), }, ) - assert response.status_code == 409 - assert "already has an association" in response.json()["detail"] + assert response.status_code == 200 + data = response.json() + assert data["id"] != str(user_policy.id) # New association created + assert data["user_id"] == str(user.id) + assert data["policy_id"] == str(policy.id) -def test_list_user_policies_with_data(client, session, tax_benefit_model, uk_tax_benefit_model): +def test_list_user_policies_with_data( + client, session, tax_benefit_model, uk_tax_benefit_model +): """List user policies returns all associations for a user.""" user = User(first_name="Test", last_name="User", email="test@example.com") policy1 = Policy( From 2cba7b1a661590f2c9ee4b408d85570b6c1d75bb Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Thu, 5 Feb 2026 23:47:38 -0800 Subject: [PATCH 05/15] feat: Add Alembic migration infrastructure --- .../versions/20260204_0001_initial_schema.py | 538 ++++++++++++++++++ ...20260205_c7254ea1c129_add_user_policies.py | 82 +++ .../migrations/20241115000000_base_schema.sql | 273 --------- uv.lock | 2 +- 4 files changed, 621 insertions(+), 274 deletions(-) create mode 100644 alembic/versions/20260204_0001_initial_schema.py create mode 100644 alembic/versions/20260205_c7254ea1c129_add_user_policies.py delete mode 100644 supabase/migrations/20241115000000_base_schema.sql diff --git a/alembic/versions/20260204_0001_initial_schema.py b/alembic/versions/20260204_0001_initial_schema.py new file mode 100644 index 0000000..f488734 --- /dev/null +++ b/alembic/versions/20260204_0001_initial_schema.py @@ -0,0 +1,538 @@ +"""Initial schema (main branch state) + +Revision ID: 0001_initial +Revises: +Create Date: 2026-02-04 + +This migration creates all base tables for the PolicyEngine API as they +exist on the main branch, BEFORE the household CRUD changes. +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "0001_initial" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Create all tables as they exist on main branch.""" + # ======================================================================== + # TIER 1: Tables with no foreign key dependencies + # ======================================================================== + + # Tax benefit models (e.g., "uk", "us") + op.create_table( + "tax_benefit_models", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + + # Users + op.create_table( + "users", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("first_name", sa.String(), nullable=False), + sa.Column("last_name", sa.String(), nullable=False), + sa.Column("email", sa.String(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), + ) + op.create_index("ix_users_email", "users", ["email"]) + + # Policies (reform definitions) + op.create_table( + "policies", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + + # Dynamics (behavioral response definitions) + op.create_table( + "dynamics", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + + # ======================================================================== + # TIER 2: Tables depending on tier 1 + # ======================================================================== + + # Tax benefit model versions + op.create_table( + "tax_benefit_model_versions", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("model_id", sa.Uuid(), nullable=False), + sa.Column("version", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["model_id"], ["tax_benefit_models.id"]), + ) + + # Datasets (h5 files in storage) + op.create_table( + "datasets", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("filepath", sa.String(), nullable=False), + sa.Column("year", sa.Integer(), nullable=False), + sa.Column("is_output_dataset", sa.Boolean(), nullable=False, default=False), + sa.Column("tax_benefit_model_id", sa.Uuid(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["tax_benefit_model_id"], ["tax_benefit_models.id"]), + ) + + # ======================================================================== + # TIER 3: Tables depending on tier 2 + # ======================================================================== + + # Parameters (tax-benefit system parameters) + op.create_table( + "parameters", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("label", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("data_type", sa.String(), nullable=True), + sa.Column("unit", sa.String(), nullable=True), + sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint( + ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] + ), + ) + + # Variables (tax-benefit system variables) + op.create_table( + "variables", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("data_type", sa.String(), nullable=True), + sa.Column("possible_values", sa.JSON(), nullable=True), + sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint( + ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] + ), + ) + + # Dataset versions + op.create_table( + "dataset_versions", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("dataset_id", sa.Uuid(), nullable=False), + sa.Column("tax_benefit_model_id", sa.Uuid(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["dataset_id"], ["datasets.id"]), + sa.ForeignKeyConstraint(["tax_benefit_model_id"], ["tax_benefit_models.id"]), + ) + + # ======================================================================== + # TIER 4: Tables depending on tier 3 + # ======================================================================== + + # Parameter values (policy/dynamic parameter modifications) + op.create_table( + "parameter_values", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("parameter_id", sa.Uuid(), nullable=False), + sa.Column("value_json", sa.JSON(), nullable=True), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("policy_id", sa.Uuid(), nullable=True), + sa.Column("dynamic_id", sa.Uuid(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["parameter_id"], ["parameters.id"]), + sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), + sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), + ) + + # Simulations (economy calculations) - NOTE: No household support yet + op.create_table( + "simulations", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("dataset_id", sa.Uuid(), nullable=False), # Required in main + sa.Column("policy_id", sa.Uuid(), nullable=True), + sa.Column("dynamic_id", sa.Uuid(), nullable=True), + sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), + sa.Column("output_dataset_id", sa.Uuid(), nullable=True), + sa.Column("status", sa.String(), nullable=False, default="pending"), + sa.Column("error_message", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["dataset_id"], ["datasets.id"]), + sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), + sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), + sa.ForeignKeyConstraint( + ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] + ), + sa.ForeignKeyConstraint(["output_dataset_id"], ["datasets.id"]), + ) + + # Household jobs (async household calculations) - legacy approach + op.create_table( + "household_jobs", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("tax_benefit_model_name", sa.String(), nullable=False), + sa.Column("request_data", sa.JSON(), nullable=False), + sa.Column("policy_id", sa.Uuid(), nullable=True), + sa.Column("dynamic_id", sa.Uuid(), nullable=True), + sa.Column("status", sa.String(), nullable=False, default="pending"), + sa.Column("error_message", sa.String(), nullable=True), + sa.Column("result", sa.JSON(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), + sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), + ) + + # ======================================================================== + # TIER 5: Tables depending on simulations + # ======================================================================== + + # Reports (analysis reports) - NOTE: No report_type yet + op.create_table( + "reports", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("label", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("user_id", sa.Uuid(), nullable=True), + sa.Column("markdown", sa.Text(), nullable=True), + sa.Column("parent_report_id", sa.Uuid(), nullable=True), + sa.Column("status", sa.String(), nullable=False, default="pending"), + sa.Column("error_message", sa.String(), nullable=True), + sa.Column("baseline_simulation_id", sa.Uuid(), nullable=True), + sa.Column("reform_simulation_id", sa.Uuid(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.ForeignKeyConstraint(["parent_report_id"], ["reports.id"]), + sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), + ) + + # Aggregates (single-simulation aggregate outputs) + op.create_table( + "aggregates", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("simulation_id", sa.Uuid(), nullable=False), + sa.Column("user_id", sa.Uuid(), nullable=True), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("variable", sa.String(), nullable=False), + sa.Column("aggregate_type", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=True), + sa.Column("filter_config", sa.JSON(), nullable=False, default={}), + sa.Column("status", sa.String(), nullable=False, default="pending"), + sa.Column("error_message", sa.String(), nullable=True), + sa.Column("result", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), + ) + + # Change aggregates (baseline vs reform comparison) + op.create_table( + "change_aggregates", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), + sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), + sa.Column("user_id", sa.Uuid(), nullable=True), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("variable", sa.String(), nullable=False), + sa.Column("aggregate_type", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=True), + sa.Column("filter_config", sa.JSON(), nullable=False, default={}), + sa.Column("change_geq", sa.Float(), nullable=True), + sa.Column("change_leq", sa.Float(), nullable=True), + sa.Column("status", sa.String(), nullable=False, default="pending"), + sa.Column("error_message", sa.String(), nullable=True), + sa.Column("result", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), + ) + + # Decile impacts + op.create_table( + "decile_impacts", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), + sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("income_variable", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=True), + sa.Column("decile", sa.Integer(), nullable=False), + sa.Column("quantiles", sa.Integer(), nullable=False, default=10), + sa.Column("baseline_mean", sa.Float(), nullable=True), + sa.Column("reform_mean", sa.Float(), nullable=True), + sa.Column("absolute_change", sa.Float(), nullable=True), + sa.Column("relative_change", sa.Float(), nullable=True), + sa.Column("count_better_off", sa.Float(), nullable=True), + sa.Column("count_worse_off", sa.Float(), nullable=True), + sa.Column("count_no_change", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), + ) + + # Program statistics + op.create_table( + "program_statistics", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), + sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("program_name", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=False), + sa.Column("is_tax", sa.Boolean(), nullable=False, default=False), + sa.Column("baseline_total", sa.Float(), nullable=True), + sa.Column("reform_total", sa.Float(), nullable=True), + sa.Column("change", sa.Float(), nullable=True), + sa.Column("baseline_count", sa.Float(), nullable=True), + sa.Column("reform_count", sa.Float(), nullable=True), + sa.Column("winners", sa.Float(), nullable=True), + sa.Column("losers", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), + ) + + # Poverty + op.create_table( + "poverty", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("simulation_id", sa.Uuid(), nullable=False), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("poverty_type", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=False, default="person"), + sa.Column("filter_variable", sa.String(), nullable=True), + sa.Column("headcount", sa.Float(), nullable=True), + sa.Column("total_population", sa.Float(), nullable=True), + sa.Column("rate", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint( + ["simulation_id"], ["simulations.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"], ondelete="CASCADE"), + ) + op.create_index("idx_poverty_simulation_id", "poverty", ["simulation_id"]) + op.create_index("idx_poverty_report_id", "poverty", ["report_id"]) + + # Inequality + op.create_table( + "inequality", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("simulation_id", sa.Uuid(), nullable=False), + sa.Column("report_id", sa.Uuid(), nullable=True), + sa.Column("income_variable", sa.String(), nullable=False), + sa.Column("entity", sa.String(), nullable=False, default="household"), + sa.Column("gini", sa.Float(), nullable=True), + sa.Column("top_10_share", sa.Float(), nullable=True), + sa.Column("top_1_share", sa.Float(), nullable=True), + sa.Column("bottom_50_share", sa.Float(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint( + ["simulation_id"], ["simulations.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["report_id"], ["reports.id"], ondelete="CASCADE"), + ) + op.create_index("idx_inequality_simulation_id", "inequality", ["simulation_id"]) + op.create_index("idx_inequality_report_id", "inequality", ["report_id"]) + + +def downgrade() -> None: + """Drop all tables in reverse order.""" + # Tier 5 + op.drop_index("idx_inequality_report_id", "inequality") + op.drop_index("idx_inequality_simulation_id", "inequality") + op.drop_table("inequality") + op.drop_index("idx_poverty_report_id", "poverty") + op.drop_index("idx_poverty_simulation_id", "poverty") + op.drop_table("poverty") + op.drop_table("program_statistics") + op.drop_table("decile_impacts") + op.drop_table("change_aggregates") + op.drop_table("aggregates") + op.drop_table("reports") + + # Tier 4 + op.drop_table("household_jobs") + op.drop_table("simulations") + op.drop_table("parameter_values") + + # Tier 3 + op.drop_table("dataset_versions") + op.drop_table("variables") + op.drop_table("parameters") + + # Tier 2 + op.drop_table("datasets") + op.drop_table("tax_benefit_model_versions") + + # Tier 1 + op.drop_table("dynamics") + op.drop_table("policies") + op.drop_index("ix_users_email", "users") + op.drop_table("users") + op.drop_table("tax_benefit_models") diff --git a/alembic/versions/20260205_c7254ea1c129_add_user_policies.py b/alembic/versions/20260205_c7254ea1c129_add_user_policies.py new file mode 100644 index 0000000..3e1470d --- /dev/null +++ b/alembic/versions/20260205_c7254ea1c129_add_user_policies.py @@ -0,0 +1,82 @@ +"""add_user_policies + +Revision ID: c7254ea1c129 +Revises: 0001_initial +Create Date: 2026-02-05 23:28:58.822168 + +This migration adds: +1. tax_benefit_model_id foreign key to policies table +2. user_policies table for user-policy associations +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c7254ea1c129" +down_revision: Union[str, Sequence[str], None] = "0001_initial" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Add user_policies table and policy.tax_benefit_model_id.""" + # Add tax_benefit_model_id to policies table + op.add_column( + "policies", sa.Column("tax_benefit_model_id", sa.Uuid(), nullable=False) + ) + op.create_index( + op.f("ix_policies_tax_benefit_model_id"), + "policies", + ["tax_benefit_model_id"], + unique=False, + ) + op.create_foreign_key( + "fk_policies_tax_benefit_model_id", + "policies", + "tax_benefit_models", + ["tax_benefit_model_id"], + ["id"], + ) + + # Create user_policies table + op.create_table( + "user_policies", + sa.Column("user_id", sa.Uuid(), nullable=False), + sa.Column("policy_id", sa.Uuid(), nullable=False), + sa.Column("label", sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_user_policies_policy_id"), + "user_policies", + ["policy_id"], + unique=False, + ) + op.create_index( + op.f("ix_user_policies_user_id"), "user_policies", ["user_id"], unique=False + ) + + +def downgrade() -> None: + """Remove user_policies table and policy.tax_benefit_model_id.""" + # Drop user_policies table + op.drop_index(op.f("ix_user_policies_user_id"), table_name="user_policies") + op.drop_index(op.f("ix_user_policies_policy_id"), table_name="user_policies") + op.drop_table("user_policies") + + # Remove tax_benefit_model_id from policies + op.drop_constraint( + "fk_policies_tax_benefit_model_id", "policies", type_="foreignkey" + ) + op.drop_index(op.f("ix_policies_tax_benefit_model_id"), table_name="policies") + op.drop_column("policies", "tax_benefit_model_id") diff --git a/supabase/migrations/20241115000000_base_schema.sql b/supabase/migrations/20241115000000_base_schema.sql deleted file mode 100644 index 1b1a0ce..0000000 --- a/supabase/migrations/20241115000000_base_schema.sql +++ /dev/null @@ -1,273 +0,0 @@ --- Base schema for PolicyEngine API v2 --- This migration creates all tables required by SQLModel definitions - --- ============================================================================ --- ENUM TYPES --- ============================================================================ - -CREATE TYPE householdjobstatus AS ENUM ('pending', 'running', 'completed', 'failed'); -CREATE TYPE simulationstatus AS ENUM ('pending', 'running', 'completed', 'failed'); -CREATE TYPE reportstatus AS ENUM ('pending', 'running', 'completed', 'failed'); -CREATE TYPE aggregatetype AS ENUM ('sum', 'mean', 'count'); -CREATE TYPE aggregatestatus AS ENUM ('pending', 'running', 'completed', 'failed'); -CREATE TYPE changeaggregatetype AS ENUM ('sum', 'mean', 'count'); -CREATE TYPE changeaggregatestatus AS ENUM ('pending', 'running', 'completed', 'failed'); - --- ============================================================================ --- TABLES (in dependency order - parents before children) --- ============================================================================ - --- Independent tables (no foreign keys) - -CREATE TABLE dynamics ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - description VARCHAR, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE tax_benefit_models ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - description VARCHAR, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE users ( - id UUID PRIMARY KEY, - first_name VARCHAR NOT NULL, - last_name VARCHAR NOT NULL, - email VARCHAR NOT NULL, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE UNIQUE INDEX ix_users_email ON users (email); - --- Tables with single foreign key dependency - -CREATE TABLE datasets ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - description VARCHAR, - filepath VARCHAR NOT NULL, - year INTEGER NOT NULL, - is_output_dataset BOOLEAN NOT NULL, - tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE policies ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - description VARCHAR, - tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE INDEX ix_policies_tax_benefit_model_id ON policies (tax_benefit_model_id); - -CREATE TABLE tax_benefit_model_versions ( - id UUID PRIMARY KEY, - model_id UUID NOT NULL REFERENCES tax_benefit_models(id), - version VARCHAR NOT NULL, - description VARCHAR, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - --- Tables with multiple foreign key dependencies - -CREATE TABLE dataset_versions ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - description VARCHAR NOT NULL, - dataset_id UUID NOT NULL REFERENCES datasets(id), - tax_benefit_model_id UUID NOT NULL REFERENCES tax_benefit_models(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE household_jobs ( - id UUID PRIMARY KEY, - tax_benefit_model_name VARCHAR NOT NULL, - request_data JSON, - policy_id UUID REFERENCES policies(id), - dynamic_id UUID REFERENCES dynamics(id), - status householdjobstatus NOT NULL, - error_message VARCHAR, - result JSON, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - started_at TIMESTAMP WITHOUT TIME ZONE, - completed_at TIMESTAMP WITHOUT TIME ZONE -); - -CREATE TABLE parameters ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - label VARCHAR, - description VARCHAR, - data_type VARCHAR, - unit VARCHAR, - tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE simulations ( - id UUID PRIMARY KEY, - dataset_id UUID NOT NULL REFERENCES datasets(id), - policy_id UUID REFERENCES policies(id), - dynamic_id UUID REFERENCES dynamics(id), - tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), - output_dataset_id UUID REFERENCES datasets(id), - status simulationstatus NOT NULL, - error_message VARCHAR, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - started_at TIMESTAMP WITHOUT TIME ZONE, - completed_at TIMESTAMP WITHOUT TIME ZONE -); - -CREATE TABLE user_policies ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL REFERENCES users(id), - policy_id UUID NOT NULL REFERENCES policies(id), - label VARCHAR, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE INDEX ix_user_policies_user_id ON user_policies (user_id); -CREATE INDEX ix_user_policies_policy_id ON user_policies (policy_id); - -CREATE TABLE variables ( - id UUID PRIMARY KEY, - name VARCHAR NOT NULL, - entity VARCHAR NOT NULL, - description VARCHAR, - data_type VARCHAR, - possible_values JSON, - tax_benefit_model_version_id UUID NOT NULL REFERENCES tax_benefit_model_versions(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE parameter_values ( - id UUID PRIMARY KEY, - parameter_id UUID NOT NULL REFERENCES parameters(id), - value_json JSON, - start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, - end_date TIMESTAMP WITHOUT TIME ZONE, - policy_id UUID REFERENCES policies(id), - dynamic_id UUID REFERENCES dynamics(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE reports ( - id UUID PRIMARY KEY, - label VARCHAR NOT NULL, - description VARCHAR, - user_id UUID REFERENCES users(id), - markdown TEXT, - parent_report_id UUID REFERENCES reports(id), - status reportstatus NOT NULL, - error_message VARCHAR, - baseline_simulation_id UUID REFERENCES simulations(id), - reform_simulation_id UUID REFERENCES simulations(id), - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE aggregates ( - id UUID PRIMARY KEY, - simulation_id UUID NOT NULL REFERENCES simulations(id), - user_id UUID REFERENCES users(id), - report_id UUID REFERENCES reports(id), - variable VARCHAR NOT NULL, - aggregate_type aggregatetype NOT NULL, - entity VARCHAR, - filter_config JSON, - status aggregatestatus NOT NULL, - error_message VARCHAR, - result FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE change_aggregates ( - id UUID PRIMARY KEY, - baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), - reform_simulation_id UUID NOT NULL REFERENCES simulations(id), - user_id UUID REFERENCES users(id), - report_id UUID REFERENCES reports(id), - variable VARCHAR NOT NULL, - aggregate_type changeaggregatetype NOT NULL, - entity VARCHAR, - filter_config JSON, - change_geq FLOAT, - change_leq FLOAT, - status changeaggregatestatus NOT NULL, - error_message VARCHAR, - result FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE decile_impacts ( - id UUID PRIMARY KEY, - baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), - reform_simulation_id UUID NOT NULL REFERENCES simulations(id), - report_id UUID REFERENCES reports(id), - income_variable VARCHAR NOT NULL, - entity VARCHAR, - decile INTEGER NOT NULL, - quantiles INTEGER NOT NULL, - baseline_mean FLOAT, - reform_mean FLOAT, - absolute_change FLOAT, - relative_change FLOAT, - count_better_off FLOAT, - count_worse_off FLOAT, - count_no_change FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE inequality ( - id UUID PRIMARY KEY, - simulation_id UUID NOT NULL REFERENCES simulations(id), - report_id UUID REFERENCES reports(id), - income_variable VARCHAR NOT NULL, - entity VARCHAR NOT NULL, - gini FLOAT, - top_10_share FLOAT, - top_1_share FLOAT, - bottom_50_share FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE poverty ( - id UUID PRIMARY KEY, - simulation_id UUID NOT NULL REFERENCES simulations(id), - report_id UUID REFERENCES reports(id), - poverty_type VARCHAR NOT NULL, - entity VARCHAR NOT NULL, - filter_variable VARCHAR, - headcount FLOAT, - total_population FLOAT, - rate FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); - -CREATE TABLE program_statistics ( - id UUID PRIMARY KEY, - baseline_simulation_id UUID NOT NULL REFERENCES simulations(id), - reform_simulation_id UUID NOT NULL REFERENCES simulations(id), - report_id UUID REFERENCES reports(id), - program_name VARCHAR NOT NULL, - entity VARCHAR NOT NULL, - is_tax BOOLEAN NOT NULL, - baseline_total FLOAT, - reform_total FLOAT, - change FLOAT, - baseline_count FLOAT, - reform_count FLOAT, - winners FLOAT, - losers FLOAT, - created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL -); diff --git a/uv.lock b/uv.lock index 466caf4..f66f5d0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] From 74ee3eefdf1b33098ef98f798a2bdceca739563a Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Thu, 5 Feb 2026 23:56:31 -0800 Subject: [PATCH 06/15] chore: Rename migration to 0002 for readability --- .DS_Store | Bin 0 -> 6148 bytes ....py => 20260205_0002_add_user_policies.py} | 6 +- docs/api-v2-alpha-architecture.md | 264 +++++++++++++++++ docs/policy-ingredient-status.md | 271 ++++++++++++++++++ docs/supabase-migration-notes.md | 174 +++++++++++ src/policyengine_api/agent_sandbox.py | 45 +-- src/policyengine_api/api/agent.py | 22 +- src/policyengine_api/api/household.py | 29 +- src/policyengine_api/api/policies.py | 4 +- src/policyengine_api/api/user_policies.py | 10 +- src/policyengine_api/api/variables.py | 4 +- src/policyengine_api/modal_app.py | 8 +- tests/test_agent.py | 1 + tests/test_agent_policy_questions.py | 8 +- tests/test_integration.py | 1 + 15 files changed, 796 insertions(+), 51 deletions(-) create mode 100644 .DS_Store rename alembic/versions/{20260205_c7254ea1c129_add_user_policies.py => 20260205_0002_add_user_policies.py} (96%) create mode 100644 docs/api-v2-alpha-architecture.md create mode 100644 docs/policy-ingredient-status.md create mode 100644 docs/supabase-migration-notes.md diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4bf6461c4c4dcb4ac126e58927c5525107ec899b GIT binary patch literal 6148 zcmeHKK~BRk5FA4hEr^0RaY4!lsKgJ_QVvLPq2dfQEvS$-C~Xnka_0|xf%kEOS=)ld z1&I?vXjj@D$KLg3C$(JzFrE2eAJ_s=rwZ2AXuc2`7hRAg-?K?Hc8)F%(ZU#mqjbSr zJCp%s;IAW)Yh97wt#I6kRib>hRCtUR>oC-M>}Ot z3y*wCkJ8Ehmcf|@L!s>I0)m5^WFxQ9~ zX9#M9@K^R;rgx4cU_uxxd)4VR=X{swQ~7lQe_^id-(Y6>ra8T@SZ9uL5Fs3uBQ|Wr zl=hqvBRsG*XWTHXz%o>hT=|F$Yb7(LSz%_5dorK8UrhClgqeyrUr^--49JS0)Yl!V zRR)v+W#EGW*&iaRVC=DQXtxd;dj%lY*lmSv`NJSFk;m9$;gBN~<3fop)c7lgapCNb zykG3GaOlEe{N=;=%*NkPjLpvZBkK+mJ5;L-C<9dnmfho)od1X4-~X#Y`lJjf1OJKv zQ|tD+9hT(J)`jBatc|D_R1xtj9NG{Xe;nI}9K}0SD;$fYL5w{X4%tJ|KLVBpHOj!R GGVl%&a?hs# literal 0 HcmV?d00001 diff --git a/alembic/versions/20260205_c7254ea1c129_add_user_policies.py b/alembic/versions/20260205_0002_add_user_policies.py similarity index 96% rename from alembic/versions/20260205_c7254ea1c129_add_user_policies.py rename to alembic/versions/20260205_0002_add_user_policies.py index 3e1470d..b19a6cf 100644 --- a/alembic/versions/20260205_c7254ea1c129_add_user_policies.py +++ b/alembic/versions/20260205_0002_add_user_policies.py @@ -1,8 +1,8 @@ """add_user_policies -Revision ID: c7254ea1c129 +Revision ID: 0002_user_policies Revises: 0001_initial -Create Date: 2026-02-05 23:28:58.822168 +Create Date: 2026-02-05 This migration adds: 1. tax_benefit_model_id foreign key to policies table @@ -17,7 +17,7 @@ from alembic import op # revision identifiers, used by Alembic. -revision: str = "c7254ea1c129" +revision: str = "0002_user_policies" down_revision: Union[str, Sequence[str], None] = "0001_initial" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/docs/api-v2-alpha-architecture.md b/docs/api-v2-alpha-architecture.md new file mode 100644 index 0000000..10924db --- /dev/null +++ b/docs/api-v2-alpha-architecture.md @@ -0,0 +1,264 @@ +# policyengine-api-v2-alpha Deep Dive + +## Overview + +A **FastAPI backend** for tax-benefit policy microsimulations using PolicyEngine's UK and US models. It's designed as a thin orchestration layer that offloads heavy calculations to **Modal.com** serverless functions. + +--- + +## Architecture + +### Three-Level Hierarchy +``` +Level 2: Reports AI-generated analysis documents +Level 1: Analyses Comparisons (economy_comparison_*) +Level 0: Simulations Single calculations (simulate_household_*, simulate_economy_*) +``` + +### Request Flow +1. Client → FastAPI (Cloud Run) +2. API creates job record in **Supabase** and triggers **Modal.com** function +3. Modal runs calculation with pre-loaded PolicyEngine models (sub-1s cold start via memory snapshotting) +4. Modal writes results directly to Supabase +5. Client polls API until `status = "completed"` + +--- + +## Tech Stack + +| Component | Technology | +|-----------|------------| +| Framework | FastAPI + async | +| Database | Supabase (PostgreSQL) via SQLModel | +| Compute | Modal.com serverless | +| Storage | Supabase Storage (S3-compatible for .h5 datasets) | +| Package mgr | UV | +| Formatting | Ruff | +| Testing | Pytest + pytest-asyncio | +| Deployment | Terraform → GCP Cloud Run | +| Observability | Logfire (OpenTelemetry) | + +--- + +## Database Models (SQLModel) + +### Core Entities + +| Model | Purpose | Key Fields | +|-------|---------|------------| +| `TaxBenefitModel` | Country identifier | `name` ("uk"/"us") | +| `TaxBenefitModelVersion` | Version of model | `version`, FK to model | +| `Dataset` | Population microdata (.h5) | `filepath` (S3), `year` | +| `DatasetVersion` | Dataset versions | FK to dataset + model | +| `User` | Basic user info | `email`, `first_name`, `last_name` | + +### Policy System (Normalized) + +| Model | Purpose | Key Fields | +|-------|---------|------------| +| `Policy` | Policy reform definition | `name`, `description`, `simulation_modifier` (Python code) | +| `Parameter` | Tax/benefit parameter metadata | `name` (path), `label`, `data_type`, FK to model version | +| `ParameterValue` | Parameter values (baseline OR reform) | `value_json`, `start_date`, `policy_id` (NULL=baseline) | +| `Variable` | Model variables metadata | `name`, `entity`, `data_type`, `possible_values` | +| `Dynamic` | Behavioral response config | Similar to Policy | + +### Simulation & Results + +| Model | Purpose | Key Fields | +|-------|---------|------------| +| `Simulation` | Economy simulation instance | FK to dataset, policy, dynamic; `status` | +| `HouseholdJob` | Single household calculation | `request_data` (JSON), `result` (JSON), `status` | +| `Report` | Analysis report | FK to baseline_sim, reform_sim; `status`, `markdown` | +| `DecileImpact` | Income impacts by decile | FK to baseline/reform sims; `absolute_change`, `relative_change` | +| `ProgramStatistics` | Budget impacts by program | `baseline_total`, `reform_total`, `change` | +| `Poverty` | Poverty rate outputs | `poverty_type`, `headcount`, `rate` | +| `Inequality` | Inequality metrics | `gini`, `top_10_share`, `bottom_50_share` | +| `AggregateOutput` | Custom aggregations | `variable`, `aggregate_type`, `filter_config` | +| `ChangeAggregate` | Change aggregations | baseline vs reform comparison | + +--- + +## API Endpoints + +### Registered Routers (`api/__init__.py`) +``` +/datasets - Dataset CRUD + listing +/policies - Policy CRUD (POST, GET, LIST - no PATCH/DELETE) +/simulations - Simulation management +/parameters - Parameter metadata +/parameter-values - Parameter value CRUD +/variables - Variable metadata +/dynamics - Behavioral response configs +/tax-benefit-models - Model listing +/tax-benefit-model-versions - Version listing +/outputs - Result outputs +/change-aggregates - Comparison aggregates +/household/* - Household calculations (async) +/analysis/* - Economy-wide analysis (async) +/agent/* - AI agent endpoint (Claude Code) +``` + +### Key Endpoint Patterns + +**Async Pattern** (household, analysis): +``` +POST /household/calculate → {"job_id": "...", "status": "pending"} +GET /household/calculate/{job_id} → poll until status="completed" +``` + +**CRUD Pattern** (policies, datasets): +``` +POST /policies - Create +GET /policies - List all +GET /policies/{id} - Get by ID +(PATCH, DELETE - NOT implemented) +``` + +--- + +## Modal.com Functions (`modal_app.py`) + +| Function | Purpose | Resources | +|----------|---------|-----------| +| `simulate_household_uk` | UK household calc | 4GB RAM, 4 CPU | +| `simulate_household_us` | US household calc | 4GB RAM, 4 CPU | +| `simulate_economy_uk` | UK economy sim | 8GB RAM, 8 CPU | +| `simulate_economy_us` | US economy sim | 8GB RAM, 8 CPU | +| `economy_comparison_uk` | UK decile/budget analysis | 8GB RAM, 8 CPU, 30min timeout | +| `economy_comparison_us` | US decile/budget analysis | 8GB RAM, 8 CPU, 30min timeout | + +### Key Feature: Memory Snapshotting +```python +# UK image - uses run_function to snapshot imported modules in memory +uk_image = base_image.run_commands( + "uv pip install --system policyengine-uk>=2.0.0" +).run_function(_import_uk) # ← pre-loads uk_latest at build time +``` +This enables **sub-1s cold starts** - PolicyEngine models are already loaded in memory. + +--- + +## Database Migrations (`supabase/migrations/`) + +| Migration | Purpose | +|-----------|---------| +| `20241119000000_storage_bucket.sql` | Create datasets S3 bucket | +| `20241121000000_storage_policies.sql` | Storage access policies | +| `20251229000000_add_parameter_values_indexes.sql` | Index optimization | +| `20260103000000_add_poverty_inequality.sql` | Poverty/inequality tables | +| `20260108000000_add_simulation_modifier.sql` | Add `simulation_modifier` to policies | + +**Note**: Tables are auto-created by SQLModel on app startup, not via migrations. Migrations are only for: +- Storage bucket setup +- Adding indexes +- Schema alterations + +--- + +## Agent Endpoint (`/agent/*`) + +An AI-powered policy analysis feature using Claude Code: +- `POST /agent/run` - Start agent with a question +- `GET /agent/logs/{call_id}` - Poll for logs and result +- `GET /agent/status/{call_id}` - Quick status check +- `POST /agent/log/{call_id}` - Modal calls this to stream logs +- `POST /agent/complete/{call_id}` - Modal calls this when done + +Runs in: +- **Production**: Modal sandbox +- **Development**: Local background thread + +--- + +## Configuration (`config/settings.py`) + +Key settings loaded from environment: +- `DATABASE_URL` - Supabase Postgres connection +- `SUPABASE_URL` / `SUPABASE_KEY` - Supabase client +- `POLICYENGINE_API_URL` - Self-reference URL +- `AGENT_USE_MODAL` - True for production, False for local + +--- + +## Key Design Decisions + +1. **Normalized parameter storage** - `parameter_values` table with FKs instead of JSON blobs +2. **Async job pattern** - All heavy calculations return immediately with job_id +3. **Deterministic UUIDs** - Simulations/reports use uuid5 for deduplication +4. **Memory snapshotting** - PolicyEngine models loaded at Modal image build time +5. **SQLModel** - Single source of truth for DB schema and Pydantic validation +6. **simulation_modifier** - Python code injection for custom variable formulas + +--- + +## What's Missing (Gaps) + +| Feature | Status | +|---------|--------| +| `PATCH /policies/{id}` | Not implemented | +| `DELETE /policies/{id}` | Not implemented | +| User-policy associations | Not implemented | +| User-simulation associations | Not implemented | +| Authentication | No endpoints exposed | + +--- + +## File Structure + +``` +src/policyengine_api/ +├── api/ # FastAPI routers +│ ├── __init__.py # Router registration +│ ├── agent.py # AI agent endpoint +│ ├── analysis.py # Economy analysis +│ ├── household.py # Household calculations +│ ├── policies.py # Policy CRUD +│ ├── parameters.py # Parameter metadata +│ ├── parameter_values.py # Parameter values +│ └── ... +├── config/ +│ └── settings.py # Environment config +├── models/ # SQLModel definitions +│ ├── __init__.py # Model exports +│ ├── policy.py +│ ├── parameter.py +│ ├── parameter_value.py +│ ├── simulation.py +│ ├── report.py +│ └── ... +├── services/ +│ ├── database.py # DB session management +│ └── storage.py # Supabase storage client +├── main.py # FastAPI app entry point +└── modal_app.py # Modal serverless functions + +supabase/ +└── migrations/ # SQL migrations (storage, indexes) + +terraform/ # GCP Cloud Run infrastructure + +tests/ # Pytest tests +``` + +--- + +## Development Commands + +```bash +make install # Install dependencies with uv +make dev # Start supabase + api via docker compose +make test # Run unit tests +make integration-test # Full integration tests +make format # Ruff formatting +make lint # Ruff linting with auto-fix +make modal-deploy # Deploy Modal.com serverless functions +make init # Reset tables and storage +make seed # Populate UK/US models with variables, parameters, datasets +``` + +--- + +## Contributors + +- **Nikhil Woodruff** - Initial implementation (Nov 2025) +- **Anthony Volk** - Parameter values filtering and indexing (Dec 2025) diff --git a/docs/policy-ingredient-status.md b/docs/policy-ingredient-status.md new file mode 100644 index 0000000..e9c79a4 --- /dev/null +++ b/docs/policy-ingredient-status.md @@ -0,0 +1,271 @@ +# Policy Ingredient Status in API v2 Alpha + +This document summarizes the current state of the policy ingredient implementation in `policyengine-api-v2-alpha`, compares it to API v1, and outlines gaps and recommendations for migration. + +## Current Implementation + +### Database Tables + +The policy system uses a normalized relational model with two main tables: + +#### `policies` table +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key, auto-generated | +| `name` | str | Policy name (required) | +| `description` | str \| None | Optional description | +| `simulation_modifier` | str \| None | Python code for custom variable formulas | +| `created_at` | datetime | Auto-generated UTC timestamp | +| `updated_at` | datetime | Auto-generated UTC timestamp | + +#### `parameter_values` table +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key | +| `parameter_id` | UUID | FK to `parameters` table | +| `policy_id` | UUID \| None | FK to `policies` (NULL = baseline/current law) | +| `dynamic_id` | UUID \| None | FK to `dynamics` table | +| `value_json` | Any | The actual parameter value (JSON) | +| `start_date` | datetime | Effective start date | +| `end_date` | datetime \| None | Effective end date | +| `created_at` | datetime | Auto-generated | + +**Key design:** `parameter_values` stores BOTH baseline values (`policy_id IS NULL`) AND reform values (`policy_id = `). Each policy is self-contained with its own set of parameter value overrides - this is intentional, not a normalization issue. When running simulations, baseline (NULL) vs reform (policy_id = X) values are compared to compute impacts. + +### Supporting Tables + +#### `parameters` table (metadata) +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key | +| `name` | str | Parameter path (e.g., `gov.irs.ctc.max`) | +| `label` | str \| None | Human-readable label | +| `description` | str \| None | Documentation | +| `data_type` | str \| None | Value type | +| `unit` | str \| None | Unit of measurement | +| `tax_benefit_model_version_id` | UUID | FK to model version | +| `created_at` | datetime | Auto-generated | + +#### `tax_benefit_models` table +| Column | Type | Description | +|--------|------|-------------| +| `id` | UUID | Primary key | +| `name` | str | Country identifier ("uk" or "us") | +| `description` | str \| None | Optional | +| `created_at` | datetime | Auto-generated | + +### API Endpoints + +| Method | Path | Status | Description | +|--------|------|--------|-------------| +| `POST` | `/policies` | ✅ Implemented | Create policy with parameter values | +| `GET` | `/policies` | ✅ Implemented | List all policies | +| `GET` | `/policies/{id}` | ✅ Implemented | Get policy by ID | +| `PATCH` | `/policies/{id}` | ❌ Missing | Update policy | +| `DELETE` | `/policies/{id}` | ❌ Missing | Delete policy | + +### Files + +| File | Purpose | +|------|---------| +| `src/policyengine_api/models/policy.py` | SQLModel definitions | +| `src/policyengine_api/models/parameter_value.py` | Parameter value model | +| `src/policyengine_api/api/policies.py` | Policy router/endpoints | +| `src/policyengine_api/api/parameter_values.py` | Parameter values router | +| `tests/test_policies.py` | Basic CRUD tests | + +### Contributors + +- **Nikhil Woodruff** - Initial implementation (Nov 2025) +- **Anthony Volk** - Added parameter_values filtering and indexing (Dec 2025) + +--- + +## Comparison: API v1 vs API v2 Alpha + +### URL Structure + +| Aspect | API v1 | API v2 Alpha | +|--------|--------|--------------| +| Create | `POST /{country_id}/policy` | `POST /policies` | +| Get | `GET /{country_id}/policy/{id}` | `GET /policies/{id}` | +| List | `GET /{country_id}/policies` | `GET /policies` | + +**Key difference:** v1 has country in URL path; v2 does not. + +### Country Handling + +| API v1 | API v2 Alpha | +|--------|--------------| +| Country in URL path (`/us/policy/123`) | Country determined by `tax_benefit_model_name` in request body | +| `@validate_country` decorator | No explicit country validation on policy endpoints | +| Policy implicitly scoped to country | Policy is country-agnostic; country context from analysis request | + +### Policy Storage + +| API v1 | API v2 Alpha | +|--------|--------------| +| `policy_hash` - hash of JSON blob | `parameter_values` - relational FK to parameters | +| Policy content stored as JSON | Policy content normalized in separate table | +| Embedded values in metadata response | Separate queries for parameter values | + +### Metadata Response (Parameters) + +**API v1 - Embedded values:** +```json +{ + "gov.irs.ctc.max": { + "type": "parameter", + "label": "CTC maximum", + "values": { + "2024-01-01": 2000, + "2023-01-01": 2000, + "2022-01-01": 2000 + } + } +} +``` + +**API v2 Alpha - Normalized:** +``` +-- parameters table +id | name | label +1 | gov.irs.ctc.max | CTC maximum + +-- parameter_values table +id | parameter_id | start_date | value_json | policy_id +1 | 1 | 2024-01-01 | 2000 | NULL (baseline) +2 | 1 | 2024-01-01 | 3000 | +``` + +### User Associations + +| API v1 | API v2 Alpha | +|--------|--------------| +| `user_policies` table exists in code | Not implemented | +| Endpoints NOT exposed (dead code) | No user association tables/endpoints | +| Fields: `user_id`, `reform_id`, `baseline_id`, `year`, `geography`, etc. | N/A | + +--- + +## Pros and Cons + +### API v2 Alpha Advantages + +| Pro | Explanation | +|-----|-------------| +| **Normalized schema** | Easier to query specific parameter changes without parsing JSON | +| **Relational integrity** | FK constraints ensure valid parameter references | +| **Better indexing** | Can index on `parameter_id`, `policy_id`, `start_date` | +| **Audit trail** | Each parameter value has its own `created_at` | +| **Cleaner reform diffs** | Query `WHERE policy_id = X` to see all reform changes | + +### API v2 Alpha Disadvantages + +| Con | Explanation | +|-----|-------------| +| **No country on policy** | Can't filter policies by country at DB level | +| **No user associations** | Must be built from scratch | +| **Missing PATCH/DELETE** | Incomplete CRUD | +| **No label field** | Only `name` + `description`, no user-friendly `label` | +| **More complex queries** | JOIN required to get policy with values | + +### API v1 Advantages + +| Pro | Explanation | +|-----|-------------| +| **Country in URL** | Clear API contract, easy filtering | +| **Simple storage** | Policy is a single JSON blob | +| **User associations designed** | Schema exists (though not exposed) | + +### API v1 Disadvantages + +| Con | Explanation | +|-----|-------------| +| **JSON blob parsing** | Must parse to query specific parameters | +| **No referential integrity** | Policy JSON could reference invalid parameters | +| **Harder to diff** | Must compare two JSON blobs to see changes | + +--- + +## Gaps for Migration + +### Must Have + +1. **User associations table and endpoints** + - `user_policies` table with `user_id`, `policy_id`, `label`, `created_at` + - `POST /user-policies` - Create association + - `GET /user-policies?user_id=X` - List user's policies + - `DELETE /user-policies/{id}` - Remove association + +2. **PATCH endpoint for policies** + - Update `name`, `description` + - Update parameter values + +3. **DELETE endpoint for policies** + - Cascade delete parameter values + +### Should Have + +4. **Country validation** + - Either add `country_id` to policy model OR + - Validate at creation that all parameter_ids belong to same country + +5. **Label field on policy** + - User-friendly display name separate from `name` + +### Nice to Have + +6. **Soft delete** + - `deleted_at` field instead of hard delete + +7. **Policy versioning** + - Track changes over time + +--- + +## Suggested Schema Changes + +### Option A: Add country_id to Policy + +```python +class Policy(PolicyBase, table=True): + # ... existing fields ... + country_id: str # "us" or "uk" +``` + +**Pros:** Simple filtering, matches v1 pattern +**Cons:** Redundant with parameter → tax_benefit_model → name + +### Option B: Derive country from parameters (current approach) + +Keep as-is, derive country from first parameter's tax_benefit_model. + +**Pros:** No schema change, DRY +**Cons:** Requires JOIN to filter by country + +### Recommendation: Option A + +Add explicit `country_id` for simpler queries and clearer data model. + +--- + +## Next Steps + +1. [ ] Add `country_id` to Policy model +2. [ ] Add `label` field to Policy model +3. [ ] Create UserPolicy model and table +4. [ ] Implement `PATCH /policies/{id}` endpoint +5. [ ] Implement `DELETE /policies/{id}` endpoint +6. [ ] Implement user policy association endpoints +7. [ ] Add database migration for schema changes +8. [ ] Update tests + +--- + +## References + +- Policy model: `src/policyengine_api/models/policy.py` +- Policy router: `src/policyengine_api/api/policies.py` +- Parameter value model: `src/policyengine_api/models/parameter_value.py` +- Tests: `tests/test_policies.py` diff --git a/docs/supabase-migration-notes.md b/docs/supabase-migration-notes.md new file mode 100644 index 0000000..b3c09d7 --- /dev/null +++ b/docs/supabase-migration-notes.md @@ -0,0 +1,174 @@ +# Supabase & Database Migration Notes + +## Overview + +This document summarizes the database setup, migration issues, and workarounds discovered during local development of the PolicyEngine API v2. + +--- + +## What is Supabase? + +Supabase is a backend-as-a-service platform that provides: +- **PostgreSQL database** - The actual database +- **PostgREST** - Auto-generated REST API from your database schema +- **GoTrue** - Authentication service +- **Kong** - API gateway +- **Studio** - Web UI for managing your database +- **Realtime** - WebSocket subscriptions for live updates +- **Storage** - File storage with CDN + +### Local Development with Supabase + +Running `supabase start` spins up Docker containers for all these services locally: +- PostgreSQL on port **54322** +- Studio UI on port **54323** +- API on port **54321** + +--- + +## The Migration Bug + +### What Happened + +When running `supabase start` on a fresh machine, it failed with: + +``` +ERROR: relation "parameter_values" does not exist (SQLSTATE 42P01) +``` + +### Root Cause + +The migration file `20251229000000_add_parameter_values_indexes.sql` tries to create an index on the `parameter_values` table: + +```sql +CREATE INDEX idx_parameter_values_parameter_id ON parameter_values(parameter_id); +``` + +But there's **no base schema migration** that creates the tables first. The migrations run in timestamp order, so this index migration runs before any tables exist. + +### Why This Wasn't Caught Before + +Different developers may have had working setups because: + +1. **SQLite for local dev** - Using `SUPABASE_DB_URL="sqlite:///./test.db"` bypasses Supabase entirely. SQLModel's `create_all()` auto-creates tables. + +2. **Existing PostgreSQL** - If someone had previously created tables manually or through the API startup, the migration would succeed. + +3. **Never ran `supabase start`** - The migrations only run when Supabase starts fresh. + +--- + +## Two Ways Tables Get Created + +### 1. SQLModel's `create_all()` (Automatic) + +When the API starts, this code runs in `database.py`: + +```python +def init_db(): + SQLModel.metadata.create_all(engine) +``` + +This automatically creates all tables defined in SQLModel classes. It's idempotent (safe to run multiple times) but: +- Doesn't track schema changes over time +- Can't roll back changes +- Doesn't work for Supabase migrations (they run BEFORE the API starts) + +### 2. Migration Scripts (Manual/Versioned) + +SQL files in `supabase/migrations/` that run in timestamp order: +- `20241119000000_storage_bucket.sql` +- `20241121000000_storage_policies.sql` +- `20251229000000_add_parameter_values_indexes.sql` ← Problem file +- `20260103000000_add_poverty_inequality.sql` +- `20260111000000_add_aggregate_status.sql` + +**Why migrations exist:** +- Version-controlled schema changes +- Can roll back to previous versions +- Team members get same schema automatically +- Production deployments are reproducible + +--- + +## The Fix (Not Yet Implemented) + +Add a base schema migration with an earlier timestamp that creates all tables: + +``` +supabase/migrations/20241101000000_base_schema.sql +``` + +This file should contain `CREATE TABLE` statements for all tables, generated from the SQLModel definitions. + +--- + +## Workaround: Use SQLite for Local Testing + +For quick local development without Docker/Supabase: + +```bash +cd /Users/sakshikekre/Work/PolicyEngine/Repos/policyengine-api-v2-alpha + +# Start API with SQLite instead of PostgreSQL +SUPABASE_DB_URL="sqlite:///./test.db" .venv/bin/uvicorn policyengine_api.main:app --host 0.0.0.0 --port 8000 --reload +``` + +This: +- Creates a local `test.db` file +- SQLModel auto-creates all tables on startup +- No Docker required +- Fast iteration for development + +--- + +## Docker/Colima Setup (For Full Supabase) + +Supabase local requires Docker. Options: + +### Option A: Docker Desktop (~2-3GB) +```bash +brew install --cask docker +# Then open Docker Desktop app +``` + +### Option B: Colima (Lightweight, ~500MB) +```bash +brew install colima docker docker-compose +colima start +``` + +**Note:** Colima installation failed due to disk space. Need ~2GB free for the build process. + +--- + +## Current .env Configuration + +```env +# For Supabase (when running) +SUPABASE_URL=http://127.0.0.1:54321 +SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres + +# For SQLite (workaround) +SUPABASE_DB_URL=sqlite:///./test.db +``` + +--- + +## Summary + +| Approach | Pros | Cons | +|----------|------|------| +| **SQLite** | Fast, no Docker, instant tables | Not production-like, some SQL differences | +| **Supabase** | Production-like, full stack | Needs Docker, migration bug to fix | + +**Recommended for now:** Use SQLite for development until the base schema migration is added. + +--- + +## TODO + +- [ ] Create `20241101000000_base_schema.sql` with all table definitions +- [ ] Test `supabase start` with the new migration +- [ ] Free up disk space and install Colima/Docker +- [ ] Document the full Supabase setup process diff --git a/src/policyengine_api/agent_sandbox.py b/src/policyengine_api/agent_sandbox.py index 9d0436c..bcf1ab0 100644 --- a/src/policyengine_api/agent_sandbox.py +++ b/src/policyengine_api/agent_sandbox.py @@ -235,8 +235,7 @@ def openapi_to_claude_tools(spec: dict) -> list[dict]: prop = schema_to_json_schema(spec, param_schema) prop["description"] = ( - param.get("description", "") - + f" (in: {param_in})" + param.get("description", "") + f" (in: {param_in})" ) properties[param_name] = prop @@ -268,16 +267,18 @@ def openapi_to_claude_tools(spec: dict) -> list[dict]: if required: input_schema["required"] = list(set(required)) - tools.append({ - "name": tool_name, - "description": full_desc[:1024], # Claude has limits - "input_schema": input_schema, - "_meta": { - "path": path, - "method": method, - "parameters": operation.get("parameters", []), - }, - }) + tools.append( + { + "name": tool_name, + "description": full_desc[:1024], # Claude has limits + "input_schema": input_schema, + "_meta": { + "path": path, + "method": method, + "parameters": operation.get("parameters", []), + }, + } + ) return tools @@ -347,7 +348,9 @@ def execute_api_tool( url, params=query_params, json=body_data, headers=headers, timeout=60 ) elif method == "delete": - resp = requests.delete(url, params=query_params, headers=headers, timeout=60) + resp = requests.delete( + url, params=query_params, headers=headers, timeout=60 + ) else: return f"Unsupported method: {method}" @@ -415,9 +418,7 @@ def log(msg: str) -> None: tool_lookup = {t["name"]: t for t in tools} # Strip _meta from tools before sending to Claude (it doesn't need it) - claude_tools = [ - {k: v for k, v in t.items() if k != "_meta"} for t in tools - ] + claude_tools = [{k: v for k, v in t.items() if k != "_meta"} for t in tools] # Add the sleep tool claude_tools.append(SLEEP_TOOL) @@ -477,11 +478,13 @@ def log(msg: str) -> None: log(f"[TOOL_RESULT] {result[:300]}") - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - }) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + } + ) messages.append({"role": "assistant", "content": assistant_content}) diff --git a/src/policyengine_api/api/agent.py b/src/policyengine_api/api/agent.py index 7b7d108..6c26e80 100644 --- a/src/policyengine_api/api/agent.py +++ b/src/policyengine_api/api/agent.py @@ -24,6 +24,7 @@ def get_traceparent() -> str | None: TraceContextTextMapPropagator().inject(carrier) return carrier.get("traceparent") + router = APIRouter(prefix="/agent", tags=["agent"]) @@ -93,7 +94,9 @@ def _run_local_agent( from policyengine_api.agent_sandbox import _run_agent_impl try: - history_dicts = [{"role": m.role, "content": m.content} for m in (history or [])] + history_dicts = [ + {"role": m.role, "content": m.content} for m in (history or []) + ] result = _run_agent_impl(question, api_base_url, call_id, history_dicts) _calls[call_id]["status"] = result.get("status", "completed") _calls[call_id]["result"] = result @@ -136,9 +139,15 @@ async def run_agent(request: RunRequest) -> RunResponse: traceparent = get_traceparent() run_fn = modal.Function.from_name("policyengine-sandbox", "run_agent") - history_dicts = [{"role": m.role, "content": m.content} for m in request.history] + history_dicts = [ + {"role": m.role, "content": m.content} for m in request.history + ] call = run_fn.spawn( - request.question, api_base_url, call_id, history_dicts, traceparent=traceparent + request.question, + api_base_url, + call_id, + history_dicts, + traceparent=traceparent, ) _calls[call_id] = { @@ -166,7 +175,12 @@ async def run_agent(request: RunRequest) -> RunResponse: # Run in background using asyncio loop = asyncio.get_event_loop() loop.run_in_executor( - None, _run_local_agent, call_id, request.question, api_base_url, request.history + None, + _run_local_agent, + call_id, + request.question, + api_base_url, + request.history, ) return RunResponse(call_id=call_id, status="running") diff --git a/src/policyengine_api/api/household.py b/src/policyengine_api/api/household.py index 0e89b5e..eba986d 100644 --- a/src/policyengine_api/api/household.py +++ b/src/policyengine_api/api/household.py @@ -300,11 +300,13 @@ def _calculate_household_uk( from pathlib import Path import pandas as pd - from policyengine.core import Simulation from microdf import MicroDataFrame + from policyengine.core import Simulation from policyengine.tax_benefit_models.uk import uk_latest - from policyengine.tax_benefit_models.uk.datasets import PolicyEngineUKDataset - from policyengine.tax_benefit_models.uk.datasets import UKYearData + from policyengine.tax_benefit_models.uk.datasets import ( + PolicyEngineUKDataset, + UKYearData, + ) n_people = len(people) n_benunits = max(1, len(benunit)) @@ -466,7 +468,14 @@ def _run_local_household_us( try: result = _calculate_household_us( - people, marital_unit, family, spm_unit, tax_unit, household, year, policy_data + people, + marital_unit, + family, + spm_unit, + tax_unit, + household, + year, + policy_data, ) # Update job with result @@ -512,11 +521,13 @@ def _calculate_household_us( from pathlib import Path import pandas as pd - from policyengine.core import Simulation from microdf import MicroDataFrame + from policyengine.core import Simulation from policyengine.tax_benefit_models.us import us_latest - from policyengine.tax_benefit_models.us.datasets import PolicyEngineUSDataset - from policyengine.tax_benefit_models.us.datasets import USYearData + from policyengine.tax_benefit_models.us.datasets import ( + PolicyEngineUSDataset, + USYearData, + ) n_people = len(people) n_households = max(1, len(household)) @@ -672,7 +683,9 @@ def safe_convert(value): except (ValueError, TypeError): return str(value) - def extract_entity_outputs(entity_name: str, entity_data, n_rows: int) -> list[dict]: + def extract_entity_outputs( + entity_name: str, entity_data, n_rows: int + ) -> list[dict]: outputs = [] for i in range(n_rows): row_dict = {} diff --git a/src/policyengine_api/api/policies.py b/src/policyengine_api/api/policies.py index 1f4c744..ad5397d 100644 --- a/src/policyengine_api/api/policies.py +++ b/src/policyengine_api/api/policies.py @@ -123,7 +123,9 @@ def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)) @router.get("/", response_model=List[PolicyRead]) def list_policies( - tax_benefit_model_id: UUID | None = Query(None, description="Filter by tax benefit model"), + tax_benefit_model_id: UUID | None = Query( + None, description="Filter by tax benefit model" + ), session: Session = Depends(get_session), ): """List all policies, optionally filtered by tax benefit model.""" diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py index 3ac2786..4025cc9 100644 --- a/src/policyengine_api/api/user_policies.py +++ b/src/policyengine_api/api/user_policies.py @@ -56,7 +56,9 @@ def create_user_policy( @router.get("/", response_model=list[UserPolicyRead]) def list_user_policies( user_id: UUID = Query(..., description="User ID to filter by"), - tax_benefit_model_id: UUID | None = Query(None, description="Filter by tax benefit model"), + tax_benefit_model_id: UUID | None = Query( + None, description="Filter by tax benefit model" + ), session: Session = Depends(get_session), ): """List all policy associations for a user. @@ -66,10 +68,8 @@ def list_user_policies( query = select(UserPolicy).where(UserPolicy.user_id == user_id) if tax_benefit_model_id: - query = ( - query - .join(Policy, UserPolicy.policy_id == Policy.id) - .where(Policy.tax_benefit_model_id == tax_benefit_model_id) + query = query.join(Policy, UserPolicy.policy_id == Policy.id).where( + Policy.tax_benefit_model_id == tax_benefit_model_id ) user_policies = session.exec(query).all() diff --git a/src/policyengine_api/api/variables.py b/src/policyengine_api/api/variables.py index d660b1b..3c24f3d 100644 --- a/src/policyengine_api/api/variables.py +++ b/src/policyengine_api/api/variables.py @@ -56,9 +56,9 @@ def list_variables( # Case-insensitive search using ILIKE # Note: Variables don't have a label field, only name and description search_pattern = f"%{search}%" - search_filter = Variable.name.ilike(search_pattern) | Variable.description.ilike( + search_filter = Variable.name.ilike( search_pattern - ) + ) | Variable.description.ilike(search_pattern) query = query.where(search_filter) variables = session.exec( diff --git a/src/policyengine_api/modal_app.py b/src/policyengine_api/modal_app.py index 332c349..84f8e89 100644 --- a/src/policyengine_api/modal_app.py +++ b/src/policyengine_api/modal_app.py @@ -242,13 +242,13 @@ def simulate_household_uk( engine = create_engine(database_url) try: - from policyengine.core import Simulation from microdf import MicroDataFrame + from policyengine.core import Simulation from policyengine.tax_benefit_models.uk import uk_latest from policyengine.tax_benefit_models.uk.datasets import ( PolicyEngineUKDataset, + UKYearData, ) - from policyengine.tax_benefit_models.uk.datasets import UKYearData n_people = len(people) n_benunits = max(1, len(benunit)) @@ -487,13 +487,13 @@ def simulate_household_us( engine = create_engine(database_url) try: - from policyengine.core import Simulation from microdf import MicroDataFrame + from policyengine.core import Simulation from policyengine.tax_benefit_models.us import us_latest from policyengine.tax_benefit_models.us.datasets import ( PolicyEngineUSDataset, + USYearData, ) - from policyengine.tax_benefit_models.us.datasets import USYearData n_people = len(people) n_households = max(1, len(household)) diff --git a/tests/test_agent.py b/tests/test_agent.py index 2c591f5..55bb2c2 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -9,6 +9,7 @@ import json from unittest.mock import AsyncMock, MagicMock, patch + from fastapi.testclient import TestClient from policyengine_api.main import app diff --git a/tests/test_agent_policy_questions.py b/tests/test_agent_policy_questions.py index 1550f89..289d73c 100644 --- a/tests/test_agent_policy_questions.py +++ b/tests/test_agent_policy_questions.py @@ -11,10 +11,10 @@ pytestmark = pytest.mark.integration -from policyengine_api.agent_sandbox import _run_agent_impl - import os +from policyengine_api.agent_sandbox import _run_agent_impl + # Use local API by default, override with POLICYENGINE_API_URL env var API_BASE = os.environ.get("POLICYENGINE_API_URL", "http://localhost:8000") @@ -218,4 +218,6 @@ def test_turn_efficiency(self, question, max_expected_turns): print(f"Result: {result['result'][:300]}") if result["turns"] > max_expected_turns: - print(f"WARNING: Took {result['turns']} turns, expected <= {max_expected_turns}") + print( + f"WARNING: Took {result['turns']} turns, expected <= {max_expected_turns}" + ) diff --git a/tests/test_integration.py b/tests/test_integration.py index e044cab..e055423 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -9,6 +9,7 @@ pytestmark = pytest.mark.integration from datetime import datetime, timezone + from rich.console import Console from sqlmodel import Session, create_engine, select From d9a131f2d03bc90c4f81bf41080131625788f528 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Thu, 12 Feb 2026 20:04:27 +0530 Subject: [PATCH 07/15] refactor: Switch to auto-generated initial migration from PR #80 --- .../versions/20260204_0001_initial_schema.py | 538 ------------------ .../20260205_0002_add_user_policies.py | 4 +- 2 files changed, 2 insertions(+), 540 deletions(-) delete mode 100644 alembic/versions/20260204_0001_initial_schema.py diff --git a/alembic/versions/20260204_0001_initial_schema.py b/alembic/versions/20260204_0001_initial_schema.py deleted file mode 100644 index f488734..0000000 --- a/alembic/versions/20260204_0001_initial_schema.py +++ /dev/null @@ -1,538 +0,0 @@ -"""Initial schema (main branch state) - -Revision ID: 0001_initial -Revises: -Create Date: 2026-02-04 - -This migration creates all base tables for the PolicyEngine API as they -exist on the main branch, BEFORE the household CRUD changes. -""" - -from typing import Sequence, Union - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "0001_initial" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Create all tables as they exist on main branch.""" - # ======================================================================== - # TIER 1: Tables with no foreign key dependencies - # ======================================================================== - - # Tax benefit models (e.g., "uk", "us") - op.create_table( - "tax_benefit_models", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - ) - - # Users - op.create_table( - "users", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("first_name", sa.String(), nullable=False), - sa.Column("last_name", sa.String(), nullable=False), - sa.Column("email", sa.String(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("email"), - ) - op.create_index("ix_users_email", "users", ["email"]) - - # Policies (reform definitions) - op.create_table( - "policies", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - ) - - # Dynamics (behavioral response definitions) - op.create_table( - "dynamics", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - ) - - # ======================================================================== - # TIER 2: Tables depending on tier 1 - # ======================================================================== - - # Tax benefit model versions - op.create_table( - "tax_benefit_model_versions", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("model_id", sa.Uuid(), nullable=False), - sa.Column("version", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["model_id"], ["tax_benefit_models.id"]), - ) - - # Datasets (h5 files in storage) - op.create_table( - "datasets", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column("filepath", sa.String(), nullable=False), - sa.Column("year", sa.Integer(), nullable=False), - sa.Column("is_output_dataset", sa.Boolean(), nullable=False, default=False), - sa.Column("tax_benefit_model_id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["tax_benefit_model_id"], ["tax_benefit_models.id"]), - ) - - # ======================================================================== - # TIER 3: Tables depending on tier 2 - # ======================================================================== - - # Parameters (tax-benefit system parameters) - op.create_table( - "parameters", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("label", sa.String(), nullable=True), - sa.Column("description", sa.String(), nullable=True), - sa.Column("data_type", sa.String(), nullable=True), - sa.Column("unit", sa.String(), nullable=True), - sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] - ), - ) - - # Variables (tax-benefit system variables) - op.create_table( - "variables", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column("data_type", sa.String(), nullable=True), - sa.Column("possible_values", sa.JSON(), nullable=True), - sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] - ), - ) - - # Dataset versions - op.create_table( - "dataset_versions", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("name", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=False), - sa.Column("dataset_id", sa.Uuid(), nullable=False), - sa.Column("tax_benefit_model_id", sa.Uuid(), nullable=False), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["dataset_id"], ["datasets.id"]), - sa.ForeignKeyConstraint(["tax_benefit_model_id"], ["tax_benefit_models.id"]), - ) - - # ======================================================================== - # TIER 4: Tables depending on tier 3 - # ======================================================================== - - # Parameter values (policy/dynamic parameter modifications) - op.create_table( - "parameter_values", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("parameter_id", sa.Uuid(), nullable=False), - sa.Column("value_json", sa.JSON(), nullable=True), - sa.Column("start_date", sa.DateTime(timezone=True), nullable=False), - sa.Column("end_date", sa.DateTime(timezone=True), nullable=True), - sa.Column("policy_id", sa.Uuid(), nullable=True), - sa.Column("dynamic_id", sa.Uuid(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["parameter_id"], ["parameters.id"]), - sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), - sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), - ) - - # Simulations (economy calculations) - NOTE: No household support yet - op.create_table( - "simulations", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("dataset_id", sa.Uuid(), nullable=False), # Required in main - sa.Column("policy_id", sa.Uuid(), nullable=True), - sa.Column("dynamic_id", sa.Uuid(), nullable=True), - sa.Column("tax_benefit_model_version_id", sa.Uuid(), nullable=False), - sa.Column("output_dataset_id", sa.Uuid(), nullable=True), - sa.Column("status", sa.String(), nullable=False, default="pending"), - sa.Column("error_message", sa.String(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["dataset_id"], ["datasets.id"]), - sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), - sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), - sa.ForeignKeyConstraint( - ["tax_benefit_model_version_id"], ["tax_benefit_model_versions.id"] - ), - sa.ForeignKeyConstraint(["output_dataset_id"], ["datasets.id"]), - ) - - # Household jobs (async household calculations) - legacy approach - op.create_table( - "household_jobs", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("tax_benefit_model_name", sa.String(), nullable=False), - sa.Column("request_data", sa.JSON(), nullable=False), - sa.Column("policy_id", sa.Uuid(), nullable=True), - sa.Column("dynamic_id", sa.Uuid(), nullable=True), - sa.Column("status", sa.String(), nullable=False, default="pending"), - sa.Column("error_message", sa.String(), nullable=True), - sa.Column("result", sa.JSON(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), - sa.ForeignKeyConstraint(["dynamic_id"], ["dynamics.id"]), - ) - - # ======================================================================== - # TIER 5: Tables depending on simulations - # ======================================================================== - - # Reports (analysis reports) - NOTE: No report_type yet - op.create_table( - "reports", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("label", sa.String(), nullable=False), - sa.Column("description", sa.String(), nullable=True), - sa.Column("user_id", sa.Uuid(), nullable=True), - sa.Column("markdown", sa.Text(), nullable=True), - sa.Column("parent_report_id", sa.Uuid(), nullable=True), - sa.Column("status", sa.String(), nullable=False, default="pending"), - sa.Column("error_message", sa.String(), nullable=True), - sa.Column("baseline_simulation_id", sa.Uuid(), nullable=True), - sa.Column("reform_simulation_id", sa.Uuid(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["user_id"], ["users.id"]), - sa.ForeignKeyConstraint(["parent_report_id"], ["reports.id"]), - sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), - ) - - # Aggregates (single-simulation aggregate outputs) - op.create_table( - "aggregates", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("simulation_id", sa.Uuid(), nullable=False), - sa.Column("user_id", sa.Uuid(), nullable=True), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("variable", sa.String(), nullable=False), - sa.Column("aggregate_type", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=True), - sa.Column("filter_config", sa.JSON(), nullable=False, default={}), - sa.Column("status", sa.String(), nullable=False, default="pending"), - sa.Column("error_message", sa.String(), nullable=True), - sa.Column("result", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["user_id"], ["users.id"]), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), - ) - - # Change aggregates (baseline vs reform comparison) - op.create_table( - "change_aggregates", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), - sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), - sa.Column("user_id", sa.Uuid(), nullable=True), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("variable", sa.String(), nullable=False), - sa.Column("aggregate_type", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=True), - sa.Column("filter_config", sa.JSON(), nullable=False, default={}), - sa.Column("change_geq", sa.Float(), nullable=True), - sa.Column("change_leq", sa.Float(), nullable=True), - sa.Column("status", sa.String(), nullable=False, default="pending"), - sa.Column("error_message", sa.String(), nullable=True), - sa.Column("result", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["user_id"], ["users.id"]), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), - ) - - # Decile impacts - op.create_table( - "decile_impacts", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), - sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("income_variable", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=True), - sa.Column("decile", sa.Integer(), nullable=False), - sa.Column("quantiles", sa.Integer(), nullable=False, default=10), - sa.Column("baseline_mean", sa.Float(), nullable=True), - sa.Column("reform_mean", sa.Float(), nullable=True), - sa.Column("absolute_change", sa.Float(), nullable=True), - sa.Column("relative_change", sa.Float(), nullable=True), - sa.Column("count_better_off", sa.Float(), nullable=True), - sa.Column("count_worse_off", sa.Float(), nullable=True), - sa.Column("count_no_change", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), - ) - - # Program statistics - op.create_table( - "program_statistics", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("baseline_simulation_id", sa.Uuid(), nullable=False), - sa.Column("reform_simulation_id", sa.Uuid(), nullable=False), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("program_name", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=False), - sa.Column("is_tax", sa.Boolean(), nullable=False, default=False), - sa.Column("baseline_total", sa.Float(), nullable=True), - sa.Column("reform_total", sa.Float(), nullable=True), - sa.Column("change", sa.Float(), nullable=True), - sa.Column("baseline_count", sa.Float(), nullable=True), - sa.Column("reform_count", sa.Float(), nullable=True), - sa.Column("winners", sa.Float(), nullable=True), - sa.Column("losers", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint(["baseline_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["reform_simulation_id"], ["simulations.id"]), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"]), - ) - - # Poverty - op.create_table( - "poverty", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("simulation_id", sa.Uuid(), nullable=False), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("poverty_type", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=False, default="person"), - sa.Column("filter_variable", sa.String(), nullable=True), - sa.Column("headcount", sa.Float(), nullable=True), - sa.Column("total_population", sa.Float(), nullable=True), - sa.Column("rate", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["simulation_id"], ["simulations.id"], ondelete="CASCADE" - ), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"], ondelete="CASCADE"), - ) - op.create_index("idx_poverty_simulation_id", "poverty", ["simulation_id"]) - op.create_index("idx_poverty_report_id", "poverty", ["report_id"]) - - # Inequality - op.create_table( - "inequality", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("simulation_id", sa.Uuid(), nullable=False), - sa.Column("report_id", sa.Uuid(), nullable=True), - sa.Column("income_variable", sa.String(), nullable=False), - sa.Column("entity", sa.String(), nullable=False, default="household"), - sa.Column("gini", sa.Float(), nullable=True), - sa.Column("top_10_share", sa.Float(), nullable=True), - sa.Column("top_1_share", sa.Float(), nullable=True), - sa.Column("bottom_50_share", sa.Float(), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.func.now(), - nullable=False, - ), - sa.PrimaryKeyConstraint("id"), - sa.ForeignKeyConstraint( - ["simulation_id"], ["simulations.id"], ondelete="CASCADE" - ), - sa.ForeignKeyConstraint(["report_id"], ["reports.id"], ondelete="CASCADE"), - ) - op.create_index("idx_inequality_simulation_id", "inequality", ["simulation_id"]) - op.create_index("idx_inequality_report_id", "inequality", ["report_id"]) - - -def downgrade() -> None: - """Drop all tables in reverse order.""" - # Tier 5 - op.drop_index("idx_inequality_report_id", "inequality") - op.drop_index("idx_inequality_simulation_id", "inequality") - op.drop_table("inequality") - op.drop_index("idx_poverty_report_id", "poverty") - op.drop_index("idx_poverty_simulation_id", "poverty") - op.drop_table("poverty") - op.drop_table("program_statistics") - op.drop_table("decile_impacts") - op.drop_table("change_aggregates") - op.drop_table("aggregates") - op.drop_table("reports") - - # Tier 4 - op.drop_table("household_jobs") - op.drop_table("simulations") - op.drop_table("parameter_values") - - # Tier 3 - op.drop_table("dataset_versions") - op.drop_table("variables") - op.drop_table("parameters") - - # Tier 2 - op.drop_table("datasets") - op.drop_table("tax_benefit_model_versions") - - # Tier 1 - op.drop_table("dynamics") - op.drop_table("policies") - op.drop_index("ix_users_email", "users") - op.drop_table("users") - op.drop_table("tax_benefit_models") diff --git a/alembic/versions/20260205_0002_add_user_policies.py b/alembic/versions/20260205_0002_add_user_policies.py index b19a6cf..3af5e7f 100644 --- a/alembic/versions/20260205_0002_add_user_policies.py +++ b/alembic/versions/20260205_0002_add_user_policies.py @@ -1,7 +1,7 @@ """add_user_policies Revision ID: 0002_user_policies -Revises: 0001_initial +Revises: 36f9d434e95b Create Date: 2026-02-05 This migration adds: @@ -18,7 +18,7 @@ # revision identifiers, used by Alembic. revision: str = "0002_user_policies" -down_revision: Union[str, Sequence[str], None] = "0001_initial" +down_revision: Union[str, Sequence[str], None] = "36f9d434e95b" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None From 8825e2d6e402699dfb3c717351c4e4b0a0f17502 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Thu, 12 Feb 2026 20:56:55 +0530 Subject: [PATCH 08/15] feat: Remove user FK constraint for client-generated UUIDs --- .../20260205_0002_add_user_policies.py | 6 +- src/policyengine_api/api/user_policies.py | 12 +- src/policyengine_api/models/user_policy.py | 8 +- tests/test_user_policies.py | 106 ++++++------------ 4 files changed, 52 insertions(+), 80 deletions(-) diff --git a/alembic/versions/20260205_0002_add_user_policies.py b/alembic/versions/20260205_0002_add_user_policies.py index 3af5e7f..df3b707 100644 --- a/alembic/versions/20260205_0002_add_user_policies.py +++ b/alembic/versions/20260205_0002_add_user_policies.py @@ -7,6 +7,10 @@ This migration adds: 1. tax_benefit_model_id foreign key to policies table 2. user_policies table for user-policy associations + +Note: user_id in user_policies is NOT a foreign key to users table. +It's a client-generated UUID stored in localStorage, allowing anonymous +users to save policies without authentication. """ from typing import Sequence, Union @@ -44,6 +48,7 @@ def upgrade() -> None: ) # Create user_policies table + # Note: user_id is NOT a foreign key - it's a client-generated UUID from localStorage op.create_table( "user_policies", sa.Column("user_id", sa.Uuid(), nullable=False), @@ -53,7 +58,6 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), - sa.ForeignKeyConstraint(["user_id"], ["users.id"]), sa.PrimaryKeyConstraint("id"), ) op.create_index( diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py index 4025cc9..b01ca15 100644 --- a/src/policyengine_api/api/user_policies.py +++ b/src/policyengine_api/api/user_policies.py @@ -3,6 +3,10 @@ Associates users with policies they've saved/created. This enables users to maintain a list of their policies across sessions without duplicating the underlying policy data. + +Note: user_id is a client-generated UUID (via crypto.randomUUID()) stored in +the browser's localStorage. It is NOT validated against a users table, allowing +anonymous users to save policies without authentication. """ from datetime import datetime, timezone @@ -13,7 +17,6 @@ from policyengine_api.models import ( Policy, - User, UserPolicy, UserPolicyCreate, UserPolicyRead, @@ -34,12 +37,9 @@ def create_user_policy( Associates a user with a policy, allowing them to save it to their list. Duplicates are allowed - users can save the same policy multiple times with different labels (matching FE localStorage behavior). - """ - # Validate user exists - user = session.get(User, user_policy.user_id) - if not user: - raise HTTPException(status_code=404, detail="User not found") + Note: user_id is not validated - it's a client-generated UUID from localStorage. + """ # Validate policy exists policy = session.get(Policy, user_policy.policy_id) if not policy: diff --git a/src/policyengine_api/models/user_policy.py b/src/policyengine_api/models/user_policy.py index 6bdf9ff..b85505f 100644 --- a/src/policyengine_api/models/user_policy.py +++ b/src/policyengine_api/models/user_policy.py @@ -6,13 +6,16 @@ if TYPE_CHECKING: from .policy import Policy - from .user import User class UserPolicyBase(SQLModel): """Base user-policy association fields.""" - user_id: UUID = Field(foreign_key="users.id", index=True) + # user_id is a client-generated UUID stored in localStorage, not a foreign key. + # This allows anonymous users to save policies without requiring authentication. + # The UUID is generated once per browser via crypto.randomUUID() and persisted + # in localStorage for stable identity across sessions. + user_id: UUID = Field(index=True) policy_id: UUID = Field(foreign_key="policies.id", index=True) label: str | None = None @@ -27,7 +30,6 @@ class UserPolicy(UserPolicyBase, table=True): updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) # Relationships - user: "User" = Relationship() policy: "Policy" = Relationship() diff --git a/tests/test_user_policies.py b/tests/test_user_policies.py index f4d1910..94725f1 100644 --- a/tests/test_user_policies.py +++ b/tests/test_user_policies.py @@ -1,46 +1,45 @@ -"""Tests for user-policy association endpoints.""" +"""Tests for user-policy association endpoints. + +Note: user_id is a client-generated UUID (not validated against users table), +so tests use uuid4() directly rather than creating User records. +""" from uuid import uuid4 -from policyengine_api.models import Policy, User, UserPolicy +from policyengine_api.models import Policy, UserPolicy -def test_list_user_policies_empty(client, session): +def test_list_user_policies_empty(client): """List user policies returns empty list when user has no associations.""" - user = User(first_name="Test", last_name="User", email="test@example.com") - session.add(user) - session.commit() - - response = client.get(f"/user-policies?user_id={user.id}") + user_id = uuid4() + response = client.get(f"/user-policies?user_id={user_id}") assert response.status_code == 200 assert response.json() == [] def test_create_user_policy(client, session, tax_benefit_model): """Create a new user-policy association.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) response = client.post( "/user-policies", json={ - "user_id": str(user.id), + "user_id": str(user_id), "policy_id": str(policy.id), "label": "My test policy", }, ) assert response.status_code == 200 data = response.json() - assert data["user_id"] == str(user.id) + assert data["user_id"] == str(user_id) assert data["policy_id"] == str(policy.id) assert data["label"] == "My test policy" assert "id" in data @@ -50,22 +49,20 @@ def test_create_user_policy(client, session, tax_benefit_model): def test_create_user_policy_without_label(client, session, tax_benefit_model): """Create a user-policy association without a label.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) response = client.post( "/user-policies", json={ - "user_id": str(user.id), + "user_id": str(user_id), "policy_id": str(policy.id), }, ) @@ -74,34 +71,15 @@ def test_create_user_policy_without_label(client, session, tax_benefit_model): assert data["label"] is None -def test_create_user_policy_user_not_found(client): - """Create user-policy association with non-existent user returns 404.""" - fake_user_id = uuid4() - fake_policy_id = uuid4() - - response = client.post( - "/user-policies", - json={ - "user_id": str(fake_user_id), - "policy_id": str(fake_policy_id), - }, - ) - assert response.status_code == 404 - assert response.json()["detail"] == "User not found" - - -def test_create_user_policy_policy_not_found(client, session): +def test_create_user_policy_policy_not_found(client): """Create user-policy association with non-existent policy returns 404.""" - user = User(first_name="Test", last_name="User", email="test@example.com") - session.add(user) - session.commit() - session.refresh(user) - + user_id = uuid4() fake_policy_id = uuid4() + response = client.post( "/user-policies", json={ - "user_id": str(user.id), + "user_id": str(user_id), "policy_id": str(fake_policy_id), }, ) @@ -111,21 +89,19 @@ def test_create_user_policy_policy_not_found(client, session): def test_create_user_policy_duplicate_allowed(client, session, tax_benefit_model): """Creating duplicate user-policy association is allowed (matches FE localStorage behavior).""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) # Create first association user_policy = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy.id, ) session.add(user_policy) @@ -135,14 +111,14 @@ def test_create_user_policy_duplicate_allowed(client, session, tax_benefit_model response = client.post( "/user-policies", json={ - "user_id": str(user.id), + "user_id": str(user_id), "policy_id": str(policy.id), }, ) assert response.status_code == 200 data = response.json() assert data["id"] != str(user_policy.id) # New association created - assert data["user_id"] == str(user.id) + assert data["user_id"] == str(user_id) assert data["policy_id"] == str(policy.id) @@ -150,7 +126,7 @@ def test_list_user_policies_with_data( client, session, tax_benefit_model, uk_tax_benefit_model ): """List user policies returns all associations for a user.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy1 = Policy( name="Policy 1", description="First policy", @@ -161,21 +137,19 @@ def test_list_user_policies_with_data( description="Second policy", tax_benefit_model_id=uk_tax_benefit_model.id, ) - session.add(user) session.add(policy1) session.add(policy2) session.commit() - session.refresh(user) session.refresh(policy1) session.refresh(policy2) user_policy1 = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy1.id, label="US policy", ) user_policy2 = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy2.id, label="UK policy", ) @@ -183,7 +157,7 @@ def test_list_user_policies_with_data( session.add(user_policy2) session.commit() - response = client.get(f"/user-policies?user_id={user.id}") + response = client.get(f"/user-policies?user_id={user_id}") assert response.status_code == 200 data = response.json() assert len(data) == 2 @@ -193,7 +167,7 @@ def test_list_user_policies_filter_by_tax_benefit_model( client, session, tax_benefit_model, uk_tax_benefit_model ): """List user policies filtered by tax_benefit_model_id via Policy join.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy1 = Policy( name="Policy 1", description="First policy", @@ -204,20 +178,18 @@ def test_list_user_policies_filter_by_tax_benefit_model( description="Second policy", tax_benefit_model_id=uk_tax_benefit_model.id, ) - session.add(user) session.add(policy1) session.add(policy2) session.commit() - session.refresh(user) session.refresh(policy1) session.refresh(policy2) user_policy1 = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy1.id, ) user_policy2 = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy2.id, ) session.add(user_policy1) @@ -225,7 +197,7 @@ def test_list_user_policies_filter_by_tax_benefit_model( session.commit() response = client.get( - f"/user-policies?user_id={user.id}&tax_benefit_model_id={tax_benefit_model.id}" + f"/user-policies?user_id={user_id}&tax_benefit_model_id={tax_benefit_model.id}" ) assert response.status_code == 200 data = response.json() @@ -235,20 +207,18 @@ def test_list_user_policies_filter_by_tax_benefit_model( def test_get_user_policy(client, session, tax_benefit_model): """Get a specific user-policy association by ID.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) user_policy = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy.id, label="My policy", ) @@ -273,20 +243,18 @@ def test_get_user_policy_not_found(client): def test_update_user_policy(client, session, tax_benefit_model): """Update a user-policy association label.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) user_policy = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy.id, label="Old label", ) @@ -316,20 +284,18 @@ def test_update_user_policy_not_found(client): def test_delete_user_policy(client, session, tax_benefit_model): """Delete a user-policy association.""" - user = User(first_name="Test", last_name="User", email="test@example.com") + user_id = uuid4() policy = Policy( name="Test policy", description="A test policy", tax_benefit_model_id=tax_benefit_model.id, ) - session.add(user) session.add(policy) session.commit() - session.refresh(user) session.refresh(policy) user_policy = UserPolicy( - user_id=user.id, + user_id=user_id, policy_id=policy.id, ) session.add(user_policy) From c6988817fb7e55c27d5b1d70dba8e7eddec17a77 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Mon, 16 Feb 2026 23:26:04 +0530 Subject: [PATCH 09/15] feat: Use country_id for user-policy filtering --- .../20260205_0002_add_user_policies.py | 8 ++++++ src/policyengine_api/api/user_policies.py | 27 ++++++++++++++----- src/policyengine_api/models/user_policy.py | 1 + 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/alembic/versions/20260205_0002_add_user_policies.py b/alembic/versions/20260205_0002_add_user_policies.py index df3b707..5f061de 100644 --- a/alembic/versions/20260205_0002_add_user_policies.py +++ b/alembic/versions/20260205_0002_add_user_policies.py @@ -53,6 +53,7 @@ def upgrade() -> None: "user_policies", sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("policy_id", sa.Uuid(), nullable=False), + sa.Column("country_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column("label", sqlmodel.sql.sqltypes.AutoString(), nullable=True), sa.Column("id", sa.Uuid(), nullable=False), sa.Column("created_at", sa.DateTime(), nullable=False), @@ -69,11 +70,18 @@ def upgrade() -> None: op.create_index( op.f("ix_user_policies_user_id"), "user_policies", ["user_id"], unique=False ) + op.create_index( + op.f("ix_user_policies_country_id"), + "user_policies", + ["country_id"], + unique=False, + ) def downgrade() -> None: """Remove user_policies table and policy.tax_benefit_model_id.""" # Drop user_policies table + op.drop_index(op.f("ix_user_policies_country_id"), table_name="user_policies") op.drop_index(op.f("ix_user_policies_user_id"), table_name="user_policies") op.drop_index(op.f("ix_user_policies_policy_id"), table_name="user_policies") op.drop_table("user_policies") diff --git a/src/policyengine_api/api/user_policies.py b/src/policyengine_api/api/user_policies.py index b01ca15..e1b6958 100644 --- a/src/policyengine_api/api/user_policies.py +++ b/src/policyengine_api/api/user_policies.py @@ -24,6 +24,9 @@ ) from policyengine_api.services.database import get_session +# Valid country IDs +VALID_COUNTRY_IDS = {"us", "uk"} + router = APIRouter(prefix="/user-policies", tags=["user-policies"]) @@ -40,6 +43,13 @@ def create_user_policy( Note: user_id is not validated - it's a client-generated UUID from localStorage. """ + # Validate country_id + if user_policy.country_id not in VALID_COUNTRY_IDS: + raise HTTPException( + status_code=400, + detail=f"Invalid country_id: {user_policy.country_id}. Must be one of: {list(VALID_COUNTRY_IDS)}", + ) + # Validate policy exists policy = session.get(Policy, user_policy.policy_id) if not policy: @@ -56,21 +66,24 @@ def create_user_policy( @router.get("/", response_model=list[UserPolicyRead]) def list_user_policies( user_id: UUID = Query(..., description="User ID to filter by"), - tax_benefit_model_id: UUID | None = Query( - None, description="Filter by tax benefit model" + country_id: str | None = Query( + None, description="Filter by country (e.g., 'us', 'uk')" ), session: Session = Depends(get_session), ): """List all policy associations for a user. - Returns all policies saved by the specified user. Optionally filter by tax benefit model. + Returns all policies saved by the specified user. Optionally filter by country. """ query = select(UserPolicy).where(UserPolicy.user_id == user_id) - if tax_benefit_model_id: - query = query.join(Policy, UserPolicy.policy_id == Policy.id).where( - Policy.tax_benefit_model_id == tax_benefit_model_id - ) + if country_id: + if country_id not in VALID_COUNTRY_IDS: + raise HTTPException( + status_code=400, + detail=f"Invalid country_id: {country_id}. Must be one of: {list(VALID_COUNTRY_IDS)}", + ) + query = query.where(UserPolicy.country_id == country_id) user_policies = session.exec(query).all() return user_policies diff --git a/src/policyengine_api/models/user_policy.py b/src/policyengine_api/models/user_policy.py index b85505f..7b3917c 100644 --- a/src/policyengine_api/models/user_policy.py +++ b/src/policyengine_api/models/user_policy.py @@ -17,6 +17,7 @@ class UserPolicyBase(SQLModel): # in localStorage for stable identity across sessions. user_id: UUID = Field(index=True) policy_id: UUID = Field(foreign_key="policies.id", index=True) + country_id: str # e.g., "us", "uk" - denormalized for efficient filtering label: str | None = None From 9aa986e5a31c799b0135798af0b0d8ff2a496807 Mon Sep 17 00:00:00 2001 From: SakshiKekre Date: Tue, 17 Feb 2026 02:27:46 +0530 Subject: [PATCH 10/15] chore: Remove docs, temp files, and fix user policy tests for country_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove .DS_Store, docs/, and supabase/.temp/ from git tracking - Add these patterns to .gitignore - Update test_user_policies.py to include country_id in all tests - Rename test_list_user_policies_filter_by_tax_benefit_model to test_list_user_policies_filter_by_country 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 9 + docs/.env.example | 7 - docs/.gitignore | 42 - docs/AGENT_TESTING.md | 192 --- docs/DESIGN.md | 331 ----- docs/README.md | 30 - docs/api-v2-alpha-architecture.md | 264 ---- docs/bun.lock | 1088 ----------------- docs/eslint.config.mjs | 18 - docs/next.config.ts | 12 - docs/package.json | 29 - docs/policy-ingredient-status.md | 271 ---- docs/postcss.config.mjs | 7 - docs/public/file.svg | 1 - docs/public/globe.svg | 1 - docs/public/next.svg | 1 - docs/public/vercel.svg | 1 - docs/public/window.svg | 1 - docs/src/app/agent/page.tsx | 45 - docs/src/app/architecture/page.tsx | 138 --- docs/src/app/design/page.tsx | 151 --- docs/src/app/endpoints/aggregates/page.tsx | 127 -- .../app/endpoints/change-aggregates/page.tsx | 139 --- docs/src/app/endpoints/datasets/page.tsx | 143 --- docs/src/app/endpoints/dynamics/page.tsx | 138 --- .../app/endpoints/economic-impact/page.tsx | 272 ----- .../app/endpoints/household-impact/page.tsx | 131 -- docs/src/app/endpoints/household/page.tsx | 98 -- docs/src/app/endpoints/parameters/page.tsx | 103 -- docs/src/app/endpoints/policies/page.tsx | 160 --- docs/src/app/endpoints/simulations/page.tsx | 158 --- .../app/endpoints/tax-benefit-models/page.tsx | 96 -- docs/src/app/endpoints/variables/page.tsx | 105 -- docs/src/app/favicon.ico | Bin 25931 -> 0 bytes docs/src/app/globals.css | 234 ---- docs/src/app/layout.tsx | 45 - docs/src/app/mcp/page.tsx | 93 -- docs/src/app/modal/page.tsx | 205 ---- docs/src/app/page.tsx | 103 -- docs/src/app/quickstart/page.tsx | 149 --- docs/src/app/reference/models/page.tsx | 172 --- docs/src/app/reference/status-codes/page.tsx | 173 --- docs/src/app/setup/page.tsx | 198 --- docs/src/components/api-context.tsx | 29 - docs/src/components/api-playground.tsx | 184 --- docs/src/components/header.tsx | 46 - docs/src/components/json-viewer.tsx | 87 -- docs/src/components/policy-chat.tsx | 631 ---------- docs/src/components/sidebar.tsx | 107 -- docs/supabase-migration-notes.md | 174 --- docs/tsconfig.json | 42 - supabase/.temp/cli-latest | 1 - supabase/.temp/gotrue-version | 1 - supabase/.temp/pooler-url | 1 - supabase/.temp/postgres-version | 1 - supabase/.temp/project-ref | 1 - supabase/.temp/rest-version | 1 - supabase/.temp/storage-version | 1 - tests/test_user_policies.py | 23 +- 60 files changed, 29 insertions(+), 6982 deletions(-) delete mode 100644 .DS_Store delete mode 100644 docs/.env.example delete mode 100644 docs/.gitignore delete mode 100644 docs/AGENT_TESTING.md delete mode 100644 docs/DESIGN.md delete mode 100644 docs/README.md delete mode 100644 docs/api-v2-alpha-architecture.md delete mode 100644 docs/bun.lock delete mode 100644 docs/eslint.config.mjs delete mode 100644 docs/next.config.ts delete mode 100644 docs/package.json delete mode 100644 docs/policy-ingredient-status.md delete mode 100644 docs/postcss.config.mjs delete mode 100644 docs/public/file.svg delete mode 100644 docs/public/globe.svg delete mode 100644 docs/public/next.svg delete mode 100644 docs/public/vercel.svg delete mode 100644 docs/public/window.svg delete mode 100644 docs/src/app/agent/page.tsx delete mode 100644 docs/src/app/architecture/page.tsx delete mode 100644 docs/src/app/design/page.tsx delete mode 100644 docs/src/app/endpoints/aggregates/page.tsx delete mode 100644 docs/src/app/endpoints/change-aggregates/page.tsx delete mode 100644 docs/src/app/endpoints/datasets/page.tsx delete mode 100644 docs/src/app/endpoints/dynamics/page.tsx delete mode 100644 docs/src/app/endpoints/economic-impact/page.tsx delete mode 100644 docs/src/app/endpoints/household-impact/page.tsx delete mode 100644 docs/src/app/endpoints/household/page.tsx delete mode 100644 docs/src/app/endpoints/parameters/page.tsx delete mode 100644 docs/src/app/endpoints/policies/page.tsx delete mode 100644 docs/src/app/endpoints/simulations/page.tsx delete mode 100644 docs/src/app/endpoints/tax-benefit-models/page.tsx delete mode 100644 docs/src/app/endpoints/variables/page.tsx delete mode 100644 docs/src/app/favicon.ico delete mode 100644 docs/src/app/globals.css delete mode 100644 docs/src/app/layout.tsx delete mode 100644 docs/src/app/mcp/page.tsx delete mode 100644 docs/src/app/modal/page.tsx delete mode 100644 docs/src/app/page.tsx delete mode 100644 docs/src/app/quickstart/page.tsx delete mode 100644 docs/src/app/reference/models/page.tsx delete mode 100644 docs/src/app/reference/status-codes/page.tsx delete mode 100644 docs/src/app/setup/page.tsx delete mode 100644 docs/src/components/api-context.tsx delete mode 100644 docs/src/components/api-playground.tsx delete mode 100644 docs/src/components/header.tsx delete mode 100644 docs/src/components/json-viewer.tsx delete mode 100644 docs/src/components/policy-chat.tsx delete mode 100644 docs/src/components/sidebar.tsx delete mode 100644 docs/supabase-migration-notes.md delete mode 100644 docs/tsconfig.json delete mode 100644 supabase/.temp/cli-latest delete mode 100644 supabase/.temp/gotrue-version delete mode 100644 supabase/.temp/pooler-url delete mode 100644 supabase/.temp/postgres-version delete mode 100644 supabase/.temp/project-ref delete mode 100644 supabase/.temp/rest-version delete mode 100644 supabase/.temp/storage-version diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 4bf6461c4c4dcb4ac126e58927c5525107ec899b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKK~BRk5FA4hEr^0RaY4!lsKgJ_QVvLPq2dfQEvS$-C~Xnka_0|xf%kEOS=)ld z1&I?vXjj@D$KLg3C$(JzFrE2eAJ_s=rwZ2AXuc2`7hRAg-?K?Hc8)F%(ZU#mqjbSr zJCp%s;IAW)Yh97wt#I6kRib>hRCtUR>oC-M>}Ot z3y*wCkJ8Ehmcf|@L!s>I0)m5^WFxQ9~ zX9#M9@K^R;rgx4cU_uxxd)4VR=X{swQ~7lQe_^id-(Y6>ra8T@SZ9uL5Fs3uBQ|Wr zl=hqvBRsG*XWTHXz%o>hT=|F$Yb7(LSz%_5dorK8UrhClgqeyrUr^--49JS0)Yl!V zRR)v+W#EGW*&iaRVC=DQXtxd;dj%lY*lmSv`NJSFk;m9$;gBN~<3fop)c7lgapCNb zykG3GaOlEe{N=;=%*NkPjLpvZBkK+mJ5;L-C<9dnmfho)od1X4-~X#Y`lJjf1OJKv zQ|tD+9hT(J)`jBatc|D_R1xtj9NG{Xe;nI}9K}0SD;$fYL5w{X4%tJ|KLVBpHOj!R GGVl%&a?hs# diff --git a/.gitignore b/.gitignore index 2f8c3e6..4b856db 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,12 @@ docs/.env.local data/ *.h5 *.db + +# macOS +.DS_Store + +# Docs (Next.js site) +docs/ + +# Supabase temp files +supabase/.temp/ diff --git a/docs/.env.example b/docs/.env.example deleted file mode 100644 index bc8a7d1..0000000 --- a/docs/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# Docs site environment variables -# Copy to .env.local for local development - -# API URL for the docs site to connect to -# Local development: http://localhost:8000 -# Production: https://v2.api.policyengine.org -NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 7b8da95..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* -!.env.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/docs/AGENT_TESTING.md b/docs/AGENT_TESTING.md deleted file mode 100644 index c512ce1..0000000 --- a/docs/AGENT_TESTING.md +++ /dev/null @@ -1,192 +0,0 @@ -# Agent testing and optimisation - -This document tracks ongoing work to test and improve the PolicyEngine agent's ability to answer policy questions efficiently. - -## Goal - -Minimise the number of turns the agent needs to answer policy questions by improving API metadata, documentation, and structure - not by hacking for specific test cases. - -## Test categories - -We want comprehensive coverage across: -- **Country**: UK and US -- **Scope**: Household (single family) and Economy (population-wide) -- **Complexity**: Simple (single variable lookup) to Complex (multi-step reforms) - -## Example questions to test - -### UK Household (simple) -- "What is my income tax if I earn £50,000?" -- "How much child benefit would a family with 2 children receive?" - -### UK Household (complex) -- "Compare my net income under current law vs if the basic rate was 25%" -- "What's the marginal tax rate for someone earning £100,000?" - -### UK Economy (simple) -- "What's the total cost of child benefit?" -- "How many people pay higher rate tax?" - -### UK Economy (complex) -- "What would be the budgetary impact of raising the personal allowance to £15,000?" -- "How would a £500 UBI affect poverty rates?" - -### US Household (simple) -- "What is my federal income tax if I earn $75,000?" -- "How much SNAP would a family of 4 with $30,000 income receive?" - -### US Household (complex) -- "Compare my benefits under current law vs doubling the EITC" -- "What's my marginal tax rate including state taxes in California?" - -### US Economy (simple) -- "What's the total cost of SNAP?" -- "How many households receive the EITC?" - -### US Economy (complex) -- "What would be the budgetary impact of expanding the Child Tax Credit to $3,600?" -- "How would eliminating the SALT cap affect different income deciles?" - -## Current agent architecture - -The agent uses Claude Code in a Modal sandbox with: -- System prompt containing API documentation (see `src/policyengine_api/prompts/`) -- Direct HTTP calls via curl to the PolicyEngine API -- No MCP (it was causing issues in Modal containers) - -## Optimisation strategies - -1. **Improve system prompt** - Make API usage clearer, provide more examples -2. **Add API response examples** - Show what successful responses look like -3. **Parameter documentation** - Ensure all parameters are well-documented with valid values -4. **Error messages** - Make error messages actionable so agent can self-correct -5. **Endpoint discoverability** - Help agent find the right endpoint quickly - -## Test file location - -Tests are in `tests/test_agent_policy_questions.py` (integration tests requiring Modal). - -## How to continue this work - -1. Run existing tests: `pytest tests/test_agent_policy_questions.py -v -s` -2. Check agent logs in Logfire for turn counts and errors -3. Identify common failure patterns -4. Improve prompts/metadata to address failures -5. Add new test cases as coverage expands - -## Observed issues - -### Issue 1: Parameter search doesn't filter by country (9 turns for personal allowance) - -**Problem**: When searching for "personal allowance", the agent gets US results (Illinois AABD) mixed with UK results. It took 9 turns to find the UK personal allowance. - -**Agent's failed searches**: -1. "personal allowance" → Illinois AABD (US) -2. "income tax personal allowance" → empty -3. "income_tax" → US CBO parameters -4. "basic rate" → UK CGT (closer!) -5. "allowance" → California SSI (US) -6. "hmrc income_tax allowances personal" → empty -7. "hmrc.income_tax.allowances" → found it! - -**Solution implemented**: -- Added `tax_benefit_model_name` filter to `/parameters/` endpoint -- Updated system prompt to instruct agent to use country filter - -**NOT acceptable solutions** (test hacking): -- Adding specific parameter name examples to system prompt -- Telling agent exactly what to search for - -### Issue 2: Duplicate parameters in database - -**Problem**: Same parameter name exists with multiple IDs. One has values, one doesn't. Agent picks wrong one first. - -**Example**: `gov.hmrc.income_tax.allowances.personal_allowance.amount` has two entries with different IDs. - -**Solution implemented**: Deduplicate parameters by name in seed script (`seen_names` set). - -### Issue 6: Case-sensitive search - -**Problem**: Search for "personal allowance" didn't find "Personal allowance" (capital P). - -**Solution implemented**: Changed search to use `ILIKE` instead of `contains` for case-insensitive matching. - -### Issue 7: Model name mismatch - -**Problem**: System prompt said `policyengine_uk` but database has `policyengine-uk` (hyphen vs underscore). - -**Solution implemented**: Updated system prompt and API docstrings to use correct model names with hyphens. - -### Issue 3: Variables endpoint lacks search - -**Problem**: `/variables/` had no search or country filter. Agent can't discover variable names. - -**Solution implemented**: Added `search` and `tax_benefit_model_name` filters to `/variables/`. - -### Issue 4: Datasets endpoint lacks country filter - -**Problem**: `/datasets/` returned all datasets, mixing UK and US. - -**Solution implemented**: Added `tax_benefit_model_name` filter to `/datasets/`. - -### Issue 5: Parameter values lack "current" filter - -**Problem**: Agent had to parse through all historical values to find current one. - -**Solution implemented**: Added `current=true` filter to `/parameter-values/` endpoint. - -## API improvements summary - -| Endpoint | Improvement | -|----------|-------------| -| `/parameters/` | Added `tax_benefit_model_name` filter, case-insensitive search | -| `/variables/` | Added `search` and `tax_benefit_model_name` filters, case-insensitive search | -| `/datasets/` | Added `tax_benefit_model_name` filter | -| `/parameter-values/` | Added `current` filter | -| Seed script | Deduplicate parameters by name | -| System prompt | Fixed model names (hyphen not underscore) | - -### Issue 8: Agent not using country filter in economy-wide analysis - -**Problem**: When answering economic impact questions, agent didn't use `tax_benefit_model_name` filter despite it being in the system prompt. This led to 18 turns for UK budgetary impact question (12 turns just searching for parameter). - -**Root cause**: System prompt mentioned the filter but didn't emphasize it enough; economic impact workflow didn't show the filter in example. - -**Solution implemented**: Restructured system prompt with: -- **CRITICAL** section at the top emphasizing country filter -- Explanation of why filter is needed (mixed results waste turns) -- Added filter to all workflow examples including economic impact - -**Result**: UK budgetary impact question now completes in **6 turns** (down from 18). - -### Issue 9: Key US parameters missing from database - -**Problem**: Core CTC parameters like `gov.irs.credits.ctc.amount.base[0].amount` have `label=None` in policyengine-us package, so they're not seeded (seed script only includes parameters with labels). - -**Impact**: Agent can't find the main CTC amount parameter to double it. Had to use `refundable.individual_max` as a proxy. - -**Solution needed**: Add labels to core parameters in policyengine-us package (upstream fix). - -## Measurements - -| Question type | Baseline | After improvements | Target | -|---------------|----------|-------------------|--------| -| Parameter lookup (UK personal allowance) | 10 turns | **3 turns** | 3-4 | -| Household calculation (UK £50k income) | 6 turns | - | 5-6 | -| Economy-wide (UK budgetary impact) | 18 turns | **6 turns** | 5-8 | -| Economy-wide (US CTC impact) | 20+ turns | - | 8-10 | - -## Progress log - -- 2024-12-30: Initial setup, created test framework and first batch of questions -- 2024-12-30: Tested personal allowance lookup - 9-10 turns (target: 3-4). Root cause: no country filter on parameter search -- 2024-12-30: Added `tax_benefit_model_name` filter to `/parameters/`, `/variables/`, `/datasets/` -- 2024-12-30: Tested household calc - 6 turns (acceptable). Async polling is the overhead -- 2024-12-30: Discovered duplicate parameters in DB causing extra turns -- 2024-12-30: Fixed model name mismatch (policyengine-uk with hyphen, not underscore) -- 2024-12-30: Added case-insensitive search using ILIKE -- 2024-12-30: Tested personal allowance lookup - **3 turns** (target met!) -- 2025-12-30: Tested UK economy-wide (budgetary impact) - 18 turns initially -- 2025-12-30: Restructured system prompt to emphasize country filter at top -- 2025-12-30: UK economy-wide now **6 turns** (3x improvement) -- 2025-12-30: Discovered US CTC parameters missing labels (upstream issue in policyengine-us) diff --git a/docs/DESIGN.md b/docs/DESIGN.md deleted file mode 100644 index fbe2bd8..0000000 --- a/docs/DESIGN.md +++ /dev/null @@ -1,331 +0,0 @@ -# API hierarchy design - -## Levels of analysis - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Level 2: Reports │ -│ AI-generated documents, orchestrating multiple jobs │ -│ /reports/* │ -├─────────────────────────────────────────────────────────────┤ -│ Level 1: Analyses │ -│ Operations on simulations - thin wrappers around │ -│ policyengine package functions │ -│ │ -│ Common (baked-in, trivial to call): │ -│ /analysis/decile-impact/* │ -│ /analysis/budget-impact/* │ -│ /analysis/winners-losers/* │ -│ │ -│ Flexible (configurable): │ -│ /analysis/compare/* │ -├─────────────────────────────────────────────────────────────┤ -│ Level 0: Simulations │ -│ Single world-state calculations │ -│ /simulate/household │ -│ /simulate/economy │ -└─────────────────────────────────────────────────────────────┘ -``` - -All operations are **async** (Modal compute). The API is a thin orchestration layer - all analysis logic lives in the `policyengine` package. - -## Mapping to policyengine package - -| API endpoint | policyengine function | -|--------------|----------------------| -| `/simulate/household` | `calculate_household_impact()` | -| `/simulate/economy` | `Simulation.run()` | -| `/analysis/decile-impact/*` | `calculate_decile_impacts()` | -| `/analysis/budget-impact/*` | `ProgrammeStatistics` | -| `/analysis/winners-losers/*` | `ChangeAggregate` with filters | -| `/analysis/compare/*` | `economic_impact_analysis()` or custom | - -## Level 0: Simulations - -### `/simulate/household` - -Single household calculation. Wraps `policyengine.tax_benefit_models.uk.analysis.calculate_household_impact()`. - -``` -POST /simulate/household -{ - "model": "policyengine_uk", - "household": { - "people": [{"age": 30, "employment_income": 50000}], - "benunit": {}, - "household": {} - }, - "year": 2026, - "policy_id": null -} - -→ Returns job_id, poll for results -``` - -### `/simulate/economy` - -Population simulation. Creates and runs a `policyengine.core.Simulation`. - -``` -POST /simulate/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "policy_id": null, - "dynamic_id": null -} - -→ Returns simulation_id, poll for results -``` - -Economy simulations are **deterministic and cached** by (dataset_id, model_version, policy_id, dynamic_id). - -## Level 1: Analyses - Common (baked-in) - -These are the bread-and-butter analyses. Trivial to call, no configuration needed. - -### `/analysis/decile-impact/economy` - -Income decile breakdown. Wraps `calculate_decile_impacts()`. - -``` -POST /analysis/decile-impact/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "baseline_policy_id": null, - "reform_policy_id": "..." -} - -→ Returns job_id - -GET /analysis/decile-impact/economy/{job_id} -→ Returns: -{ - "status": "completed", - "deciles": [ - {"decile": 1, "baseline_mean": 15000, "reform_mean": 15500, "change": 500, "pct_change": 3.3, ...}, - {"decile": 2, ...}, - ... - {"decile": 10, ...} - ] -} -``` - -### `/analysis/budget-impact/economy` - -Tax and benefit programme totals. Wraps `ProgrammeStatistics`. - -``` -POST /analysis/budget-impact/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "baseline_policy_id": null, - "reform_policy_id": "..." -} - -→ Returns job_id - -GET /analysis/budget-impact/economy/{job_id} -→ Returns: -{ - "status": "completed", - "net_budget_impact": -20000000000, - "programmes": [ - {"name": "income_tax", "is_tax": true, "baseline_total": 200e9, "reform_total": 180e9, "change": -20e9}, - {"name": "universal_credit", "is_tax": false, "baseline_total": 50e9, "reform_total": 52e9, "change": 2e9}, - ... - ] -} -``` - -### `/analysis/winners-losers/economy` - -Who gains and loses. Wraps `ChangeAggregate` with change filters. - -``` -POST /analysis/winners-losers/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "baseline_policy_id": null, - "reform_policy_id": "...", - "threshold": 0 // Change threshold (default: any change) -} - -→ Returns job_id - -GET /analysis/winners-losers/economy/{job_id} -→ Returns: -{ - "status": "completed", - "winners": {"count": 15000000, "mean_gain": 500}, - "losers": {"count": 5000000, "mean_loss": -200}, - "unchanged": {"count": 30000000} -} -``` - -### `/analysis/decile-impact/household` - -Compare household across scenarios by artificial decile assignment. - -``` -POST /analysis/decile-impact/household -{ - "model": "policyengine_uk", - "household": {"people": [{"employment_income": 50000}]}, - "year": 2026, - "baseline_policy_id": null, - "reform_policy_id": "..." -} - -→ Returns which decile this household falls into and their change -``` - -## Level 1: Analyses - Flexible - -### `/analysis/compare/economy` - -Full comparison with all outputs. Wraps `economic_impact_analysis()`. - -``` -POST /analysis/compare/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "scenarios": [ - {"label": "baseline"}, - {"label": "reform", "policy_id": "..."}, - {"label": "reform_dynamic", "policy_id": "...", "dynamic_id": "..."} - ] -} - -→ Returns job_id - -GET /analysis/compare/economy/{job_id} -→ Returns: -{ - "status": "completed", - "scenarios": {...simulation results...}, - "comparisons": { - "reform": { - "relative_to": "baseline", - "decile_impacts": [...], - "budget_impact": {...}, - "winners_losers": {...} - }, - "reform_dynamic": {...} - } -} -``` - -### `/analysis/compare/household` - -Compare multiple scenarios for a household. - -``` -POST /analysis/compare/household -{ - "model": "policyengine_uk", - "household": {...}, - "year": 2026, - "scenarios": [ - {"label": "baseline"}, - {"label": "reform", "policy_id": "..."} - ] -} - -→ Returns all scenario results + computed differences -``` - -### `/analysis/aggregate/economy` (power user) - -Custom aggregation with full filter control. Directly exposes `Aggregate` / `ChangeAggregate`. - -``` -POST /analysis/aggregate/economy -{ - "model": "policyengine_uk", - "dataset_id": "...", - "simulation_id": "...", // or policy_id to create - "variable": "household_net_income", - "aggregate_type": "mean", - "entity": "household", - "filters": { - "quantile": {"variable": "household_net_income", "n": 10, "eq": 1} - } -} - -→ Returns single aggregate value -``` - -## Adding new analysis types - -To add a new common analysis (e.g. marginal tax rates): - -1. **policyengine package**: Add `MarginalTaxRate` output class and `calculate_marginal_rates()` function -2. **API**: Add `/analysis/marginal-rates/*` endpoint that wraps the function -3. **Modal**: Add function to run it - -The API endpoint is ~20 lines - just parameter parsing and calling the policyengine function. - -## URL structure summary - -``` -# Level 0: Simulations -POST /simulate/household -GET /simulate/household/{job_id} -POST /simulate/economy -GET /simulate/economy/{simulation_id} - -# Level 1: Common analyses (baked-in, trivial) -POST /analysis/decile-impact/economy -GET /analysis/decile-impact/economy/{job_id} -POST /analysis/budget-impact/economy -GET /analysis/budget-impact/economy/{job_id} -POST /analysis/winners-losers/economy -GET /analysis/winners-losers/economy/{job_id} - -# Level 1: Flexible analyses -POST /analysis/compare/economy -GET /analysis/compare/economy/{job_id} -POST /analysis/compare/household -GET /analysis/compare/household/{job_id} -POST /analysis/aggregate/economy -GET /analysis/aggregate/economy/{job_id} - -# Level 2: Reports (future) -POST /reports/policy-impact -GET /reports/policy-impact/{report_id} -``` - -## Use cases - -| Use case | Endpoint | -|----------|----------| -| My tax under current law | `/simulate/household` | -| Reform impact on my household | `/analysis/compare/household` with 2 scenarios | -| Revenue impact of reform | `/analysis/budget-impact/economy` | -| Decile breakdown of reform | `/analysis/decile-impact/economy` | -| Who wins and loses | `/analysis/winners-losers/economy` | -| Full reform analysis | `/analysis/compare/economy` | -| Compare 3 reform proposals | `/analysis/compare/economy` with 4 scenarios | -| Static vs dynamic comparison | `/analysis/compare/economy` with 3 scenarios | -| Custom aggregation | `/analysis/aggregate/economy` | - -## Migration - -Deprecate existing endpoints: -- `/household/calculate` → `/simulate/household` -- `/household/impact` → `/analysis/compare/household` -- `/analysis/economic-impact` → `/analysis/compare/economy` - -## Implementation notes - -1. All Modal functions import from `policyengine` package -2. API endpoints do minimal work: parse request, call Modal, store results -3. New analysis types require: - - Add to policyengine package (logic) - - Add API endpoint (orchestration) - - Add Modal function (compute) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9d487a6..0000000 --- a/docs/README.md +++ /dev/null @@ -1,30 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/docs/api-v2-alpha-architecture.md b/docs/api-v2-alpha-architecture.md deleted file mode 100644 index 10924db..0000000 --- a/docs/api-v2-alpha-architecture.md +++ /dev/null @@ -1,264 +0,0 @@ -# policyengine-api-v2-alpha Deep Dive - -## Overview - -A **FastAPI backend** for tax-benefit policy microsimulations using PolicyEngine's UK and US models. It's designed as a thin orchestration layer that offloads heavy calculations to **Modal.com** serverless functions. - ---- - -## Architecture - -### Three-Level Hierarchy -``` -Level 2: Reports AI-generated analysis documents -Level 1: Analyses Comparisons (economy_comparison_*) -Level 0: Simulations Single calculations (simulate_household_*, simulate_economy_*) -``` - -### Request Flow -1. Client → FastAPI (Cloud Run) -2. API creates job record in **Supabase** and triggers **Modal.com** function -3. Modal runs calculation with pre-loaded PolicyEngine models (sub-1s cold start via memory snapshotting) -4. Modal writes results directly to Supabase -5. Client polls API until `status = "completed"` - ---- - -## Tech Stack - -| Component | Technology | -|-----------|------------| -| Framework | FastAPI + async | -| Database | Supabase (PostgreSQL) via SQLModel | -| Compute | Modal.com serverless | -| Storage | Supabase Storage (S3-compatible for .h5 datasets) | -| Package mgr | UV | -| Formatting | Ruff | -| Testing | Pytest + pytest-asyncio | -| Deployment | Terraform → GCP Cloud Run | -| Observability | Logfire (OpenTelemetry) | - ---- - -## Database Models (SQLModel) - -### Core Entities - -| Model | Purpose | Key Fields | -|-------|---------|------------| -| `TaxBenefitModel` | Country identifier | `name` ("uk"/"us") | -| `TaxBenefitModelVersion` | Version of model | `version`, FK to model | -| `Dataset` | Population microdata (.h5) | `filepath` (S3), `year` | -| `DatasetVersion` | Dataset versions | FK to dataset + model | -| `User` | Basic user info | `email`, `first_name`, `last_name` | - -### Policy System (Normalized) - -| Model | Purpose | Key Fields | -|-------|---------|------------| -| `Policy` | Policy reform definition | `name`, `description`, `simulation_modifier` (Python code) | -| `Parameter` | Tax/benefit parameter metadata | `name` (path), `label`, `data_type`, FK to model version | -| `ParameterValue` | Parameter values (baseline OR reform) | `value_json`, `start_date`, `policy_id` (NULL=baseline) | -| `Variable` | Model variables metadata | `name`, `entity`, `data_type`, `possible_values` | -| `Dynamic` | Behavioral response config | Similar to Policy | - -### Simulation & Results - -| Model | Purpose | Key Fields | -|-------|---------|------------| -| `Simulation` | Economy simulation instance | FK to dataset, policy, dynamic; `status` | -| `HouseholdJob` | Single household calculation | `request_data` (JSON), `result` (JSON), `status` | -| `Report` | Analysis report | FK to baseline_sim, reform_sim; `status`, `markdown` | -| `DecileImpact` | Income impacts by decile | FK to baseline/reform sims; `absolute_change`, `relative_change` | -| `ProgramStatistics` | Budget impacts by program | `baseline_total`, `reform_total`, `change` | -| `Poverty` | Poverty rate outputs | `poverty_type`, `headcount`, `rate` | -| `Inequality` | Inequality metrics | `gini`, `top_10_share`, `bottom_50_share` | -| `AggregateOutput` | Custom aggregations | `variable`, `aggregate_type`, `filter_config` | -| `ChangeAggregate` | Change aggregations | baseline vs reform comparison | - ---- - -## API Endpoints - -### Registered Routers (`api/__init__.py`) -``` -/datasets - Dataset CRUD + listing -/policies - Policy CRUD (POST, GET, LIST - no PATCH/DELETE) -/simulations - Simulation management -/parameters - Parameter metadata -/parameter-values - Parameter value CRUD -/variables - Variable metadata -/dynamics - Behavioral response configs -/tax-benefit-models - Model listing -/tax-benefit-model-versions - Version listing -/outputs - Result outputs -/change-aggregates - Comparison aggregates -/household/* - Household calculations (async) -/analysis/* - Economy-wide analysis (async) -/agent/* - AI agent endpoint (Claude Code) -``` - -### Key Endpoint Patterns - -**Async Pattern** (household, analysis): -``` -POST /household/calculate → {"job_id": "...", "status": "pending"} -GET /household/calculate/{job_id} → poll until status="completed" -``` - -**CRUD Pattern** (policies, datasets): -``` -POST /policies - Create -GET /policies - List all -GET /policies/{id} - Get by ID -(PATCH, DELETE - NOT implemented) -``` - ---- - -## Modal.com Functions (`modal_app.py`) - -| Function | Purpose | Resources | -|----------|---------|-----------| -| `simulate_household_uk` | UK household calc | 4GB RAM, 4 CPU | -| `simulate_household_us` | US household calc | 4GB RAM, 4 CPU | -| `simulate_economy_uk` | UK economy sim | 8GB RAM, 8 CPU | -| `simulate_economy_us` | US economy sim | 8GB RAM, 8 CPU | -| `economy_comparison_uk` | UK decile/budget analysis | 8GB RAM, 8 CPU, 30min timeout | -| `economy_comparison_us` | US decile/budget analysis | 8GB RAM, 8 CPU, 30min timeout | - -### Key Feature: Memory Snapshotting -```python -# UK image - uses run_function to snapshot imported modules in memory -uk_image = base_image.run_commands( - "uv pip install --system policyengine-uk>=2.0.0" -).run_function(_import_uk) # ← pre-loads uk_latest at build time -``` -This enables **sub-1s cold starts** - PolicyEngine models are already loaded in memory. - ---- - -## Database Migrations (`supabase/migrations/`) - -| Migration | Purpose | -|-----------|---------| -| `20241119000000_storage_bucket.sql` | Create datasets S3 bucket | -| `20241121000000_storage_policies.sql` | Storage access policies | -| `20251229000000_add_parameter_values_indexes.sql` | Index optimization | -| `20260103000000_add_poverty_inequality.sql` | Poverty/inequality tables | -| `20260108000000_add_simulation_modifier.sql` | Add `simulation_modifier` to policies | - -**Note**: Tables are auto-created by SQLModel on app startup, not via migrations. Migrations are only for: -- Storage bucket setup -- Adding indexes -- Schema alterations - ---- - -## Agent Endpoint (`/agent/*`) - -An AI-powered policy analysis feature using Claude Code: -- `POST /agent/run` - Start agent with a question -- `GET /agent/logs/{call_id}` - Poll for logs and result -- `GET /agent/status/{call_id}` - Quick status check -- `POST /agent/log/{call_id}` - Modal calls this to stream logs -- `POST /agent/complete/{call_id}` - Modal calls this when done - -Runs in: -- **Production**: Modal sandbox -- **Development**: Local background thread - ---- - -## Configuration (`config/settings.py`) - -Key settings loaded from environment: -- `DATABASE_URL` - Supabase Postgres connection -- `SUPABASE_URL` / `SUPABASE_KEY` - Supabase client -- `POLICYENGINE_API_URL` - Self-reference URL -- `AGENT_USE_MODAL` - True for production, False for local - ---- - -## Key Design Decisions - -1. **Normalized parameter storage** - `parameter_values` table with FKs instead of JSON blobs -2. **Async job pattern** - All heavy calculations return immediately with job_id -3. **Deterministic UUIDs** - Simulations/reports use uuid5 for deduplication -4. **Memory snapshotting** - PolicyEngine models loaded at Modal image build time -5. **SQLModel** - Single source of truth for DB schema and Pydantic validation -6. **simulation_modifier** - Python code injection for custom variable formulas - ---- - -## What's Missing (Gaps) - -| Feature | Status | -|---------|--------| -| `PATCH /policies/{id}` | Not implemented | -| `DELETE /policies/{id}` | Not implemented | -| User-policy associations | Not implemented | -| User-simulation associations | Not implemented | -| Authentication | No endpoints exposed | - ---- - -## File Structure - -``` -src/policyengine_api/ -├── api/ # FastAPI routers -│ ├── __init__.py # Router registration -│ ├── agent.py # AI agent endpoint -│ ├── analysis.py # Economy analysis -│ ├── household.py # Household calculations -│ ├── policies.py # Policy CRUD -│ ├── parameters.py # Parameter metadata -│ ├── parameter_values.py # Parameter values -│ └── ... -├── config/ -│ └── settings.py # Environment config -├── models/ # SQLModel definitions -│ ├── __init__.py # Model exports -│ ├── policy.py -│ ├── parameter.py -│ ├── parameter_value.py -│ ├── simulation.py -│ ├── report.py -│ └── ... -├── services/ -│ ├── database.py # DB session management -│ └── storage.py # Supabase storage client -├── main.py # FastAPI app entry point -└── modal_app.py # Modal serverless functions - -supabase/ -└── migrations/ # SQL migrations (storage, indexes) - -terraform/ # GCP Cloud Run infrastructure - -tests/ # Pytest tests -``` - ---- - -## Development Commands - -```bash -make install # Install dependencies with uv -make dev # Start supabase + api via docker compose -make test # Run unit tests -make integration-test # Full integration tests -make format # Ruff formatting -make lint # Ruff linting with auto-fix -make modal-deploy # Deploy Modal.com serverless functions -make init # Reset tables and storage -make seed # Populate UK/US models with variables, parameters, datasets -``` - ---- - -## Contributors - -- **Nikhil Woodruff** - Initial implementation (Nov 2025) -- **Anthony Volk** - Parameter values filtering and indexing (Dec 2025) diff --git a/docs/bun.lock b/docs/bun.lock deleted file mode 100644 index 8b46639..0000000 --- a/docs/bun.lock +++ /dev/null @@ -1,1088 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "docs-site", - "dependencies": { - "next": "16.0.10", - "react": "19.2.1", - "react-dom": "19.2.1", - "react-markdown": "^10.1.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "16.0.10", - "tailwindcss": "^4", - "typescript": "^5", - }, - }, - }, - "packages": { - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], - - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], - - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], - - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], - - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="], - - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.0.10", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw=="], - - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="], - - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="], - - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - - "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], - - "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], - - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.0", "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.0", "@typescript-eslint/tsconfig-utils": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], - - "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], - - "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], - - "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], - - "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], - - "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], - - "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], - - "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], - - "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], - - "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], - - "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], - - "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], - - "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], - - "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], - - "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], - - "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], - - "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], - - "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], - - "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - - "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], - - "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], - - "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], - - "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], - - "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], - - "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - - "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], - - "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], - - "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], - - "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], - - "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], - - "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], - - "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], - - "eslint-config-next": ["eslint-config-next@16.0.10", "", { "dependencies": { "@next/eslint-plugin-next": "16.0.10", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-BxouZUm0I45K4yjOOIzj24nTi0H2cGo0y7xUmk+Po/PYtJXFBYVDS1BguE7t28efXjKdcN0tmiLivxQy//SsZg=="], - - "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], - - "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], - - "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], - - "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], - - "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], - - "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], - - "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - - "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - - "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], - - "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], - - "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], - - "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], - - "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], - - "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], - - "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - - "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], - - "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], - - "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - - "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], - - "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - - "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], - - "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], - - "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], - - "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], - - "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], - - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], - - "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - - "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], - - "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - - "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], - - "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - - "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], - - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], - - "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], - - "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], - - "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], - - "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - - "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], - - "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], - - "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], - - "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], - - "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="], - - "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - - "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - } -} diff --git a/docs/eslint.config.mjs b/docs/eslint.config.mjs deleted file mode 100644 index 05e726d..0000000 --- a/docs/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/docs/next.config.ts b/docs/next.config.ts deleted file mode 100644 index 33527ec..0000000 --- a/docs/next.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - output: "export", - basePath: "/docs", - trailingSlash: true, - images: { - unoptimized: true, - }, -}; - -export default nextConfig; diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 4bafb7f..0000000 --- a/docs/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "docs-site", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "eslint" - }, - "dependencies": { - "next": "16.0.10", - "react": "19.2.1", - "react-dom": "19.2.1", - "react-markdown": "^10.1.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "16.0.10", - "tailwindcss": "^4", - "typescript": "^5" - } -} diff --git a/docs/policy-ingredient-status.md b/docs/policy-ingredient-status.md deleted file mode 100644 index e9c79a4..0000000 --- a/docs/policy-ingredient-status.md +++ /dev/null @@ -1,271 +0,0 @@ -# Policy Ingredient Status in API v2 Alpha - -This document summarizes the current state of the policy ingredient implementation in `policyengine-api-v2-alpha`, compares it to API v1, and outlines gaps and recommendations for migration. - -## Current Implementation - -### Database Tables - -The policy system uses a normalized relational model with two main tables: - -#### `policies` table -| Column | Type | Description | -|--------|------|-------------| -| `id` | UUID | Primary key, auto-generated | -| `name` | str | Policy name (required) | -| `description` | str \| None | Optional description | -| `simulation_modifier` | str \| None | Python code for custom variable formulas | -| `created_at` | datetime | Auto-generated UTC timestamp | -| `updated_at` | datetime | Auto-generated UTC timestamp | - -#### `parameter_values` table -| Column | Type | Description | -|--------|------|-------------| -| `id` | UUID | Primary key | -| `parameter_id` | UUID | FK to `parameters` table | -| `policy_id` | UUID \| None | FK to `policies` (NULL = baseline/current law) | -| `dynamic_id` | UUID \| None | FK to `dynamics` table | -| `value_json` | Any | The actual parameter value (JSON) | -| `start_date` | datetime | Effective start date | -| `end_date` | datetime \| None | Effective end date | -| `created_at` | datetime | Auto-generated | - -**Key design:** `parameter_values` stores BOTH baseline values (`policy_id IS NULL`) AND reform values (`policy_id = `). Each policy is self-contained with its own set of parameter value overrides - this is intentional, not a normalization issue. When running simulations, baseline (NULL) vs reform (policy_id = X) values are compared to compute impacts. - -### Supporting Tables - -#### `parameters` table (metadata) -| Column | Type | Description | -|--------|------|-------------| -| `id` | UUID | Primary key | -| `name` | str | Parameter path (e.g., `gov.irs.ctc.max`) | -| `label` | str \| None | Human-readable label | -| `description` | str \| None | Documentation | -| `data_type` | str \| None | Value type | -| `unit` | str \| None | Unit of measurement | -| `tax_benefit_model_version_id` | UUID | FK to model version | -| `created_at` | datetime | Auto-generated | - -#### `tax_benefit_models` table -| Column | Type | Description | -|--------|------|-------------| -| `id` | UUID | Primary key | -| `name` | str | Country identifier ("uk" or "us") | -| `description` | str \| None | Optional | -| `created_at` | datetime | Auto-generated | - -### API Endpoints - -| Method | Path | Status | Description | -|--------|------|--------|-------------| -| `POST` | `/policies` | ✅ Implemented | Create policy with parameter values | -| `GET` | `/policies` | ✅ Implemented | List all policies | -| `GET` | `/policies/{id}` | ✅ Implemented | Get policy by ID | -| `PATCH` | `/policies/{id}` | ❌ Missing | Update policy | -| `DELETE` | `/policies/{id}` | ❌ Missing | Delete policy | - -### Files - -| File | Purpose | -|------|---------| -| `src/policyengine_api/models/policy.py` | SQLModel definitions | -| `src/policyengine_api/models/parameter_value.py` | Parameter value model | -| `src/policyengine_api/api/policies.py` | Policy router/endpoints | -| `src/policyengine_api/api/parameter_values.py` | Parameter values router | -| `tests/test_policies.py` | Basic CRUD tests | - -### Contributors - -- **Nikhil Woodruff** - Initial implementation (Nov 2025) -- **Anthony Volk** - Added parameter_values filtering and indexing (Dec 2025) - ---- - -## Comparison: API v1 vs API v2 Alpha - -### URL Structure - -| Aspect | API v1 | API v2 Alpha | -|--------|--------|--------------| -| Create | `POST /{country_id}/policy` | `POST /policies` | -| Get | `GET /{country_id}/policy/{id}` | `GET /policies/{id}` | -| List | `GET /{country_id}/policies` | `GET /policies` | - -**Key difference:** v1 has country in URL path; v2 does not. - -### Country Handling - -| API v1 | API v2 Alpha | -|--------|--------------| -| Country in URL path (`/us/policy/123`) | Country determined by `tax_benefit_model_name` in request body | -| `@validate_country` decorator | No explicit country validation on policy endpoints | -| Policy implicitly scoped to country | Policy is country-agnostic; country context from analysis request | - -### Policy Storage - -| API v1 | API v2 Alpha | -|--------|--------------| -| `policy_hash` - hash of JSON blob | `parameter_values` - relational FK to parameters | -| Policy content stored as JSON | Policy content normalized in separate table | -| Embedded values in metadata response | Separate queries for parameter values | - -### Metadata Response (Parameters) - -**API v1 - Embedded values:** -```json -{ - "gov.irs.ctc.max": { - "type": "parameter", - "label": "CTC maximum", - "values": { - "2024-01-01": 2000, - "2023-01-01": 2000, - "2022-01-01": 2000 - } - } -} -``` - -**API v2 Alpha - Normalized:** -``` --- parameters table -id | name | label -1 | gov.irs.ctc.max | CTC maximum - --- parameter_values table -id | parameter_id | start_date | value_json | policy_id -1 | 1 | 2024-01-01 | 2000 | NULL (baseline) -2 | 1 | 2024-01-01 | 3000 | -``` - -### User Associations - -| API v1 | API v2 Alpha | -|--------|--------------| -| `user_policies` table exists in code | Not implemented | -| Endpoints NOT exposed (dead code) | No user association tables/endpoints | -| Fields: `user_id`, `reform_id`, `baseline_id`, `year`, `geography`, etc. | N/A | - ---- - -## Pros and Cons - -### API v2 Alpha Advantages - -| Pro | Explanation | -|-----|-------------| -| **Normalized schema** | Easier to query specific parameter changes without parsing JSON | -| **Relational integrity** | FK constraints ensure valid parameter references | -| **Better indexing** | Can index on `parameter_id`, `policy_id`, `start_date` | -| **Audit trail** | Each parameter value has its own `created_at` | -| **Cleaner reform diffs** | Query `WHERE policy_id = X` to see all reform changes | - -### API v2 Alpha Disadvantages - -| Con | Explanation | -|-----|-------------| -| **No country on policy** | Can't filter policies by country at DB level | -| **No user associations** | Must be built from scratch | -| **Missing PATCH/DELETE** | Incomplete CRUD | -| **No label field** | Only `name` + `description`, no user-friendly `label` | -| **More complex queries** | JOIN required to get policy with values | - -### API v1 Advantages - -| Pro | Explanation | -|-----|-------------| -| **Country in URL** | Clear API contract, easy filtering | -| **Simple storage** | Policy is a single JSON blob | -| **User associations designed** | Schema exists (though not exposed) | - -### API v1 Disadvantages - -| Con | Explanation | -|-----|-------------| -| **JSON blob parsing** | Must parse to query specific parameters | -| **No referential integrity** | Policy JSON could reference invalid parameters | -| **Harder to diff** | Must compare two JSON blobs to see changes | - ---- - -## Gaps for Migration - -### Must Have - -1. **User associations table and endpoints** - - `user_policies` table with `user_id`, `policy_id`, `label`, `created_at` - - `POST /user-policies` - Create association - - `GET /user-policies?user_id=X` - List user's policies - - `DELETE /user-policies/{id}` - Remove association - -2. **PATCH endpoint for policies** - - Update `name`, `description` - - Update parameter values - -3. **DELETE endpoint for policies** - - Cascade delete parameter values - -### Should Have - -4. **Country validation** - - Either add `country_id` to policy model OR - - Validate at creation that all parameter_ids belong to same country - -5. **Label field on policy** - - User-friendly display name separate from `name` - -### Nice to Have - -6. **Soft delete** - - `deleted_at` field instead of hard delete - -7. **Policy versioning** - - Track changes over time - ---- - -## Suggested Schema Changes - -### Option A: Add country_id to Policy - -```python -class Policy(PolicyBase, table=True): - # ... existing fields ... - country_id: str # "us" or "uk" -``` - -**Pros:** Simple filtering, matches v1 pattern -**Cons:** Redundant with parameter → tax_benefit_model → name - -### Option B: Derive country from parameters (current approach) - -Keep as-is, derive country from first parameter's tax_benefit_model. - -**Pros:** No schema change, DRY -**Cons:** Requires JOIN to filter by country - -### Recommendation: Option A - -Add explicit `country_id` for simpler queries and clearer data model. - ---- - -## Next Steps - -1. [ ] Add `country_id` to Policy model -2. [ ] Add `label` field to Policy model -3. [ ] Create UserPolicy model and table -4. [ ] Implement `PATCH /policies/{id}` endpoint -5. [ ] Implement `DELETE /policies/{id}` endpoint -6. [ ] Implement user policy association endpoints -7. [ ] Add database migration for schema changes -8. [ ] Update tests - ---- - -## References - -- Policy model: `src/policyengine_api/models/policy.py` -- Policy router: `src/policyengine_api/api/policies.py` -- Parameter value model: `src/policyengine_api/models/parameter_value.py` -- Tests: `tests/test_policies.py` diff --git a/docs/postcss.config.mjs b/docs/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/docs/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/docs/public/file.svg b/docs/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/docs/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/globe.svg b/docs/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/docs/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/next.svg b/docs/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/docs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/vercel.svg b/docs/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/docs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/public/window.svg b/docs/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/docs/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/src/app/agent/page.tsx b/docs/src/app/agent/page.tsx deleted file mode 100644 index 7f18816..0000000 --- a/docs/src/app/agent/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { PolicyChat } from "../../components/policy-chat"; - -export default function DemoPage() { - return ( -
-

- AI policy analyst -

-

- Ask questions in natural language and get AI-generated policy analysis reports. -

- -
-
-

- How it works -

-

- This demo uses an AI agent powered by Claude that interacts with the PolicyEngine API to answer your questions. - When you ask a question, the agent: -

-
    -
  1. Searches for relevant policy parameters
  2. -
  3. Creates a policy reform based on your question
  4. -
  5. Runs an economy-wide impact analysis
  6. -
  7. Generates a report with the findings
  8. -
-

- Analysis typically takes 30-60 seconds as it runs full microsimulations on population data. -

-
- - - -
-

- Note: This is a demo of the API's capabilities. - Results are from real PolicyEngine microsimulations but should be verified for policy research. - The agent uses Claude Sonnet via a Modal Sandbox for secure, isolated execution. -

-
-
-
- ); -} diff --git a/docs/src/app/architecture/page.tsx b/docs/src/app/architecture/page.tsx deleted file mode 100644 index 9a168c7..0000000 --- a/docs/src/app/architecture/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -export default function ArchitecturePage() { - return ( -
-

- Architecture -

-

- The PolicyEngine API v2 is a distributed system for running tax-benefit microsimulations with persistence and async processing. -

- -
-
-

Components

- -
-
-

API server

-

- FastAPI application exposing RESTful endpoints for creating and managing datasets, defining policy reforms, queueing simulations, and computing aggregates. The server validates requests, persists to PostgreSQL, and queues background tasks. -

-
- -
-

Database

-

- PostgreSQL (via Supabase) stores all persistent data using SQLModel for type-safe ORM with Pydantic integration. -

-
- {["datasets", "policies", "simulations", "aggregates", "reports", "decile_impacts", "program_statistics", "parameters"].map((table) => ( - - {table} - - ))} -
-
- -
-

Worker

-

- Background workers poll for pending simulations and reports. They load datasets from storage, run PolicyEngine simulations, compute aggregates and impact statistics, then store results to the database. -

-
- -
-

Storage

-

- Dataset files (HDF5 format) are stored in Supabase Storage with local caching for performance. The storage layer handles downloads and caching transparently. -

-
-
-
- -
-

Request flow

- -
- {[ - "Client creates simulation via POST /analysis/economic-impact", - "API validates request and persists simulation + report records", - "API returns pending status immediately", - "Worker picks up pending simulation from queue", - "Worker loads dataset and runs PolicyEngine simulation", - "Worker updates simulation status to completed", - "Worker picks up pending report", - "Worker computes decile impacts and program statistics", - "Client polls GET /analysis/economic-impact/{id} to check status", - "Once complete, response includes full analysis results", - ].map((step, index) => ( -
- - {index + 1} - -

{step}

-
- ))} -
-
- -
-

Data models

-

- All models follow Pydantic/SQLModel patterns for type safety across API, database, and business logic: -

- -
-
- Base -

Shared fields across models

-
-
- Table -

Database model with ID and timestamps

-
-
- Create -

Request schema (no ID)

-
-
- Read -

Response schema (with ID and timestamps)

-
-
-
- -
-

Scaling

- -
-
-

API scaling

-

- Multiple uvicorn workers behind load balancer for horizontal scaling. -

-
-
-

Worker scaling

-

- Increase worker count for parallel simulation processing. -

-
-
-

Database

-

- PostgreSQL supports read replicas for high read throughput. -

-
-
-

Caching

-

- Deterministic UUIDs ensure same requests reuse cached results. -

-
-
-
-
-
- ); -} diff --git a/docs/src/app/design/page.tsx b/docs/src/app/design/page.tsx deleted file mode 100644 index b700d90..0000000 --- a/docs/src/app/design/page.tsx +++ /dev/null @@ -1,151 +0,0 @@ -export default function DesignPage() { - return ( -
-

- API hierarchy design -

-

- Three-level architecture for simulations, analyses, and reports. -

- -
-
-

Levels of analysis

- -
-
-
- Level 2 -

Reports

-
-

- AI-generated documents orchestrating multiple jobs. Future feature. -

-
- -
-
- Level 1 -

Analyses

-
-

- Operations on simulations - thin wrappers around policyengine package functions. -

-
- /analysis/decile-impact/* - /analysis/budget-impact/* - /analysis/winners-losers/* - /analysis/compare/* -
-
- -
-
- Level 0 -

Simulations

-
-

- Single world-state calculations - the foundation for all analyses. -

-
- /simulate/household - /simulate/economy -
-
-
-
- -
-

Modal functions

-

- All compute runs on Modal.com serverless functions with sub-1s cold starts. -

- -
- - - - - - - - - - - - - - - - - - - - - -
FunctionPurpose
simulate_household_uk/usSingle household calculation
simulate_economy_uk/usSingle economy simulation
economy_comparison_uk/usEconomy comparison (decile impacts, budget)
-
-
- -
-

Mapping to policyengine package

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
API endpointpolicyengine function
/simulate/householdcalculate_household_impact()
/simulate/economySimulation.run()
/analysis/decile-impact/*calculate_decile_impacts()
/analysis/budget-impact/*ProgrammeStatistics
/analysis/winners-losers/*ChangeAggregate
-
-
- -
-

Use cases

- -
- {[ - { case: "My tax under current law", endpoint: "/simulate/household" }, - { case: "Reform impact on my household", endpoint: "/analysis/compare/household" }, - { case: "Revenue impact of reform", endpoint: "/analysis/budget-impact/economy" }, - { case: "Decile breakdown of reform", endpoint: "/analysis/decile-impact/economy" }, - { case: "Who wins and loses", endpoint: "/analysis/winners-losers/economy" }, - { case: "Full reform analysis", endpoint: "/analysis/compare/economy" }, - ].map((item) => ( -
- {item.case} - - {item.endpoint} - -
- ))} -
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/aggregates/page.tsx b/docs/src/app/endpoints/aggregates/page.tsx deleted file mode 100644 index 11b5f40..0000000 --- a/docs/src/app/endpoints/aggregates/page.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function AggregatesPage() { - return ( -
-

- Aggregates -

-

- Compute aggregate statistics from simulation results. Supports sums, means, and counts with optional filtering. Accepts a list of specifications. -

- -
-
-

- List aggregates -

- -
- -
-

- Get aggregate -

- -
- -
-

- Create aggregates (batch) -

- -
- -
-

- Aggregate specification -

-
-
- simulation_id - UUID - Source simulation -
-
- variable - string - Variable name to aggregate -
-
- aggregate_type - sum | mean | count -
-
- entity - string - Entity level (person, household, benunit, etc.) -
-
- filter_config - object | null - Optional quantile filtering -
-
-
- -
-

- Aggregate types -

-
-
- sum - Total across population (weighted) -
-
- mean - Weighted average -
-
- count - Number of entities (weighted) -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/change-aggregates/page.tsx b/docs/src/app/endpoints/change-aggregates/page.tsx deleted file mode 100644 index 980a4e0..0000000 --- a/docs/src/app/endpoints/change-aggregates/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function ChangeAggregatesPage() { - return ( -
-

- Change aggregates -

-

- Compute change statistics comparing baseline vs reform simulations. Accepts a list of specifications. -

- -
-
-

- List change aggregates -

- -
- -
-

- Get change aggregate -

- -
- -
-

- Create change aggregates (batch) -

- -
- -
-

- Change aggregate specification -

-
-
- baseline_simulation_id - UUID - Baseline simulation -
-
- reform_simulation_id - UUID - Reform simulation -
-
- variable - string - Variable name to compare -
-
- aggregate_type - sum | mean | count -
-
- entity - string - Entity level -
-
- change_geq - number | null - Filter: change greater than or equal to -
-
- change_leq - number | null - Filter: change less than or equal to -
-
-
- -
-

- Aggregate types -

-
-
- sum - Total change across population (weighted) -
-
- mean - Weighted average change -
-
- count - Number of entities (use with change_geq/change_leq to count winners/losers) -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/datasets/page.tsx b/docs/src/app/endpoints/datasets/page.tsx deleted file mode 100644 index a260323..0000000 --- a/docs/src/app/endpoints/datasets/page.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function DatasetsPage() { - return ( -
-

- Datasets -

-

- Manage microdata datasets for tax-benefit simulations. -

- -
-
-

- List datasets -

- -
- -
-

- Get dataset -

- -
- -
-

- Create dataset -

- -
- -
-

- Request parameters -

-
-
- name - string - Human-readable dataset name -
-
- description - string | null - Optional description -
-
- filepath - string - Path to HDF5 file in storage -
-
- year - integer - Simulation year for this dataset -
-
- tax_benefit_model_version_id - UUID - Model version this dataset is compatible with -
-
-
- -
-

- Delete dataset -

- -
- -
-

- Dataset object -

-
-
- id - UUID - Unique identifier -
-
- name - string - Human-readable name -
-
- description - string | null - Optional description -
-
- filepath - string - Path in storage bucket -
-
- year - integer - Simulation year -
-
- created_at - datetime - Creation timestamp -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/dynamics/page.tsx b/docs/src/app/endpoints/dynamics/page.tsx deleted file mode 100644 index 6400a4d..0000000 --- a/docs/src/app/endpoints/dynamics/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function DynamicsPage() { - return ( -
-

- Dynamics -

-

- Define behavioural response models that modify simulation parameters based on policy changes. -

- -
-
-

- List dynamics -

- -
- -
-

- Get dynamic -

- -
- -
-

- Create dynamic -

- -
- -
-

- Request parameters -

-
-
- name - string - Model name -
-
- description - string | null - Optional description -
-
- parameter_values - array - Behavioural parameter modifications (see below) -
-
-
- -
-

- Parameter value object -

-
-
- parameter_id - UUID - Reference to parameter -
-
- value_json - object - New parameter value -
-
- start_date - date - When change takes effect -
-
- end_date - date - When change ends -
-
-
- -
-

- Dynamic object -

-
-
- id - UUID - Unique identifier -
-
- name - string - Model name -
-
- description - string | null - Optional description -
-
- parameter_values - array - Behavioural parameter modifications -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/economic-impact/page.tsx b/docs/src/app/endpoints/economic-impact/page.tsx deleted file mode 100644 index feab5af..0000000 --- a/docs/src/app/endpoints/economic-impact/page.tsx +++ /dev/null @@ -1,272 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; -import { JsonViewer } from "@/components/json-viewer"; - -export default function EconomicImpactPage() { - return ( -
-

- Economic impact -

-

- Calculate the distributional impact of policy reforms. Compares reform scenario against baseline and produces decile-level analysis plus program statistics. -

- -
-

- Recommended endpoint: This is the primary way to analyse policy reforms. It handles simulation creation, comparison, and statistical analysis automatically. -

-
- -
-
-

- Create economic impact analysis -

- -
- -
-

- Get economic impact status -

- -
- -
-

- Request parameters -

-
-
- tax_benefit_model_name - policyengine_uk or policyengine_us -
-
- dataset_id - UUID - Dataset to run simulations on -
-
- policy_id - UUID | null - Reform policy (baseline uses current law) -
-
- dynamic_id - UUID | null - Optional behavioural response model -
-
-
- -
-

- Example response (completed) -

-
- -
-
- -
-

- Decile impact object -

-
-
- decile - integer (1-10) - Income decile -
-
- income_variable - string - Variable used for decile ranking -
-
- baseline_mean - number - Mean income under baseline -
-
- reform_mean - number - Mean income under reform -
-
- absolute_change - number - reform_mean - baseline_mean -
-
- relative_change - number - Percentage change (0.05 = 5%) -
-
- count_better_off - number - People gaining from reform -
-
- count_worse_off - number - People losing from reform -
-
-
- -
-

- Program statistics object -

-
-
- program_name - string - Tax or benefit name -
-
- entity - string - Entity level (person, household, etc.) -
-
- is_tax - boolean - True for taxes, false for benefits -
-
- baseline_total - number - Total revenue/spending under baseline -
-
- reform_total - number - Total revenue/spending under reform -
-
- change - number - Difference (reform - baseline) -
-
- winners - number - Count of people benefiting -
-
- losers - number - Count of people losing out -
-
-
- -
-

- Programs analysed -

-
-
-

UK

-
    -
  • income_tax
  • -
  • national_insurance
  • -
  • vat
  • -
  • council_tax
  • -
  • universal_credit
  • -
  • child_benefit
  • -
  • pension_credit
  • -
  • income_support
  • -
  • working_tax_credit
  • -
  • child_tax_credit
  • -
-
-
-

US

-
    -
  • income_tax
  • -
  • employee_payroll_tax
  • -
  • snap
  • -
  • tanf
  • -
  • ssi
  • -
  • social_security
  • -
-
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/household-impact/page.tsx b/docs/src/app/endpoints/household-impact/page.tsx deleted file mode 100644 index 94f3f62..0000000 --- a/docs/src/app/endpoints/household-impact/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; -import { JsonViewer } from "@/components/json-viewer"; - -export default function HouseholdImpactPage() { - return ( -
-

- Household impact comparison -

-

- Compare a household under baseline (current law) vs a policy reform. Returns both calculations plus computed differences. -

- -
-
-

- Calculate household impact -

- -
- -
-

- Request parameters -

-
-
- tax_benefit_model_name - policyengine_uk or policyengine_us -
-
- people - Array of person objects (age, employment_income, etc.) -
-
- household - Household-level variables (region, etc.) -
-
- year - integer | null - Simulation year -
-
- policy_id - UUID | null - Reform policy to compare against baseline -
-
- dynamic_id - UUID | null - Optional behavioural response model -
-
-
- -
-

- Example response -

-
- -
-
- -
-

- Response structure -

-
-
- baseline - Full household calculation under current law -
-
- reform - Full household calculation with policy applied -
-
- impact - Computed differences (baseline, reform, change) for numeric variables -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/household/page.tsx b/docs/src/app/endpoints/household/page.tsx deleted file mode 100644 index 8b24c1f..0000000 --- a/docs/src/app/endpoints/household/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function HouseholdPage() { - return ( -
-

- Household calculate -

-

- Calculate tax and benefit impacts for a single household. Returns computed values for all variables. -

- -
-
-

- Calculate household -

- -
- -
-

- Request parameters -

-
-
- tax_benefit_model_name - policyengine_uk or policyengine_us -
-
- people - Array of person objects with variable values (e.g., age, employment_income) -
-
- benunit - UK: Benefit unit configuration -
-
- household - Household-level variables -
-
- year - Simulation year (default: 2026 UK, 2024 US) -
-
- policy_id - UUID | null - Optional policy reform to apply -
-
-
- -
-

- US-specific entities -

-
-
- marital_unit - US marital unit configuration -
-
- family - US family configuration -
-
- spm_unit - US SPM unit configuration -
-
- tax_unit - US tax unit configuration -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/parameters/page.tsx b/docs/src/app/endpoints/parameters/page.tsx deleted file mode 100644 index d9f62ad..0000000 --- a/docs/src/app/endpoints/parameters/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function ParametersPage() { - return ( -
-

- Parameters -

-

- Tax-benefit system parameters that can be modified in policy reforms. Each parameter has a name, description, and default values over time. -

- -
-
-

- List parameters -

- -
- -
-

- Get parameter -

- -
- -
-

- List parameter values -

- -
- -
-

- Parameter object -

-
-
- id - UUID - Unique identifier -
-
- name - string - Full parameter path -
-
- label - string - Human-readable label -
-
- description - string | null - Detailed description -
-
- unit - string | null - Unit (currency, percent, etc.) -
-
-
- -
-

- Example UK parameters -

-
- - gov.hmrc.income_tax.allowances.personal_allowance.amount - - - gov.hmrc.income_tax.rates.uk.basic - - - gov.dwp.universal_credit.elements.standard_allowance.single_young - -
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/policies/page.tsx b/docs/src/app/endpoints/policies/page.tsx deleted file mode 100644 index eefba8e..0000000 --- a/docs/src/app/endpoints/policies/page.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function PoliciesPage() { - return ( -
-

- Policies -

-

- Define policy reforms by modifying tax-benefit system parameters. -

- -
-
-

- List policies -

- -
- -
-

- Get policy -

- -
- -
-

- Create policy -

- -
- -
-

- Request parameters -

-
-
- name - string - Policy name -
-
- description - string | null - Optional description -
-
- parameter_values - array - Parameter modifications (see below) -
-
-
- -
-

- Delete policy -

- -
- -
-

- Policy object -

-
-
- id - UUID - Unique identifier -
-
- name - string - Policy name -
-
- description - string | null - Optional description -
-
- parameter_values - array - Parameter modifications -
-
- created_at - datetime - Creation timestamp -
-
-
- -
-

- Parameter value object -

-
-
- parameter_id - UUID - Reference to parameter -
-
- value_json - object - New parameter value -
-
- start_date - date - When change takes effect -
-
- end_date - date - When change ends -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/simulations/page.tsx b/docs/src/app/endpoints/simulations/page.tsx deleted file mode 100644 index 75e5d8f..0000000 --- a/docs/src/app/endpoints/simulations/page.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function SimulationsPage() { - return ( -
-

- Simulations -

-

- Run tax-benefit microsimulations on datasets. Simulations are processed asynchronously by background workers. -

- -
-

- Note: Simulations use deterministic UUIDs based on inputs. Requesting the same simulation twice returns the cached result instead of running again. -

-
- -
-
-

- List simulations -

- -
- -
-

- Get simulation -

- -
- -
-

- Create simulation -

- -
- -
-

- Request parameters -

-
-
- dataset_id - UUID - Dataset to run simulation on -
-
- tax_benefit_model_version_id - UUID - Model version to use -
-
- policy_id - UUID | null - Optional policy reform -
-
- dynamic_id - UUID | null - Optional behavioural response model -
-
-
- -
-

- Response object -

-
-
- id - UUID - Deterministic identifier -
-
- dataset_id - UUID - Reference to dataset -
-
- policy_id - UUID | null - Optional policy reform -
-
- dynamic_id - UUID | null - Optional behavioural response -
-
- status - enum - pending | running | completed | failed -
-
- error_message - string | null - Error details if failed -
-
- started_at - datetime | null - When processing started -
-
- completed_at - datetime | null - When processing finished -
-
-
- -
-

- Status values -

-
-
- pending - Queued, waiting for worker -
-
- running - Worker is processing -
-
- completed - Successfully finished -
-
- failed - Error occurred (see error_message) -
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/tax-benefit-models/page.tsx b/docs/src/app/endpoints/tax-benefit-models/page.tsx deleted file mode 100644 index 45a6c06..0000000 --- a/docs/src/app/endpoints/tax-benefit-models/page.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function TaxBenefitModelsPage() { - return ( -
-

- Tax benefit models -

-

- Available tax-benefit system models (UK and US). Each model has versions with associated variables and parameters. -

- -
-
-

- List models -

- -
- -
-

- Get model -

- -
- -
-

- List model versions -

- -
- -
-

- Get model version -

- -
- -
-

- Available models -

-
-
- policyengine-uk -

- UK tax and benefit system including income tax, NI, benefits, and more. -

-
-
- policyengine-us -

- US federal and state tax system including income tax, payroll tax, SNAP, TANF, etc. -

-
-
-
-
-
- ); -} diff --git a/docs/src/app/endpoints/variables/page.tsx b/docs/src/app/endpoints/variables/page.tsx deleted file mode 100644 index 2eabb27..0000000 --- a/docs/src/app/endpoints/variables/page.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; - -import { ApiPlayground } from "@/components/api-playground"; - -export default function VariablesPage() { - return ( -
-

- Variables -

-

- Tax-benefit system variables that can be calculated or used as inputs. Variables are associated with model versions. -

- -
-
-

- List variables -

- -
- -
-

- Get variable -

- -
- -
-

- Variable object -

-
-
- id - UUID - Unique identifier -
-
- name - string - Variable name (e.g., income_tax) -
-
- label - string - Human-readable label -
-
- description - string | null - Detailed description -
-
- entity - string - Entity level (person, household, etc.) -
-
- value_type - string - Data type (float, int, bool, enum) -
-
- unit - string | null - Unit of measurement -
-
-
- -
-

- Common UK variables -

-
- {["income_tax", "national_insurance", "universal_credit", "child_benefit", "council_tax", "household_net_income", "employment_income", "pension_income"].map((v) => ( - {v} - ))} -
-
- -
-

- Common US variables -

-
- {["income_tax", "employee_payroll_tax", "snap", "tanf", "ssi", "social_security", "employment_income", "spm_unit_net_income"].map((v) => ( - {v} - ))} -
-
-
-
- ); -} diff --git a/docs/src/app/favicon.ico b/docs/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css deleted file mode 100644 index 34f6080..0000000 --- a/docs/src/app/globals.css +++ /dev/null @@ -1,234 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); -@import "tailwindcss"; - -@theme { - /* PolicyEngine brand colors */ - --color-pe-green: #2c6e49; - --color-pe-green-light: #4c9a6d; - --color-pe-green-dark: #1a4a2e; - --color-pe-blue: #1a365d; - --color-pe-slate: #334155; - --color-pe-gray: #64748b; - - /* Neutral palette */ - --color-surface: #fafbfc; - --color-surface-elevated: #ffffff; - --color-surface-sunken: #f1f5f9; - --color-border: #e2e8f0; - --color-border-strong: #cbd5e1; - - /* Text colors */ - --color-text-primary: #0f172a; - --color-text-secondary: #475569; - --color-text-muted: #94a3b8; - - /* Code colors */ - --color-code-bg: #1e293b; - --color-code-text: #e2e8f0; - - /* Status colors */ - --color-success: #16a34a; - --color-error: #dc2626; - --color-warning: #d97706; - --color-info: #2563eb; - - /* Typography */ - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; - --font-mono: "JetBrains Mono", ui-monospace, monospace; - --font-display: "Instrument Serif", Georgia, serif; -} - -body { - font-family: var(--font-sans); - background: var(--color-surface); - color: var(--color-text-primary); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Custom scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--color-surface-sunken); -} - -::-webkit-scrollbar-thumb { - background: var(--color-border-strong); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--color-pe-gray); -} - -/* Code block styling */ -pre { - font-family: var(--font-mono); - font-size: 13px; - line-height: 1.6; -} - -code { - font-family: var(--font-mono); -} - -/* Method badges */ -.method-get { background: #dbeafe; color: #1d4ed8; } -.method-post { background: #dcfce7; color: #16a34a; } -.method-put { background: #fef3c7; color: #d97706; } -.method-delete { background: #fee2e2; color: #dc2626; } - -/* Smooth transitions */ -* { - transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -/* Focus states */ -*:focus-visible { - outline: 2px solid var(--color-pe-green); - outline-offset: 2px; -} - -/* Selection */ -::selection { - background: var(--color-pe-green); - color: white; -} - -/* Agent response content */ -.response-content { - font-family: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif; - font-size: 15px; - line-height: 1.7; - color: #1e293b; - -webkit-font-smoothing: antialiased; -} -.response-content p { - margin: 0.75em 0; -} -.response-content p:first-child { - margin-top: 0; -} -.response-content p:last-child { - margin-bottom: 0; -} -.response-content h1, -.response-content h2, -.response-content h3 { - font-weight: 600; - color: #0f172a; - margin-top: 1.25em; - margin-bottom: 0.5em; - line-height: 1.3; -} -.response-content h1 { - font-size: 1.2em; -} -.response-content h2 { - font-size: 1.1em; -} -.response-content h3 { - font-size: 1em; -} -.response-content h1:first-child, -.response-content h2:first-child, -.response-content h3:first-child { - margin-top: 0; -} -.response-content strong { - font-weight: 600; - color: #0f172a; -} -.response-content ul { - margin: 0.75em 0; - padding-left: 1.25em; - list-style-type: disc; -} -.response-content ol { - margin: 0.75em 0; - padding-left: 1.25em; - list-style-type: decimal; -} -.response-content li { - margin: 0.35em 0; - padding-left: 0.25em; -} -.response-content ul ul { - list-style-type: circle; -} -.response-content ul ul ul { - list-style-type: square; -} -.response-content code { - font-family: "JetBrains Mono", ui-monospace, monospace; - font-size: 0.875em; - background: #f1f5f9; - padding: 0.2em 0.4em; - border-radius: 4px; - color: #334155; -} -.response-content pre { - font-family: "JetBrains Mono", ui-monospace, monospace; - font-size: 13px; - background: #1e293b; - color: #e2e8f0; - padding: 1em 1.25em; - border-radius: 8px; - overflow-x: auto; - margin: 1em 0; - line-height: 1.6; -} -.response-content pre code { - background: none; - padding: 0; - font-size: inherit; - color: inherit; -} -.response-content table { - width: 100%; - border-collapse: collapse; - margin: 1em 0; - font-family: var(--font-inter), -apple-system, sans-serif; - font-size: 14px; -} -.response-content th { - background: #f8fafc; - border: 1px solid #e2e8f0; - padding: 0.625em 0.875em; - text-align: left; - font-weight: 600; - color: #475569; -} -.response-content td { - border: 1px solid #e2e8f0; - padding: 0.625em 0.875em; - color: #334155; -} -.response-content tr:hover td { - background: #f8fafc; -} -.response-content blockquote { - border-left: 3px solid #2c6e49; - padding-left: 1em; - margin: 1em 0; - color: #64748b; - font-style: italic; -} -.response-content a { - color: #2c6e49; - text-decoration: underline; -} -.response-content a:hover { - color: #1a4a2e; -} -.response-content hr { - border: none; - border-top: 1px solid #e2e8f0; - margin: 1.5em 0; -} diff --git a/docs/src/app/layout.tsx b/docs/src/app/layout.tsx deleted file mode 100644 index 77e529c..0000000 --- a/docs/src/app/layout.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { Metadata } from "next"; -import { Inter, JetBrains_Mono } from "next/font/google"; -import "./globals.css"; -import { Sidebar } from "@/components/sidebar"; -import { Header } from "@/components/header"; -import { ApiProvider } from "@/components/api-context"; - -const inter = Inter({ - variable: "--font-inter", - subsets: ["latin"], -}); - -const jetbrainsMono = JetBrains_Mono({ - variable: "--font-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "PolicyEngine API v2 | Documentation", - description: "Interactive API documentation for PolicyEngine's tax-benefit microsimulation API", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - -
- -
-
-
- {children} -
-
-
-
- - - ); -} diff --git a/docs/src/app/mcp/page.tsx b/docs/src/app/mcp/page.tsx deleted file mode 100644 index 11df87c..0000000 --- a/docs/src/app/mcp/page.tsx +++ /dev/null @@ -1,93 +0,0 @@ -export default function McpPage() { - return ( -
-

- MCP integration -

-

- Use the PolicyEngine API as an MCP server for AI assistants like Claude. -

- -
-
-

- What is MCP? -

-

- The Model Context Protocol (MCP) is a standard for AI assistants to interact with external tools and data sources. - The PolicyEngine API exposes all endpoints as MCP tools at /mcp, - allowing AI assistants to calculate taxes and benefits, run economic impact analyses, and query policy data. -

-
- -
-

- Add to Claude Code -

-

- Run this command: -

-
-{`claude mcp add --transport http policyengine https://v2.api.policyengine.org/mcp/`}
-          
-
- -
-

- Add to Claude Desktop -

-

- Add this to your claude_desktop_config.json file: -

-
-{`{
-  "mcpServers": {
-    "policyengine": {
-      "type": "url",
-      "url": "https://v2.api.policyengine.org/mcp/"
-    }
-  }
-}`}
-          
-
- -
-

- Available tools -

-

- All API endpoints are exposed as MCP tools. Key capabilities include: -

-
    -
  • household_calculate — calculate taxes and benefits for a household
  • -
  • household_impact — compare baseline vs reform policy impact
  • -
  • analysis_economic_impact — run population-wide economic analysis
  • -
  • policies_list / policies_create — manage policy reforms
  • -
  • variables_list / parameters_list — query tax-benefit system metadata
  • -
  • datasets_list — list available population datasets
  • -
-
- -
-

- Example prompts -

-

- Once connected, you can ask Claude things like: -

-
-
- "Calculate the net income for a UK household with two adults earning £40,000 and £30,000" -
-
- "What would happen to this household's benefits if we increased the personal allowance to £15,000?" -
-
- "List all the parameters related to child benefit" -
-
-
-
-
- ); -} diff --git a/docs/src/app/modal/page.tsx b/docs/src/app/modal/page.tsx deleted file mode 100644 index bc50001..0000000 --- a/docs/src/app/modal/page.tsx +++ /dev/null @@ -1,205 +0,0 @@ -export default function ModalPage() { - return ( -
-

- Modal compute -

-

- PolicyEngine uses Modal.com for serverless compute, with two separate apps for different workloads. -

- -
-
-

Why two apps?

-

- The API uses two separate Modal apps rather than one combined app. This separation is intentional and provides several benefits: -

-
-
-

Image size

-

- The policyengine app has massive container images (multiple GB) with the full UK and US tax-benefit models pre-loaded. The policyengine-sandbox app is minimal - just the Anthropic SDK and requests library. -

-
-
-

Cold start optimisation

-

- The main app uses Modal's memory snapshot feature to pre-load PolicyEngine models at build time. When a function cold starts, it restores from the snapshot rather than re-importing the models, achieving sub-1s cold starts for functions that would otherwise take 30+ seconds to import. -

-
-
-

Architectural decoupling

-

- The sandbox/agent calls the public API endpoints, which then trigger the simulation functions. They're independent - the agent doesn't directly import PolicyEngine models, it makes HTTP calls. -

-
-
-

Independent scaling

-

- Simulation workloads scale differently from agent chat sessions. Keeping them separate lets Modal scale each independently based on demand. -

-
-
-
- -
-

policyengine app

-

- The main compute app for running simulations. Located at src/policyengine_api/modal_app.py. -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FunctionImageMemoryPurpose
simulate_household_ukuk_image4GBSingle UK household calculation
simulate_household_usus_image4GBSingle US household calculation
simulate_economy_ukuk_image8GBUK economy simulation
simulate_economy_usus_image8GBUS economy simulation
economy_comparison_ukuk_image8GBUK decile impacts, budget impact
economy_comparison_usus_image8GBUS decile impacts, budget impact
-
- -
-

- Deploy with: modal deploy src/policyengine_api/modal_app.py -

-
-
- -
-

policyengine-sandbox app

-

- Lightweight app for the AI agent. Located at src/policyengine_api/agent_sandbox.py. -

- -
- - - - - - - - - - - - - - - -
FunctionDependenciesPurpose
run_agentanthropic, requestsAgentic loop using Claude with API tools
-
- -

- The agent dynamically generates Claude tools from the OpenAPI spec, then executes an agentic loop to answer policy questions by making API calls. It doesn't import PolicyEngine directly. -

- -
-

- Deploy with: modal deploy src/policyengine_api/agent_sandbox.py -

-
-
- -
-

Memory snapshots

-

- The policyengine app uses Modal's run_function to snapshot the Python interpreter state after importing the models: -

-
-{`def _import_uk():
-    from policyengine.tax_benefit_models.uk import uk_latest
-    print("UK model loaded and snapshotted")
-
-uk_image = base_image.run_commands(
-    "uv pip install --system policyengine-uk>=2.0.0"
-).run_function(_import_uk)`}
-          
-

- When a cold start happens, Modal restores from this snapshot rather than re-running the imports. This turns a 30+ second import into sub-second startup. -

-
- -
-

Secrets

-

- Each app uses different Modal secrets: -

-
-
- policyengine-db -

Database credentials for the main app (DATABASE_URL, SUPABASE_URL, SUPABASE_KEY)

-
-
- anthropic-api-key -

Anthropic API key for the sandbox app (ANTHROPIC_API_KEY)

-
-
-
- -
-

Request flow

-
- {[ - "Client calls API endpoint (e.g. POST /household/calculate)", - "FastAPI validates request and creates job record in Supabase", - "FastAPI triggers Modal function asynchronously", - "API returns job ID immediately", - "Modal function runs calculation with pre-loaded models", - "Modal function writes results directly to Supabase", - "Client polls API until job status = completed", - ].map((step, index) => ( -
- - {index + 1} - -

{step}

-
- ))} -
-
-
-
- ); -} diff --git a/docs/src/app/page.tsx b/docs/src/app/page.tsx deleted file mode 100644 index a77ef67..0000000 --- a/docs/src/app/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Link from "next/link"; - -export default function Home() { - return ( -
-

- PolicyEngine API v2 -

-

- A distributed system for running tax-benefit microsimulations with persistence and async processing. -

- -
- -

- Quick start -

-

- Get up and running with the API in minutes. Complete walkthrough from setup to your first simulation. -

- - - -

- Economic impact -

-

- Calculate distributional impacts, decile analysis, and program statistics for policy reforms. -

- - - -

- Architecture -

-

- Understand the system design: API server, worker, database, and how they work together. -

- - - -

- API reference -

-

- Interactive documentation for all endpoints with live testing capabilities. -

- -
- -
-

- Features -

-
-
-

Async processing

-

- Simulations run asynchronously via a worker queue. Poll for status or use webhooks. -

-
-
-

Deterministic caching

-

- Same inputs produce the same simulation ID. Results are cached and reused automatically. -

-
-
-

UK and US models

-

- Full support for PolicyEngine UK and US tax-benefit systems with comprehensive datasets. -

-
-
-

Policy reforms

-

- Define custom parameter changes and compare reform scenarios against baselines. -

-
-
-
- -
-

- Tip: Use the base URL input in the header to switch between local and production environments. - The default is https://v2.api.policyengine.org. -

-
-
- ); -} diff --git a/docs/src/app/quickstart/page.tsx b/docs/src/app/quickstart/page.tsx deleted file mode 100644 index 4785324..0000000 --- a/docs/src/app/quickstart/page.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { JsonViewer } from "@/components/json-viewer"; - -export default function QuickstartPage() { - return ( -
-

- Quick start -

-

- Get up and running with the PolicyEngine API in minutes. -

- -
-
-

1. Start the services

-

- Clone the repository and start the services using Docker Compose: -

-
-{`git clone https://github.com/PolicyEngine/policyengine-api-v2
-cd policyengine-api-v2
-docker compose up -d`}
-          
-

- Wait for all services to be healthy before proceeding. -

-
- -
-

2. Create a policy reform

-

- Define a policy reform by specifying parameter changes: -

-
-{`curl -X POST https://v2.api.policyengine.org/policies \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "name": "Increased personal allowance",
-    "description": "Raises personal allowance to £15,000",
-    "parameter_values": [{
-      "parameter_id": "",
-      "value_json": {"value": 15000},
-      "start_date": "2026-01-01",
-      "end_date": "2026-12-31"
-    }]
-  }'`}
-          
-
- -
-

3. Run economic impact analysis

-

- Submit an economic impact analysis request. This creates baseline and reform simulations and queues them for processing: -

-
-{`curl -X POST https://v2.api.policyengine.org/analysis/economic-impact \\
-  -H "Content-Type: application/json" \\
-  -d '{
-    "tax_benefit_model_name": "policyengine_uk",
-    "dataset_id": "",
-    "policy_id": ""
-  }'`}
-          
-

- The response includes a report_id and initial status. -

-
- -
-

4. Poll for results

-

- Check the status until the report is complete: -

-
-{`curl https://v2.api.policyengine.org/analysis/economic-impact/`}
-          
-

- Once complete, the response includes decile impacts and program statistics: -

-
- -
-
- -
-

Python example

-

- Complete workflow using httpx: -

-
-{`import httpx
-import time
-
-BASE_URL = "https://v2.api.policyengine.org"
-
-# Create economic impact analysis
-response = httpx.post(
-    f"{BASE_URL}/analysis/economic-impact",
-    json={
-        "tax_benefit_model_name": "policyengine_uk",
-        "dataset_id": "",
-        "policy_id": "",
-    },
-).json()
-
-report_id = response["report_id"]
-
-# Poll for completion
-while True:
-    result = httpx.get(f"{BASE_URL}/analysis/economic-impact/{report_id}").json()
-    if result["status"] == "completed":
-        break
-    elif result["status"] == "failed":
-        raise Exception(f"Analysis failed: {result['error_message']}")
-    time.sleep(5)
-
-# Access results
-for decile in result["decile_impacts"]:
-    print(f"Decile {decile['decile']}: {decile['relative_change']:.1%} change")
-
-for prog in result["program_statistics"]:
-    print(f"{prog['program_name']}: £{prog['change']/1e9:.1f}bn change")`}
-          
-
-
-
- ); -} diff --git a/docs/src/app/reference/models/page.tsx b/docs/src/app/reference/models/page.tsx deleted file mode 100644 index 7c66417..0000000 --- a/docs/src/app/reference/models/page.tsx +++ /dev/null @@ -1,172 +0,0 @@ -export default function ModelsPage() { - const models = [ - { - name: "Dataset", - description: "Microdata file metadata", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "name", type: "string", description: "Human-readable name" }, - { name: "description", type: "string | null", description: "Optional description" }, - { name: "filepath", type: "string", description: "Path in storage bucket" }, - { name: "year", type: "integer", description: "Simulation year" }, - { name: "tax_benefit_model_version_id", type: "UUID", description: "Associated model version" }, - { name: "created_at", type: "datetime", description: "Creation timestamp" }, - ], - }, - { - name: "Policy", - description: "Parameter reform definition", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "name", type: "string", description: "Policy name" }, - { name: "description", type: "string | null", description: "Optional description" }, - { name: "parameter_values", type: "ParameterValue[]", description: "Parameter modifications" }, - { name: "created_at", type: "datetime", description: "Creation timestamp" }, - ], - }, - { - name: "ParameterValue", - description: "Single parameter modification", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "parameter_id", type: "UUID", description: "Reference to parameter" }, - { name: "value_json", type: "object", description: "New parameter value" }, - { name: "start_date", type: "date", description: "Effective start date" }, - { name: "end_date", type: "date", description: "Effective end date" }, - ], - }, - { - name: "Simulation", - description: "Tax-benefit microsimulation run", - fields: [ - { name: "id", type: "UUID", description: "Deterministic identifier" }, - { name: "dataset_id", type: "UUID", description: "Input dataset" }, - { name: "tax_benefit_model_version_id", type: "UUID", description: "Model version" }, - { name: "policy_id", type: "UUID | null", description: "Optional reform policy" }, - { name: "dynamic_id", type: "UUID | null", description: "Optional behavioural model" }, - { name: "status", type: "SimulationStatus", description: "Processing status" }, - { name: "error_message", type: "string | null", description: "Error if failed" }, - { name: "started_at", type: "datetime | null", description: "Processing start time" }, - { name: "completed_at", type: "datetime | null", description: "Processing end time" }, - ], - }, - { - name: "Report", - description: "Economic impact analysis container", - fields: [ - { name: "id", type: "UUID", description: "Deterministic identifier" }, - { name: "label", type: "string", description: "Human-readable label" }, - { name: "baseline_simulation_id", type: "UUID", description: "Baseline simulation" }, - { name: "reform_simulation_id", type: "UUID", description: "Reform simulation" }, - { name: "status", type: "ReportStatus", description: "Processing status" }, - { name: "error_message", type: "string | null", description: "Error if failed" }, - { name: "created_at", type: "datetime", description: "Creation timestamp" }, - ], - }, - { - name: "DecileImpact", - description: "Distributional impact by income decile", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "report_id", type: "UUID", description: "Parent report" }, - { name: "decile", type: "integer", description: "Income decile (1-10)" }, - { name: "income_variable", type: "string", description: "Variable for ranking" }, - { name: "baseline_mean", type: "number", description: "Baseline mean income" }, - { name: "reform_mean", type: "number", description: "Reform mean income" }, - { name: "absolute_change", type: "number", description: "Absolute difference" }, - { name: "relative_change", type: "number", description: "Percentage change" }, - { name: "count_better_off", type: "number", description: "Winners count" }, - { name: "count_worse_off", type: "number", description: "Losers count" }, - { name: "count_no_change", type: "number", description: "No change count" }, - ], - }, - { - name: "ProgramStatistics", - description: "Tax/benefit program impact statistics", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "report_id", type: "UUID", description: "Parent report" }, - { name: "program_name", type: "string", description: "Program name" }, - { name: "entity", type: "string", description: "Entity level" }, - { name: "is_tax", type: "boolean", description: "True for taxes" }, - { name: "baseline_total", type: "number", description: "Baseline total" }, - { name: "reform_total", type: "number", description: "Reform total" }, - { name: "change", type: "number", description: "Difference" }, - { name: "baseline_count", type: "number", description: "Baseline recipients" }, - { name: "reform_count", type: "number", description: "Reform recipients" }, - { name: "winners", type: "number", description: "Winners count" }, - { name: "losers", type: "number", description: "Losers count" }, - ], - }, - { - name: "AggregateOutput", - description: "Computed aggregate statistic", - fields: [ - { name: "id", type: "UUID", description: "Unique identifier" }, - { name: "simulation_id", type: "UUID", description: "Source simulation" }, - { name: "variable", type: "string", description: "Variable name" }, - { name: "aggregate_type", type: "AggregateType", description: "Calculation type" }, - { name: "entity", type: "string", description: "Entity level" }, - { name: "filter_config", type: "object | null", description: "Optional filter" }, - { name: "result", type: "number", description: "Computed value" }, - ], - }, - ]; - - return ( -
-

- Models -

-

- Data models used throughout the API. All models use UUID identifiers and include timestamps. -

- -
- {models.map((model) => ( -
-

- {model.name} -

-

- {model.description} -

-
- - - - - - - - - - {model.fields.map((field) => ( - - - - - - ))} - -
- Field - - Type - - Description -
- {field.name} - - {field.type} - {field.description}
-
-
- ))} -
-
- ); -} diff --git a/docs/src/app/reference/status-codes/page.tsx b/docs/src/app/reference/status-codes/page.tsx deleted file mode 100644 index 83d58a4..0000000 --- a/docs/src/app/reference/status-codes/page.tsx +++ /dev/null @@ -1,173 +0,0 @@ -export default function StatusCodesPage() { - const statusCodes = [ - { - code: "200", - name: "OK", - description: "Request succeeded. Response body contains the requested data.", - color: "bg-green-100 text-green-700", - }, - { - code: "201", - name: "Created", - description: "Resource created successfully. Response body contains the new resource.", - color: "bg-green-100 text-green-700", - }, - { - code: "204", - name: "No Content", - description: "Request succeeded with no response body (e.g., DELETE operations).", - color: "bg-green-100 text-green-700", - }, - { - code: "400", - name: "Bad Request", - description: "Invalid request body or parameters. Check the error message for details.", - color: "bg-red-100 text-red-700", - }, - { - code: "404", - name: "Not Found", - description: "The requested resource does not exist.", - color: "bg-red-100 text-red-700", - }, - { - code: "422", - name: "Unprocessable Entity", - description: "Request body failed validation. Response contains field-level errors.", - color: "bg-red-100 text-red-700", - }, - { - code: "500", - name: "Internal Server Error", - description: "Unexpected server error. Please report if persistent.", - color: "bg-red-100 text-red-700", - }, - ]; - - const simulationStatuses = [ - { - status: "pending", - description: "Queued and waiting for a worker to pick up", - color: "bg-gray-100 text-gray-700", - }, - { - status: "running", - description: "Currently being processed by a worker", - color: "bg-blue-100 text-blue-700", - }, - { - status: "completed", - description: "Successfully finished processing", - color: "bg-green-100 text-green-700", - }, - { - status: "failed", - description: "An error occurred during processing", - color: "bg-red-100 text-red-700", - }, - ]; - - return ( -
-

- Status codes -

-

- HTTP status codes and resource statuses used by the API. -

- -
-
-

- HTTP status codes -

-
- {statusCodes.map((item) => ( -
- - {item.code} - -
- {item.name} -

- {item.description} -

-
-
- ))} -
-
- -
-

- Simulation status -

-

- Simulations and reports progress through these statuses: -

-
- {simulationStatuses.map((item) => ( -
- - {item.status} - -

- {item.description} -

-
- ))} -
-
- -
-

- Error response format -

-

- Error responses follow a consistent format: -

-
-{`{
-  "detail": "Error message describing what went wrong"
-}`}
-          
-

- Validation errors (422) include field-level details: -

-
-{`{
-  "detail": [
-    {
-      "loc": ["body", "dataset_id"],
-      "msg": "field required",
-      "type": "value_error.missing"
-    }
-  ]
-}`}
-          
-
- -
-

- Polling strategy -

-

- For async operations like simulations and reports: -

-
    -
  1. Submit the request (POST) and receive the resource with pending status
  2. -
  3. Poll the GET endpoint every 2-5 seconds
  4. -
  5. Check the status field in the response
  6. -
  7. Stop polling when status is completed or failed
  8. -
  9. If failed, check the error_message field for details
  10. -
-
-

- Tip: Use exponential backoff for production systems to avoid overwhelming the API during high load. -

-
-
-
-
- ); -} diff --git a/docs/src/app/setup/page.tsx b/docs/src/app/setup/page.tsx deleted file mode 100644 index cad7242..0000000 --- a/docs/src/app/setup/page.tsx +++ /dev/null @@ -1,198 +0,0 @@ -export default function SetupPage() { - return ( -
-

- Environment setup -

-

- Environment variables across services: local dev, Cloud Run, Modal, and GitHub Actions. -

- -
-
-

Overview

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ServiceConfig locationSecrets storage
Local dev.env fileLocal file
Cloud Run (API)TerraformGitHub Secrets
Modal.comModal secretsModal dashboard
GitHub ActionsWorkflow filesGitHub Secrets
-
-
- -
-

Local development

-

- Copy .env.example to .env and configure: -

- -
-
{`# Supabase (from \`supabase start\` output)
-SUPABASE_URL=http://127.0.0.1:54321
-SUPABASE_KEY=eyJ...
-SUPABASE_SERVICE_KEY=eyJ...
-SUPABASE_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
-
-# Storage
-STORAGE_BUCKET=datasets
-
-# API
-API_TITLE=PolicyEngine API
-API_VERSION=0.1.0
-API_PORT=8000
-DEBUG=true
-
-# Observability
-LOGFIRE_TOKEN=...
-LOGFIRE_ENVIRONMENT=local
-
-# Modal (for local testing)
-MODAL_TOKEN_ID=ak-...
-MODAL_TOKEN_SECRET=as-...`}
-
-
- -
-

Modal.com secrets

-

- Modal functions read from a secret named policyengine-db: -

- -
-
{`modal secret create policyengine-db \\
-  DATABASE_URL="postgresql://..." \\
-  SUPABASE_URL="https://xxx.supabase.co" \\
-  SUPABASE_KEY="eyJ..." \\
-  STORAGE_BUCKET="datasets"`}
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
KeyDescription
DATABASE_URLSupabase Postgres (use connection pooler)
SUPABASE_URLSupabase project URL
SUPABASE_KEYSupabase anon or service key
STORAGE_BUCKETSupabase storage bucket name
-
-
- -
-

GitHub Actions

-

- Required secrets for CI/CD (Settings → Secrets): -

- -
-
-

Secrets

-
- {["SUPABASE_URL", "SUPABASE_KEY", "SUPABASE_DB_URL", "LOGFIRE_TOKEN", "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET", "GCP_WORKLOAD_IDENTITY_PROVIDER", "GCP_SERVICE_ACCOUNT"].map((secret) => ( -
- {secret} -
- ))} -
-
-
-

Variables

-
- {["GCP_PROJECT_ID", "GCP_REGION", "PROJECT_NAME", "API_SERVICE_NAME"].map((variable) => ( -
- {variable} -
- ))} -
-
-
-
- -
-

Database URLs

-

- Supabase provides multiple connection options: -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
TypeUse casePort
DirectLocal dev54322
Pooler (transaction)Cloud Run, Modal6543
Pooler (session)Long connections5432
-
- -

- Use the transaction pooler (port 6543) for serverless environments - handles IPv4 and connection limits. -

-
-
-
- ); -} diff --git a/docs/src/components/api-context.tsx b/docs/src/components/api-context.tsx deleted file mode 100644 index a76573d..0000000 --- a/docs/src/components/api-context.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { createContext, useContext, useState, ReactNode } from "react"; - -interface ApiContextType { - baseUrl: string; - setBaseUrl: (url: string) => void; -} - -const defaultBaseUrl = process.env.NEXT_PUBLIC_API_URL || "https://v2.api.policyengine.org"; - -const ApiContext = createContext({ - baseUrl: defaultBaseUrl, - setBaseUrl: () => {}, -}); - -export function ApiProvider({ children }: { children: ReactNode }) { - const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); - - return ( - - {children} - - ); -} - -export function useApi() { - return useContext(ApiContext); -} diff --git a/docs/src/components/api-playground.tsx b/docs/src/components/api-playground.tsx deleted file mode 100644 index 3ea693b..0000000 --- a/docs/src/components/api-playground.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useApi } from "./api-context"; -import { JsonViewer } from "./json-viewer"; - -type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; - -interface ApiPlaygroundProps { - method: HttpMethod; - endpoint: string; - description: string; - defaultBody?: Record | Record[]; - pathParams?: { name: string; description: string; example: string }[]; -} - -export function ApiPlayground({ - method, - endpoint, - description, - defaultBody, - pathParams = [], -}: ApiPlaygroundProps) { - const { baseUrl } = useApi(); - const [body, setBody] = useState(defaultBody ? JSON.stringify(defaultBody, null, 2) : ""); - const [response, setResponse] = useState<{ status: number; data: unknown } | null>(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [pathValues, setPathValues] = useState>(() => { - const initial: Record = {}; - pathParams.forEach((p) => { - initial[p.name] = p.example; - }); - return initial; - }); - - const resolvedEndpoint = endpoint.replace(/:(\w+)/g, (_, name) => pathValues[name] || `:${name}`); - - const handleSubmit = async () => { - setLoading(true); - setError(null); - setResponse(null); - - try { - const url = `${baseUrl}${resolvedEndpoint}`; - const options: RequestInit = { - method, - headers: { - "Content-Type": "application/json", - }, - }; - - if (body && (method === "POST" || method === "PUT")) { - options.body = body; - } - - const res = await fetch(url, options); - const data = await res.json().catch(() => null); - - setResponse({ - status: res.status, - data, - }); - } catch (err) { - setError(err instanceof Error ? err.message : "Request failed"); - } finally { - setLoading(false); - } - }; - - const methodColors: Record = { - GET: "bg-blue-100 text-blue-700", - POST: "bg-green-100 text-green-700", - PUT: "bg-amber-100 text-amber-700", - DELETE: "bg-red-100 text-red-700", - }; - - return ( -
- {/* Header */} -
-
- - {method} - - {endpoint} -
-

{description}

-
- - {/* Path parameters */} - {pathParams.length > 0 && ( -
-

- Path parameters -

-
- {pathParams.map((param) => ( -
- - - setPathValues((prev) => ({ ...prev, [param.name]: e.target.value })) - } - className="w-full px-3 py-2 text-sm border border-[var(--color-border)] rounded-lg font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-pe-green)]" - /> -
- ))} -
-
- )} - - {/* Request body */} - {(method === "POST" || method === "PUT") && ( -
-

- Request body -

-