Scrum4Me/docs/plans/M11-claude-questions.md
Janpeter Visser 7e45bbdbc0
docs: AI-optimized docs restructure (Phases 1–8) (#61)
* docs(dialog-pattern): add generic entity-dialog spec

Introduceert docs/patterns/dialog.md als bron-of-truth voor elke
create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende
dataobject. Bevat 14 secties: uitgangspunten, stack, component-
architectuur, layout, validatie, drielaagse demo-policy, submission,
dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit
profile-template, out-of-scope, en een verificatie-checklist.

Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel
zodat Claude (en mensen) de spec verplicht raadplegen voor elke
nieuwe dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(dialog-pattern): convert task spec + add pbi/story entity-profiles

Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle
gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document
bevat nu alleen Task-specifieke velden, URL-pattern, status-veld,
server actions, triggers en bewuste out-of-scope-keuzes.

Voegt twee nieuwe entity-profielen toe voor bestaande dialogen:
- docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij,
  PbiStatusSelect, geen delete in v1)
- docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met
  status/priority badges, inline activity-log, demo-readonly-fallback,
  inline-delete-confirm i.p.v. AlertDialog)

Beide profielen documenteren expliciet de "Bekende gaps t.o.v.
generieke spec" zodat opvolgende PR's de afwijkingen kunnen
rechtzetten of bewust kunnen accorderen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added pdevelopment docs

* docs(plans): add docs-restructure plan for AI-optimized lookup

Audit of existing 39 doc files (~10.700 lines) and a phased restructure
proposal aimed at minimising the tokens an AI agent has to read to find
the right reference. Captures resolved decisions on language (English),
ADR template (Nygard default with MADR escape-hatch), index generator
(node script), and folder taxonomy. Proposal status — fase 1 to follow.

* docs(adr): add ADR scaffolding (templates, README, meta-ADR)

Set up docs/adr/ as the canonical home for architecture decisions:

- templates/nygard.md — default four-section format (Status, Context,
  Decision, Consequences) for one-way-door decisions.
- templates/madr.md — MADR v4 with YAML front-matter and explicit
  Considered Options for decisions where rejected alternatives matter.
- README.md — naming convention (NNNN-kebab-case), template-selection
  guidance (Nygard default; MADR for auth, queue mechanics, agent
  integration), status lifecycle, and ADR roster.
- 0000-record-architecture-decisions.md — meta-ADR establishing the
  practice itself, in Nygard format.

Backfilling existing implicit decisions (base-ui-over-radix, float
sort_order, demo-user three-layer policy, etc.) is fase 6 of the
docs-restructure plan.

* feat(docs): add docs index generator + initial INDEX.md

scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML
front-matter (or first H1 fallback) and a Nygard-style ## Status
section, then writes docs/INDEX.md with grouped tables for ADRs,
Specs, Plans (with archive subsection), Patterns, and Other.

Pure Node 20 (no external deps); idempotent — running it twice
produces byte-identical output. Excludes adr/templates/, the ADR
README, INDEX.md itself, and any *_*.md sidecar file.

Wire-up:
- package.json: docs:index → node scripts/generate-docs-index.mjs

Initial run indexed 35 docs across the existing structure; the
generated INDEX.md is committed so the table is reviewable in the
PR before hooking generation into a pre-commit step.

* chore: ignore Obsidian vault and personal sidecar files

Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar
notes) to .gitignore so the docs/ tree can serve as canonical source
of truth while still being usable as an Obsidian vault for personal
authoring. The docs index generator already excludes the same _*.md
pattern from INDEX.md.

* docs(plans): add PBI bulk-create spec for docs-restructure

Machine-parseable spec for an executor that calls the scrum4me MCP
(create_pbi → create_story → create_task) to seed the docs-restructure
work into the DB.

- Section 1 (Context) is the PBI description; serves as task-context
  via mcp__scrum4me__get_claude_context.
- Section 2 lists the 6 resolved decisions (English, MD3+styling
  merged, solo-paneel merged, .Plans archived, Nygard ADR default,
  node index script).
- Section 3 records what already shipped on this branch so the
  executor doesn't duplicate the ADR scaffolding or index generator.
