# ImpactFlow Self-Discovery Module ImpactFlow Self-Discovery is a standalone FastAPI service that turns seven short reflection responses into a structured Enneagram + Ikigai + goals 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. ## Running (quick reference) Prerequisites: Docker with the `docker compose` plugin, and a `.env` (`cp .env.example .env`, then set `ANTHROPIC_API_KEY`; add `GOOGLE_*`, `JWT_SECRET`, `IMPACTFLOW_API_KEY` for browser auth; `COOKIE_SECURE=false` for local http). ```bash docker compose up -d --build discovery # build + start (migrations run on startup) docker compose logs -f discovery # follow logs docker compose down # stop ``` Access: | Where | URL | | --- | --- | | Deployed (HTTPS) | `https://impactflow.teamci.org:8011` | | Local, from Windows | `http://localhost:8011` (→ discovery flow) | | Local, from WSL | `http://192.168.245.52:8011` (`localhost` forwards from Windows only) | | Health | `http://localhost:8011/health` | User journey (all served from `/static`): discovery → profile → reflect → coaching → dashboard → visuals. Quick API check without a browser (machine auth): ```bash KEY=$(grep '^IMPACTFLOW_API_KEY=' .env | cut -d= -f2 | tr -d '\r') curl -s http://localhost:8011/health curl -s -H "X-API-Key: $KEY" http://localhost:8011/api/me ``` Run the tests (all AI calls mocked — no key/network needed): ```bash .venv/Scripts/python.exe -m pytest # local venv (Windows) # or in-container, if the local toolchain is unavailable: docker build -t ifd-test . && docker run --rm -v "$PWD:/app" --entrypoint python ifd-test -m pytest -q ``` Details below: [Quick Start With Docker](#quick-start-with-docker), [Local Development](#local-development), [Tests And Verification](#tests-and-verification), and the production deployment + weekly-check-in scheduler notes under [Configuration](#configuration). ## 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 + refresh token, sets them as httpOnly cookies, and redirects back into the app. The browser holds no tokens itself — the cookies are sent automatically on later requests. 3. The browser starts a discovery conversation with `POST /discovery/start` — the backend derives the `user_id` from the session cookie, not the body. 4. The user answers seven open-ended prompts: five discovery prompts plus a near-term (6–12 month) and a long-term (3–5 year) goal prompt. 5. The browser saves all 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, articulated short- and long-term goals, 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`. While it is unlocked the user can revise their words with `PATCH /discovery/profile/me`, then lock it with `PUT /discovery/profile/me/confirm`. 12. Optionally (Phase 2) the user opens `/static/reflect.html` and refines the profile through an AI-coach reflection loop (`POST /discovery/profile/me/reflect`) — the coach mirrors the profile back and applies the person's own corrections — before affirming with the same confirm/lock. 13. After affirming (Phase 3) the user opens `/static/coaching.html` to tune coaching preferences (auto-derived from their profile) and receive periodic check-ins that quote their own words and ask if their direction still holds. A weekly cron calls `POST /discovery/coaching/run` to generate due check-ins. 14. (Phase 4) The ImpactFlow core time-tracker maps each logged task to a profile foundation via `POST /discovery/integration/task-mappings`. The rolled-up work patterns drive `/static/dashboard.html` and are fed into the coaching check-ins so they can reflect where time has actually gone. 15. (Phase 5) `/static/visuals.html` shows the Ikigai/Enneagram visuals and the goal-evolution timeline; the tracker can call `suggest-foundation` for smart tagging, and the reflect loop accepts a `focus` for deeper goal-refinement. 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 + httpOnly cookie sessions for browser auth, and Anthropic for the analysis step. ## What The App Does The module collects seven narrative prompts — five discovery prompts plus two goal 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 | | `prompt_goals_short` | The Near Horizon | The person's own near-term (6–12 month) goals, articulated back | | `prompt_goals_long` | The Long Horizon | The person's own long-term (3–5 year) goals, articulated back | 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 | | `short_term_goals` | The user's own near-term (6–12 month) goals, articulated back | | `long_term_goals` | The user's own long-term (3–5 year) goals, articulated back | | `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 + | | session cookie | 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 seven-prompt flow | | `app/static/profile.html` | Browser-based profile display, edit, and confirm actions; links to reflection | | `app/static/reflect.html` | Phase 2 AI-coach reflection chat (mirror loop, applies revisions, affirm) | | `app/static/coaching.html` | Phase 3 coaching preferences form + check-in feed | | `app/static/dashboard.html` | Phase 4 goal dashboard: time per foundation + neglected ones | | `app/static/visuals.html` | Phase 5 visualizations: Ikigai Venn, Enneagram diagram, goal-evolution timeline | | `app/static/auth.js` | Shared `authedFetch` helper: sends session cookies, silently refreshes on `401`, redirects to login | | `app/static/style.css` | Shared UI styling | | `app/services/reflector.py` | `ReflectionCoach`: Anthropic-backed mirror loop, JSON parsing, revision filtering | | `app/services/coaching.py` | Deterministic preference generator + `CheckinCoach` (Anthropic check-in text) | | `app/services/foundations.py` | The six foundations + pure work-pattern aggregator (Phase 4) | | `app/services/profile_history.py` | Pure goal-evolution aggregator over profile revisions (Phase 5) | | `app/services/tagging.py` | `FoundationTagger`: Anthropic smart-tagging suggestion (Phase 5) | | `app/routers/coaching.py` | Phase 3 coaching routes: preferences, check-ins, weekly batch `/run` | | `app/routers/integration.py` | Phase 4/5 integration: foundations, task-mappings, work-patterns, suggest-foundation | | `alembic/versions/001_initial.py` | Initial database schema migration | | `alembic/versions/002_add_auth.py` | Adds `users`, `refresh_tokens`, and `activity_log` tables | | `alembic/versions/003_add_goals.py` | Adds the goal columns to `discovery_conversation` and `discovery_profile` | | `alembic/versions/004_add_reflection.py` | Adds `reflection_message` and `profile_revision` tables (Phase 2) | | `alembic/versions/005_add_coaching.py` | Adds `coaching_preferences` and `coaching_checkin` tables (Phase 3) | | `alembic/versions/006_add_task_mapping.py` | Adds the `task_mapping` table (Phase 4) | | `tests/conftest.py` | Shared `app_client` fixture (isolated app + temp DB) | | `tests/test_extractor.py` | Unit tests for extraction plumbing, goals, and retry behavior | | `tests/test_reflector.py` | Unit tests for `ReflectionCoach` (mirror, revision filtering, retry) | | `tests/test_coaching_prefs.py` | Unit tests for the deterministic coaching-preference generator | | `tests/test_coaching.py` | Tests for coaching endpoints (preferences, check-ins, due-logic batch, work-pattern wiring) | | `tests/test_foundations.py` | Unit tests for the pure work-pattern aggregator | | `tests/test_integration.py` | Tests for the task-to-goal integration endpoints | | `tests/test_profile_history.py` | Unit tests for the pure goal-evolution aggregator | | `tests/test_phase5.py` | Tests for goal-history and smart-tagging endpoints | | `tests/test_auth.py` | Tests for the dual-auth dependency (JWT + cookie + API key), token refresh/logout, admin enforcement, and domain allow-list | | `tests/test_profile_edit.py` | Tests for `PATCH /discovery/profile/me` (edit, partial update, lock/`409`) | | `tests/test_reflection.py` | Tests for the reflection endpoints (turns, applied revisions, lock/`409`, history) | | `tests/test_migration_bootstrap.py` | Unit tests for the pre-Alembic SQLite stamping helper | | `tests/test_static_discovery.py` | Guard tests for the static pages' cookie-session and edit contract | | `tests/fixtures/{gut,head,heart}_type_responses.json` | Synthetic seven-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 `get_current_user` dependency in `app/auth.py` resolves a request from three credential sources, tried in order: `X-API-Key`, then an `Authorization: Bearer` access token, then the session cookie. | Caller | Mechanism | Notes | | --- | --- | --- | | Browser users | Google OAuth → httpOnly session cookies | Set by `/api/auth/callback`; the access cookie is refreshed silently via `/api/auth/refresh` | | Scripts / API clients | `Authorization: Bearer ` | The same access token presented manually instead of via cookie | | Machine-to-machine (MCP server) | `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` exactly (scheme, host, port, path — Google treats `localhost` and `127.0.0.1` as distinct): - `http://localhost:8011/api/auth/callback` for local http dev (set `COOKIE_SECURE=false`) - `https://impactflow.teamci.org:8011/api/auth/callback` for the deployed instance (HTTPS; set `COOKIE_SECURE=true`) 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 (session cookie, Bearer token, 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 once the browser has a session cookie from the OAuth callback, and retries on submit if the first start call failed. ### 3. Saving Answers `PUT /discovery/{conversation_id}/respond` accepts all seven prompt responses (five discovery prompts plus the two goal prompts): ```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...", "prompt_goals_short": "In the next 6–12 months I want to...", "prompt_goals_long": "In the next 3–5 years I want to..." } ``` All fields default to empty, so a partial save is accepted. 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`, `future`, `goals_short`, and `goals_long`, then calls `DiscoveryExtractor.extract()`. The route rejects completion with: - `404` if the conversation does not exist - `400` if all seven 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. Editing The Profile `PATCH /discovery/profile/me` edits the prose of the newest profile. The body is a partial update — only the fields supplied are changed — over the seven editable text fields: `love_summary`, `strength_summary`, `mission_summary`, `vocation_summary`, `overlap_narrative`, `short_term_goals`, and `long_term_goals`. The AI's structural read (`triad`, `probable_type`, `wing`, `instinctual_variant`, `instinctual_stack`) and confidence are not editable here. It rejects with: - `404` if the user has no profile - `409` if the profile is locked (affirming makes it final) - `400` if the body contains no editable fields ### 7. Confirming The Profile `PUT /discovery/profile/me/confirm` locks the newest profile by setting `locked = true`. This is the "This is me" affirmation on the profile page; once locked, the profile can no longer be edited (`PATCH` returns `409`). It does not prevent future conversations from generating newer profiles. ### 8. Reflecting With The Coach (Phase 2) The AI-coach reflection loop lets the person refine their profile through a conversation before affirming it. The coach is a **mirror, not a compass**: it reflects the profile back, asks whether it fits, and — only when the person explicitly corrects or adds something — proposes revised text in the person's own direction. It never prescribes goals or invents direction. Affirmation is still the `/confirm` lock above; the loop is what happens before it. - `POST /discovery/profile/me/reflect` advances the loop by one turn. An empty `message` starts it (the coach's opening reflection); a non-empty `message` is recorded as the person's turn before the coach replies. When the coach proposes revisions, they are applied to the editable prose fields (never the structural Enneagram read) and snapshotted. Returns the coach turn, the (possibly revised) profile, and `revised`/`revision_note`. `409` once locked. - `GET /discovery/profile/me/reflection` returns the dialogue (ordered `coach`/`person` turns) plus the current profile. - `GET /discovery/profile/me/revisions` returns the profile's edit/iteration history, newest first. Every change is snapshotted in `profile_revision` with a `source` of `extraction` (initial), `reflection`, or `manual_edit`. ### 9. Coaching Preferences & Check-ins (Phase 3) How the person wants to be coached, plus a periodic check-in engine. Coaching **preferences** are auto-generated from the profile's Enneagram centre (a deterministic mapping — gut → direct/higher-friction, heart → warm/sensitive to drift, head → reflective/question-led) and are fully overridable. Fields: `coaching_frequency`, `coaching_style`, `misalignment_threshold`, `friction_tolerance`, `prefer_questions_over_directives`, `time_of_day_preference`. - `GET /discovery/coaching/preferences` returns the preferences, deriving defaults from the latest profile on first access (`404` if no profile yet). - `PUT /discovery/coaching/preferences` overrides any field (validated against the allowed value sets) and marks them user-customized. - `POST /discovery/coaching/preferences/regenerate` re-derives the defaults from the latest profile, discarding overrides. A **check-in** quotes the person's own words and asks whether their stated direction still feels valid — mirror, not compass: it asks, it never judges or prescribes. The person's answer is recorded in `still_valid`. - `POST /discovery/coaching/checkins` generates a check-in now (on demand). - `GET /discovery/coaching/checkins` lists them, newest first. - `PUT /discovery/coaching/checkins/{id}/respond` records the self-assessment (`still_valid` + optional note). - `POST /discovery/coaching/run` is the **weekly batch job** (admin-only, intended for a cron): it generates a check-in for every eligible user whose cadence is due. Eligible = coaching cadence not `off` and an affirmed (locked) profile; due = no prior check-in or the cadence interval has elapsed. ### 10. Task-to-Goal Integration (Phase 4) This is the boundary the ImpactFlow **core time-tracker** plugs into. A task maps to a **foundation** — one of the six stable profile elements: `love`, `strength`, `mission`, `vocation`, `short_term`, `long_term`. **Core-tracker contract:** when a user logs time, the tracker (1) fetches the options from `GET /discovery/integration/foundations`, (2) asks the person "which goal does this build toward?", and (3) posts the answer to `POST /discovery/integration/task-mappings` with `{external_task_id, foundation, minutes, task_label?, occurred_at?}`. It calls these endpoints as the user (forwarded session/JWT) or service-to-service with `X-API-Key`. - `GET /discovery/integration/foundations` — the six foundations with the person's own text (what the tracker shows). `404` if no profile. - `POST /discovery/integration/task-mappings` — record one logged unit of work. - `GET /discovery/integration/task-mappings?days=N` — the user's mappings. - `GET /discovery/integration/work-patterns?days=N` — per-foundation rollup (minutes, share, task count, last activity) plus `neglected` foundations. Powers the goal dashboard and feeds the coaching reminder engine: a check-in is given a plain-language summary of the last 14 days so it can reflect where time has and hasn't gone — as an observation to check against the person's own words, never a verdict (mirror, not compass). ### 11. Iteration & Polish (Phase 5) - **Goal-evolution history:** `GET /discovery/profile/me/goal-history` derives a per-goal timeline (near- and long-term) from the `profile_revision` snapshots captured since Phase 2 — one entry per actual change, with `source` and `at`. No new storage; it reads existing revisions. - **Smart tagging:** `POST /discovery/integration/suggest-foundation` (`{task_label}`) returns a suggested `foundation` + `rationale` + `confidence`. It only suggests — the person confirms by posting the task mapping. The core tracker uses this to pre-fill "which goal does this build toward?". - **Deeper goal-refinement:** the reflect loop (Phase 2) accepts an optional `focus` (e.g. `"goals"`) that steers the coach toward sharpening the near/long-term goals — still a mirror. - **Visualizations:** `visuals.html` renders an Ikigai four-circle Venn and an Enneagram diagram from the profile (plain-language callouts — it does not headline the raw type number), plus the goal-evolution timeline. ## API Reference All `/discovery/*`, `/api/me*`, `/api/activity*`, and `/api/admin/*` routes require authentication. The browser is authenticated by the session cookie set at the OAuth callback; machine callers send `X-API-Key`. (A raw `Authorization: Bearer` access token is also still accepted, e.g. for scripted clients.) `/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; sets session cookies and redirects into the app | | `POST` | `/api/auth/refresh` | public (refresh cookie or body) | Mint a new access token; refreshes the access cookie | | `POST` | `/api/auth/logout` | public (refresh cookie or body) | Revoke the refresh token and clear session cookies | | `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 seven 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 | | `PATCH` | `/discovery/profile/me` | yes | Edit the newest profile's prose (`409` if locked) | | `PUT` | `/discovery/profile/me/confirm` | yes | Lock newest profile for the authenticated user | | `POST` | `/discovery/profile/me/reflect` | yes | Advance the AI-coach reflection loop (Phase 2); applies revisions (`409` if locked) | | `GET` | `/discovery/profile/me/reflection` | yes | The reflection dialogue for the latest profile | | `GET` | `/discovery/profile/me/revisions` | yes | Profile edit/iteration history (newest first) | | `GET` | `/discovery/conversation/{conversation_id}` | yes | Fetch stored conversation responses (owner only) | | `GET` | `/discovery/coaching/preferences` | yes | Coaching preferences (auto-derived on first access) | | `PUT` | `/discovery/coaching/preferences` | yes | Override coaching preferences | | `POST` | `/discovery/coaching/preferences/regenerate` | yes | Re-derive preference defaults from the profile | | `GET` | `/discovery/coaching/checkins` | yes | List the user's check-ins (newest first) | | `POST` | `/discovery/coaching/checkins` | yes | Generate a check-in now | | `PUT` | `/discovery/coaching/checkins/{id}/respond` | yes | Record "is your direction still valid?" | | `POST` | `/discovery/coaching/run` | admin | Weekly batch: generate due check-ins for eligible users | | `GET` | `/discovery/integration/foundations` | yes | The six mappable foundations with the person's own text | | `POST` | `/discovery/integration/task-mappings` | yes | Record a logged unit of work mapped to a foundation | | `GET` | `/discovery/integration/task-mappings` | yes | List the user's task mappings in a window | | `GET` | `/discovery/integration/work-patterns` | yes | Per-foundation work-pattern rollup over a window | | `POST` | `/discovery/integration/suggest-foundation` | yes | Smart tagging: suggest a foundation for a task (person confirms) | | `GET` | `/discovery/profile/me/goal-history` | yes | Timeline of how the person's goals evolved | ## 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 seven-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 | | `prompt_goals_short` | text nullable | Near-term (6–12 month) goal response | | `prompt_goals_long` | text nullable | Long-term (3–5 year) goal 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 | | `short_term_goals` | text nullable | Articulated near-term (6–12 month) goals | | `long_term_goals` | text nullable | Articulated long-term (3–5 year) goals | | `confidence_json` | text nullable | JSON string for confidence flags | | `locked` | boolean | Defaults to false | ### `reflection_message` One turn in the Phase 2 AI-coach reflection loop (migration `004`). | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `profile_id` | string | FK to `discovery_profile.id`, indexed | | `user_id` | string | FK to `users.id`, indexed | | `role` | string | `coach` (AI mirror) or `person` (the human) | | `content` | text | The turn's text | | `sequence` | integer | Monotonic order within a profile's thread | | `created_at` | datetime | UTC | ### `profile_revision` A snapshot of a profile's editable prose at a point in time, so edits and iterations are captured rather than overwritten (migration `004`). | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `profile_id` | string | FK to `discovery_profile.id`, indexed | | `user_id` | string | FK to `users.id`, indexed | | `source` | string | `extraction` (initial), `reflection`, or `manual_edit` | | `fields_json` | text | JSON snapshot of the seven editable prose fields | | `note` | text nullable | What changed (e.g. the coach's revision note) | | `created_at` | datetime | UTC | ### `coaching_preferences` How the person wants to be coached; one row per user (migration `005`). | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, unique, indexed | | `profile_id` | string nullable | The profile the defaults were derived from | | `coaching_frequency` | string | `weekly`, `biweekly`, `monthly`, or `off` | | `coaching_style` | string | `direct`, `warm`, or `reflective` | | `misalignment_threshold` | string | `low`, `medium`, or `high` | | `friction_tolerance` | string | `low`, `medium`, or `high` | | `prefer_questions_over_directives` | boolean | Lead with questions vs statements | | `time_of_day_preference` | string | `morning`, `afternoon`, or `evening` | | `auto_generated` | boolean | True until the user edits a field | | `created_at` / `updated_at` | datetime | UTC | ### `coaching_checkin` A periodic coaching check-in and the person's response (migration `005`). | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `profile_id` | string | FK to `discovery_profile.id` | | `body` | text | The check-in text (quotes the person's own words) | | `created_at` | datetime | UTC, indexed | | `still_valid` | boolean nullable | The person's answer: is their direction still valid? | | `response_note` | text nullable | Optional note with their response | | `acknowledged_at` | datetime nullable | When they responded | ### `task_mapping` One logged unit of work from the core tracker, mapped to a foundation (migration `006`). | Column | Type | Notes | | --- | --- | --- | | `id` | string | UUID primary key | | `user_id` | string | FK to `users.id`, indexed | | `external_task_id` | string | Opaque task id from the core tracker (not an FK) | | `task_label` | string nullable | Human label of the task | | `foundation` | string | `love`/`strength`/`mission`/`vocation`/`short_term`/`long_term`, indexed | | `minutes` | integer | Time logged toward it | | `occurred_at` | datetime | When the work happened, indexed | | `created_at` | datetime | UTC | ## 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 seven 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-6` | | 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, served from `/static`. The browser session is a server-set httpOnly cookie (issued by the OAuth callback), so the pages hold no tokens of their own. `auth.js` (shared helper): - `authedFetch()` sends every request with `credentials: "include"` so the session cookies ride along - on a `401` it makes a single silent `POST /api/auth/refresh` (the refresh cookie is scoped to `/api/auth`) and retries the original request - if the refresh also fails, it redirects to `GET /api/auth/login` — which is also how an unauthenticated first visit gets bounced through Google sign-in `discovery.html`: - shows the seven prompts one at a time (five discovery prompts plus near-term and long-term goal prompts) - keeps answers in memory while navigating back and next - starts a conversation on page load via `authedFetch("/discovery/start")` - saves all responses on submit (`PUT /discovery/{id}/respond`), then triggers extraction (`POST /discovery/{id}/complete`) - redirects to `/static/profile.html` on success — no user id in the URL, since the backend derives the user from the session cookie - shows an error box and reload button if submission fails `profile.html`: - fetches the authenticated user's newest profile via `GET /discovery/profile/me` - escapes all model-generated text before rendering - shows the overlap narrative, Ikigai cards, a triad description, and the person's near-term/long-term goals, with confidence dots for triad and Ikigai - while the profile is unlocked, offers two actions: "Edit my words" and "This is me" - edit mode turns the narrative, the four Ikigai summaries, and both goal fields into textareas and saves with `PATCH /discovery/profile/me`; the AI's structural read (triad/type/wing/variant) is shown but not editable here - "This is me" locks the profile with `PUT /discovery/profile/me/confirm`; a locked profile shows the confirmed state and no longer offers edit - links to `reflect.html` ("talk it through with your coach") while unlocked `reflect.html` (Phase 2 AI-coach reflection): - loads the dialogue + profile via `GET /discovery/profile/me/reflection`, and auto-starts the coach's opening reflection when the thread is empty - shows a chat thread (coach / person bubbles) plus a live profile summary that refreshes when the coach applies a revision - each turn posts to `POST /discovery/profile/me/reflect`; when `revised` is true it updates the summary and notes what changed - "This is me — affirm" locks the profile via the same confirm endpoint `coaching.html` (Phase 3 coaching): - loads preferences via `GET /discovery/coaching/preferences` (auto-derived on first visit) and renders the six fields as selects + a toggle; Save (`PUT`) marks them customized, "Reset to suggested" re-derives from the profile - lists check-ins and can generate one on demand (`POST .../checkins`); each unanswered check-in offers "still feels true" / "it's shifted" which posts to `.../respond` - linked from `profile.html` ("Coaching preferences & check-ins") `dashboard.html` (Phase 4 goal dashboard): - reads `GET /discovery/integration/work-patterns?days=N` and renders minutes and share per foundation as bars, with a selectable window - surfaces foundations with no logged time and asks whether that matches where the person wants their energy — it observes, it does not prescribe - linked from `profile.html` ("Where your time goes") `visuals.html` (Phase 5 visualizations): - reads `GET /discovery/profile/me` and renders an Ikigai four-circle Venn and an Enneagram diagram in SVG (callouts stay in plain language) - reads `GET /discovery/profile/me/goal-history` and shows how the near- and long-term goals have changed over time - linked from `profile.html` ("See your profile visualized") ## 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-6` | 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 | | `COOKIE_SECURE` | `true` | Set `false` for local `http://localhost` dev (Secure cookies aren't sent over plain http); must be `true` in production over HTTPS | | `POST_LOGIN_REDIRECT` | `/static/discovery.html` | Path the OAuth callback redirects to after setting the session cookies | | `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:8011` | 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. ### Production deployment The service is deployed (Docker Compose on the `ci-gpu` host) behind TLS at: ``` https://impactflow.teamci.org:8011 ``` The container reads its env from `.env` (`env_file` in `docker-compose.yml`), which is gitignored and lives only on the host — it does not travel with the repo. For this HTTPS deployment the env must set: - `OAUTH_REDIRECT_URI=https://impactflow.teamci.org:8011/api/auth/callback` (and the same URI registered as an Authorized redirect URI on the Google OAuth client), and - `COOKIE_SECURE=true` so session cookies carry the `Secure` flag. After changing `.env`, apply with `docker compose up -d` (recreates the container with the new env). **Weekly check-in cron.** `scripts/run-weekly-checkins.sh` reads the admin API key from `.env` and POSTs `/discovery/coaching/run` (the Phase 3 batch). Install it as a weekly cron on the host: ```bash ( crontab -l 2>/dev/null | grep -v run-weekly-checkins.sh; \ echo '0 8 * * 1 /mnt/c/syncdata/impactflow-discovery/scripts/run-weekly-checkins.sh >/dev/null 2>&1' ) | crontab - ``` Running weekly services all cadences — the endpoint skips users who are not yet due (biweekly/monthly), so a weekly tick is the finest needed. Output is appended to `data/coaching-cron.log`. (On WSL, cron only runs while WSL is up.) **More robust: Windows Task Scheduler.** `scripts/run-weekly-checkins.ps1` does the same POST from Windows (reading the key from `.env`, hitting `localhost:8011`). Register it to run weekly — and to catch up if the machine was asleep — from PowerShell: ```powershell $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\syncdata\impactflow-discovery\scripts\run-weekly-checkins.ps1"' $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 8am $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun Register-ScheduledTask -TaskName 'ImpactFlow Weekly Check-ins' ` -Action $action -Trigger $trigger -Settings $settings ` -Description 'Phase 3 weekly coaching check-in batch' ``` Use either the WSL cron or the Task Scheduler job — not both is fine, but running both is harmless since the endpoint dedupes by due-ness. ## 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: ANTHROPIC_API_KEY (+ GOOGLE_*/JWT_SECRET/IMPACTFLOW_API_KEY for auth); # set COOKIE_SECURE=false for local http dev docker compose up -d --build discovery ``` On startup the container runs migrations then uvicorn (`python -m app.migration_bootstrap && alembic upgrade head && uvicorn …`), so the SQLite schema is brought up to the latest revision automatically. Manage the running container: ```bash docker compose ps # status docker compose logs -f discovery # follow logs docker compose restart discovery # restart docker compose up -d --build discovery # apply .env or code changes docker compose down # stop & remove ``` Open: - Deployed (HTTPS): - Local (from Windows): — redirects to the discovery flow - Health check: - From WSL, `localhost` does not forward in; use the WSL IP (e.g. `http://192.168.245.52:8011`) or the Windows host gateway. 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 All Anthropic-backed services (extractor, reflection coach, check-in coach, foundation tagger) are mocked in the suite, so the tests need no real API key or network access: ```bash .venv/Scripts/python.exe -m pytest # Windows venv # or, on macOS/Linux: pytest ``` If the local Python toolchain is unavailable (e.g. the WSL↔Windows interop is down), run the suite inside the image, which has every dependency: ```bash docker build -t ifd-test . docker run --rm -v "$PWD:/app" --entrypoint python ifd-test -m pytest -q ``` `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 comes from Google OAuth (httpOnly session cookies) 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); the session cookie set by `/api/auth/callback` is sent automatically on subsequent calls, and `auth.js` handles silent token refresh on `401` - 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, and `PATCH /discovery/profile/me` if the surrounding app wants its own edit affordance - 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.