# Project export: AlterScore

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: AlterScore reveals where your financial knowledge stand and shows you how to improve it through real-life money decisions.
- Devpost: https://devpost.com/software/alterscore
- GitHub: https://github.com/kaustubh-dot/AlterScore
- Demo: https://alterscore.vercel.app/
- Video: https://www.youtube.com/embed/nujcJme8d5U?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — kaustubh-dot (33 commits), Ayush Jha (4 commits)

## Devpost submission (written by the team)

### Inspiration

Most financial quizzes give you a number and leave you there. You get 62 out of 100, or a label such as "average," but what are you supposed to do with that? It does not tell you which ideas you understand, which decisions caused problems, or what you should learn next. We kept coming back to that problem while building AlterScore. We did not want to make another quiz that marks answers right or wrong and then produces a mysterious score. We wanted users to see where their financial knowledge stands and understand why. We also wanted to avoid judging people through their salary, identity, credit history, or personal documents. AlterScore looks at how someone thinks through practical money situations. That felt like a fairer and more useful place to start. What AlterScore does AlterScore is an educational financial-readiness assessment for students, first-time earners, and anyone trying to become more confident with money. The questions deal with situations people can recognise: paying bills, protecting savings, handling an unexpected expense, deciding whether to borrow, and managing money when income is uncertain. Some of those questions are connected. If you spend more money now, you have less available in the next situation. If you protect your emergency reserve, you may need to accept a different cost somewhere else. Earlier decisions change what happens later. Once the assessment is complete, AlterScore shows a Financial Decision Index from 0 to 100. The number is only one part of the result. Users can also see where they did well, where their understanding appears weaker, how their choices affected the outcome, and what they could work on next. AlterScore is meant for learning. It does not decide whether someone deserves a loan, predict whether they will repay one, or replace professional financial advice.

### How we built it

We built the frontend with React, Vite, and CSS. The backend runs on FastAPI with Python and Pydantic. The assessment uses a mix of short calculations, judgement questions, and connected scenarios. We chose that mix because knowing a formula is different from making a decision when several priorities compete for the same money. During a scenario, AlterScore keeps track of values such as available cash, unpaid obligations, emergency savings, and added costs. Each answer updates that financial state. The next question then begins with the situation the user created. The quick trial looks at four parts of the final position: $$ S = 0.40O + 0.25L + 0.20C + 0.15P $$ In this formula: (O) measures how much of the required obligation was covered. (L) measures how much usable liquidity remained. (C) measures how well the user avoided unnecessary costs. (P) measures whether the remaining plan is still workable. There are no isolated 20-point questions hidden behind the interface. The score comes from the final financial position created by the user's full path. The full assessment is scored on the server. It uses one-time attempts so the same assessment cannot be submitted twice by accident. AlterScore also returns a signed, redacted summary that can be verified without exposing the user's identity or raw answers. How we used Codex and GPT-5.6 We used Codex with GPT-5.6 while planning, coding, testing, and polishing AlterScore. A lot of its work began with reading code that already existed. Codex traced how assessment data moved between the frontend and backend, found places where those contracts did not quite match, and helped us fix them without replacing working parts of the project. It also helped us build the quick trial, debug state and navigation problems, improve the experience on smaller screens, and test keyboard and reduced-motion behaviour. When a result page occasionally appeared blank after a route change, Codex helped trace the problem across storage, navigation, and rendering instead of treating it as an isolated UI bug. We also used it to write focused tests and check the final release. That saved us time, especially when a change touched several parts of the assessment. Codex does not score users while AlterScore is running. We used it to build and verify the product. The scoring itself stays deterministic because users should be able to understand where their result came from. Challenges we faced The hardest question was where to draw the line. We were building something related to financial readiness, but we did not want it to become a softer-looking version of a credit score. That affected almost every decision we made. Identity stays outside the scoring process. Reflection questions do not affect the result. The interface repeatedly explains that AlterScore is for education, not lending or approval. The connected scenarios caused plenty of headaches too. If a user went back and changed an earlier decision, the answers that followed might no longer make sense. We had to clear those answers, rebuild the later state, and make sure the final explanation matched the new path exactly. The result page was another difficult part. There was a lot we wanted to show: the score, calculations, financial state, trade-offs, recommendations, and a replay of each decision. Putting everything on screen at once felt like handing the user a spreadsheet. We ended up showing the main result first and placing the detailed evidence behind sections users can open when they want to dig deeper. Then there were the less glamorous problems that still mattered: narrow phone screens, keyboard focus, reduced-motion settings, expired attempts, accidental double submissions, and route changes that did not always behave as expected. Solving those issues took a surprising amount of the build time.