- Section 4 carries the structured YAML graph: 1 PBI, 8 stories
  (one per phase), 39 tasks. product_id is REPLACE_ME — fill before
  running.
- YAML validated with PyYAML; field schema sanity-checked.

* docs(junk-cleanup): remove stub patterns/test.md

* docs(junk-cleanup): archive .Plans/ to docs/plans/archive/

* docs(front-matter): add YAML front-matter to docs/ root

* docs(front-matter): add YAML front-matter to patterns/

* docs(front-matter): add YAML front-matter to plans + agent files

* docs(index): regenerate INDEX.md after front-matter pass

* docs(naming): drop scrum4me- prefix from doc filenames

* docs(naming): lowercase API.md and MD3 filenames

* docs(naming): rename plan file to kebab-case ASCII

* docs(naming): rename middleware.md to proxy.md (next 16)

* docs(naming): polish CLAUDE.md doc-index after renames

* docs(taxonomy): scaffold topical folders under docs/

* docs(taxonomy): move spec files into docs/specs/

* docs(taxonomy): move design/api/qa/backlog/assets into folders

* docs(taxonomy): move agent-instruction-audit into decisions/

* docs(split): break architecture.md into 6 topical files

* docs(split): merge solo-paneel-spec into specs/functional.md

* docs(split): merge md3-color-scheme into design/styling

* docs(trim): extract branch/commit rules into runbook

* docs(trim): extract MCP integration into runbook

* docs(adr): add 0001-base-ui-over-radix

* docs(adr): add 0002-float-sort-order

* docs(adr): add 0003-one-branch-per-milestone

* docs(adr): add 0004-status-enum-mapping

* docs(adr): add 0005-iron-session-over-nextauth

* docs(adr): add 0006-demo-user-three-layer-policy

* docs(adr): add 0007-claude-question-channel-design

* docs(adr): add 0008-agent-instructions-in-claude-md + update README index

* docs(index): regenerate after ADR 0001-0008

* docs(glossary): add docs/glossary.md

* chore(docs): regenerate INDEX.md in pre-commit hook

* docs(readme): link INDEX + glossary + agent instructions

* feat(docs): add doc-link checker script

* chore(docs): wire docs:check-links and docs npm scripts

* ci(docs): block merge on broken doc links

* docs(links): fix broken cross-references after restructure

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:21:59 +02:00

