feat(cortex): no-show casus — intent flow, brief-rescript en document artifact

Cortex handelt een no-show af vanuit één chatcommando: afspraak
annuleren (declarabiliteits-nudge), en de openstaande concept-
huisartsbrief wordt via LLM herschreven en ter review aangeboden
in een document artifact (human-in-the-loop, PATCH dispatch zet
status op verzendklaar).

- API-routes: context, rescript, cancel, dispatch
- NoShowDocumentBlock: review/edit UI met origineel-vergelijk
- Mock-data voor concept huisartsbrief
- PRD, FO, bouwplan en epics in docs/intent/noshow-case/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2026-07-09 23:14:55 +02:00
parent d08db12765
commit 924988dd15
14 changed files with 3022 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
# NS.E1 — Intent Foundation
**Casus:** Cortex No Show Afhandeling
**Epic doel:** `register_no_show` herkenbaar maken door het volledige classificatiesysteem — types, reflex patterns, en orchestrator prompt.
**Geschatte tijd:** ~1.5 uur
**Afhankelijkheden:** Geen (dit is de basis voor alle andere epics)
---
## Context & waarschuwingen
### Dubbele BlockType definitie — kritiek
Er zijn **twee** `BlockType` definities in de codebase die allebei bijgewerkt moeten worden:
| Bestand | Definitie |
|---|---|
| `lib/cortex/types.ts` | `Exclude<CortexIntent, 'unknown' \| 'intake_navigeer'> \| 'patient-dashboard'` |
| `stores/cortex-store.ts` | `Exclude<CortexIntent, 'unknown'> \| 'fallback' \| 'patient-dashboard'` |
De store-versie is breder (includeert `intake_navigeer` en `fallback`). Door `register_no_show` toe te voegen aan `CortexIntent` wordt het automatisch onderdeel van beide `BlockType` afgeleidingen — maar alleen als je ook `BLOCK_CONFIGS` bijwerkt.
### `INTENT_PATTERNS` is strikt getypeerd
`reflex-classifier.ts` regel 27:
```typescript
const INTENT_PATTERNS: Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]> = { ... }
```
Dit betekent: als je `register_no_show` toevoegt aan `CortexIntent`, **eist TypeScript** dat je ook een entry toevoegt aan `INTENT_PATTERNS`. De build faalt anders. Dit is bewust — het voorkomt vergeten patronen.
---
## NS.E1.S1 — CortexIntent type + BLOCK_CONFIGS uitbreiden
**Bestand:** `lib/cortex/types.ts`
### Wijziging 1 — CortexIntent union
Voeg `'register_no_show'` toe na de intake-intents:
```typescript
export type CortexIntent =
| 'dagnotitie'
| 'zoeken'
| 'overdracht'
| 'agenda_query'
| 'create_appointment'
| 'cancel_appointment'
| 'reschedule_appointment'
| 'intake_status'
| 'intake_navigeer'
| 'risico_query'
| 'diagnose_query'
// No Show casus
| 'register_no_show'
| 'unknown';
```
### Wijziging 2 — BLOCK_CONFIGS
Voeg een entry toe in `BLOCK_CONFIGS` (het is een `Record<BlockType, BlockConfig>` — alle BlockTypes moeten erin):
```typescript
register_no_show: {
type: 'register_no_show',
title: 'No Show Registratie',
size: 'md',
icon: 'UserX',
},
```
Locatie: onderaan de `BLOCK_CONFIGS` definitie, na `diagnose_query`.
### Done criteria
- `pnpm build` slaagt — geen TypeScript errors
- `register_no_show` is een geldige waarde voor `CortexIntent`
- `BLOCK_CONFIGS['register_no_show']` bestaat
---
## NS.E1.S2 — Reflex Classifier patronen toevoegen
**Bestand:** `lib/cortex/reflex-classifier.ts`
### Context
`INTENT_PATTERNS` is getypeerd als `Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]>`. Na toevoeging van `register_no_show` aan de union **moet** je hier ook een entry toevoegen — anders faalt de build met:
```
Type '{ dagnotitie: ...; ... }' is missing the following properties
from type 'Record<...>': register_no_show
```
### Wijziging — patronen toevoegen
Voeg toe aan `INTENT_PATTERNS`, na het `diagnose_query` blok:
```typescript
// =========================================================================
// No Show casus
// =========================================================================
register_no_show: [
// Exacte no-show varianten
{ pattern: /no.?show/i, weight: 1.0 },
{ pattern: /no show/i, weight: 1.0 },
// "Niet verschenen" varianten
{ pattern: /niet\s+verschenen/i, weight: 0.95 },
{ pattern: /niet\s+gekomen/i, weight: 0.95 },
{ pattern: /niet\s+op\s+komen\s+dagen/i, weight: 0.95 },
// "Afwezig" + context
{ pattern: /afwezig\s+(bij|voor)\s+(de\s+)?afspraak/i, weight: 0.9 },
{ pattern: /pati[eë]nt\s+afwezig/i, weight: 0.85 },
// Werkwoordvormen
{ pattern: /komt?\s+niet\s+(op|naar)/i, weight: 0.85 },
{ pattern: /is\s+er\s+niet\s+(geweest)?/i, weight: 0.7 },
],
```
**Toelichting weights:**
- `1.0` — directe "no show" termen, geen ambiguïteit
- `0.95` — sterke klinische uitdrukkingen
- `0.850.9` — contextuele varianten
- `0.7` — vaag ("is er niet geweest") — escaleer naar AI bij twijfel
### Escalatie check
Controleer of de bestaande escalatiepatronen niet per ongeluk triggeren op no-show input:
- `"patiënt niet verschenen"` — bevat geen conjuncties → geen multi_intent
- `"patiënt is er niet"` — bevat geen voornaamwoorden die escaleren → OK
- `"patiënt niet verschenen morgen"` — bevat `morgen` → triggert `relative_time` escalatie → correct (AI lost dit op)
### Done criteria
- `classifyWithReflex("patiënt is niet verschenen")` geeft `{ intent: 'register_no_show', confidence: 0.95, shouldEscalateToAI: false }`
- `classifyWithReflex("no show vandaag")` geeft `{ intent: 'register_no_show', confidence: 1.0, shouldEscalateToAI: false }`
- `classifyWithReflex("patiënt niet verschenen morgen")` geeft `shouldEscalateToAI: true, escalationReason: 'relative_time'`
- `pnpm build` slaagt — TypeScript tevreden met de nieuwe entry
---
## NS.E1.S3 — Orchestrator system prompt uitbreiden
**Bestand:** `app/api/cortex/chat/route.ts`
### Context
De chat API bouwt een system prompt via `buildSystemPrompt(context)`. Die prompt bevat een opsomming van alle intents met beschrijvingen. De Orchestrator (Layer 2) én de chat AI (Layer 3) gebruiken allebei deze prompt.
Zoek de sectie in de prompt waar intents worden opgesomd — het zal er zo uitzien:
```
- dagnotitie: Gebruik wanneer...
- zoeken: Gebruik wanneer...
```
### Wijziging — intent beschrijving toevoegen
Voeg toe aan de intent-opsomming in `buildSystemPrompt`:
```
- register_no_show: Gebruik wanneer de zorgverlener aangeeft dat een patiënt
niet op de geplande afspraak is verschenen. Signaalwoorden: "no show",
"niet verschenen", "niet gekomen", "afwezig bij afspraak", "komt niet op".
Entiteiten: geen specifieke entiteiten nodig — de actieve patiënt en
huidige context worden gebruikt.
```
### Done criteria
- System prompt in de chat API bevat `register_no_show` met beschrijving
- Handmatige test: typ "cliënt was er niet vandaag" in de chat → AI classificeert als `register_no_show` (zichtbaar in console via `[ChatPanel] Action detected:`)
- Typ "patiënt heeft de afspraak gemist" → zelfde resultaat
---
---
## NS.E1.S4 — `action-parser.ts` uitbreiden
**Bestand:** `lib/cortex/action-parser.ts`
### Context — waarom dit hier hoort
`action-parser.ts` bevat vier hardcoded enums en switches die de volledige lijst van geldige intents bevatten. Ze zijn **niet** afgeleid van `CortexIntent` in `types.ts` — ze zijn handmatig gesynchroniseerd. Dit is het meest kritieke gat: als `register_no_show` hier ontbreekt, faalt de Zod validatie silently op de AI response en wordt `parsed.action` altijd `null`. De nudge triggert dan **nooit** — ook niet na alle andere fixes.
### Wijziging 1 — `ActionSchema.intent` enum (regel ~16)
Voeg `'register_no_show'` toe aan de `z.enum([...])` lijst:
```typescript
intent: z.enum([
'dagnotitie',
'zoeken',
'overdracht',
'agenda_query',
'create_appointment',
'cancel_appointment',
'reschedule_appointment',
'intake_status',
'intake_navigeer',
'risico_query',
'diagnose_query',
// No Show casus
'register_no_show',
'unknown',
]),
```
### Wijziging 2 — `ActionSchema.artifact.type` enum (regel ~77)
Voeg `'register_no_show'` toe aan de artifact type enum:
```typescript
type: z.enum([
'dagnotitie',
'zoeken',
'overdracht',
'agenda_query',
'create_appointment',
'cancel_appointment',
'reschedule_appointment',
'intake_status',
'risico_query',
'diagnose_query',
'fallback',
'patient-dashboard',
// No Show casus
'register_no_show',
]),
```
### Wijziging 3 — `routeIntentToArtifact` switch
De switch heeft een `default: return null`. Voeg een expliciete case toe **vóór** de `default`, na het `intake_navigeer` blok:
```typescript
case 'register_no_show':
// Artifact wordt geopend via de no-show handler in chat-panel.tsx,
// niet via de generieke routing. Geef null terug zodat de handler
// de controle houdt.
return null;
```
**Toelichting:** We returnen bewust `null` — het artifact voor no-show wordt door `handleNoShowRescriptStep` geopend met volledige prefill data (documentId, content, originalContent) die pas beschikbaar is ná de rescript API call. De generieke router heeft die data niet.
### Wijziging 4 — `getDefaultConfirmationMessage` switch
Voeg toe na het `overdracht` case, vóór de `default`:
```typescript
case 'register_no_show':
return 'Ik registreer de no show en controleer de agenda op declarabiliteit.';
```
### Wijziging 5 — `getAcceptButtonText` in `nudge-chat-message.tsx`
**Bestand:** `components/cortex/chat/nudge-chat-message.tsx`
De `getAcceptButtonText` functie bepaalt de knoptekst op basis van `suggestion.suggestion.intent`:
- Nudge 1 heeft `intent: 'cancel_appointment'` → toont al correct **"Ja, annuleren"**
- Nudge 2 heeft `intent: 'register_no_show'` → valt op `default: 'Ja, uitvoeren'`
Voeg toe in de switch van `getAcceptButtonText`:
```typescript
case 'register_no_show':
return 'Ja, pas brief aan';
```
### Done criteria
- `ActionSchema.safeParse({ type: 'action', intent: 'register_no_show', entities: {}, confidence: 0.9 })` geeft `success: true`
- `routeIntentToArtifact('register_no_show', {}, 0.9)` geeft `null` terug (geen artifact via generieke router)
- `getDefaultConfirmationMessage('register_no_show', {})` geeft correcte Nederlandse zin
- Nudge 2 accept-knop toont "Ja, pas brief aan"
- `pnpm build` slaagt
---
## Validatie na NS.E1 (alle stories)
Run na voltooiing van alle vier stories:
```bash
pnpm build
pnpm lint
```
Verwacht: geen errors. Als er TypeScript errors zijn over `INTENT_PATTERNS` of `BLOCK_CONFIGS`, controleer dan of beide `Record<>` types volledig zijn bijgewerkt.
**Snelle handmatige smoke test:**
1. Open de app (`pnpm dev`)
2. Navigeer naar het Cortex dashboard
3. Typ: `"patiënt niet verschenen"`
4. Verwacht: AI response met intent `register_no_show` zichtbaar in browser console via `[ChatPanel] Action detected: { intent: 'register_no_show', ... }`
5. Verwacht: nudge bubble verschijnt (NS.E2 vereist — maar de action parse moet nu wel slagen)