### Accomplishments we're proud of

Seeing the full assessment work from beginning to end was a big moment for us. A user can make a decision, watch it change the next financial situation, finish the assessment, and then trace the result back through every choice they made. The score does not appear from a black box. Users can see where their financial knowledge stands and why. We are especially happy that we achieved this without asking for a login, credit history, or personal financial documents. The full assessment still has secure one-time attempts and a signed result that can be independently verified. Getting explainability, privacy, and a smooth user experience to work together took a lot of effort, and it is the part of AlterScore we are proudest of.

### What we learned

We learned that an explainable score has to be designed backwards from the explanation. If the scoring system does not keep track of evidence and state changes, the interface cannot honestly explain the result later. A paragraph generated after the fact is not enough. The questions, scoring rules, API responses, and result page all need to agree. We also learned that consequences teach better than answer keys. A choice can solve today's payment problem while using up the emergency reserve needed tomorrow. Another option may protect cash but add borrowing costs. Seeing that happen makes the lesson easier to understand than simply being told that an answer was wrong. Our experience with Codex changed during the project as well. At first, it was tempting to think of it mainly as a faster way to write code. It became more useful when we asked it to trace complete user journeys, question our assumptions, test awkward edge cases, and inspect how a small change affected the rest of the system. What comes next We want to add more scenarios around budgeting, saving, borrowing, irregular income, and emergency planning. Different regions also use different currencies and talk about money differently, so localised examples are high on our list. We are interested in letting users see how their understanding changes over time, but only if we can do it without weakening the privacy choices already built into AlterScore. The next step is to put the product in front of students and first-time earners. We have tested whether the system works. Now we need to learn whether the explanations make sense to the people it was built for.

## README (from the GitHub repository)

# AlterScore

AlterScore is a deterministic financial decision-readiness assessment. It helps people practise practical money decisions and see how a result was formed.