475 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "M11 — Claude vraagt, gebruiker antwoordt"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M11]
---
# M11 — Claude vraagt, gebruiker antwoordt
Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Claude schrijft een vraag naar `claude_questions` als hij vastloopt op een keuze; een Postgres-trigger emit op het bestaande `scrum4me_changes`-kanaal; de app toont een notificatie-badge; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling of in latere sessie via `get_question_answer`) en gaat door.
Eerste concrete uitwerking van strategische **richting B** (verdiepen van de unieke AI-driven dev-flow).
Backlog-entries: zie [backlog.md § M11](../backlog/index.md#m11-claude-vraagt-gebruiker-antwoordt) (op te leveren in ST-1108).
**Beveiligingsuitgangspunten:**
- Atomic answer via `updateMany WHERE status='open'` — concurrent dubbele submit kan niet
- Demo-blok op `ask_user_question` (MCP) en `answerQuestion` (Server Action)
- Access-check via `productAccessFilter` in DB-query én SSE-filter; vraag-tekst en antwoord komen pas via een aparte authenticated query
- Cron-endpoint beveiligd via `Authorization: Bearer ${CRON_SECRET}`
- Logging: alleen `question_id`, nooit vraag/antwoord-tekst (kan gevoelige info bevatten)
**Gekozen kaders (uit overleg):**
- **Sync-model**: default async — `ask_user_question` retourneert direct met `question_id`; optionele `wait_seconds` (max 600) voor polling tot het antwoord er is
- **Answer-policy**: iedereen met product-toegang mag antwoorden; story-assignee krijgt visuele *"wacht op jou"*-emphase
- **Realtime**: hergebruik `scrum4me_changes`-kanaal (uitgebreid met `entity: 'question'`); aparte user-scoped SSE-route `/api/realtime/notifications` zodat solo-board-SSE product-scoped blijft
---
## ST-1101 — `ClaudeQuestion` schema + Postgres-trigger
**Bestanden**
- `prisma/schema.prisma` — model `ClaudeQuestion` + relations op `User`/`Story`/`Task`/`Product`
- `prisma/migrations/<ts>_add_claude_questions/migration.sql` — table-DDL + trigger
- `vendor/scrum4me`-submodule in `mcp` — schema-sync ná merge
**Stappen**
1. Schema-uitbreiding:
```prisma
model ClaudeQuestion {
id String @id @default(cuid())
story_id String
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
task_id String?
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
product_id String // gedenormaliseerd voor SSE-filter
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
asked_by String
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
question String @db.Text
options Json? // string[] voor multi-choice; null voor free-text
status String // 'open' | 'answered' | 'cancelled' | 'expired'
answer String? @db.Text
answered_by String?
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
answered_at DateTime?
created_at DateTime @default(now())
expires_at DateTime
@@index([story_id, status])
@@index([product_id, status])
@@index([status, expires_at])
@@map("claude_questions")
}
```
Plus op `User`: `asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")` en `answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")`.
2. Migratie-SQL voegt naast tabel + indexes ook trigger toe (mirror van `notify_pairing_change` uit M10 ST-1001):
```sql
CREATE OR REPLACE FUNCTION notify_question_change() RETURNS trigger AS $$
DECLARE
story_row record;
payload jsonb;
BEGIN
SELECT assignee_id INTO story_row FROM stories WHERE id = NEW.story_id;
payload := jsonb_build_object(
'op', CASE TG_OP WHEN 'INSERT' THEN 'I' ELSE 'U' END,
'entity', 'question',
'id', NEW.id,
'product_id', NEW.product_id,
'story_id', NEW.story_id,
'task_id', NEW.task_id,
'assignee_id', story_row.assignee_id,
'status', NEW.status
);
PERFORM pg_notify('scrum4me_changes', payload::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER claude_questions_notify
AFTER INSERT OR UPDATE ON claude_questions
FOR EACH ROW EXECUTE FUNCTION notify_question_change();
```
3. `npx prisma migrate dev --name add_claude_questions`
**Aandachtspunten**
- `entity: 'question'` is een nieuwe waarde naast bestaande `'task'`/`'story'`. Solo-route in M8 filter't via `payload.entity` — moet `'question'`-events expliciet wegfilteren (niet emitten naar solo-clients)
- `product_id` op de question is gedenormaliseerd uit `story.product_id` — voorkomt extra join in SSE-filter (zelfde keuze als `story.product_id` in M3)
- `vendor/scrum4me`-submodule sync vereist na merge (drift-check `trig_015FFUnxjz9WMuhhWNGBQKFD`)
**Verificatie**
- `npx prisma migrate dev` slaagt; `npx prisma validate` clean
- `psql $DIRECT_URL -c "LISTEN scrum4me_changes;"` toont payload met `entity: 'question'` bij INSERT
- Bestaande solo-flow nog steeds werkend (regressie-check)
---
## ST-1102 — MCP-tools (in `mcp`-repo)
**Bestanden**
- `mcp/src/tools/ask-user-question.ts` — nieuw
- `mcp/src/tools/get-question-answer.ts` — nieuw
- `mcp/src/tools/list-open-questions.ts` — nieuw
- `mcp/src/tools/cancel-question.ts` — nieuw
- `mcp/src/index.ts` — register de vier tools
- `mcp/scripts/smoke-test.ts` — uitbreiden met question-roundtrip
- `mcp/README.md` — tool-tabel uitbreiden
**Stappen**
1. **`ask_user_question`** (write-tool, sjabloon `create-todo.ts`):
- Input: `{ story_id, question, options?, task_id?, wait_seconds? }` — `wait_seconds` 0600 (Zod `.min(0).max(600)`)
- `requireWriteAccess` (demo-blok)
- Access-check: `userCanAccessProduct(story.product_id, auth.userId)`
- Insert met `expires_at = now() + 24h`, `status = 'open'`
- Als `wait_seconds === 0` (default): return `{ question_id, status: 'open' }`
- Als `wait_seconds > 0`: poll elke 2s tot `status !== 'open'` of timeout. Bij answered: return `{ question_id, status, answer, answered_by, answered_at }`. Bij timeout: return `{ question_id, status: 'pending' }` zodat Claude met `get_question_answer` later kan ophalen
- Polling-implementatie: `setInterval` met `Promise` en abort-signal voor schone cleanup
2. **`get_question_answer`** (read-tool):
- Input: `{ question_id }`
- Access-check via `userCanAccessProduct(question.product_id, auth.userId)`
- Output: full row (`status`, `answer`, `answered_by`, `answered_at`, `expires_at`)
3. **`list_open_questions`** (read-tool):
- Input: `{ story_id? }` (optionele filter)
- Output: array van eigen vragen (`asked_by === auth.userId`) met status open of answered, max 50, geordend op `created_at desc`
- Bedoeld voor Claude om bij begin van een sessie te zien of eerdere vragen inmiddels beantwoord zijn
4. **`cancel_question`** (write-tool):
- Input: `{ question_id }`
- Alleen de asker mag cancelen; `requireWriteAccess` voor demo-blok
- Atomic `updateMany WHERE id=… AND status='open' AND asked_by=…`
- Bedoeld voor wanneer Claude zelf de oplossing vindt en de vraag overbodig wordt
5. Smoke-test in `scripts/smoke-test.ts`: `ask_user_question` met `wait_seconds=5` + parallel `answerQuestion` (via REST of direct DB-write) → verifieer dat de tool het antwoord retourneert binnen het venster
**Aandachtspunten**
- `wait_seconds` polling moet aborten als de MCP-cliënt disconnect (signal-check) — anders blijft de Node-process hangen op een dood socket
- `options`-veld accepteert string-array; in zod als `z.array(z.string()).optional()`
- Als `wait_seconds` > 300 raakt 'm Vercel-deploy onmogelijk (Vercel-functies cap op 300s) — maar de MCP-server draait *lokaal* bij Claude Code, dus 600s mag
**Verificatie**
- MCP Inspector toont 4 nieuwe tools (totaal 13)
- Smoke-test groen: ask + answer roundtrip binnen 5s
- Demo-token op `ask_user_question` of `cancel_question` geeft `PERMISSION_DENIED`
- `tsc --noEmit` clean op `mcp`
---
## ST-1103 — Server Actions voor de browser-UI
**Bestanden**
- `actions/questions.ts` — nieuw
- `__tests__/actions/questions.test.ts` — nieuw
**Stappen**
1. **`answerQuestion(questionId, answer)`** (volgt `docs/patterns/server-action.md`):
- `getSession` + `requireUser`; demo-blok via `if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' }`
- Zod-input: `{ questionId: cuid, answer: string.min(1).max(4000) }`
- Lookup question + access-check via `userCanAccessProduct(question.product_id, userId)`
- Atomic `updateMany WHERE id=… AND status='open' AND expires_at > now()` met `data: { status: 'answered', answer, answered_by: userId, answered_at: now }`
- Bij `count === 0`: disambigueer (al-answered → 'al beantwoord', expired → 'verlopen', geen access → 'geen toegang')
- `revalidatePath('/', 'layout')` zodat badge-count overal updatet
2. **`cancelQuestionByAnswerer(questionId)`** — *uitgesteld naar v2*. Voor v1 alleen Claude (asker) kan annuleren via MCP. Als de UI later een dismiss-functie krijgt, komt het hier.
3. **Tests** `__tests__/actions/questions.test.ts` (6 cases):
- happy answer → status='answered', `revalidatePath` aangeroepen
- demo-user → error + geen DB-write
- user zonder product-access → error
- already-answered → race-error (`updateMany count=0` met status='answered' fallback)
- expired → error
- empty answer → Zod-validatie
**Aandachtspunten**
- `revalidatePath('/', 'layout')` is correct (zelfde keuze als M9 `setActiveProductAction`) — badge zit in app-layout
- Geen `revalidatePath` op `/sprint` of `/solo` nodig — die zien de question niet
- Bij multi-tab: na answer in tab-1 verdwijnt het item in tab-2 via SSE-event, niet via revalidate. revalidate is voor de SSR-render-na-navigatie
**Verificatie**
- `npm test` 6/6 voor questions
- Handmatig: open vraag in browser, antwoord, badge-count zakt met 1
- Demo-test: log in als demo, klik antwoord → toast "Niet beschikbaar in demo-modus"
---
## ST-1104 — User-scoped SSE-route `/api/realtime/notifications`
**Bestanden**
- `app/api/realtime/notifications/route.ts` — nieuw
- `app/api/realtime/solo/route.ts` — uitbreiden om `entity: 'question'` te filteren (anders krijgt solo-client question-events ongewenst door)
- `__tests__/api/notifications-stream.test.ts` — nieuw (auth-cases)
**Stappen**
1. Route Handler — sjabloon uit `app/api/realtime/solo/route.ts`:
- `runtime: 'nodejs'`, `maxDuration: 300`, `dynamic: 'force-dynamic'`
- Auth via iron-session cookie; 401 zonder
- **User-scoped** (geen `?product_id=`-param). Bij connect: query `productAccessFilter(userId)` om alle accessible product-IDs te krijgen
- LISTEN op `scrum4me_changes`; filter:
- `payload.entity === 'question'` (anders skip)
- `payload.product_id IN accessibleProductIds`
- Initial-state-event direct na connect, **na LISTEN actief**: query `claude_questions` met `status='open'` voor deze user's accessible products. Stuur als `event: state\ndata: [{...question-summary...}]`. Voorkomt race tussen connect en LISTEN (zelfde fix als M10 ST-1004)
- Auto-close bij hard-close 240s; client herconnect
2. Solo-route bijwerken: in `shouldEmit` toevoegen `if (payload.entity === 'question') return false`
3. Tests (auth-paden, full-stream blijft handmatig):
- 401 zonder iron-session cookie
- Bij connect met sessie: list van accessible products correct gefilterd
- Question-event op een product zonder access → niet doorgegeven
**Aandachtspunten**
- Twee parallelle SSE-streams in browser (solo-route op product-pagina + notifications-route in app-layout) — netwerk-overhead aanvaardbaar; Vercel rekent per-actieve-functie ongeacht aantal streams
- Initial-state event content: een kleine summary (id, story_code, question, options?) per open vraag — voorkomt dat de bridge eerst een aparte fetch moet doen voor de initial badge-count
- Path expliciet maken in een client `useNotificationsRealtime`-hook (volgt `useSoloRealtime`-pattern)
**Verificatie**
- `curl -N --cookie session-jar /api/realtime/notifications` blijft openstaan, levert `event: state` direct
- INSERT op `claude_questions` voor een toegankelijk product → event binnen 1s
- INSERT voor een ontoegankelijk product → geen event
- Solo-route op `/api/realtime/solo?product_id=…` levert geen question-events meer
---
## ST-1105 — Notifications-UI (Bell + Sheet + Answer-modal)
**Bestanden**
- `components/shared/notifications-bell.tsx` — nieuw
- `components/notifications/notifications-sheet.tsx` — nieuw
- `components/notifications/answer-modal.tsx` — nieuw
- `components/notifications/notifications-bridge.tsx` — nieuw, hookt SSE-listener aan store
- `stores/notifications-store.ts` — nieuw
- `lib/realtime/use-notifications-realtime.ts` — nieuw
- `components/shared/nav-bar.tsx` — `<NotificationsBell />` toevoegen rechts (links van `<UserMenu>`)
- `app/(app)/layout.tsx` — `<NotificationsBridge />` mounten (analoog aan `<SoloRealtimeBridge />`)
**Stappen**
1. **`stores/notifications-store.ts`** — Zustand store; volgt `stores/solo-store.ts`-pattern:
- State: `{ questions: Question[], pendingAnswerIds: Set<string> }`
- Actions: `init(q[])`, `add(q)`, `update(q)`, `remove(id)`, `optimisticAnswer(id)`, `rollbackAnswer(id, q)`
- Selectors: `openCount(userId)`, `forYouCount(userId)` (waar story-assignee = userId)
2. **`lib/realtime/use-notifications-realtime.ts`** — analoog aan `useSoloRealtime`. EventSource opent op `/api/realtime/notifications`, dispatcht `state`/`message`-events naar store via `add`/`update`/`remove`. Reconnect met exponential backoff.
3. **`<NotificationsBridge />`** — Server Component die initial questions ophaalt en aan de store geeft via `init`-prop. Mount in `(app)/layout.tsx` zodat de bridge altijd actief is wanneer user is ingelogd.
4. **`<NotificationsBell />`** — Client Component:
- Lucide `Bell`-icon met badge: `openCount` (totaal) + accent-dot als `forYouCount > 0`
- Klik: `setOpen(true)` op de Sheet
- Geen badge als count === 0
5. **`<NotificationsSheet />`** — shadcn `Sheet` van rechts:
- Header: "Vragen van Claude (N)"
- Lijst gegroepeerd op product (analoog aan M5 todo-data-table-styling), elk item: story-code + truncated title, vraag-preview (line-clamp-2), assignee-emphase als forYou, "Beantwoord" knop opent answer-modal
- Lege staat: "Geen openstaande vragen. Lekker bezig!"
6. **`<AnswerModal />`** — shadcn `Dialog`:
- Story-context-link bovenaan (kleine kaart)
- Volledige vraag-tekst
- Als `options`: `<RadioGroup>` met opties; geen vrije tekst
- Anders: `<Textarea>` (max 4000 chars, char-counter)
- "Verstuur" + "Annuleer" knoppen; submit roept `answerQuestion`-action via `useTransition`
- Demo-modus: knop disabled met tooltip
7. NavBar-edit: `<NotificationsBell />` rechts naast de huidige avatar-trigger. Nieuwe gap-spacing in NavBar's right-section.
**Aandachtspunten**
- Bell-icon en avatar moeten visueel balanceren — hoogte/padding gelijktrekken
- MD3-tokens uit `docs/design/styling.md`: badge `bg-error text-error-foreground` voor critical-count, `bg-primary` voor neutraal. Geen willekeurige Tailwind-kleuren
- Optimistic-answer in store: voor het Server Action-resultaat zet item op pending; bij error rollback met sonner-error-toast
- Sheet-content blijft open zodat de user meerdere vragen achter elkaar kan beantwoorden (zelfde patroon als ST-358 openstaande-stories-sheet)
- ARIA: bell-icon heeft `aria-label="Notificaties — N open vragen"`, badge `role="status"`
**Verificatie**
- Bell verschijnt in NavBar links van avatar; badge count = open question count
- Klik opent Sheet; lijst rendert correct met assignee-emphase
- Submit schiet event door — in tweede tab van zelfde user verdwijnt item binnen 1-2s
- Demo-modus: Sheet rendert, Modal opent, "Verstuur" disabled
- E2E-flow: Claude `ask_user_question` → bell-badge wordt 1 → klik → modal → submit → badge wordt 0 → Claude's `get_question_answer` levert antwoord
---
## ST-1106 — Demo-policy + access-rules + tests
**Bestanden**
- `__tests__/actions/questions.test.ts` — uitbreiden met access-cases (al opgezet in ST-1103)
- `__tests__/api/notifications-stream.test.ts` — access-cases
- Documentatie-aanpassingen in `actions/questions.ts` en SSE-route met expliciete demo-blok-comment
**Stappen**
1. Verifieer dat `requireProductWriter` alle Server-Action-mutaties al dekt (zou moeten — uit M3.5)
2. Voeg expliciete demo-test toe: demo-user opent answer-modal → Verstuur disabled met tooltip
3. Voeg access-test toe: user-A heeft geen access tot product van user-B → user-A's notification-stream krijgt geen events voor user-B's questions
**Aandachtspunten**
- Story-assignee-emphase is **alleen visueel** — toegang is product-membership-breed. Dit is bewust: als de assignee niet beschikbaar is moet een andere member kunnen invallen
- Demo kan een vraag wel **lezen** (transparantie over hoe de feature werkt) — alleen niet beantwoorden
**Verificatie**
- 6+ tests groen (al gedekt in ST-1103/1104)
- Handmatige cross-product-test met 2 users + 2 producten
---
## ST-1107 — Auto-expire + Vercel cron-cleanup
**Bestanden**
- `app/api/cron/expire-questions/route.ts` — nieuw
- `vercel.ts` — `crons`-entry toevoegen
- `lib/env.ts` — `CRON_SECRET` toevoegen aan Zod-schema
- `.env.example` — `CRON_SECRET` documenteren
**Stappen**
1. **Cron-handler**:
```ts
export const runtime = 'nodejs'
export async function POST(request: Request) {
const auth = request.headers.get('authorization')
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const result = await prisma.claudeQuestion.updateMany({
where: { status: 'open', expires_at: { lt: new Date() } },
data: { status: 'expired' },
})
// Optioneel: ook M10 login_pairings cleanup hier (eerder geparkeerd)
return Response.json({ expired: result.count })
}
```
2. **`vercel.ts`**:
```ts
export const config: VercelConfig = {
// ... bestaande config
crons: [{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }],
}
```
3. Documenteer in `.env.example`: `CRON_SECRET=<openssl rand -base64 32>`
**Aandachtspunten**
- Vercel cron POST't standaard zonder body; auth is alleen via header
- Op Hobby-plan zijn crons beperkt — check budget. M11 cron is 4x/dag, prima
- Cron-trigger update't `claude_questions` → trigger fired → SSE-events → UI updates badge real-time. Geen extra plumbing nodig
**Verificatie**
- Handmatig: `curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" /api/cron/expire-questions` met een vraag waar `expires_at < now` → response `{expired: 1}`, vraag verdwijnt uit notifications
- Onbevoegde call zonder secret → 401
- Vercel dashboard toont cron-config na deploy
---
## ST-1108 — Documentatie + acceptatietest
**Bestanden**
- `docs/api/rest-contract.md` — secties "SSE — Notifications" + "Cron — Expire questions"
- `docs/architecture.md` — sectie "Vraag-antwoord-kanaal" met sequence-diagram
- `docs/patterns/claude-question-channel.md` — herbruikbaar pattern-doc
- `docs/backlog/index.md` — M11-tabel-rij + M11-sectie
- `prisma/seed-data/parse-backlog.ts` — `M11: 'ACTIVE'`, `M10: 'COMPLETED'`, `M3.5: 'COMPLETED'`
- `CLAUDE.md` — pattern-doc verwijzing in Implementatiepatronen-tabel
**Stappen**
1. Backlog-tabel-rij + M11-sectie in `docs/backlog/index.md` (mirror M10-format met **Implementatieplan:** verwijzing naar dit doc)
2. `docs/architecture.md` § "Vraag-antwoord-kanaal":
- Mermaid sequence-diagram: Claude → MCP → DB → trigger → SSE → user → Server Action → DB → trigger → polling-tool
- Threat-model-tabel (replay, demo-block, access-leak, expiry, race)
- "Waarom hergebruik scrum4me_changes-kanaal" sub-sectie
3. `docs/patterns/claude-question-channel.md` — generiek pattern voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users
4. Parser-flip: M11 wordt nieuwe ACTIVE-milestone, M10 → COMPLETED. (Zelfde patroon als bij M10-start: chore-commit met vlag-flip + re-seed.)
5. **Acceptatie-scenario's** (zes, deels door unit-tests gedekt):
1. **Sync happy path**: Claude `ask_user_question(wait_seconds=300)` → user antwoordt binnen 30s → MCP-tool retourneert het antwoord ✅
2. **Async happy path**: `ask_user_question(wait_seconds=0)` → tool returnt direct → user antwoordt later → Claude `get_question_answer` → ziet antwoord ✅
3. **Demo-block**: demo-user opent vraag → kan inhoud lezen → "Verstuur" disabled (UI + Server Action ✅)
4. **Access-isolation**: vraag op product zonder access → onzichtbaar in andere user's notifications-bell (SSE-filter ✅)
5. **Expiry**: vraag met `expires_at < now` → na cron-run niet meer in badge-count ✅
6. **Race**: concurrent answer-poging op al-beantwoorde vraag → schone foutmelding (atomic `updateMany count=0` ✅)
**Aandachtspunten**
- Acceptatie-scenario's 1-2 zijn handmatig (full Claude+browser cyclus); 3-6 worden in unit-tests vastgelegd
- Pattern-doc moet ook beschrijven wanneer NIET te gebruiken (bv. wanneer een gewone API-call met sessie volstaat)
**Verificatie**
- Alle docs gepubliceerd in repo
- Backlog-parser-self-test: `npx tsx prisma/seed-data/parse-backlog.ts` toont M11 met `priority=4 sprint=ACTIVE`
- 6/6 acceptatie-scenario's groen
- `npm run lint && npx tsc --noEmit && npm test && npm run build` clean
- `vendor/scrum4me`-submodule sync in mcp na merge
---
## Branch- en commit-strategie
Per [Branch & PR Strategy](../runbooks/branch-and-commit.md):
- **Eén branch op Scrum4Me**: `feat/M11-claude-questions` afgesplitst van `main` ná M10-merge
- **Aparte branch op mcp**: `feat/M11-question-tools`
- Commits chronologisch per stap met ST-code in titel:
```
chore(M11): swap demo-active sprint from M10 to M11
feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
feat(ST-1102): add 4 MCP question tools (in mcp)
feat(ST-1103): add answerQuestion server action
feat(ST-1104): add /api/realtime/notifications user-scoped SSE
feat(ST-1104): filter entity='question' from solo-realtime stream
feat(ST-1105): add Zustand notifications-store + realtime hook
feat(ST-1105): add NotificationsBridge in app layout
feat(ST-1105): add NotificationsBell + Sheet + AnswerModal
chore(ST-1107): add CRON_SECRET to env schema
feat(ST-1107): add /api/cron/expire-questions handler
feat(ST-1107): wire vercel.ts cron entry
docs(ST-1108): document notifications SSE + cron in api.md
docs(ST-1108): add vraag-antwoord-kanaal flow to architecture
docs(ST-1108): add claude-question-channel pattern doc
chore(ST-1108): backlog M11 + parser ACTIVE-flip
```
**Push + PR pas na handmatige acceptatie** van scenario 1 (sync happy path) + 3 (demo-block) op localhost.
**MCP-PR pas mergen ná Scrum4Me-PR** + submodule-sync — anders wijzen MCP-tools naar een schema-tabel die op main nog niet bestaat.
---
## Reseed-stap (eenmalig vóór ST-1101-implementatie)
Backlog-markdown moet eerst de M11-stories bevatten en de parser moet M11 als ACTIVE-milestone kennen voordat `mcp__scrum4me__get_claude_context` ze als next-story kan teruggeven. Workflow:
1. Doe ST-1108 backlog-edit + parser-flip eerst (commit `chore(M11): swap demo-active sprint from M10 to M11` + de backlog-uitbreiding)
2. `npm run seed` — re-seed met M11=ACTIVE
3. `mcp__scrum4me__get_claude_context` levert nu ST-1101 als next-story
4. Verder met ST-1101-implementatie
> **Let op:** seed wist user-data. Doe dit op een dev-DB.
---
## Buiten scope (volgende milestones)
- **AI-suggested antwoorden** Claude leest de codebase en stelt 3 mogelijke antwoorden voor; user kiest. Vereist tweede LLM-call per vraag.
- **Mobile-push notifications** bouwt op M10 paired-flow + service-worker. v3.
- **Question-templates** "ambiguous-naming"-vraag, "missing-test-case"-vraag etc. voor consistentie.
- **Threading** vervolgvraag op een antwoord. v1 is single-shot Q&A.
- **File-uploads als antwoord** bv. een screenshot.
- **Stats/dashboard** gemiddelde antwoord-tijd, meest-gestelde-vraagsoorten.
- **Dismiss-per-user** een member negeert een vraag voor zichzelf zonder 'm te beantwoorden.