From 194936a1467db1aa4c8c27e9ccd3c78cfbf42b09 Mon Sep 17 00:00:00 2001 From: John Durrant Date: Tue, 9 Jun 2026 13:13:43 +0100 Subject: [PATCH] feat(questionnaires): F9.4 demo-content seed + F9.2 operational runbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F9.4 — env-gated, DEMO-ONLY seed (025-demo-content.ts) that populates a "Northwind Logistics" demo client plus a fully launched, attributed sample questionnaire on a fresh DB. No-ops unless LOAD_DEMO_CONTENT=1, so it never loads in production; idempotent on re-seed; writes through the real ingestion writeGraph seam rather than hand-rolled inserts. A fork strips it via the DEMO-ONLY marker. F9.2 — operational runbook (runbook.md) taking a presenter from fresh checkout to a respondent answering the first question: seeded fast path, branding, content (clone-first), launch, invite, first session, reset. Cross-linked from demo-clients.md. Also: mark P0–P9 feature trackers done, document LOAD_DEMO_CONTENT in .env.example, refresh development-plan status. Co-Authored-By: Claude Opus 4.8 (1M context) --- .context/app/planning/development-plan.md | 91 +++--- .context/app/planning/features/f0.1.md | 2 +- .context/app/planning/features/f2.1.md | 2 +- .context/app/planning/features/f2.2.md | 2 +- .context/app/planning/features/f2.3.md | 2 +- .context/app/planning/features/f2.4.md | 2 +- .context/app/planning/features/f3.1.md | 2 +- .context/app/planning/features/f3.2.md | 2 +- .context/app/planning/features/f3.3.md | 2 +- .context/app/planning/features/f4.1.md | 2 +- .context/app/planning/features/f4.2.md | 2 +- .context/app/planning/features/f4.3.md | 2 +- .context/app/planning/features/f4.4.md | 2 +- .context/app/planning/features/f4.5.md | 2 +- .context/app/planning/features/f4.6.md | 2 +- .context/app/planning/features/f6.1.md | 2 +- .context/app/planning/features/f6.3.md | 2 +- .context/app/planning/features/f7.1.md | 2 +- .context/app/planning/features/f7.2.md | 2 +- .context/app/planning/features/f8.1.md | 2 +- .context/app/planning/features/f8.2.md | 2 +- .context/app/planning/features/f8.3.md | 2 +- .context/app/planning/features/f9.1.md | 2 +- .context/app/planning/features/f9.2.md | 66 +++++ .context/app/planning/features/f9.4.md | 93 ++++++ .context/app/questionnaire/demo-clients.md | 4 + .context/app/questionnaire/runbook.md | 203 +++++++++++++ .env.example | 16 + .../app-questionnaire/025-demo-content.ts | 274 ++++++++++++++++++ 29 files changed, 732 insertions(+), 59 deletions(-) create mode 100644 .context/app/planning/features/f9.2.md create mode 100644 .context/app/planning/features/f9.4.md create mode 100644 .context/app/questionnaire/runbook.md create mode 100644 prisma/seeds/app-questionnaire/025-demo-content.ts diff --git a/.context/app/planning/development-plan.md b/.context/app/planning/development-plan.md index 0979c7dd1..a610fb34a 100644 --- a/.context/app/planning/development-plan.md +++ b/.context/app/planning/development-plan.md @@ -21,14 +21,14 @@ supersedes: Conversational Questionnaire Phases.md ## Project -| Field | Value | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Name | Conversational Questionnaire | -| Repo | `human-centric-engineering/conquest` (forked from `human-centric-engineering/sunrise` at v0.0.1) | -| Host platform | Sunrise v0.0.1 | -| Lead | Simon Holmes | -| Status | `building` — P0 done (F0.1, PR #10); P1 done (F1.1 ingestion: PR #13/#14/#15); P2.5 done (F2.5.1, #21); P2 done (F2.1 #18/#19, F2.2 #23, F2.3 #24, F2.4 #25); P3 done (F3.1 #26, F3.2 #27, F3.3 #28, F3.4 #29); P4 done (F4.1 #30, F4.2 #32, F4.3 #33, F4.4 #34, F4.5 #35, F4.6 #36); P5 done (F5.1 #37, F5.2 #38, F5.3 #39); P6 done (F6.1 #40, F6.2 #41, F6.3 #42, F6.4 #43); P7 done (F7.1 #44, F7.2 #45, F7.3 #46, F7.4 #47) + admin upload-questionnaire UI (#48). **Next: P8** (analytics, exports, anonymous-mode hardening), then P9. A 2026-06-07 audit found five deferred items whose home feature shipped but which were never delivered — being closed on `feat/close-deferred-gaps` (see decisions log). | -| Opened | 2026-05-30 | +| Field | Value | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Name | Conversational Questionnaire | +| Repo | `human-centric-engineering/conquest` (forked from `human-centric-engineering/sunrise` at v0.0.1) | +| Host platform | Sunrise v0.0.1 | +| Lead | Simon Holmes | +| Status | `building` — P0 done (F0.1, PR #10); P1 done (F1.1 ingestion: PR #13/#14/#15); P2.5 done (F2.5.1, #21); P2 done (F2.1 #18/#19, F2.2 #23, F2.3 #24, F2.4 #25); P3 done (F3.1 #26, F3.2 #27, F3.3 #28, F3.4 #29); P4 done (F4.1 #30, F4.2 #32, F4.3 #33, F4.4 #34, F4.5 #35, F4.6 #36); P5 done (F5.1 #37, F5.2 #38, F5.3 #39); P6 done (F6.1 #40, F6.2 #41, F6.3 #42, F6.4 #43); P7 done (F7.1 #44, F7.2 #45, F7.3 #46, F7.4 #47) + admin upload-questionnaire UI (#48) + deferred-gaps close (#49); P8 done (F8.1 #53, F8.2 #54, F8.3 #55). **P9 in progress** — F9.1 done (#56), F9.4 done (demo-content seed, on `feat/F9.4-demo-seed-and-F9.2-runbook`), F9.2 in flight (operational runbook — awaiting clean-machine road-test); **next: F9.3** (forking docs). | +| Opened | 2026-05-30 | --- @@ -133,19 +133,19 @@ When a need arises that Sunrise's public surface doesn't cover, the rule is _not The build moves from scaffolding → ingestion → admin manage → demo branding → configuration → conversational core → evaluation → streaming → user UI → analytics → hardening. Phases are sequenced so each one's surface area is exercisable end-to-end before the next adds new abstraction. -| Phase | Title | Status | Notes | -| -------- | ----------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **P0** | Foundations | done | F0.1 shipped (PR #10) — substantially lighter than the original plan; Sunrise v0.0.1 provides the seams that used to need workarounds. | -| **P1** | Questionnaire ingestion | done | Admin uploads a doc; LLM extracts structure; changes recorded for review. API-only. F1.1 (P1's sole feature) complete: PR1 (schema) + PR2 (pure core) in PR #13; PR3 (extractor capability) in PR #14; PR4 (ingestion route + persistence) in PR #15. | -| **P2** | Admin CRUD over questionnaires | done | Admin UI: list, edit, version, tag, review extraction changes. All four features merged: F2.1 authoring (PR #18/#19); F2.2 tagging (#23); F2.3 extraction-change review (#24); F2.4 re-ingest (#25). | -| **P2.5** | Demo-client foundation | done | F2.5.1 shipped (PR #21) — demo-client identity + `AppQuestionnaire` FK + admin attribution, the slice that must **lead** so P3+ build tenant-aware. The rest of demo tenancy (theming, invitation branding, session reset, content seed) is **distributed** into P3/P6/P7/P9 as marked `// DEMO-ONLY:` sub-features. | -| **P3** | Configuration, invitations, and cost estimation | done | Per-version config; invitation flow; pre-launch cost estimate. All four features merged: F3.1 config (#26); F3.2 invitation flow (#27); F3.3 cost estimation (#28); F3.4 demo-client invitation branding (#29). | -| **P4** | Conversational engine (non-streaming) | in flight | Selection · extraction · contradiction · completion logic, exercised without the streaming surface. F4.1 selection strategies (#30) + F4.2 answer extraction (#32) + F4.3 contradiction detection (#33) + F4.4 answer refinement (#34) + F4.5 completion logic (#35) merged; F4.6 session state machine in flight. | -| **P5** | Design-time evaluation (agents-as-judges) | done | Judges score a questionnaire against goal/audience; suggestion review queue. F5.1 judge agents done (app-native dispatch — `evaluate-structure` capability + 7 `kind='judge'` seeds + preview route); F5.2 run persistence done (synchronous run route + app-owned run/finding models + admin run history); F5.3 review queue done (structured `proposedEdit` ops on the findings contract + apply-through-fork engine + read-time staleness + interactive queue UI). | -| **P6** | Conversational session (streaming) | done | F6.1 per-turn orchestrator + streaming (#40); F6.2 voice input (#41); F6.3 cost-cap enforcement (#42); F6.4 demo session reset (#43). _Attachment input closed by the 2026-06-07 audit gap-fill (Item 5)._ | -| **P7** | User-facing conversational UI | done | F7.1 chat surface (#44); F7.2 answer-slot panel (#45); F7.3 session lifecycle UX (#46); F7.4 PDF export (#47); admin upload-questionnaire UI (#48). _Attachment input + invite claim-via-login closed by the 2026-06-07 audit gap-fills._ | -| **P8** | Admin analytics, exports, anonymous mode | not started | Dashboards, CSV/JSON export, anonymous-mode handling. | -| **P9** | Hardening + forking docs | not started | Runbook, flag inventory, `forking.md`, concurrent-session sanity. | +| Phase | Title | Status | Notes | +| -------- | ----------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **P0** | Foundations | done | F0.1 shipped (PR #10) — substantially lighter than the original plan; Sunrise v0.0.1 provides the seams that used to need workarounds. | +| **P1** | Questionnaire ingestion | done | Admin uploads a doc; LLM extracts structure; changes recorded for review. API-only. F1.1 (P1's sole feature) complete: PR1 (schema) + PR2 (pure core) in PR #13; PR3 (extractor capability) in PR #14; PR4 (ingestion route + persistence) in PR #15. | +| **P2** | Admin CRUD over questionnaires | done | Admin UI: list, edit, version, tag, review extraction changes. All four features merged: F2.1 authoring (PR #18/#19); F2.2 tagging (#23); F2.3 extraction-change review (#24); F2.4 re-ingest (#25). | +| **P2.5** | Demo-client foundation | done | F2.5.1 shipped (PR #21) — demo-client identity + `AppQuestionnaire` FK + admin attribution, the slice that must **lead** so P3+ build tenant-aware. The rest of demo tenancy (theming, invitation branding, session reset, content seed) is **distributed** into P3/P6/P7/P9 as marked `// DEMO-ONLY:` sub-features. | +| **P3** | Configuration, invitations, and cost estimation | done | Per-version config; invitation flow; pre-launch cost estimate. All four features merged: F3.1 config (#26); F3.2 invitation flow (#27); F3.3 cost estimation (#28); F3.4 demo-client invitation branding (#29). | +| **P4** | Conversational engine (non-streaming) | done | Selection · extraction · contradiction · completion logic, exercised without the streaming surface. All six features merged: F4.1 selection strategies (#30) + F4.2 answer extraction (#32) + F4.3 contradiction detection (#33) + F4.4 answer refinement (#34) + F4.5 completion logic (#35) + F4.6 session state machine (#36). | +| **P5** | Design-time evaluation (agents-as-judges) | done | Judges score a questionnaire against goal/audience; suggestion review queue. F5.1 judge agents done (app-native dispatch — `evaluate-structure` capability + 7 `kind='judge'` seeds + preview route); F5.2 run persistence done (synchronous run route + app-owned run/finding models + admin run history); F5.3 review queue done (structured `proposedEdit` ops on the findings contract + apply-through-fork engine + read-time staleness + interactive queue UI). | +| **P6** | Conversational session (streaming) | done | F6.1 per-turn orchestrator + streaming (#40); F6.2 voice input (#41); F6.3 cost-cap enforcement (#42); F6.4 demo session reset (#43). _Attachment input closed by the 2026-06-07 audit gap-fill (Item 5)._ | +| **P7** | User-facing conversational UI | done | F7.1 chat surface (#44); F7.2 answer-slot panel (#45); F7.3 session lifecycle UX (#46); F7.4 PDF export (#47); admin upload-questionnaire UI (#48). _Attachment input + invite claim-via-login closed by the 2026-06-07 audit gap-fills._ | +| **P8** | Admin analytics, exports, anonymous mode | done | Dashboards, CSV/JSON export, anonymous-mode handling. All three features merged: F8.1 admin analytics dashboards (#53); F8.2 result exports (#54); F8.3 anonymous-mode hardening (#55). | +| **P9** | Hardening + forking docs | in flight | Concurrent-session sanity, flag inventory, runbook, `forking.md`. F9.1 production hardening (#56) + F9.4 demo-content seed (on `feat/F9.4-demo-seed-and-F9.2-runbook`) done; F9.2 operational runbook in flight (awaiting clean-machine road-test); F9.3 forking docs not started. | --- @@ -411,7 +411,7 @@ _Indicative tasks:_ ### F4.3 — Contradiction detection -_Status:_ in flight — on `feat/f4.3-contradiction-detection` · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.2 +_Status:_ done — merged ([PR #33](https://github.com/human-centric-engineering/conquest/pull/33)) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.2 > Committable tracker: [`planning/features/f4.3.md`](features/f4.3.md). Build decision (confirmed): **align to the committed F3.1 config schema** — behaviour mode is `off | flag | probe` (`flag` surfaces, `probe` follows up in-conversation), **not** the cadence list this prose originally sketched. **Cadence** (per-turn vs completion-sweep) is built as a pure `shouldRunDetection(mode, windowN, phase)` scheduler — `phase:'turn'` covers _every_turn_ (windowed by `contradictionWindowN`), `phase:'completion-sweep'` covers _sweep_only_ — with **no cadence config column**; `every_n_turns` (a pure cost knob) is deferred to F4.6 where the real turn loop consumes it. Surfaces contradictions to the agent for confirmation; **never auto-overwrites** (resolution is F4.4). Pure core + capability + no-persistence preview route, mirroring F4.2. See `.context/app/questionnaire/contradiction-detection.md`. @@ -423,7 +423,7 @@ _Indicative tasks:_ ### F4.4 — Answer refinement -_Status:_ in flight — on `feat/f4.4-answer-refinement` · _Size:_ 3 PRs · _Owner:_ TBD · _Deps:_ F4.2 +_Status:_ done — merged ([PR #34](https://github.com/human-centric-engineering/conquest/pull/34)) · _Size:_ 3 PRs · _Owner:_ TBD · _Deps:_ F4.2 > Committable tracker: [`planning/features/f4.4.md`](features/f4.4.md). Build decisions (confirmed): **persistence is in scope** — F4.4 introduces the answer-persistence foundation (a minimal `AppQuestionnaireSession` + the `AppAnswerSlot` model with `refinementHistory` + migration + a real write path), a **deliberate divergence** from the no-persistence F4.1–F4.3 previews (F4.6 builds its live loop on these same tables); **`overwrite` keeps the original provenance, only a genuine `refine` → `refined`** (the precise "evolved across turns" signal); **own agent + capability** (the LLM decides refine/overwrite/leave) with the _apply_ step a pure deterministic `applyRefinement`; **core-local `REFINEMENT_ACTIONS`/`REFINEMENT_SOURCES` tuples**; **no clock in the pure core** (the persistence seam stamps `createdAt`); **reuse F4.2 `validateAnswerValue`**; **own opt-in sub-flag** (`APP_QUESTIONNAIRES_ANSWER_REFINEMENT_ENABLED`, off by default); the route **persists** against a per-version preview session (`isPreview`, excluded from P8). See `.context/app/questionnaire/answer-refinement.md`. @@ -437,7 +437,7 @@ _Indicative tasks:_ ### F4.5 — Completion logic -_Status:_ in flight — on `feat/f4.5-completion-logic` · _Size:_ 3 PRs · _Owner:_ TBD · _Deps:_ F4.1, F4.2, F4.3, F4.4 +_Status:_ done — merged ([PR #35](https://github.com/human-centric-engineering/conquest/pull/35)) · _Size:_ 3 PRs · _Owner:_ TBD · _Deps:_ F4.1, F4.2, F4.3, F4.4 Decides when the agent offers submission (based on completion config) and accepts/holds when the user confirms. Drives the contradiction sweep in `sweep_only` mode at the moment of offer. @@ -451,7 +451,7 @@ _Indicative tasks:_ ### F4.6 — Session state machine -_Status:_ in flight — on `feat/f4.6-session-state-machine` · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.4 (session/answer persistence), F4.5 (markSessionCompleted seam) +_Status:_ done — merged ([PR #36](https://github.com/human-centric-engineering/conquest/pull/36)) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.4 (session/answer persistence), F4.5 (markSessionCompleted seam) > Committable tracker: [`planning/features/f4.6.md`](features/f4.6.md). Build decisions (confirmed): **deterministic, no LLM** (no capability/agent/sub-flag — a pure transition table + the seam writes, gated by the master flag only); **vocab keeps `active`, adds `paused`** (Option B — the code shipped `active` in F4.4, so an additive `SESSION_STATUSES` edit over a rename, no data-backfill migration, `@default("active")` unchanged); **one thin admin transition route** (`pause | resume | abandon`) — **completion stays the single submit entrypoint** on F4.5's `/complete` route; **status + audit event written atomically** in one `$transaction` so a status never changes without its row; **`markSessionCompleted` generalised** into `transitionSession` (moved to `_lib/sessions.ts`, re-exported from `answer-slots.ts` so the F4.5 route is untouched — completion now writes a `completed` event); **`cost_cap_reached` wired but not fired** (`recordCostCapReached` ready for F6.3/F6.5) and **`created` reserved** for real respondent sessions (F6.1); **`AppQuestionnaireSessionEvent` fully Prisma-modelled** (no raw-SQL object → no drift probe), create-only + stripped migration; **minimal resume state** (`{ status, answeredSlots }` — coverage/next-question stay in the F4.1/F4.5 builders); **no CHANGELOG entry** (app surface, not the Sunrise platform contract). See `.context/app/questionnaire/session-state-machine.md`. @@ -541,7 +541,7 @@ _Delivered:_ the transcribe route (reuses `resolveTurnAccess` for all three resp ### F6.3 — Cost cap enforcement at turn boundary -_Status:_ in flight · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F4.6, F6.1 +_Status:_ done — merged ([PR #42](https://github.com/human-centric-engineering/conquest/pull/42)) · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F4.6, F6.1 The per-session cost cap that fires the wrap-up turn at 90% (soft) and halts with 402 + auto-pause at 100% (hard). Both write `AppQuestionnaireSessionEvent` rows. Built on the existing `AppQuestionnaireConfig.costBudgetUsd` field (the indicative `perSessionCostCapUsd` name) — no schema change. Confirmed divergences from the sketch: the soft cap **biases the orchestrator toward offering completion early** (not just a prose hint), and F6.3 also **closes the F6.1-deferred per-turn cost aggregation** (the extract/detect/refine invokers stubbed `0`, so the cap had nothing meaningful to grade) by surfacing each capability's real `costUsd` on its app-owned `data`. Dark-launch sub-flag `APP_QUESTIONNAIRES_COST_CAP_ENABLED` (seed 023), ANDed with master + live-sessions. Tracker: `planning/features/f6.3.md`. Docs: `cost-cap-enforcement.md`. @@ -575,7 +575,7 @@ _Indicative tasks:_ ### F7.1 — Chat surface -_Status:_ not started · _Size:_ multi-PR · _Owner:_ TBD · _Deps:_ F6.1, F6.2, theming module (built in F3.4 if themed invites were adopted, else here) +_Status:_ done — merged ([PR #44](https://github.com/human-centric-engineering/conquest/pull/44)) · _Size:_ multi-PR · _Owner:_ TBD · _Deps:_ F6.1, F6.2, theming module (built in F3.4 if themed invites were adopted, else here) Live SSE rendering with voice + attachment input wired. Consumes Sunrise's `useVoiceRecording` hook and `` verbatim — F6.2 deliberately shipped a **separate** transcribe endpoint (`POST …/questionnaire-sessions/:id/transcribe` → `{ text, durationMs, language? }`) precisely so `` (which expects an endpoint returning `{ text }`, not a turn stream) can be reused as-is: point its `endpoint` prop at the transcribe route, let `onTranscript` drop the text into the composer (respondent can edit), then send through the existing text `/messages` path. Branding hookup from P2.5 happens here. Includes the demo-flow E2E test (Playwright as an app dev-dep). @@ -590,7 +590,7 @@ _Indicative tasks:_ ### F7.2 — Answer-slot panel -_Status:_ in flight (on `feat/F7.2-answer-slot-panel`) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F6.1, F7.1 +_Status:_ done — merged ([PR #45](https://github.com/human-centric-engineering/conquest/pull/45)) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F6.1, F7.1 > Committable tracker: [`planning/features/f7.2.md`](features/f7.2.md). Build decisions (confirmed): **read endpoint, not a stream event** (`GET …/questionnaire-sessions/:id/answers`, reusing `resolveTurnAccess` for both respondent kinds; the F6.1 SSE stream emits no slot events, so the panel refetches when each turn settles); **panel scope is an admin config setting** (new `answerSlotPanelScope` config field, `full_progress | answered_only` — `answered_only` filters server-side so pending prompts never reach the client); **click = inline-expand + a confirm-gated Revisit** that re-asks through the same turn loop (no chat-transcript scroll, so `QuestionnaireChat` stays untouched visually); **rationale shown quietly** in the expanded view. `SessionWorkspace` lifts the stream hook so chat + panel share it (an additive `onTurnSettled` drives the refetch; the chat now takes `stream` as a prop). See `.context/app/questionnaire/answer-slot-panel.md`. @@ -605,7 +605,7 @@ _Indicative tasks:_ ### F7.3 — Session lifecycle UX -_Status:_ done (on `feat/F7.3-session-lifecycle-ux`) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.6, F6.3, F7.1, F7.2 +_Status:_ done — merged ([PR #46](https://github.com/human-centric-engineering/conquest/pull/46)) · _Size:_ 2 PRs · _Owner:_ TBD · _Deps:_ F4.6, F6.3, F7.1, F7.2 > Committable tracker: [`planning/features/f7.3.md`](features/f7.3.md). Build decisions (confirmed): **offer signal = a `GET …/status` read** refetched on the F7.2 `onTurnSettled` (the SSE contract stays closed); **submit gate is upstream** — the offer never appears with a required question outstanding, so submit just re-asserts the offer state via the F4.5 `resolveCompletion` with **no contradiction sweep** (capped-session-always-submits honoured as-is); **pause is signed-in only** (`403 PAUSE_NOT_PERMITTED` for no-login — the anonymous token is client-only). No migration (every status + event exists from F4.6). @@ -621,7 +621,7 @@ _Indicative tasks:_ ### F7.4 — PDF export -_Status:_ not started · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F8.1 (analytics data shape) or independent +_Status:_ done — merged ([PR #47](https://github.com/human-centric-engineering/conquest/pull/47)) · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F8.1 (analytics data shape) or independent `@react-pdf/renderer` as an app dependency (vertical — not promoted to Sunrise). Admin-facing export of a session's answers first; user-facing PDF download is a nice-to-have. @@ -642,7 +642,7 @@ _Indicative tasks:_ ### F8.1 — Admin analytics dashboards -_Status:_ not started · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ F4.2 (slots), F4.6 (session events), F3.2 (invitations) +_Status:_ done — merged ([PR #53](https://github.com/human-centric-engineering/conquest/pull/53)) · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ F4.2 (slots), F4.6 (session events), F3.2 (invitations) The admin's read-side view of completed-session data: per-question distributions, completion funnel, cost actuals. Tag-aware filtering throughout. @@ -655,7 +655,7 @@ _Indicative tasks:_ ### F8.2 — Result exports -_Status:_ not started · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F8.1 +_Status:_ done — merged ([PR #54](https://github.com/human-centric-engineering/conquest/pull/54)) · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F8.1 CSV + JSON export of session results. CSV is one row per session × question; JSON is the full session graph including provenance + turns. Both respect anonymous mode. @@ -667,7 +667,7 @@ _Indicative tasks:_ ### F8.3 — Anonymous-mode hardening -_Status:_ in flight ([tracker](./features/f8.3.md)) · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ F8.1, F8.2 + any surface that touches session data +_Status:_ done — merged ([PR #55](https://github.com/human-centric-engineering/conquest/pull/55)) ([tracker](./features/f8.3.md)) · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ F8.1, F8.2 + any surface that touches session data Verification pass across every surface that touches session data, ensuring no PII leak when `anonymousMode = true`. Flag-gating tightened where needed. @@ -688,7 +688,7 @@ _Indicative tasks:_ ### F9.1 — Production hardening -_Status:_ in flight ([tracker](./features/f9.1.md)) · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ everything (final pass) +_Status:_ done — merged ([PR #56](https://github.com/human-centric-engineering/conquest/pull/56)) ([tracker](./features/f9.1.md)) · _Size:_ ~1–2 PRs · _Owner:_ TBD · _Deps:_ everything (final pass) The pre-ship technical hardening: concurrent-session sanity, master + sub-flag inventory, verification that every flag and sub-flag controls the right surfaces independently. @@ -701,7 +701,7 @@ _Indicative tasks:_ ### F9.2 — Operational runbook -_Status:_ not started · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F9.1 +_Status:_ in flight ([tracker](./features/f9.2.md)) · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F9.1, F9.4 `.context/app/questionnaire/runbook.md`: how to spin up a new demo client end-to-end. Road-tested by John or Simon before the phase ships, with friction corrected into the doc. @@ -725,13 +725,13 @@ _Indicative tasks:_ ### F9.4 — Demo content seed (DEMO-ONLY) -_Status:_ not started · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F2.5.1, F7.1 (a complete themeable vertical to seed) · _Relocated from the old P2.5._ +_Status:_ done — code complete on `feat/F9.4-demo-seed-and-F9.2-runbook` ([tracker](./features/f9.4.md)) · _Size:_ ~1 PR · _Owner:_ TBD · _Deps:_ F2.5.1, F7.1 (a complete themeable vertical to seed) · _Relocated from the old P2.5._ The `LOAD_DEMO_CONTENT=1` mechanism that populates a sample demo client + branded questionnaire on a fresh DB. Idempotent; refuses to run without the env var. Strictly demo-only — a fork strips it entirely. Lands **before** F9.2's runbook road-test so the runbook can exercise it. _Indicative tasks:_ -- `prisma/seeds/app-questionnaire/010-demo-content.ts`, idempotent, env-gated. +- `prisma/seeds/app-questionnaire/025-demo-content.ts`, idempotent, env-gated. _(prefix `025`; `010` was already taken.)_ - Sample demo client + theme + sample questionnaire with realistic content. **Definition of phase complete.** Platform ships; runbook road-tested; a notional fork can be spun up using only `forking.md`; external-fork upgrade path documented. @@ -820,6 +820,23 @@ Append-only. Newest at the top. Each entry: date, decision, context, link. Append-only. Newest at the top. +- **2026-06-09 — F9.2 operational runbook (in flight, on `feat/F9.4-demo-seed-and-F9.2-runbook`).** `.context/app/questionnaire/runbook.md` — the end-to-end "spin up a demo client" walkthrough: prerequisites (flags are DB rows, not env vars), the F9.4 seeded fast path, manual client + content (clone-first / upload-and-extract), review/configure/launch, invite, first session, reset, troubleshooting, and a clean-machine road-test checklist. Demo-doc convention: a `> **DEMO-ONLY**` callout, cross-linked from `demo-clients.md` rather than the namespace index. **Awaiting the human road-test** (John/Simon) before flipping to done. No CHANGELOG entry (app docs). Tracker: [`features/f9.2.md`](features/f9.2.md). +- **2026-06-09 — F9.4 demo content seed (done, on `feat/F9.4-demo-seed-and-F9.2-runbook`).** `prisma/seeds/app-questionnaire/025-demo-content.ts` — env-gated (`LOAD_DEMO_CONTENT=1`), idempotent, DEMO-ONLY seed that builds a branded "Northwind Logistics" demo client + a launched, attributed questionnaire (2 sections, 6 questions across the type range, name/email profile capture) in one `executeTransaction`, reusing the ingestion `writeGraph` writer and re-asserting the launch gate before flipping the version to `launched`. Built ahead of F9.2 so the runbook can exercise it; `.env.example` documents the flag + the no-op reload gotcha. Verified against the dev DB (seed → correct rows → idempotent re-run → no-op without the flag). No CHANGELOG entry (app/demo-only seed). Tracker: [`features/f9.4.md`](features/f9.4.md). + +> Backfill note (2026-06-09): entries for F6.4 through F9.1 below were reconstructed in one pass from their per-feature trackers (`features/f*.md`) + git history — the aggregate log had lagged since F6.3. They're accurate to the trackers but deliberately concise; the trackers remain the fuller record. + +- **2026-06-09 — F9.1 production hardening ([PR #56](https://github.com/human-centric-engineering/conquest/pull/56)).** Pre-ship verification pass: a 24-session concurrency smoke (`npm run smoke:concurrent-sessions`) proving no deadlocks / orphan turns / missed audit writes against the real DB, the master + 10 sub-flag inventory doc (`feature-flags.md`), and a data-driven per-flag verification suite (independence + dependency-cascade). No new runtime capability. Tracker: [`features/f9.1.md`](features/f9.1.md). +- **2026-06-09 — F8.3 anonymous-mode hardening ([PR #55](https://github.com/human-centric-engineering/conquest/pull/55)).** Cross-surface PII contract: every read path that touches the respondent profile snapshot gated on `anonymousMode`, k-anonymity suppression on analytics, erasure cascade, plus the respondent profile snapshot itself. Integration tests flip the flag and assert PII absence on every surface. Doc: `anonymous-mode.md`. Tracker: [`features/f8.3.md`](features/f8.3.md). +- **2026-06-08 — F8.2 result exports ([PR #54](https://github.com/human-centric-engineering/conquest/pull/54)).** CSV (one row per session × question) + JSON (full session graph incl. provenance + turns) export endpoints + admin buttons, both respecting anonymous mode. Tracker: [`features/f8.2.md`](features/f8.2.md). +- **2026-06-08 — F8.1 admin analytics dashboards ([PR #53](https://github.com/human-centric-engineering/conquest/pull/53)).** Per-questionnaire analytics — popular topics, unanswered questions, engagement, coverage gaps — over the F4.2 slots / F4.6 session events / F3.2 invitations, served from enriched list endpoints (no per-row fetches). Tracker: [`features/f8.1.md`](features/f8.1.md). +- **2026-06-08 — F7.4 PDF export ([PR #47](https://github.com/human-centric-engineering/conquest/pull/47)).** PDF export of a completed session's results. Tracker: [`features/f7.4.md`](features/f7.4.md). +- **2026-06-08 — F7.3 session lifecycle UX ([PR #46](https://github.com/human-centric-engineering/conquest/pull/46)).** Respondent pause / resume / complete UX over the F4.6 state machine + F6.3 cost-cap auto-pause. Doc: `session-lifecycle.md`. Tracker: [`features/f7.3.md`](features/f7.3.md). +- **2026-06-07 — F7.2 answer-slot panel ([PR #45](https://github.com/human-centric-engineering/conquest/pull/45)).** The live respondent answer panel beside the chat — `GET …/answers` read endpoint, per-version scope config (`full_progress` / `answered_only`), confidence language, Revisit wiring. Doc: `answer-slot-panel.md`. Tracker: [`features/f7.2.md`](features/f7.2.md). +- **2026-06-07 — F7.1 chat surface ([PR #44](https://github.com/human-centric-engineering/conquest/pull/44)).** The user-facing split-screen streaming conversational UI over the F6.1 SSE orchestrator + theming. Tracker: [`features/f7.1.md`](features/f7.1.md). +- **2026-06-07 — Deferred-gaps close ([PR #49](https://github.com/human-centric-engineering/conquest/pull/49)).** A 2026-06-07 audit found items whose home feature shipped but which were never delivered; closed in one branch: F4.3 `every_n_turns` contradiction-detection cadence, attachment input for live turns, answer-refine real confidence, and the `clone-for-client` admin utility (DEMO-ONLY). See the decisions log. +- **2026-06-07 — Admin upload-questionnaire UI ([PR #48](https://github.com/human-centric-engineering/conquest/pull/48)).** The admin "Upload Questionnaire" dialog over the F1.1 ingestion route (file picker, goal/audience override, table-extraction toggle) — the UI the API-first F1.1 deferred. +- **2026-06-06 — F6.4 demo session reset ([PR #43](https://github.com/human-centric-engineering/conquest/pull/43)).** DEMO-ONLY between-prospects clean slate: `POST /api/v1/app/demo-clients/:id/reset-sessions` hard-deletes the session graph (sessions + answers + turns + events) for every questionnaire attributed to the client, behind a typed-confirmation guard, leaving questionnaires/versions/config/invitations intact. Doc: `demo-session-reset.md`. Tracker: [`features/f6.4.md`](features/f6.4.md). + > Backfill note (2026-06-06): entries for F4.3 through F6.2 were reconstructed in one pass from their per-feature trackers (`features/f*.md`) + git history — the aggregate log had lagged since F4.2 while the per-feature trackers stayed current. They're accurate to the trackers but less granular than the first-hand entries above F4.2; the trackers remain the fuller record. - **2026-06-06 — F6.3 cost cap enforcement ([PR #42](https://github.com/human-centric-engineering/conquest/pull/42)).** Puts a ceiling on respondent LLM spend: the per-session USD budget (`AppQuestionnaireConfig.costBudgetUsd`) enforced at the live `/messages` turn boundary, firing the `cost_cap_reached` hook F4.6 wired but never fired. **No migration** (reuses the existing field; the indicative `perSessionCostCapUsd` is `costBudgetUsd`). **Decisions (confirmed):** **include the cost-accuracy wiring** — the recorded per-turn `costUsd` only captured offer-stream (+ adaptive) cost because the extract/detect/refine invokers stubbed `0`, so a cap had nothing meaningful to grade; F6.3 closes the F6.1-deferred per-turn aggregation by surfacing each capability's real `costUsd` on its app-owned `data` (from the `runStructuredCompletion` result already logged to `AiCostLog`), invokers read it, the orchestrator sums it — **no platform `CapabilityResult` change**; **soft cap biases toward offering early** (not just a prose hint) — at ≥90% a `not_ready` assessment with ≥1 answered is responded to as an offer (never bypassing the `blocked_on_required` gate, never on an empty session) **and** the offer prose carries a wrap-up instruction; **dedicated dark-launch sub-flag** `APP_QUESTIONNAIRES_COST_CAP_ENABLED` (seed 023, off by default), ANDed with master + live-sessions via `isCostCapEnforcementEnabled`; **turn-boundary semantics** — "spend so far" is `sumSessionTurnCost` (`_sum` of prior `AppQuestionnaireTurn.costUsd`), the current turn's cost recorded afterward and caught next boundary; **hard cap is pre-stream** — 402 `COST_CAP_REACHED` + `pauseSession` + a `cost_cap_reached` (`tier:'hard'`) event before any turn work, then the status gate locks every later turn; **soft event fires once** (deduped by `hasCostCapReachedEvent`). **Delivered:** the pure `classifyCostCap` (`session/cost-cap.ts`); `sumSessionTurnCost` + `hasCostCapReachedEvent` seams + `recordCostCapReached` `tier` metadata; the flag/resolver/seed; `TurnState.costPressure` + `OfferComposeInput.costWrapUp` threaded through the orchestrator + offer composer; the route pre-stream gate. **Tests:** classifier boundaries + orchestrator soft-bias (unit); capability + invoker cost surfacing, `sumSessionTurnCost`, event tier/dedupe, offer wrap-up prose, and the `/messages` matrix (null-cap/below/hard-402+pause/soft-once+biased-offer/dedupe/flag-off) at integration — full session suite green (914). **No CHANGELOG entry** (app surface). Tracker: `planning/features/f6.3.md`. Docs: `cost-cap-enforcement.md`. **Gates green:** `/pre-pr` (changed-file coverage 88–100%, anti-patterns clean), `/code-review high` (2 robustness fixes — hard-cap pause-before-record ordering, soft-cap best-effort audit write), `/test-review` (4 findings ≥80 actioned — symmetric capability cost-surfacing assertions, ordering pin, mock fidelity), `/security-review` (no findings). diff --git a/.context/app/planning/features/f0.1.md b/.context/app/planning/features/f0.1.md index 277763e7f..ee9a6736d 100644 --- a/.context/app/planning/features/f0.1.md +++ b/.context/app/planning/features/f0.1.md @@ -2,7 +2,7 @@ feature: F0.1 title: Foundation scaffolding phase: P0 — Foundations -status: in flight +status: done owner: Simon Holmes deps: none (first feature) opened: 2026-06-01 diff --git a/.context/app/planning/features/f2.1.md b/.context/app/planning/features/f2.1.md index 38d388ede..b9fb2d64c 100644 --- a/.context/app/planning/features/f2.1.md +++ b/.context/app/planning/features/f2.1.md @@ -2,7 +2,7 @@ feature: F2.1 title: Questionnaire authoring phase: P2 — Admin CRUD over questionnaires -status: in flight +status: done owner: TBD deps: F1.1 opened: 2026-06-02 diff --git a/.context/app/planning/features/f2.2.md b/.context/app/planning/features/f2.2.md index 33f009d8f..dba3f1b47 100644 --- a/.context/app/planning/features/f2.2.md +++ b/.context/app/planning/features/f2.2.md @@ -2,7 +2,7 @@ feature: F2.2 title: Tagging phase: P2 — Admin CRUD over questionnaires -status: in flight +status: done owner: TBD deps: F2.1 opened: 2026-06-02 diff --git a/.context/app/planning/features/f2.3.md b/.context/app/planning/features/f2.3.md index 35214b9c9..6a2eceed8 100644 --- a/.context/app/planning/features/f2.3.md +++ b/.context/app/planning/features/f2.3.md @@ -2,7 +2,7 @@ feature: F2.3 title: Extraction-change review phase: P2 — Admin CRUD over questionnaires -status: in flight +status: done owner: TBD deps: F1.1, F2.1 opened: 2026-06-03 diff --git a/.context/app/planning/features/f2.4.md b/.context/app/planning/features/f2.4.md index bbd7ae93c..3ed4f412d 100644 --- a/.context/app/planning/features/f2.4.md +++ b/.context/app/planning/features/f2.4.md @@ -2,7 +2,7 @@ feature: F2.4 title: Re-ingest phase: P2 — Admin CRUD over questionnaires -status: in flight +status: done owner: TBD deps: F1.1, F2.1 opened: 2026-06-04 diff --git a/.context/app/planning/features/f3.1.md b/.context/app/planning/features/f3.1.md index 4483c67d0..d5ced5960 100644 --- a/.context/app/planning/features/f3.1.md +++ b/.context/app/planning/features/f3.1.md @@ -2,7 +2,7 @@ feature: F3.1 title: Questionnaire configuration phase: P3 — Configuration, invitations, and cost estimation -status: in flight +status: done owner: TBD deps: F2.1 opened: 2026-06-04 diff --git a/.context/app/planning/features/f3.2.md b/.context/app/planning/features/f3.2.md index ac1d07198..ce84f2a10 100644 --- a/.context/app/planning/features/f3.2.md +++ b/.context/app/planning/features/f3.2.md @@ -2,7 +2,7 @@ feature: F3.2 title: Invitation flow phase: P3 — Configuration, invitations, and cost estimation -status: in flight +status: done owner: TBD deps: F3.1 opened: 2026-06-04 diff --git a/.context/app/planning/features/f3.3.md b/.context/app/planning/features/f3.3.md index 5c9ea89ba..64dff66ef 100644 --- a/.context/app/planning/features/f3.3.md +++ b/.context/app/planning/features/f3.3.md @@ -2,7 +2,7 @@ feature: F3.3 title: Pre-launch cost estimation phase: P3 — Configuration, invitations, and cost estimation -status: in flight +status: done owner: TBD deps: F3.1 opened: 2026-06-04 diff --git a/.context/app/planning/features/f4.1.md b/.context/app/planning/features/f4.1.md index d0d53db2c..8dc19f859 100644 --- a/.context/app/planning/features/f4.1.md +++ b/.context/app/planning/features/f4.1.md @@ -2,7 +2,7 @@ feature: F4.1 title: Selection strategies phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F0.1, F2.2 (tags), F3.1 (config) opened: 2026-06-04 diff --git a/.context/app/planning/features/f4.2.md b/.context/app/planning/features/f4.2.md index 86b686a2b..67a785345 100644 --- a/.context/app/planning/features/f4.2.md +++ b/.context/app/planning/features/f4.2.md @@ -2,7 +2,7 @@ feature: F4.2 title: Answer extraction into slots phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F0.1, F1.1 (slots), F2.1 (typeConfig) opened: 2026-06-04 diff --git a/.context/app/planning/features/f4.3.md b/.context/app/planning/features/f4.3.md index 714db3923..7e8cf6663 100644 --- a/.context/app/planning/features/f4.3.md +++ b/.context/app/planning/features/f4.3.md @@ -2,7 +2,7 @@ feature: F4.3 title: Contradiction detection phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F0.1, F1.1 (slots), F2.1 (typeConfig), F3.1 (contradiction config), F4.2 (answer shapes) opened: 2026-06-05 diff --git a/.context/app/planning/features/f4.4.md b/.context/app/planning/features/f4.4.md index cad11a84e..b4ed7cd60 100644 --- a/.context/app/planning/features/f4.4.md +++ b/.context/app/planning/features/f4.4.md @@ -2,7 +2,7 @@ feature: F4.4 title: Answer refinement phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F0.1, F1.1 (slots), F2.1 (typeConfig), F4.2 (answer shapes + value validation) opened: 2026-06-05 diff --git a/.context/app/planning/features/f4.5.md b/.context/app/planning/features/f4.5.md index 6525a4a93..de6ae6b03 100644 --- a/.context/app/planning/features/f4.5.md +++ b/.context/app/planning/features/f4.5.md @@ -2,7 +2,7 @@ feature: F4.5 title: Completion logic phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F4.1 (coverage helpers + config), F4.2 (answers), F4.3 (completion-sweep), F4.4 (session/answer persistence) opened: 2026-06-05 diff --git a/.context/app/planning/features/f4.6.md b/.context/app/planning/features/f4.6.md index 3ce9233cc..8b156fc86 100644 --- a/.context/app/planning/features/f4.6.md +++ b/.context/app/planning/features/f4.6.md @@ -2,7 +2,7 @@ feature: F4.6 title: Session state machine phase: P4 — Conversational engine (non-streaming) -status: in flight +status: done owner: TBD deps: F4.4 (session/answer persistence), F4.5 (markSessionCompleted seam) opened: 2026-06-05 diff --git a/.context/app/planning/features/f6.1.md b/.context/app/planning/features/f6.1.md index ec0b8dc62..d01b812ca 100644 --- a/.context/app/planning/features/f6.1.md +++ b/.context/app/planning/features/f6.1.md @@ -2,7 +2,7 @@ feature: F6.1 title: Per-turn orchestrator + streaming phase: P6 — Conversational session (streaming) -status: in flight +status: done owner: TBD deps: F4.1–F4.6 (P4 capabilities + session state machine) opened: 2026-06-06 diff --git a/.context/app/planning/features/f6.3.md b/.context/app/planning/features/f6.3.md index 1a0270e75..a7f9859bb 100644 --- a/.context/app/planning/features/f6.3.md +++ b/.context/app/planning/features/f6.3.md @@ -2,7 +2,7 @@ feature: F6.3 title: Cost cap enforcement at turn boundary phase: P6 — Conversational session (streaming) -status: in flight +status: done owner: TBD deps: F4.6 (session state machine), F6.1 (per-turn orchestrator + streaming) opened: 2026-06-06 diff --git a/.context/app/planning/features/f7.1.md b/.context/app/planning/features/f7.1.md index 0791c9e6f..3ebeb47f2 100644 --- a/.context/app/planning/features/f7.1.md +++ b/.context/app/planning/features/f7.1.md @@ -2,7 +2,7 @@ feature: F7.1 title: Chat surface phase: P7 — Demo-grade respondent experience -status: in flight +status: done owner: TBD deps: F6.1 (live respondent surface + SSE turn loop), F6.2 (transcribe endpoint), F3.4 (theming module) opened: 2026-06-06 diff --git a/.context/app/planning/features/f7.2.md b/.context/app/planning/features/f7.2.md index 28b74222f..a60fb22d5 100644 --- a/.context/app/planning/features/f7.2.md +++ b/.context/app/planning/features/f7.2.md @@ -2,7 +2,7 @@ feature: F7.2 title: Answer-slot panel phase: P7 — Demo-grade respondent experience -status: in flight +status: done owner: TBD deps: F6.1 (live respondent surface + SSE turn loop), F7.1 (chat surface) opened: 2026-06-06 diff --git a/.context/app/planning/features/f8.1.md b/.context/app/planning/features/f8.1.md index 756019a57..c83d178c6 100644 --- a/.context/app/planning/features/f8.1.md +++ b/.context/app/planning/features/f8.1.md @@ -2,7 +2,7 @@ feature: F8.1 title: Admin analytics dashboards phase: P8 — Admin analytics, exports, anonymous mode -status: in flight +status: done owner: TBD deps: F4.2 (answer slots), F4.6 (session events), F3.2 (invitations), F6.1 (live turns) opened: 2026-06-08 diff --git a/.context/app/planning/features/f8.2.md b/.context/app/planning/features/f8.2.md index 71c6c74ec..27c43d71e 100644 --- a/.context/app/planning/features/f8.2.md +++ b/.context/app/planning/features/f8.2.md @@ -2,7 +2,7 @@ feature: F8.2 title: Result exports phase: P8 — Admin analytics, exports, anonymous mode -status: in flight +status: done owner: TBD deps: F8.1 (analytics scope + filter), F4.2 (answer slots), F4.6 (session events), F6.1 (turns) opened: 2026-06-08 diff --git a/.context/app/planning/features/f8.3.md b/.context/app/planning/features/f8.3.md index d97209f7d..42f178804 100644 --- a/.context/app/planning/features/f8.3.md +++ b/.context/app/planning/features/f8.3.md @@ -2,7 +2,7 @@ feature: F8.3 title: Anonymous-mode hardening (+ respondent profile collection) phase: P8 — Admin analytics, exports, anonymous mode -status: in flight +status: done owner: TBD deps: F8.1 (analytics surfaces), F8.2 (result exports), F7.4 (PDF export), F6.1 (sessions) opened: 2026-06-09 diff --git a/.context/app/planning/features/f9.1.md b/.context/app/planning/features/f9.1.md index c64063e6f..e00be8a54 100644 --- a/.context/app/planning/features/f9.1.md +++ b/.context/app/planning/features/f9.1.md @@ -2,7 +2,7 @@ feature: F9.1 title: Production hardening phase: P9 — Hardening + forking docs -status: in flight +status: done owner: TBD deps: everything (final pass — P0–P8 complete) opened: 2026-06-09 diff --git a/.context/app/planning/features/f9.2.md b/.context/app/planning/features/f9.2.md new file mode 100644 index 000000000..7a3e644bf --- /dev/null +++ b/.context/app/planning/features/f9.2.md @@ -0,0 +1,66 @@ +--- +feature: F9.2 +title: Operational runbook +phase: P9 — Hardening + forking docs +status: in flight +owner: TBD +deps: F9.1, F9.4 (demo content seed — built first so the runbook can exercise it) +opened: 2026-06-09 +plan: .context/app/planning/development-plan.md#f92--operational-runbook +docs: .context/app/questionnaire/runbook.md +--- + +# F9.2 — Operational runbook + +> Committable tracker for **F9.2**. A road-tested "spin up a demo client end-to-end" runbook so +> a presenter can go from a fresh checkout to a respondent answering the first question without +> reverse-engineering the admin UI. A **documentation** feature — it adds no runtime capability. + +## Intent + +Every surface the demo needs already exists (demo clients, theming, ingestion, clone-for-client, +config, launch, invitations, sessions, reset). What was missing was the **sequence**: which +screens, in what order, with the gotchas that trip a first-timer. F9.2 captures that as +`.context/app/questionnaire/runbook.md`, then road-tests it on a clean machine and folds the +friction back in. F9.4's demo-content seed (built first) gives the runbook a one-command fast +path so a stock demo needs zero manual setup. + +## Decisions + +- **Build F9.4 first.** The plan calls for the `LOAD_DEMO_CONTENT` seed to land before the + road-test so the runbook can exercise it. F9.4 was not started, so it was built first on this + same branch ([f9.4.md](./f9.4.md)); the runbook leads with that seeded fast path. +- **Two content paths, clone-first.** The runbook leads with clone-for-client (fastest demo + path from a template) and documents upload-and-extract as the alternative for net-new content + — rather than picking only one. +- **Demo-doc convention, not the index.** The runbook is demo-oriented, so it follows the + established demo-doc pattern: a `> **DEMO-ONLY**` callout, cross-linked from + [demo-clients.md](../../app/questionnaire/demo-clients.md) (the demo hub) rather than added to + the namespace `README.md` index — which deliberately excludes demo docs to stay fork-clean. A + callout notes the middle of the flow (content → configure → launch → invite → session) is the + core product, identical for a real engagement. +- **Road-test is a human task.** The "live road-test on a clean machine" is John/Simon's to run. + This feature delivers the draft + a road-test checklist (a tick-and-note table in the doc); + the reported friction is folded back as the closing change before the phase ships. + +## Build shape (branch `feat/F9.4-demo-seed-and-F9.2-runbook`) + +- **Runbook** — `.context/app/questionnaire/runbook.md`. Sections: prerequisites (flags are DB + rows, not env vars; LLM key; email) → seeded fast path (§1A) → manual client + content + (clone-first / upload) → review/configure/launch → attribute → invite → first session + (respondent view) → reset between prospects → troubleshooting table → road-test checklist → + see-also. Concrete admin routes + API endpoints throughout. +- **Cross-link** — a pointer from `demo-clients.md`'s header callout into the runbook. + +## Verification + +- `npm run validate` clean (the runbook + tracker are docs; no code beyond F9.4). +- Doc links resolve (sibling `.md` files exist; `forking.md` is a known P9 forward-reference, + matching the existing `demo-clients.md` reference). +- **Pending — human road-test.** John/Simon run the checklist on a clean machine; friction is + folded into the doc, then the feature flips to `shipped`. + +## No CHANGELOG entry + +F9.2 is app-owned documentation — no Sunrise platform surface. Per the repo's platform-scoped +CHANGELOG policy, it adds no `CHANGELOG.md` bullet. diff --git a/.context/app/planning/features/f9.4.md b/.context/app/planning/features/f9.4.md new file mode 100644 index 000000000..ab4741251 --- /dev/null +++ b/.context/app/planning/features/f9.4.md @@ -0,0 +1,93 @@ +--- +feature: F9.4 +title: Demo content seed (DEMO-ONLY) +phase: P9 — Hardening + forking docs +status: done +owner: TBD +deps: F2.5.1 (AppDemoClient), F7.1 (themeable vertical to seed) +opened: 2026-06-09 +plan: .context/app/planning/development-plan.md#f94--demo-content-seed-demo-only +docs: .context/app/questionnaire/runbook.md +--- + +# F9.4 — Demo content seed (DEMO-ONLY) + +> Committable tracker for **F9.4**. A one-command seed that populates a sample demo client +> plus a fully launched, attributed questionnaire on a fresh DB, so the F9.2 operational +> runbook can be road-tested end-to-end without manual admin clicks. **Strictly demo-only** — +> a fork strips it. Built ahead of F9.2 (per the plan) so the runbook can exercise it. + +## Intent + +F9.4 removes the manual click-path from "fresh DB → something to demo". Before it, a presenter +had to create a demo client, upload or clone a questionnaire, configure it, and launch it by +hand before they could invite anyone. F9.4 collapses that into `LOAD_DEMO_CONTENT=1 npm run +db:seed`: a believable "Northwind Logistics" demo client and a launched, attributed +questionnaire appear, ready to invite a respondent against. It adds **no runtime capability** — +it is pure demo scaffolding that takes the same persistence path a real upload does. + +## Decisions + +- **Env-gated, off by default.** The seed no-ops unless `LOAD_DEMO_CONTENT=1`, so it can never + load in production. The gate lives **inside** `run()` (not as a file-skip) so the runner still + stamps `SeedHistory` on the no-op — see the gotcha below. +- **Reuse the ingestion writer, don't hand-roll inserts.** The section/slot graph is written + through `writeGraph` (the same function the ingestion route uses), fed a hand-authored + `ExtractQuestionnaireStructureData`. The demo content therefore exercises the real + persistence seam, not a parallel one. `persistIngestion` was rejected — it demands a fake + source-document row the demo has no use for. +- **Set `status: 'launched'` directly.** There is no pure launch helper (the gate lives in the + status HTTP route), so the seed re-asserts the gate's invariants itself (goal, non-empty + audience, ≥1 section, ≥1 question, config row) and flips the version status in the same + transaction. The whole build is one `executeTransaction` so a partial demo never exists. +- **Replace-on-rerun idempotency.** The demo client upserts by unique `slug`; the questionnaire + is found-and-replaced by a stable title marker (`Northwind Logistics — Onboarding Experience +Review`). Editing the sample content and re-seeding refreshes the demo with no duplicates. +- **Profile capture on.** `anonymousMode` stays false and two profile fields (name, work email) + are seeded, so the demo visibly shows the F8.3 session-start profile step. + +## Build shape (branch `feat/F9.4-demo-seed-and-F9.2-runbook`) + +- **Seed unit** — `prisma/seeds/app-questionnaire/025-demo-content.ts`, name + `app-questionnaire/025-demo-content` (next free prefix after `024`). Env-gate → upsert demo + client → replace prior demo questionnaire → create questionnaire (attributed) → version + (goal + audience + provenance) → `writeGraph` (2 sections, 6 slots spanning the question-type + range) → config row (defaults + profile fields) → flip version to `launched`. Whole file + marked `// DEMO-ONLY`; nothing imports it, so a fork deletes it wholesale. +- **`.env.example`** — a documented, commented `# LOAD_DEMO_CONTENT=1` block (DEMO-ONLY) under + a new "Demo content" section, including the re-load recipe for the no-op gotcha. + +## The no-op gotcha + +The runner records a `SeedHistory` row with the file's content hash after **any** successful +`run()` — including the env-gated no-op. So if `db:seed` runs once without +`LOAD_DEMO_CONTENT=1` (the normal case), the row is stamped; later setting the env var alone +won't reload, because the file content is unchanged and the runner skips it. To load demo +content after a prior no-op, clear the history row and re-seed: + +```sql +DELETE FROM "SeedHistory" WHERE name = 'app-questionnaire/025-demo-content'; +``` + +then `LOAD_DEMO_CONTENT=1 npm run db:seed`. Documented in `.env.example` and the F9.2 runbook. + +## Sample content + +Vertical: **B2B SaaS onboarding feedback** — "Northwind Logistics" demo client (branded +CTA/accent/logo/welcome copy) and a launched questionnaire "Northwind Logistics — Onboarding +Experience Review": 2 sections (Getting started · Value & support), 6 questions covering +`likert | free_text | single_choice | numeric | multi_choice | boolean`, a full 7-field +audience, and name/email profile capture. + +## Verification + +- `LOAD_DEMO_CONTENT=1 npm run db:seed` — seeds the client + launched questionnaire; logged. +- Row check: 1 demo client, 1 questionnaire (attributed), 1 version (`launched`, goal set), + 2 sections, 6 slots, 1 config row (2 profile fields, `anonymousMode=false`). +- `npm run db:seed` again **without** the env var → seed no-ops (skip log), creates nothing. +- `npm run validate` clean (type-check + lint + format). + +## No CHANGELOG entry + +F9.4 is app-owned, demo-only seed scaffolding — no Sunrise platform surface. Per the repo's +platform-scoped CHANGELOG policy, it adds no `CHANGELOG.md` bullet. diff --git a/.context/app/questionnaire/demo-clients.md b/.context/app/questionnaire/demo-clients.md index ed6fcd15f..90bfb3a32 100644 --- a/.context/app/questionnaire/demo-clients.md +++ b/.context/app/questionnaire/demo-clients.md @@ -4,6 +4,10 @@ > sales demo — **not** a security boundary and **not** real multi-tenancy. A real > client engagement strips it. See [forking.md] § "Replacing demo tenancy" (P9) and > the [development plan][plan] P2.5 section. +> +> **Spinning one up?** The end-to-end operational walkthrough — seed fast path, +> branding, content, launch, invite, first session, reset — is in +> [runbook.md](./runbook.md). ## What it is (and isn't) diff --git a/.context/app/questionnaire/runbook.md b/.context/app/questionnaire/runbook.md new file mode 100644 index 000000000..86210b5d5 --- /dev/null +++ b/.context/app/questionnaire/runbook.md @@ -0,0 +1,203 @@ +# Operational runbook — spin up a demo client (F9.2) + +> **DEMO-ONLY (mostly).** This runbook spins up a sales **demo client** end-to-end. The +> demo-tenancy steps (create a demo client, attribute, reset sessions) are stripped by a real +> client engagement — see [demo-clients.md] § "Fork guidance" and (P9) `forking.md`. The +> middle of the flow — load content → review → configure → launch → invite → run a session — +> is the **core product** and is identical for a real engagement (it just skips the demo-client +> attribution and reset). Read this before presenting to a prospect. + +The goal: from a fresh checkout to a respondent answering the first question, in one sitting. +There are two paths — the **seeded fast path** (one command, for a stock demo) and the +**manual path** (when you need bespoke content for a specific prospect). Both end at the same +place: a launched questionnaire attributed to a demo client, ready to invite against. + +--- + +## 0. Prerequisites (once per machine) + +1. **App running against a real Postgres.** `docker-compose up` (or a local Postgres), then + `npm run db:migrate:deploy && npm run db:seed`, then `npm run dev`. +2. **Feature flags are ON.** The questionnaire surface is gated by feature flags that are + **database rows in the `feature_flag` table, not env vars** — the `APP_*_ENABLED` names look + like env vars but are not. `npm run db:seed` turns the master flag (`APP_QUESTIONNAIRES_ENABLED`) + and the sub-flags on. If a route 404s, the flag is off — see [feature-flags.md] for the full + matrix and how to toggle one. Live respondent sessions additionally need + `APP_QUESTIONNAIRES_LIVE_SESSIONS_ENABLED`. +3. **At least one LLM provider key** (e.g. `ANTHROPIC_API_KEY` in `.env.local`) — the manual + upload path runs the extractor agent, and every respondent turn calls a model. The seeded + fast path needs a key only once you start a session, not to seed. +4. **Email** — invitations send a real email. In dev, check the configured mail transport / + preview; the invitation link also appears in logs. + +--- + +## 1A. Seeded fast path (stock demo) + +One command loads a ready-to-demo "Northwind Logistics" client + a launched questionnaire: + +```bash +LOAD_DEMO_CONTENT=1 npm run db:seed +``` + +This creates (idempotently — safe to re-run): + +- **Demo client** "Northwind Logistics (Demo)" (slug `northwind-logistics-demo`), branded + (CTA/accent colours, logo, welcome copy). +- **Launched questionnaire** "Northwind Logistics — Onboarding Experience Review" — 2 sections, + 6 questions, attributed to that client, with name + work-email profile capture on. + +Then skip to **§3 Invite a respondent**. To confirm it loaded, open `/admin/questionnaires` — +the questionnaire shows `launched` and attributed to Northwind. + +> **Gotcha — it didn't appear?** The seed no-ops unless `LOAD_DEMO_CONTENT=1`. If `db:seed` ran +> once **without** the flag, the runner recorded it as applied and won't re-run on an unchanged +> file. Clear its history row and re-seed: +> +> ```sql +> DELETE FROM "SeedHistory" WHERE name = 'app-questionnaire/025-demo-content'; +> ``` +> +> then `LOAD_DEMO_CONTENT=1 npm run db:seed`. (Re-seeding **replaces** the demo questionnaire, +> cascading any sessions/invitations against it — an explicit reset.) + +--- + +## 1B. Manual path — create the demo client + +For bespoke content, build it by hand. First the client: + +1. Go to **`/admin/demo-clients`** → **New demo client** (`/admin/demo-clients/new`). +2. Fill **name** (e.g. "Acme Bank Demo"). The slug is derived from the name; you can override + it (a collision returns 409). API equivalent: `POST /api/v1/app/demo-clients`. +3. Optional **branding** (the edit page has a live preview): CTA colour, accent colour, logo + URL, welcome copy. Null fields fall back to the Sunrise default. These are snapshotted onto + each invitation at send time — see [demo-clients.md] § "Theming module". + +## 2. Load content, review, configure, launch + +### 2.1 Load content — clone first, upload if net-new + +**Clone (fastest)** — reuse an existing template for this client: + +- On a template questionnaire's detail page (`/admin/questionnaires/[id]`), use **Clone for + client** → pick the demo client. Copies the structure, config, and tags into a fresh draft. + API: `POST /api/v1/app/questionnaires/[id]/clone-for-client`. + +**Upload + extract (net-new content)** — the platform's headline capability: + +- On **`/admin/questionnaires`**, **Upload Questionnaire** → pick a `.pdf` / `.docx` / `.md` / + `.txt`, optionally override goal/audience, submit. The extractor agent parses it into + sections + questions + an inferred goal/audience synchronously and lands you on the detail + page. API: `POST /api/v1/app/questionnaires` (multipart). See [ingestion.md]. + +### 2.2 Review & configure + +- On the detail page, review the inferred **goal / audience** and accept or revert extraction + changes (extraction-changes tab). +- Open the **Configuration** editor and set what the demo needs: selection strategy, completion + thresholds, optional cost budget, voice toggle, **profile fields** (collected at session + start unless anonymous mode is on), answer-panel scope. Saving the config row is itself part + of the launch gate. See [configuration.md]. + +### 2.3 Launch + +- In the status section, transition the version **draft → launched**. The launch gate requires: + a goal, a non-empty audience, ≥1 section, ≥1 question, and a saved config row. API: + `PATCH /api/v1/app/questionnaires/[id]/versions/[vid]/status`. + +### 2.4 Attribute to the demo client + +- On the detail page, use the **Demo client** picker to attribute the questionnaire to your + client (the seeded path already did this). API: `PATCH /api/v1/app/questionnaires/[id]` with + `{ demoClientId }`. Attribution is snapshotted onto invitations at send time, so re-attributing + later doesn't change already-sent invites. + +--- + +## 3. Invite a respondent + +1. Go to the questionnaire's **Invitations** page: `/admin/questionnaires/[id]/invitations`. +2. Enter one email (or paste several). **Send** → mints a tokenised link and emails it, branded + with the demo client's theme. API: `POST /api/v1/app/questionnaires/[id]/invitations`. + - A 409 here means the version isn't launched — go back to **§2.3**. + - Resend a failed one from the table. Full lifecycle + token security: [invitations.md]. + +The respondent receives an email; the link is `/questionnaire-invite?token=…`. + +--- + +## 4. First session (respondent's view) + +Walk this yourself with the invited email to rehearse the prospect's experience: + +1. Open the email link → **`/questionnaire-invite?token=…`** (no login). Register (set a + password) or, if the email already has an account, sign in. You're auto-logged-in and + forwarded on. +2. **`/questionnaires/start`** bootstraps the session. If profile fields are configured (and + not anonymous mode), you fill them here; they're snapshotted onto the session. +3. **`/questionnaires/[sessionId]`** — the live chat. The agent asks the first question; answer + conversationally and watch answers land in the side panel. This needs + `APP_QUESTIONNAIRES_LIVE_SESSIONS_ENABLED`. See [per-turn-orchestrator.md] and + [answer-slot-panel.md]. + +--- + +## 5. Reset between prospects + +After a demo, wipe respondent data so the next prospect starts clean: + +- On the demo client's edit page (or `POST /api/v1/app/demo-clients/[id]/reset-sessions`), + run **Reset sessions** (typed-slug confirmation). It hard-deletes every session/turn/answer/ + event for all questionnaires attributed to the client; the questionnaires, versions, config, + and invitations remain. Details + the anonymous-mode refusal: [demo-session-reset.md]. + +--- + +## 6. Troubleshooting + +| Symptom | Cause / fix | +| --------------------------------------- | -------------------------------------------------------------------------------------- | +| Admin route or page **404s** | A feature flag is off — they're DB rows, not env vars. See [feature-flags.md]. | +| Demo content didn't load after the seed | Seed no-op'd earlier — clear its `SeedHistory` row and re-seed (see **§1A** gotcha). | +| **409** when sending an invitation | The version isn't launched / has no launched version — launch it (**§2.3**). | +| Chat page loads but won't stream | `APP_QUESTIONNAIRES_LIVE_SESSIONS_ENABLED` off, or no LLM provider key configured. | +| Upload returns **422** | Scanned PDF with no extractable text — use a text-based source. | +| Upload returns **409** | Exact bytes already ingested (SHA-256 dedup) — change the file or re-ingest the draft. | + +--- + +## Road-test checklist (clean machine) + +Run this on a fresh checkout + fresh DB before the phase ships. Tick each step; jot any friction +in the **Notes** column and fold the correction back into this doc. + +| ✓ | Step | Expected | Notes | +| --- | ------------------ | ----------------------------------------------------------------------- | ----- | +| | Setup | `docker-compose up`, migrate, seed, `npm run dev` come up clean | | +| | Flags on | `/admin/questionnaires` loads (no 404) | | +| | Seeded fast path | `LOAD_DEMO_CONTENT=1 npm run db:seed` → Northwind client + launched q'n | | +| | Manual client | create a demo client with branding; preview renders | | +| | Clone | Clone-for-client copies structure into a draft | | +| | Upload | upload a `.docx`/`.pdf`; extraction produces sections + questions | | +| | Configure + launch | set config, launch passes the gate | | +| | Invite | invitation email arrives, branded, with a working link | | +| | Session | register → start → first question streams; answers land in the panel | | +| | Reset | reset-sessions clears runs; questionnaire + invitations remain | | + +--- + +## See also + +- [demo-clients.md] — the demo-tenancy model, branding, clone-for-client, fork guidance. +- [demo-session-reset.md] — the between-demos reset in full. +- [ingestion.md] · [configuration.md] · [invitations.md] · [feature-flags.md] — the core surfaces. + +[demo-clients.md]: ./demo-clients.md +[demo-session-reset.md]: ./demo-session-reset.md +[ingestion.md]: ./ingestion.md +[configuration.md]: ./configuration.md +[invitations.md]: ./invitations.md +[feature-flags.md]: ./feature-flags.md +[per-turn-orchestrator.md]: ./per-turn-orchestrator.md +[answer-slot-panel.md]: ./answer-slot-panel.md diff --git a/.env.example b/.env.example index ce0b8e5fb..a9b7c948a 100644 --- a/.env.example +++ b/.env.example @@ -383,3 +383,19 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000" # tests/setup.ts sets this for the entire unit suite so route-handler tests # don't have to think about rate-limit state. MUST NOT be set in production. # RATE_LIMIT_BYPASS=true + +# ============================================================ +# DEMO CONTENT (DEMO-ONLY — F9.4) +# ============================================================ +# Set to 1 to load sample demo content when seeding (`npm run db:seed`): a demo +# client ("Northwind Logistics") plus a fully launched, attributed sample +# questionnaire, so the operational runbook +# (.context/app/questionnaire/runbook.md) can be road-tested end-to-end. Unset or +# any value other than 1 = the seed no-ops, so this NEVER loads in production. +# +# The seed is idempotent (re-running replaces the demo questionnaire, no +# duplicates). If demo content didn't appear after setting this, the seed had +# already run as a no-op: clear its history row and re-seed — +# DELETE FROM "SeedHistory" WHERE name = 'app-questionnaire/025-demo-content'; +# DEMO-ONLY: strip this block on fork. +# LOAD_DEMO_CONTENT=1 diff --git a/prisma/seeds/app-questionnaire/025-demo-content.ts b/prisma/seeds/app-questionnaire/025-demo-content.ts new file mode 100644 index 000000000..a89a11df7 --- /dev/null +++ b/prisma/seeds/app-questionnaire/025-demo-content.ts @@ -0,0 +1,274 @@ +// DEMO-ONLY (F9.4): sample demo client + a fully launched, attributed questionnaire +// so the F9.2 operational runbook can be road-tested end-to-end without any manual +// admin clicks. This whole file is demo scaffolding — a fork strips it via +// `grep -rl "DEMO-ONLY"` (see .context/app/questionnaire/forking.md). Nothing else +// imports it; the seed runner discovers it by glob, so deletion is a clean removal. +// +// Env-gated: it no-ops unless `LOAD_DEMO_CONTENT=1`, so it never loads in production. +// Idempotent: re-running replaces the demo questionnaire (keyed on a stable title) +// and upserts the demo client by slug — no duplicates. + +import type { SeedUnit } from '@/prisma/runner'; +import { executeTransaction } from '@/lib/db/utils'; +import { assertPersistable, writeGraph } from '@/app/api/v1/app/questionnaires/_lib/persist'; +import { slugifyDemoClient } from '@/lib/app/questionnaire/demo-clients/slug'; +import type { ExtractQuestionnaireStructureData } from '@/lib/app/questionnaire/capabilities'; +import { + DEFAULT_QUESTIONNAIRE_CONFIG, + type AudienceProvenance, + type AudienceShape, + type ProfileFieldConfig, +} from '@/lib/app/questionnaire/types'; + +// DEMO-ONLY: the demo prospect. Slug is derived from the name (the admin UI does the +// same), so the client appears at /admin/demo-clients/. +const DEMO_CLIENT_NAME = 'Northwind Logistics (Demo)'; +const DEMO_CLIENT = { + name: DEMO_CLIENT_NAME, + slug: slugifyDemoClient(DEMO_CLIENT_NAME), + description: 'DEMO-ONLY: sample prospect for the F9.2 spin-up walkthrough.', + isActive: true, + // Brand snapshot the invitation email + F7.1 chat surface resolve. Valid hex / + // absolute-https so the themed paths are visibly exercised, not defaulted away. + ctaColor: '#2563eb', + accentColor: '#0ea5e9', + logoUrl: 'https://dummyimage.com/200x48/2563eb/ffffff&text=Northwind', + welcomeCopy: 'Thanks for trialing Northwind — a few quick questions about your onboarding.', +} as const; + +// DEMO-ONLY: stable idempotency marker. The seed finds-and-replaces the questionnaire +// with this exact title on every run, so editing the sample content below refreshes +// the demo without duplicating it. (A respondent session/invitation against the prior +// launched version cascades away on replace — re-running the seed is an explicit reset.) +const DEMO_QUESTIONNAIRE_TITLE = 'Northwind Logistics — Onboarding Experience Review'; + +// DEMO-ONLY: the questionnaire's goal + audience. Both are required by the launch gate +// (assertLaunchable: goal set + audience has ≥1 defined field); the audience populates +// all seven AudienceShape fields so it reads like a real engagement. +const DEMO_GOAL = + 'Understand how new Northwind Logistics customers experience the first 30 days of ' + + 'onboarding, and identify the friction points that most affect early retention.'; + +const DEMO_AUDIENCE: AudienceShape = { + description: 'Recently onboarded Northwind Logistics customers (first 30 days)', + role: 'Operations / logistics coordinator', + expertiseLevel: 'intermediate', + estimatedDurationMinutes: 8, + locale: 'en', + sensitivity: 'low', + notes: 'Demo questionnaire — not a real customer engagement.', +}; + +// DEMO-ONLY: every audience field was hand-authored, so mark each admin-supplied. +const DEMO_AUDIENCE_PROVENANCE: AudienceProvenance = { + description: 'admin-supplied', + role: 'admin-supplied', + expertiseLevel: 'admin-supplied', + estimatedDurationMinutes: 'admin-supplied', + locale: 'admin-supplied', + sensitivity: 'admin-supplied', + notes: 'admin-supplied', +}; + +// DEMO-ONLY: session-start profile capture, so the demo shows the F8.3 profile step +// (anonymousMode stays false). Keys are lowercase-snake per profileFieldKeySchema. +const DEMO_PROFILE_FIELDS: ProfileFieldConfig[] = [ + { key: 'name', label: 'Your name', type: 'text', required: true }, + { key: 'work_email', label: 'Work email', type: 'email', required: true }, +]; + +// DEMO-ONLY: the hand-authored extraction graph (2 sections, 6 questions spanning the +// question-type range). Fed through `writeGraph` exactly as the ingestion route feeds +// the extractor's output, so the demo content takes the same persistence path as a +// real upload. `changes: []` — hand-authored content has no editorial change log. +const DEMO_EXTRACTION: ExtractQuestionnaireStructureData = { + inferredGoal: DEMO_GOAL, + inferredAudience: DEMO_AUDIENCE, + changes: [], + sections: [ + { + ordinal: 0, + title: 'Getting started', + description: 'Your first experience setting up Northwind.', + }, + { + ordinal: 1, + title: 'Value & support', + description: 'Getting value and getting help.', + }, + ], + questions: [ + { + sectionOrdinal: 0, + key: 'setup_ease', + prompt: 'How easy was it to set up your account during onboarding?', + suggestedType: 'likert', + suggestedTypeConfig: { min: 1, max: 5, minLabel: 'Very difficult', maxLabel: 'Very easy' }, + extractionConfidence: 0.95, + }, + { + sectionOrdinal: 0, + key: 'setup_blockers', + prompt: 'What, if anything, slowed you down while getting started?', + guidelines: 'Encourage a specific example rather than a yes/no answer.', + suggestedType: 'free_text', + extractionConfidence: 0.9, + }, + { + sectionOrdinal: 0, + key: 'onboarding_channel', + prompt: 'How did you primarily complete onboarding?', + suggestedType: 'single_choice', + suggestedTypeConfig: { + choices: ['Self-serve docs', 'Guided call with a CSM', 'In-app walkthrough', 'A mix'], + }, + extractionConfidence: 0.92, + }, + { + sectionOrdinal: 1, + key: 'first_value_days', + prompt: 'Roughly how many days passed before you got real value from Northwind?', + suggestedType: 'numeric', + suggestedTypeConfig: { min: 0, max: 365, unit: 'days' }, + extractionConfidence: 0.85, + }, + { + sectionOrdinal: 1, + key: 'support_channels_used', + prompt: 'Which support channels did you use in your first 30 days?', + suggestedType: 'multi_choice', + suggestedTypeConfig: { choices: ['Email', 'Live chat', 'Phone', 'Help center', 'None'] }, + extractionConfidence: 0.9, + }, + { + sectionOrdinal: 1, + key: 'would_recommend', + prompt: 'Based on onboarding alone, would you recommend Northwind to a peer?', + rationale: 'Quick proxy for early-onboarding sentiment.', + suggestedType: 'boolean', + extractionConfidence: 0.88, + }, + ], +}; + +/** + * DEMO-ONLY (F9.4): seed a sample demo client + a launched, attributed questionnaire. + * + * No-ops unless `LOAD_DEMO_CONTENT=1` (the env gate lives inside `run()` so the runner + * still stamps SeedHistory on the no-op; to load demo content on an environment that + * previously no-op'd, set the env var AND clear the `app-questionnaire/025-demo-content` + * SeedHistory row, since the file content is unchanged — see the F9.2 runbook). + * + * Builds the whole graph in one transaction so a partial demo never exists: upsert the + * demo client → (replace any prior demo questionnaire) → questionnaire → version → + * section/slot graph via `writeGraph` → config row → flip the version to `launched`. + * The seed sets `status: 'launched'` directly (there is no pure launch helper — the + * gate lives in the status HTTP route), so it re-checks the gate's invariants itself: + * goal ✓, non-empty audience ✓, ≥1 section ✓, ≥1 question ✓, config row ✓. + */ +const unit: SeedUnit = { + name: 'app-questionnaire/025-demo-content', + async run({ logger }) { + if (process.env.LOAD_DEMO_CONTENT !== '1') { + logger.info('⏭ DEMO-ONLY: LOAD_DEMO_CONTENT!=1 — skipping demo content seed'); + return; + } + + logger.info('🎬 DEMO-ONLY: seeding demo client + launched questionnaire...'); + + // Fail fast before touching the DB if the hand-authored graph is incoherent. + assertPersistable(DEMO_EXTRACTION); + + await executeTransaction(async (tx) => { + // Demo client — upsert by unique slug (idempotent on every field). + const demoClient = await tx.appDemoClient.upsert({ + where: { slug: DEMO_CLIENT.slug }, + update: { + name: DEMO_CLIENT.name, + description: DEMO_CLIENT.description, + isActive: DEMO_CLIENT.isActive, + ctaColor: DEMO_CLIENT.ctaColor, + accentColor: DEMO_CLIENT.accentColor, + logoUrl: DEMO_CLIENT.logoUrl, + welcomeCopy: DEMO_CLIENT.welcomeCopy, + }, + create: { ...DEMO_CLIENT }, + select: { id: true }, + }); + + // Replace-on-rerun: drop any prior demo questionnaire (version/section/slot/ + // config/sessions/invitations cascade) so edits to the content above take hold + // without duplicating. + const existing = await tx.appQuestionnaire.findFirst({ + where: { title: DEMO_QUESTIONNAIRE_TITLE }, + select: { id: true }, + }); + if (existing) { + await tx.appQuestionnaire.delete({ where: { id: existing.id } }); + } + + // Questionnaire attributed to the demo client at create (the UI does this via + // a later PATCH; the seed sets it inline). Questionnaire-level status stays + // 'draft' — the launch lifecycle is version-scoped. + const questionnaire = await tx.appQuestionnaire.create({ + data: { + title: DEMO_QUESTIONNAIRE_TITLE, + status: 'draft', + demoClientId: demoClient.id, + }, + select: { id: true }, + }); + + // Version 1 — carries the launch-gate goal + audience (+ provenance). + const version = await tx.appQuestionnaireVersion.create({ + data: { + questionnaireId: questionnaire.id, + versionNumber: 1, + status: 'draft', + goal: DEMO_GOAL, + audience: DEMO_AUDIENCE, + goalProvenance: 'admin-supplied', + audienceProvenance: DEMO_AUDIENCE_PROVENANCE, + }, + select: { id: true }, + }); + + // Section + slot graph — same writer the ingestion route uses. + const counts = await writeGraph(tx, version.id, DEMO_EXTRACTION); + + // Config row — the launch gate requires the row to exist. Mirror the schema + // defaults, overriding only the demo-relevant knobs (profile capture on). + await tx.appQuestionnaireConfig.create({ + data: { + versionId: version.id, + selectionStrategy: DEFAULT_QUESTIONNAIRE_CONFIG.selectionStrategy, + minQuestionsAnswered: DEFAULT_QUESTIONNAIRE_CONFIG.minQuestionsAnswered, + coverageThreshold: DEFAULT_QUESTIONNAIRE_CONFIG.coverageThreshold, + costBudgetUsd: DEFAULT_QUESTIONNAIRE_CONFIG.costBudgetUsd, + maxQuestionsPerSession: DEFAULT_QUESTIONNAIRE_CONFIG.maxQuestionsPerSession, + voiceEnabled: DEFAULT_QUESTIONNAIRE_CONFIG.voiceEnabled, + contradictionMode: DEFAULT_QUESTIONNAIRE_CONFIG.contradictionMode, + contradictionWindowN: DEFAULT_QUESTIONNAIRE_CONFIG.contradictionWindowN, + contradictionEveryNTurns: DEFAULT_QUESTIONNAIRE_CONFIG.contradictionEveryNTurns, + anonymousMode: DEFAULT_QUESTIONNAIRE_CONFIG.anonymousMode, + answerSlotPanelScope: DEFAULT_QUESTIONNAIRE_CONFIG.answerSlotPanelScope, + profileFields: DEMO_PROFILE_FIELDS, + }, + }); + + // Launch gate satisfied (goal, audience, sections, questions, config) — flip the + // version to launched so the runbook can invite a respondent immediately. + await tx.appQuestionnaireVersion.update({ + where: { id: version.id }, + data: { status: 'launched' }, + }); + + logger.info( + `✅ DEMO-ONLY: seeded "${DEMO_QUESTIONNAIRE_TITLE}" — ${counts.sectionCount} sections, ` + + `${counts.questionCount} questions, launched and attributed to ${DEMO_CLIENT.slug}` + ); + }); + }, +}; + +export default unit;