[![CI](https://img.shields.io/github/actions/workflow/status/kaustubh-dot/AlterScore/ci.yml?branch=main&style=for-the-badge&label=CI)](https://github.com/kaustubh-dot/AlterScore/actions/workflows/ci.yml)
[![Live](https://img.shields.io/badge/Live-Try%20AlterScore-00C2A8?style=for-the-badge)](https://alterscore.vercel.app/)

**[Open the live app](https://alterscore.vercel.app/)** · **[Take the quick trial](https://alterscore.vercel.app/assessment?mode=trial)** · **[Read the API contract](docs/API_CONTRACTS.md)**

<p align="center">
  <img src="docs/assets/readme/landing-desktop.png" alt="AlterScore landing page on desktop" width="960">
</p>

## What it does

AlterScore is aimed at students, first-time earners, and anyone who wants to practise financial decisions without submitting identity or credit information. The app keeps the scoring path narrow:

- practical calculations and judgement questions;
- branching scenarios where each choice changes the next state;
- a deterministic rubric with inspectable contributions;
- no lender, underwriting, approval, or creditworthiness decision.

The production scorer is ordinary Python code. It does not load machine-learning models, serialized model files, embeddings, or research artifacts at runtime.

## Two ways to try it

### Quick trial

The quick trial runs five questions in the browser and returns an immediate preview. It is labelled illustrative and unsigned, so it is useful for orientation rather than as an authoritative record.

### Full assessment

The full assessment is issued by the FastAPI service. The service validates opaque response IDs, consumes an attempt once, carries state through branching decisions, applies the deterministic rubric, and returns an explainable 0 to 100 Financial Decision Index. The public result is redacted and signed with HMAC-SHA256 so it can be verified without exposing scoring authority.

<p align="center">
  <img src="docs/assets/readme/trial-results-desktop.png" alt="AlterScore quick trial result with score ring and explanation" width="960">
</p>

The layout also adapts to narrow screens. This is the same landing page at a mobile viewport:

<p align="center">
  <img src="docs/assets/readme/landing-mobile.png" alt="AlterScore landing page on a mobile viewport" width="420">
</p>

## Assessment lifecycle

The assessment lifecycle is intentionally short and explicit:

<p align="center">
  <img src="docs/assets/readme/assessment-flow.svg" alt="AlterScore assessment flow from visitor choice through submission and verification" width="760">
</p>

<p align="center"><a href="docs/diagrams/assessment-flow.html">Open the assessment-flow diagram source</a></p>

## Runtime boundaries

- The public v2 API is deterministic and does not depend on ML packages or model artifacts.
- Retired model-backed v1 routes return `410 Gone`; they are not part of the current scoring path.
- Answer keys and rubric logic stay on the server for the full assessment.
- Attempts and verification records are bounded in memory, so a restart can invalidate active tokens.
- The product does not collect accounts, identity documents, device fingerprints, credit history, or lender data.

The boundary is documented in [`docs/BACKEND_RUNTIME_ARCHITECTURE.md`](docs/BACKEND_RUNTIME_ARCHITECTURE.md), [`docs/DATA_SCHEMA.md`](docs/DATA_SCHEMA.md), and [`docs/API_CONTRACTS.md`](docs/API_CONTRACTS.md).

## Repository layout

| Area | Purpose |
| --- | --- |
| [`frontend/`](frontend/) | React 19 and Vite application, responsive UI, and browser contract tests |
| [`backend/app/`](backend/app/) | FastAPI application, v2 contracts, attempt lifecycle, and scoring |
| [`tests/`](tests/) | Backend unit and integration coverage |
| [`docs/`](docs/) | API, runtime, deployment, setup, and diagram documentation |
| [`scripts/ci/`](scripts/ci/) | Release packaging, smoke checks, and provenance validation |
| [`Dockerfile`](Dockerfile) | Allow-listed backend serving image for deployment |

## Run locally

Requirements: Python 3.12 and Node.js `20.19.x` or `>=22.12.0`.

```bash
# Terminal 1: backend
python -m venv venv
# Windows: venv\Scripts\activate
python -m pip install -r backend/requirements.txt
python -m uvicorn backend.app.main:app --reload --port 8000

# Terminal 2: frontend
cd frontend
npm install
npm run dev
```

Copy [`.env.example`](.env.example) to `.env` for local values. Set `ALTERSCORE_SIGNING_SECRET` to a generated base64url secret with at least 32 random bytes. Set `VITE_API_BASE_URL` when the API is not at `http://127.0.0.1:8000/api`.

## Validate changes

```bash
# Backend
python -m pip install -r backend/requirements-dev.txt
python -m pytest

# Frontend
cd frontend
npm run lint
npm run build
npm run test:phase5
npm run test:phase6
npm run test:phase7
npm run test:phase8
```

Production frontend builds require `VITE_RELEASE_SHA` to contain the exact reviewed 40-character Git SHA. CI also checks the API contract, explainability invariants, release boundaries, serving image, and paired deployment metadata.

## Deploy

The frontend is deployed to Vercel. The backend is a Docker Space on Hugging Face. Hugging Face does not need an ML model for this project: it runs the FastAPI container and its deterministic scorer. The release package intentionally excludes local data, research directories, and model artifacts.

The trusted workflow on `main` builds both sides from one SHA and publishes the backend package with [`scripts/ci/prepare_hf_release.py`](scripts/ci/prepare_hf_release.py). Configure these backend values in the hosting environment:

```text
ALTERSCORE_ENV=production
ALTERSCORE_API_VERSION=0.2.0
ALTERSCORE_RELEASE_SHA=<exact deployed commit>
ALTERSCORE_SIGNING_SECRET=<base64url secret with at least 32 random bytes>
ALTERSCORE_SIGNING_KEY_VERSION=<non-local key reference>
ALTERSCORE_CORS_ORIGINS=https://alterscore.vercel.app
```

See [`docs/SETUP.md`](docs/SETUP.md) for local configuration and [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for the release, smoke-test, and rollback gates.

## Safety boundary

AlterScore is an educational demonstration. It is not a lender, credit bureau, underwriting system, repayment predictor, financial adviser, approval tool, or source of credit offers. Do not use it for lending, eligibility, pricing, approval, denial, or another high-impact financial decision.

## License

Released under the [MIT License](LICENSE).


## Detected evidence (automated analysis)

Indexed codebase: 103 recognized source files, 781 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 126)

```
.dockerignore
.env.example
.gitattributes
.github/workflows/ci.yml
.github/workflows/deploy-hf.yml
.github/workflows/keepalive.yml
.github/workflows/rollback-release.yml
.gitignore
.python-version
.ruff.toml
backend/app/api/v1/router.py
backend/app/api/v1/routes/retired.py
backend/app/api/v2/__init__.py
backend/app/api/v2/models.py
backend/app/api/v2/router.py
backend/app/api/v2/security.py
backend/app/api/v2/service.py
backend/app/branching/__init__.py
backend/app/branching/emi.py
backend/app/branching/engine.py
backend/app/branching/model.py
backend/app/branching/negotiation.py
backend/app/branching/scenarios.py
backend/app/core/settings.py
backend/app/instrument/__init__.py
backend/app/instrument/canonical.py
backend/app/main.py
backend/app/schemas/common.py
backend/app/unified_scoring/__init__.py
backend/app/unified_scoring/models.py
backend/app/unified_scoring/service.py
backend/README.md
backend/requirements-dev.txt
backend/requirements.lock
backend/requirements.txt
CONTRIBUTING.md
Dockerfile
docs/API_CONTRACTS.md
docs/BACKEND_RUNTIME_ARCHITECTURE.md
docs/DATA_SCHEMA.md
docs/DEPLOYMENT.md
docs/diagrams/assessment-flow.html
docs/GOVERNANCE_WORKFLOW.md
docs/PROJECT_STRUCTURE.md
docs/RELEASE_MANIFEST_TEMPLATE.json
docs/ROLLBACK_CHECKLIST.md
docs/SETUP.md
frontend/.env.production
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/App.jsx
frontend/src/components/animation/PageCurtain.css
frontend/src/components/animation/PageCurtain.jsx
frontend/src/components/animation/TextReveal.jsx
frontend/src/components/hero/SignalCanvas.jsx
frontend/src/components/layout/Footer.css
frontend/src/components/layout/Footer.jsx
frontend/src/components/layout/Navbar.css
frontend/src/components/layout/Navbar.jsx
frontend/src/components/ui/MagneticButton.jsx
frontend/src/components/ui/Modal.css
frontend/src/components/ui/Modal.jsx
frontend/src/components/ui/Preloader.css
frontend/src/components/ui/Preloader.jsx
frontend/src/hooks/PageTransitionContext.jsx
frontend/src/hooks/transitionContext.js
frontend/src/hooks/useLenis.js
frontend/src/hooks/usePageTransition.js
frontend/src/hooks/useSound.js
frontend/src/lib/api.js
frontend/src/lib/assessmentV2.js
frontend/src/lib/motionPreferences.js
frontend/src/lib/releaseMetadata.js
frontend/src/lib/safeStorage.js
frontend/src/lib/trialAssessment.js
frontend/src/main.jsx
frontend/src/pages/Assessment.css
frontend/src/pages/Assessment.jsx
frontend/src/pages/Dashboard.css
frontend/src/pages/Dashboard.jsx
frontend/src/pages/Landing.css
frontend/src/pages/Landing.jsx
frontend/src/pages/NotFound.css
frontend/src/pages/NotFound.jsx
frontend/src/pages/Processing.css
frontend/src/pages/Processing.jsx
frontend/src/pages/ResearchLab.css
frontend/src/pages/ResearchLab.jsx
frontend/src/pages/Results.css
frontend/src/pages/Results.jsx
frontend/src/pages/TrialAssessment.jsx
frontend/src/pages/TrialResults.jsx
frontend/src/styles/global.css
frontend/src/styles/tokens.css
frontend/src/utils/apiErrors.js
frontend/tests/phase5-contract.test.mjs
frontend/tests/phase6-explainability.test.mjs
frontend/tests/phase7-separation.test.mjs
frontend/tests/phase8-release.test.mjs
frontend/tests/trial-assessment.test.mjs
frontend/vercel.json
frontend/verify-release-sha.mjs
frontend/vite.config.js
LICENSE
pytest.ini
README.md
scripts/ci/prepare_hf_release.py
scripts/ci/smoke_release.py
scripts/ci/validate_release_manifest.py
scripts/ci/validate_release_provenance.py
scripts/ci/write_release_manifest.py
tests/conftest.py
tests/integration/api/test_phase4_secure_anonymous_api.py
tests/integration/api/test_phase7_legacy_retirement.py
tests/unit/backend/test_branching_emi.py
tests/unit/backend/test_branching_model.py
tests/unit/backend/test_branching_negotiation.py
[6 more files omitted for size]
```

### Dependencies

- backend/requirements.txt: fastapi@==0.115.6, pydantic@==2.10.6, uvicorn[standard]@==0.34.3
- frontend/package.json: @eslint/js@^10.0.1, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, axios@^1.17.0, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, lenis@^1.3.23, lucide-react@^1.17.0, react@^19.2.6, react-dom@^19.2.6, react-router-dom@^7.17.0, vite@^8.0.12

### Recent commits (newest first)

- docs: remove architecture visuals
- Merge remote-tracking branch 'origin/main'
- chore: clean main branch docs and UI assets
- Merge pull request #5 from kaustubh-dot/codex/frontend-ui-polish
- fix(ci): retry HF readiness probe
- fixes in trial Assesment
- Improve stateful trial assessment and submission guide
- fix: separate score ring labels from values
- feat: restore rolling film preloader
- feat: refine frontend trial and preloader experience
- fix: polish score presentation and trial recovery
- docs: present AlterScore for hackathon judges
- feat: add quick trial and harden responsive UI
- feat: prepare hackathon production experience
- fix: keep README art outside retired assets
- docs: present AlterScore as an open-source project
- Disable duplicate Vercel Git deployments
- Polish assessment experience
- refine landing page copy
- Merge pull request #3 from kaustubh-dot/codex/frontend-ui-polish

## Key source files (fetched from GitHub, selected and truncated for size)

### CONTRIBUTING.md

```markdown
# Contributing to AlterScore

Thanks for helping improve AlterScore. Small, focused pull requests are easiest to review.

## Before you start

- Search existing issues and pull requests before starting work.
- For changes to scoring, assessment content, security, or public API behavior, open an issue first to agree on the approach.
- Do not submit personal, financial, or other sensitive data in issues, pull requests, fixtures, or screenshots.

## Development workflow

1. Create a branch from `main`.
2. Keep the change focused and update relevant documentation.
3. Run the narrowest relevant checks. The full local commands are in the [README](README.md#verify-changes).
4. Open a pull request that explains the problem, the change, and how you verified it.

## Project boundaries

AlterScore is an educational demonstration. Contributions must preserve its explicit boundaries: it is not a lending, underwriting, approval, denial, creditworthiness, or automated financial-decision system. The public scorer must remain deterministic and explainable.

## Reporting security concerns

Do not disclose vulnerabilities or secrets in a public issue. Use GitHub's private vulnerability-reporting feature for this repository when it is available. If it is not available, open a public issue containing only a non-sensitive request for a private reporting channel.

```

### docs/PROJECT_STRUCTURE.md

```markdown
# Project structure

AlterScore is organized around a public deterministic v2 service and a Vercel
React frontend. Retired research payloads are excluded from the production
repository.

## Active layout

| Path | Role |
| --- | --- |
| `backend/app/api/v2/` | Anonymous form, score, verification, liveness, and readiness routes |
| `backend/app/instrument/` | Canonical server-owned objective and judgment instrument |
| `backend/app/branching/` | Deterministic financial-state transitions and replays |
| `backend/app/unified_scoring/` | Frozen score composition and explanation construction |
| `frontend/` | Assessment, processing, explainable results, dashboard, and static Research Lab |
| `tests/` | Public v2, instrument, branching, unified-scoring, and Phase 7 coverage |
| `docs/` | Active contract, architecture, setup, deployment, rollback, and governance docs |

## Retired research boundary

The former ML source tree, serialized artifacts, explainers, training scripts,
client question bank, Admin surface, and legacy tests are intentionally absent
from the production branch. They remain recoverable through Git history and
the pre-production backup branch, but are never imported, packaged, or served.

## Deployment-critical files

| File | Role |
| --- | --- |
| `Dockerfile` | Allow-listed v2 serving image |
| `.dockerignore` | Build-context defense in depth |
| `backend/requirements.txt` | Human-reviewed direct serving dependencies |
| `backend/requirements.lock` | Hash-locked Linux production serving environment |
| `frontend/.env.production` | Public frontend API base URL |
| `.github/workflows/deploy-hf.yml` | Trusted CI-gated paired-release authority with verified manifest retention |

The production entrypoint is `backend.app.main:app`. It does not load a model
manifest, serialized artifact, report, or legacy request logger.

```

### Dockerfile

```
FROM python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a1bfc852199fbf

WORKDIR /code

# Install only the hash-locked public v2 serving runtime. requirements.txt is
# retained beside the lock as the human-reviewed direct-dependency source.
COPY backend/requirements.txt ./requirements.txt
COPY backend/requirements.lock ./requirements.lock
RUN pip install --no-cache-dir --require-hashes -r requirements.lock

# Whitelist the serving application. Research source, training scripts, and
# model artifacts are deliberately outside the production image context.
COPY backend/app ./backend/app

EXPOSE 7860

ARG ALTERSCORE_RELEASE_SHA=local
ENV ALTERSCORE_RELEASE_SHA=${ALTERSCORE_RELEASE_SHA}
ARG ALTERSCORE_SIGNING_KEY_VERSION=local
ENV ALTERSCORE_SIGNING_KEY_VERSION=${ALTERSCORE_SIGNING_KEY_VERSION}

# Container health follows the public readiness contract, so a missing
# signing configuration or serving-store failure cannot appear healthy.
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD python -c "import json, sys, urllib.request; payload=json.load(urllib.request.urlopen('http://localhost:7860/api/ready', timeout=8)); checks=payload.get('checks', []); expected=('instrument', 'scorer', 'signing', 'attempt_store', 'verification_store', 'rate_limits'); sys.exit(0 if payload.get('status') == 'ready' and tuple(check.get('name') for check in checks) == expected and all(check.get('status') == 'pass' for check in checks) else 1)"

ENV ALTERSCORE_ENV=production
ENV ALTERSCORE_API_VERSION=0.2.0
ENV ALTERSCORE_CORS_ORIGINS=https://alterscore.vercel.app
ENV ALTERSCORE_CORS_ORIGIN_REGEX=https://alterscore-[a-z0-9-]+\.vercel\.app

CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers", "--forwarded-allow-ips", "127.0.0.1,10.0.0.0/8"]

```

### backend/requirements.txt

```
# Public AlterScore v2 serving dependencies.
# Research/model-training dependencies live outside the production graph.
fastapi==0.115.6
uvicorn[standard]==0.34.3
pydantic==2.10.6

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "engines": {
    "node": "^20.19.0 || >=22.12.0"
  },
  "scripts": {
    "dev": "vite",
    "build": "npm run verify:release && vite build",
    "lint": "eslint .",
    "verify:release": "node verify-release-sha.mjs",
    "test:phase5": "node --test tests/phase5-contract.test.mjs",
    "test:phase6": "node --test tests/phase6-explainability.test.mjs",
    "test:phase7": "node --test tests/phase7-separation.test.mjs",
    "test:phase8": "node --test tests/phase8-release.test.mjs",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.17.0",
    "lenis": "^1.3.23",
    "lucide-react": "^1.17.0",
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-router-dom": "^7.17.0"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "vite": "^8.0.12"
  }
}

```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)


```

### backend/app/main.py

```python
"""FastAPI application entrypoint for the public v2 assessment service."""

from __future__ import annotations

from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from backend.app.api.v1.router import api_router
from backend.app.api.v2.router import router as api_v2_router
from backend.app.api.v2.service import AnonymousAssessmentService
from backend.app.core.settings import Settings, get_settings


def create_app(settings: Settings | None = None) -> FastAPI:
    """Create the public app without loading archived research artifacts."""

    resolved_settings = settings or get_settings()

    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncIterator[None]:
        app.state.settings = resolved_settings
        app.state.anonymous_assessment_service = AnonymousAssessmentService(
            resolved_settings
        )
        yield

    app = FastAPI(
        title="AlterScore Public Assessment API",
        version=resolved_settings.api_version,
        lifespan=lifespan,
    )
    app.add_middleware(
        CORSMiddleware,
        allow_origins=list(resolved_settings.cors_origins),
        allow_origin_regex=resolved_settings.cors_origin_regex,
        allow_credentials=False,
        allow_methods=["GET", "POST", "OPTIONS"],
        allow_headers=["Authorization", "Content-Type"],
    )
    app.include_router(api_router, prefix="/api")
    app.include_router(api_v2_router, prefix="/api")

    @app.middleware("http")
    async def add_public_privacy_headers(request, call_next):
        is_public_route = request.url.path.startswith(
            "/api/v2/"
        ) or request.url.path in {
            "/api/live",
            "/api/ready",
            "/api/score",
            "/api/debug-score",
        }
        if is_public_route:
            # Keep the source address only long enough for the v2 limiter to
            # derive its salted hash, then redact it before access logging.
            request.state.phase4_network_host = (
                request.client.host if request.client is not None else None
            )
            request.scope["client"] = ("redacted", 0)
        response = await call_next(request)
        if is_public_route:
            response.headers["Cache-Control"] = "no-store"
            response.headers["Referrer-Policy"] = "no-referrer"
            response.headers["Strict-Transport-Security"] = (
                "max-age=31536000; includeSubDomains"
            )
        return response

    return app


app = create_app()


__all__ = ["app", "create_app"]

```

### frontend/src/App.jsx

```javascript
import { lazy, Suspense, useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
import Navbar from './components/layout/Navbar';
import Footer from './components/layout/Footer';
import Landing from './pages/Landing';
import Assessment from './pages/Assessment';
import Preloader from './components/ui/Preloader';
import useLenis from './hooks/useLenis';
import useSound from './hooks/useSound';
import { PageTransitionProvider } from './hooks/PageTransitionContext';
import { getSessionStorage, readStorageItem, writeStorageItem } from './lib/safeStorage';
import './styles/global.css';

const Results = lazy(() => import('./pages/Results'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const ResearchLab = lazy(() => import('./pages/ResearchLab'));
const NotFound = lazy(() => import('./pages/NotFound'));

function AppContent() {
  const [showPreloader, setShowPreloader] = useState(() => {
    return readStorageItem(getSessionStorage(), 'alterscore_preloader_seen') !== 'true';
  });
  const location = useLocation();
  const { initAudio } = useSound();

  // Initialize Lenis smooth scroll
  useLenis();

  useEffect(() => {
    if (showPreloader) return undefined;
    const focusHeading = () => {
      const heading = document.querySelector('h1');
      if (!heading) return false;
      heading.setAttribute('tabindex', '-1');
      heading.focus({ preventScroll: true });
      return true;
    };
    const observer = new MutationObserver(() => {
      if (focusHeading()) observer.disconnect();
    });
    if (!focusHeading()) observer.observe(document.querySelector('.content-wrap'), { childList: true, subtree: true });
    return () => observer.disconnect();
  }, [location.pathname, showPreloader]);

  // Block scroll on page load during preloader
  useEffect(() => {
    if (showPreloader) {
      document.body.style.overflow = 'hidden';
      document.body.classList.remove('preloader-done');
    } else {
      document.body.style.overflow = '';
      document.body.classList.add('preloader-done');
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [showPreloader]);

  // Resume audio only for users who explicitly enabled sound earlier.
  useEffect(() => {
    const handleFirstInteraction = () => {
      initAudio();
      window.removeEventListener('click', handleFirstInteraction);
      window.removeEventListener('keydown', handleFirstInteraction);
      window.removeEventListener('touchstart', handleFirstInteraction);
    };

    window.addEventListener('click', handleFirstInteraction);
    window.addEventListener('keydown', handleFirstInteraction);
    window.addEventListener('touchstart', handleFirstInteraction);

    return () => {
      window.removeEventListener('click', handleFirstInteraction);
      window.removeEventListener('keydown', handleFirstInteraction);
      window.removeEventListener('touchstart', handleFirstInteraction);
    };
  }, [initAudio]);

  const handlePreloadComplete = () => {
    writeStorageItem(getSessionStorage(), 'alterscore_preloader_seen', 'true');
    setShowPreloader(false);
    document.body.style.overflow = '';
    document.body.classList.add('preloader-done');
    window.dispatchEvent(new CustomEvent('preloadComplete'));
  };

  const isAssessment = location.pathname === '/assessment';
  const hideGlobalChrome = isAssessment || location.pathname === '/dashboard';
  const showFooter = location.pathname === '/' || location.pathname === '/results';

  return (
    <PageTransitionProvider>
      <div className="app-container">
        {showPreloader && (
          <Preloader onComplete={handlePreloadComplete} />
        )}
        <a className="skip-link" href="#main-content">Skip to main content</a>
        
        {/* Dashboard and assessment render their own focused navigation. */}
        {!hideGlobalChrome && <Navbar />}

        <div id="main-content" className="content-wrap" tabIndex={-1}>
          <Suspense fallback={<div className="route-loading" role="status"><span>Loading interface</span></div>}>
            <Routes>
              <Route path="/" element={<Landing />} />
              <Route path="/assessment" element={<Assessment />} />
              <Route path="/results" element={<Results />} />
              <Route path="/dashboard" element={<Dashboard />} />
              <Route path="/research" element={<ResearchLab />} />
              <Route path="*" element={<NotFound />} />
            </Routes>
          </Suspense>
        </div>

        {showFooter && <Footer />}
      </div>
    </PageTransitionProvider>
  );
}

export default function App() {
  return (
    <Router>
      <AppContent />
    </Router>
  );
}

```

### frontend/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import process from 'node:process'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: process.env.VITE_PROXY_TARGET || 'http://localhost:8000',
        changeOrigin: true,
        secure: false,
      }
    }
  }
})


```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
  },
])

```

[93 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]