# ImpactFlow Self-Discovery Module ImpactFlow Self-Discovery is a standalone FastAPI service that turns five short reflection stories into a structured Enneagram + Ikigai profile. It is designed to run independently from the main ImpactFlow app on port `8011` and can be integrated into the larger product later. The service presents a lightweight browser flow, stores the user's narrative answers in SQLite, sends those answers to Anthropic for structured extraction, persists the resulting profile, and displays a plain-language profile page. ## Quick Explanation For AI Assistants If you need to explain this app in detail, use this mental model: 1. A user opens `/static/discovery.html`. 2. The browser sends them through Google sign-in at `GET /api/auth/login`; `GET /api/auth/callback` mints an access JWT + refresh token and the browser stores the access JWT for subsequent requests. 3. With the JWT in the `Authorization: Bearer …` header, the browser starts a discovery conversation with `POST /discovery/start` — the backend derives the `user_id` from the JWT, not from the request body. 4. The user answers five open-ended prompts. 5. The browser saves all five answers with `PUT /discovery/{conversation_id}/respond`. 6. The browser asks the backend to analyze the saved answers with `POST /discovery/{conversation_id}/complete`. 7. The backend calls Anthropic through `DiscoveryExtractor`. 8. The extractor asks for JSON containing Enneagram, instinctual variant, Ikigai summaries, confidence flags, and optional extraction notes. 9. The backend stores that JSON as a `DiscoveryProfile` row owned by the authenticated user. 10. The browser redirects to `/static/profile.html`. 11. The profile page loads the newest profile with `GET /discovery/profile/me` and lets the user lock it with `PUT /discovery/profile/me/confirm`. Machine-to-machine callers (e.g. the MCP server) skip the OAuth dance and authenticate with `X-API-Key: $IMPACTFLOW_API_KEY` instead. That header resolves to a synthetic admin user (`api-key-admin`) so foreign keys stay valid and admin-only routes work without a real Google sign-in. The app is intentionally small: static HTML/CSS for the UI, FastAPI for the API, async SQLAlchemy for persistence, Alembic for migrations, SQLite for local storage, Google OAuth + JWT for browser auth, and Anthropic for the analysis step. ## What The App Does The module collects five narrative prompts: | Stored field | User-facing prompt | Purpose in extraction | | --- | --- | --- | | `prompt_alive` | The Alive Moment | Reveals energy, motivation, strengths, and core need | | `prompt_friction` | The Friction Moment | Strongest signal for Enneagram triad | | `prompt_pull` | The Natural Pull | Helps infer instinctual variant and recurring interests | | `prompt_recognition` | The Recognition Moment | Reveals what the person values being seen for | | `prompt_future` | The Future Pull | Helps infer mission, vocation, and ideal future direction | The generated profile includes: | Field | Meaning | | --- | --- | | `triad` | One of `gut`, `heart`, or `head` | | `probable_type` | Likely Enneagram type number, `1` through `9` | | `wing` | Adjacent Enneagram wing type | | `instinctual_variant` | One of `sp`, `so`, or `sx` | | `instinctual_stack` | Ordered stack such as `sp/so/sx` | | `love_summary` | Ikigai: what the user loves | | `strength_summary` | Ikigai: what the user is good at | | `mission_summary` | Ikigai: what the world needs from the user | | `vocation_summary` | Ikigai: what the user can be paid for | | `overlap_narrative` | Plain-language convergence narrative | | `confidence` | Confidence flags for triad, type, variant, and Ikigai | | `extraction_notes` | Optional ambiguity or caveat from the model | | `locked` | Whether the user has confirmed the profile | The profile page deliberately avoids showing the raw Enneagram type number to the user. It translates the triad into plain-language pattern descriptions and shows the Ikigai summaries as cards. ## Architecture ```text Browser static UI MCP server / other machines discovery.html X-API-Key: $IMPACTFLOW_API_KEY profile.html | | | Google OAuth + | | Bearer JWT | v v +--------------------------------------------+ | FastAPI app | | app/main.py (middleware stack) | | app/auth.py (dual-auth dependency)| | app/routers/auth.py /api/auth, /api/me | app/routers/activity.py /api/activity | app/routers/discovery.py /discovery/* | | app/tracking.py (activity middleware) | +--------------------------------------------+ | v Async SQLAlchemy + SQLite app/database.py app/models.py (users, refresh_tokens, activity_log, discovery_conversation, discovery_profile) data/discovery.db | v Anthropic extraction app/services/extractor.py ``` Important files: | File | Role | | --- | --- | | `app/main.py` | FastAPI entrypoint, middleware wiring, lifespan hooks, health check, static file mount, root redirect | | `app/auth.py` | Google OAuth registration, JWT issue/decode, dual-auth dependency, refresh-token hashing, domain allow-list | | `app/tracking.py` | `ActivityTrackingMiddleware` and `log_activity` helper | | `app/routers/auth.py` | OAuth endpoints, `/api/me`, sessions, refresh, logout, `/api/me/stats` | | `app/routers/activity.py` | Activity feed, per-user summary, admin activity view, 90-day retention pruner | | `app/routers/discovery.py` | Discovery API routes; all routes user-scoped via the auth dependency | | `app/schemas.py` | Pydantic request and response models | | `app/models.py` | SQLAlchemy ORM models for users, refresh tokens, activity log, conversations, and profiles | | `app/database.py` | Async database engine, session factory, SQLite directory setup | | `app/services/extractor.py` | Anthropic client wrapper, prompt, JSON parsing, retry logic | | `app/migration_bootstrap.py` | Stamps pre-Alembic SQLite DBs as revision `001` so `alembic upgrade head` succeeds on older local databases | | `app/static/discovery.html` | Browser-based five-prompt flow | | `app/static/profile.html` | Browser-based profile display and confirm action | | `app/static/style.css` | Shared UI styling | | `alembic/versions/001_initial.py` | Initial database schema migration | | `alembic/versions/002_add_auth.py` | Adds `users`, `refresh_tokens`, and `activity_log` tables | | `tests/test_extractor.py` | Unit tests for extraction plumbing and retry behavior | | `tests/test_auth.py` | Tests for the dual-auth dependency, JWT minting/decode, admin enforcement, and domain allow-list | | `tests/test_migration_bootstrap.py` | Unit tests for the pre-Alembic SQLite stamping helper | | `tests/test_static_discovery.py` | Guard test for the insecure-context UUID fallback in `discovery.html` | | `tests/fixtures/{gut,head,heart}_type_responses.json` | Synthetic five-prompt responses used by extractor tests | | `smoke_test.py` | In-process end-to-end API smoke test with fake extraction; also exercises the auth and activity tracking surface | ## Authentication The API supports two ways to authenticate, both resolved by a single `get_current_user` dependency in `app/auth.py`: | Caller | Mechanism | Notes | | --- | --- | --- | | Browser users | Google OAuth + signed JWT in `Authorization: Bearer …` | Issued by `/api/auth/callback` after a successful Google sign-in | | Machine-to-machine (MCP server, scripts) | `X-API-Key: $IMPACTFLOW_API_KEY` | Resolves to a synthetic admin user `api-key-admin` so FK constraints stay valid | The first real Google user to sign in is auto-promoted to `role=admin`; every subsequent user defaults to `role=user`. Admin-only routes (e.g. `/api/admin/activity`) check the role on the resolved user. ### One-Time Google Cloud Setup 1. In open APIs & Services → Credentials. 2. Create an **OAuth 2.0 Client ID** of type **Web application**. 3. Add authorized redirect URIs that match `OAUTH_REDIRECT_URI` in `.env`: - `http://localhost:8000/api/auth/callback` for local dev - `http://:8000/api/auth/callback` for the deployed instance 4. Copy the client id and client secret into `.env` as `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. 5. Generate two random 64-character strings and put them in `.env` as `JWT_SECRET` and `IMPACTFLOW_API_KEY`: ```bash .venv/Scripts/python.exe -c "import secrets; print(secrets.token_urlsafe(48))" ``` ### OAuth Endpoints | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/api/auth/login` | 302s the browser to Google's consent screen | | `GET` | `/api/auth/callback` | Google redirects here with `code`; we exchange it for a Google access token, find-or-create the user, then return `{access_token, refresh_token, user}` | | `POST` | `/api/auth/refresh` | Body `{refresh_token}` → new short-lived access token | | `POST` | `/api/auth/logout` | Body `{refresh_token}` → revokes that refresh token (idempotent) | `access_token` lifetime defaults to 15 minutes; `refresh_token` lifetime defaults to 7 days. Both are configurable through `.env`. Refresh tokens are stored as SHA-256 hashes — the raw value only exists in the response from `/api/auth/callback` and `/api/auth/refresh`. ### Profile And Session Endpoints | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/api/me`, `/api/auth/me` | Current user profile | | `PATCH` | `/api/me` | Update `display_name` (email is owned by Google) | | `GET` | `/api/me/stats` | Conversations / profiles / locked-profiles / 30-day activity counts | | `GET` | `/api/me/sessions` | List active refresh tokens (`id`, `device`, `created_at`, `expires_at`) | | `DELETE` | `/api/me/sessions/{id}` | Revoke a refresh token | ### Activity Tracking `ActivityTrackingMiddleware` records one `activity_log` row per authenticated, successful (`status < 400`), non-noisy request. The `source` column is set to `mcp` when the request carries an `X-API-Key` header and `web` otherwise, so Claude-initiated calls are distinguishable from browser activity. Rows older than 90 days are pruned on app startup. | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/api/activity?page=…&limit=…` | Paginated activity feed for the current user | | `GET` | `/api/activity/summary?days=30` | Per-day counts, top resources, web/mcp ratio | | `GET` | `/api/admin/activity?user_id=…` | Admin-only cross-user feed | ## Runtime Flow ### 1. App Startup `app/main.py` creates the FastAPI app, wires the middleware stack (`ActivityTrackingMiddleware` innermost, then `SessionMiddleware` for the OAuth state cookie, `CORSMiddleware` outermost), includes the auth, activity, and discovery routers, mounts `/static`, and redirects `/` to `/static/discovery.html`. The lifespan hook: 1. Calls `Base.metadata.create_all()` as a local development safety net. Alembic remains the source of truth for schema changes. 2. Calls `ensure_api_key_admin()` so the synthetic admin user backing `X-API-Key` exists before the first request arrives. 3. Calls `prune_old_activity()` to drop activity log rows older than 90 days. In Docker, the container's `CMD` runs three steps in order before serving: 1. `python -m app.migration_bootstrap` — if `data/discovery.db` already exists with the app tables but no `alembic_version` row (older local DBs created via `create_all` before Alembic existed), stamp it as revision `001` so step 2 does not try to recreate existing tables. 2. `alembic upgrade head` — apply any outstanding migrations. 3. `uvicorn app.main:app --host 0.0.0.0 --port 8011` — serve the app. The Dockerfile also runs `alembic upgrade head` at build time against a throwaway in-image DB as a sanity check that migrations apply cleanly. At runtime the `./data` volume shadows `/app/data`, so the runtime migration step above is what populates the persistent database. ### 2. Starting A Conversation `POST /discovery/start` requires authentication (Bearer JWT or `X-API-Key`). It takes no body — the `user_id` is derived from the authenticated user. It creates a `DiscoveryConversation` row with: - a UUID conversation id - `user_id` set to the authenticated user's id - `started_at` in UTC - empty prompt fields It returns: ```json { "conversation_id": "uuid" } ``` The static UI starts this conversation after the browser has a JWT from the OAuth callback, and retries on submit if the first start call failed. ### 3. Saving Answers `PUT /discovery/{conversation_id}/respond` accepts all five prompt responses: ```json { "prompt_alive": "I felt most alive when...", "prompt_friction": "Something felt wrong when...", "prompt_pull": "I naturally keep returning to...", "prompt_recognition": "I felt seen when...", "prompt_future": "If I could not fail..." } ``` It stores the responses on the existing conversation and returns: ```json { "conversation_id": "uuid", "status": "responses_saved" } ``` If the conversation id does not exist, it returns `404`. ### 4. Completing Analysis `POST /discovery/{conversation_id}/complete` loads the conversation, builds a compact response dictionary with keys `alive`, `friction`, `pull`, `recognition`, and `future`, then calls `DiscoveryExtractor.extract()`. The route rejects completion with: - `404` if the conversation does not exist - `400` if all five responses are blank - `502` if Anthropic extraction fails or returns unusable output after retry On success, it stores a new `DiscoveryProfile`, marks the conversation `completed_at`, and returns the profile. ### 5. Loading The Profile `GET /discovery/profile/me` fetches the newest profile for the authenticated user by descending `generated_at`. The profile page uses this route after redirect. This means one user can have multiple completed conversations, but the UI always displays the latest one. ### 6. Confirming The Profile `PUT /discovery/profile/me/confirm` locks the newest profile by setting `locked = true`. This is the current confirmation mechanism for "This is me" on the profile page. It does not prevent future conversations from generating newer profiles. ## API Reference All `/discovery/*`, `/api/me*`, `/api/activity*`, and `/api/admin/*` routes require authentication (Bearer JWT or `X-API-Key`). `/api/auth/login`, `/api/auth/callback`, `/health`, `/`, and `/static/*` are public. | Method | Path | Auth | Purpose | | --- | --- | --- | --- | | `GET` | `/` | public | Redirects to `/static/discovery.html` | | `GET` | `/health` | public | Liveness check, returns `{"status": "ok"}` | | `GET` | `/api/auth/login` | public | 302 to Google consent screen | | `GET` | `/api/auth/callback` | public | OAuth callback; issues `{access_token, refresh_token, user}` | | `POST` | `/api/auth/refresh` | public (token in body) | Exchange refresh token for a new access token | | `POST` | `/api/auth/logout` | public (token in body) | Revoke a refresh token | | `GET` | `/api/me`, `/api/auth/me` | yes | Current user profile | | `PATCH` | `/api/me` | yes | Update `display_name` | | `GET` | `/api/me/stats` | yes | Per-user usage stats | | `GET` | `/api/me/sessions` | yes | List active refresh tokens | | `DELETE` | `/api/me/sessions/{id}` | yes | Revoke a refresh token | | `GET` | `/api/activity` | yes | Paginated activity feed for current user | | `GET` | `/api/activity/summary` | yes | Aggregate activity stats | | `GET` | `/api/admin/activity` | admin | All-users activity feed | | `POST` | `/discovery/start` | yes | Begin a conversation (user derived from auth) | | `PUT` | `/discovery/{conversation_id}/respond` | yes | Save all five responses | | `POST` | `/discovery/{conversation_id}/complete` | yes | Run extraction, store profile, return profile | | `GET` | `/discovery/profile/me` | yes | Fetch newest profile for the authenticated user | | `PUT` | `/discovery/profile/me/confirm` | yes | Lock newest profile for the authenticated user | | `GET` | `/discovery/conversation/{conversation_id}` | yes | Fetch stored conversation responses (owner only) | ## Data Model ### `users` One row per signed-in human, plus a single synthetic `api-key-admin` row backing the `X-API-Key` dual-auth path. | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key; `api-key-admin` for the synthetic row | | `email` | string | Unique, from Google | | `display_name` | string | From Google `name` claim; editable via `PATCH /api/me` | | `avatar_url` | string nullable | Google profile picture | | `google_id` | string nullable, unique | Google `sub` claim; null only for the API-key row | | `role` | string | `user` or `admin`; first real user is auto-promoted | | `created_at` | datetime | UTC | | `last_login_at` | datetime nullable | Updated on every OAuth callback | ### `refresh_tokens` | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `token_hash` | string | SHA-256 of the raw token; unique | | `device` | string nullable | Captured from `User-Agent` at issue time | | `created_at` | datetime | UTC | | `expires_at` | datetime | UTC | | `revoked_at` | datetime nullable | Set by logout or `DELETE /api/me/sessions/{id}` | ### `activity_log` Append-only audit trail. Pruned to 90 days on app startup. | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `action` | string | `view`, `create`, `update`, `delete`, or `other` | | `resource` | string | Coarse resource name parsed from the URL path | | `resource_id` | string nullable | When a specific record id is identifiable | | `metadata_json` | text nullable | JSON-encoded extra context | | `source` | string | `web` or `mcp` (set from presence of `X-API-Key`) | | `ip_address` | string nullable | From `request.client.host` | | `user_agent` | text nullable | Truncated to 1000 chars | | `created_at` | datetime | UTC, indexed | ### `discovery_conversation` Stores one five-prompt response set. | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `started_at` | datetime | UTC timestamp | | `completed_at` | datetime nullable | Set after successful profile generation | | `prompt_alive` | text nullable | First narrative response | | `prompt_friction` | text nullable | Second narrative response | | `prompt_pull` | text nullable | Third narrative response | | `prompt_recognition` | text nullable | Fourth narrative response | | `prompt_future` | text nullable | Fifth narrative response | ### `discovery_profile` Stores one extracted profile for one conversation. | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `conversation_id` | string | Foreign key to `discovery_conversation.id` | | `generated_at` | datetime | UTC timestamp | | `triad` | string nullable | `gut`, `heart`, or `head` | | `probable_type` | integer nullable | Enneagram type number | | `wing` | integer nullable | Adjacent Enneagram wing | | `instinctual_variant` | string nullable | `sp`, `so`, or `sx` | | `instinctual_stack` | string nullable | Ordered stack | | `love_summary` | text nullable | Ikigai love summary | | `strength_summary` | text nullable | Ikigai strength summary | | `mission_summary` | text nullable | Ikigai mission summary | | `vocation_summary` | text nullable | Ikigai vocation summary | | `overlap_narrative` | text nullable | Convergence narrative | | `confidence_json` | text nullable | JSON string for confidence flags | | `locked` | boolean | Defaults to false | ## Extraction Details `DiscoveryExtractor` is intentionally responsible for plumbing, not business logic hidden elsewhere. It: 1. Validates that `ANTHROPIC_API_KEY` exists. 2. Builds one user message from the five responses. 3. Calls Anthropic with `SYSTEM_PROMPT`. 4. Parses the model response as JSON. 5. Strips Markdown code fences if present. 6. Validates that all required top-level keys exist. 7. Validates that `confidence` contains `triad`, `type`, `variant`, and `ikigai`. 8. Retries once with an explicit JSON-only reminder if the first response cannot be parsed or is missing required keys. 9. Raises `DiscoveryExtractionError` if the API call fails or retry also fails. Default extraction settings: | Setting | Value | | --- | --- | | Default model | `claude-sonnet-4-5` | | Max tokens | `2000` | | Required response type | Single JSON object | | Retry count | One retry after invalid JSON or missing keys | The system prompt instructs the model to infer: - Enneagram triad and type from emotional center signals - instinctual variant from recurring attention patterns - Ikigai love, strength, mission, and vocation from repeated story themes - confidence levels based on consistency and strength of evidence ## Frontend Behavior The frontend is static HTML with embedded JavaScript. `discovery.html`: - stores a generated `impactflow_user_id` in `localStorage` - generates that id via `createUserId()`, which prefers `crypto.randomUUID()` when available and falls back to `crypto.getRandomValues()` so the flow still works in insecure contexts (e.g. plain `http://` over LAN) - shows five prompts one at a time - keeps answers in memory while navigating back and next - starts a conversation on page load - saves all responses on submit - triggers extraction - redirects to the profile page on success - shows an error box and reload button if submission fails `profile.html`: - reads `user_id` from the query string - fetches the newest profile for that user - escapes all model-generated text before rendering - shows Ikigai cards and a triad description - uses confidence dots for triad and Ikigai confidence - sends the confirm request when the user clicks "This is me" ## Configuration Populate `.env` with at minimum the Anthropic key and the auth-related secrets: ```text ANTHROPIC_API_KEY=your-key-here GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxx JWT_SECRET=random-64-char-string IMPACTFLOW_API_KEY=random-64-char-string ``` Environment variables: | Env var | Default | Notes | | --- | --- | --- | | `ANTHROPIC_API_KEY` | required | Used by `DiscoveryExtractor` | | `DATABASE_URL` | `sqlite+aiosqlite:///./data/discovery.db` | Async SQLAlchemy database URL | | `HOST_BIND_IP` | `0.0.0.0` | Docker host IP for publishing port `8011`; use this when Docker runs inside WSL | | `HOST_PORT` | `8011` | WSL/Docker host port; Windows portproxy exposes the same port | | `ANTHROPIC_MODEL` | `claude-sonnet-4-5` | Override extraction model | | `GOOGLE_CLIENT_ID` | required for browser auth | OAuth 2.0 client id from Google Cloud Console | | `GOOGLE_CLIENT_SECRET` | required for browser auth | OAuth 2.0 client secret | | `OAUTH_REDIRECT_URI` | derived from request | Override the callback URL Google redirects to; must be registered in the Cloud Console | | `JWT_SECRET` | required | HS256 signing key for access tokens; also used for the OAuth `state` session cookie | | `JWT_ACCESS_MINUTES` | `15` | Access-token lifetime | | `JWT_REFRESH_DAYS` | `7` | Refresh-token lifetime | | `IMPACTFLOW_API_KEY` | required for MCP/machine auth | Header value for `X-API-Key`; resolves to the synthetic admin user | | `CORS_ALLOWED_ORIGINS` | `http://localhost:8000` | Comma-separated allow-list of browser origins | | `ALLOWED_EMAIL_DOMAINS` | empty (any) | Comma-separated allow-list of email domains; empty means accept any verified Google email | `app/database.py` creates the SQLite directory automatically when the URL uses a local SQLite file path. ## Quick Start With Docker This path requires Docker Desktop or another Docker Engine installation with the Compose plugin available as `docker compose`. If Docker is installed inside WSL, run these commands from your WSL shell and change into the Windows-mounted project directory first: ```bash cd /mnt/c/SyncData/impactflow-discovery ``` ```bash cp .env.example .env # edit .env and add ANTHROPIC_API_KEY docker compose up --build ``` Open: - Discovery flow: - Health check: When Docker runs inside WSL and you want the app to behave like the other WSL-published services, keep `HOST_BIND_IP=0.0.0.0` in `.env`, then create a Windows portproxy from an elevated PowerShell prompt: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\setup-wsl-bridge.ps1 ``` That creates an entry like `0.0.0.0 8011 -> 8011`, so the app is available on localhost, LAN addresses, and the Tailscale address `100.103.206.4` without colliding with any existing WSL service on port `8001`. If you cannot run an elevated PowerShell prompt, use the non-admin TCP bridge fallback while the app is running: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\start-tcp-bridge.ps1 ``` Keep that window open for as long as you need `http://100.103.206.4:8011` to forward to the app. The SQLite file lives in `./data/discovery.db` and persists across restarts because `docker-compose.yml` mounts `./data` into the container. ## Local Development Windows: ```bash python -m venv .venv .venv/Scripts/python.exe -m pip install -r requirements.txt cp .env.example .env alembic upgrade head uvicorn app.main:app --reload --port 8011 ``` macOS/Linux: ```bash python -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env alembic upgrade head uvicorn app.main:app --reload --port 8011 ``` ## Tests And Verification The extractor tests mock Anthropic, so they do not need a real API key or network access: ```bash .venv/Scripts/python.exe -m pytest ``` `smoke_test.py` drives the whole API in process with `httpx.ASGITransport` and patches `DiscoveryExtractor` so it also avoids a real Anthropic call: ```bash .venv/Scripts/python.exe smoke_test.py ``` The smoke test verifies: - `/health` - unauthenticated requests return `401` - `X-API-Key` resolves to the synthetic admin user via `/api/me` - a malformed bearer token is rejected - `/api/auth/login` redirects to `accounts.google.com` - conversation creation, response saving, conversation fetching - profile completion, fetching, and confirmation - the activity log captures the calls and tags them `source=mcp` - `/api/me/stats` reports the correct conversation, profile, and locked counts - key `404` paths ## Error Handling And Edge Cases The backend currently handles: | Scenario | Result | | --- | --- | | Missing conversation on respond, complete, or fetch | `404` | | Completing a conversation with all blank answers | `400` | | Missing Anthropic API key | `502` from complete route | | Anthropic transport or SDK error | `502` from complete route | | Invalid model JSON on first try | One retry | | Invalid model JSON after retry | `502` from complete route | | Missing profile for user | `404` | The frontend currently handles submission and profile-load failures by showing a simple error box. It does not persist partially typed answers across a full page reload, except for the browser's normal form restoration behavior. ## Integration Notes This service is ready to be called from a larger ImpactFlow app. The authenticated user identity now comes from Google OAuth on the browser side and `X-API-Key` on the machine-to-machine side; the per-call `user_id` body parameter is gone. Likely integration points: - route users into `/static/discovery.html` (or recreate the flow in the main UI) and rely on the JWT issued by `/api/auth/callback` for subsequent calls - the static frontend still needs to be updated to consume the new auth flow (read the JWT from the callback response, store it, and send it as a `Bearer` header on every `/discovery/*` call) - the MCP server should send `X-API-Key: $IMPACTFLOW_API_KEY` on every request — no OAuth dance needed - use `locked` as the user's confirmation signal - decide whether future profiles should supersede locked profiles or be versioned in the main product experience ## Privacy And Data Notes The app stores personal narrative answers and model-generated personality summaries in SQLite. Treat `data/discovery.db` as sensitive user data. Do not commit: - `.env` - real API keys - production SQLite databases - exported user response data The repository includes test fixtures with synthetic responses for extractor tests. ## Common Changes When changing prompts: 1. Update the prompt text in `app/static/discovery.html`. 2. Keep the request body keys aligned with `RespondRequest` in `app/schemas.py`. 3. Update `PROMPT_LABELS` in `app/services/extractor.py` if the extraction labels should change. 4. Adjust tests or fixtures if the extractor prompt expectations change. When changing the profile schema: 1. Update `app/schemas.py`. 2. Update `app/models.py`. 3. Add a new Alembic migration. 4. Update `REQUIRED_KEYS` and `SYSTEM_PROMPT` in `app/services/extractor.py`. 5. Update `app/routers/discovery.py` mapping logic. 6. Update `app/static/profile.html` rendering. 7. Add or update tests. When changing extraction behavior: 1. Update `SYSTEM_PROMPT` in `app/services/extractor.py`. 2. Keep the JSON contract explicit. 3. Update `REQUIRED_KEYS` or `REQUIRED_CONFIDENCE_KEYS` only when the response contract changes. 4. Add extractor tests for parsing, retry, or validation changes. ## Known Limitations - The static discovery page keeps in-progress answers in memory only. - The static profile page always loads the newest profile for a user. - Confirming a profile does not prevent a newer profile from being generated. - The database is SQLite by default and intended for standalone/local service operation. - Extraction quality depends on model output and the clarity of the user's stories. - The system prompt asks for structured interpretation, but personality results should be treated as reflective guidance rather than clinical or diagnostic truth.