# NS.E2 — Nudge Rules & Chained Flow

**Casus:** Cortex No Show Afhandeling
**Epic doel:** Twee proactieve nudges bouwen die sequentieel volgen op de no-show registratie, plus de store-uitbreiding die de multi-step state bijhoudt.
**Geschatte tijd:** ~2 uur
**Afhankelijkheden:** NS.E1 volledig klaar (CortexIntent type moet bestaan)

---

## Context & waarschuwingen

### Nudge evaluatie wordt NIET aangeroepen vanuit de hoofd-chat flow

Dit is het grootste architecturele gat. `evaluateNudge()` wordt momenteel **alleen** aangeroepen vanuit `handleConfirmAction` in `chat-panel.tsx` — dat is de V2 chain flow (feature-flagged). De hoofd-chat SSE flow (`onDone` callback) roept dit **niet** aan.

Gevolg: als een gebruiker "patiënt niet verschenen" typt en de AI antwoordt met `register_no_show`, triggert er **geen nudge** via de protocol rules engine.

**Oplossing in dit epic:** We voegen nudge-evaluatie toe aan de `onDone` callback van de hoofd-chat flow, maar alleen voor de `register_no_show` intent. Dit is bewust scopebegrensd — geen generieke uitbreiding van de flow.

### NudgeSuggestion ID bevat timestamp

Nudge IDs worden gegenereerd als `nudge-${rule.id}-${Date.now()}`. We kunnen dus **niet** matchen op het volledige ID in `handleAcceptNudge`. Match op `suggestion.trigger.intent` of de `suggestion.suggestion.rationale` (= `rule.name`).

**Gekozen aanpak:** `suggestion.trigger.intent` gebruiken als discriminator.
- Nudge 1: `trigger.intent === 'register_no_show'`
- Nudge 2: handmatig geconstrueerd (niet via `evaluateNudge`) — zie NS.E2.S3

### ProtocolRule interface vereisten

De `ProtocolRule` interface in `nudge.ts` heeft **verplichte** velden die het bouwplan oorspronkelijk wegliet:

```typescript
interface ProtocolRule {
  id: string;
  name: string;           // ← verplicht
  trigger: {
    intent: CortexIntent;
    conditions: ProtocolCondition[];  // ← verplicht, mag [] zijn
  };
  suggestion: {
    intent: CortexIntent;
    message: string;
    prefillEntities: (source: ExtractedEntities) => Partial<ExtractedEntities>;  // ← FUNCTIE, niet object
  };
  protocol?: ProtocolMetadata;
  priority: NudgePriority;
  enabled: boolean;       // ← verplicht
  expiresAfterMs: number;
}
```

---

## NS.E2.S1 — No-show flow state aan store toevoegen

**Bestand:** `stores/cortex-store.ts`

### Context

De no-show flow heeft 5 sequentiële stappen. We slaan de state op in de Zustand store zodat alle componenten er toegang toe hebben.

### Wijziging 1 — Types toevoegen (boven `CortexStore` interface)

```typescript
// No-show flow state machine
export type NoShowStep = 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done';

export interface NoShowFlowState {
  step: NoShowStep;
  appointmentId: string | null;
  documentId: string | null;
  originalContent: string | null;
}
```

### Wijziging 2 — State toevoegen aan `CortexStore` interface

Voeg toe na `pendingClarification`:
```typescript
// No-show flow state
noShowFlow: NoShowFlowState;
```

Voeg actions toe na bestaande clarification actions:
```typescript
// No-show flow actions
setNoShowStep: (step: NoShowStep) => void;
setNoShowContext: (ctx: Partial<Omit<NoShowFlowState, 'step'>>) => void;
resetNoShowFlow: () => void;
```

### Wijziging 3 — Initiële waarde in `initialState`

```typescript
noShowFlow: {
  step: 'idle',
  appointmentId: null,
  documentId: null,
  originalContent: null,
} as NoShowFlowState,
```

### Wijziging 4 — Actions implementeren (in `create()` body)

```typescript
setNoShowStep: (step) =>
  set(
    (state) => ({ noShowFlow: { ...state.noShowFlow, step } }),
    false,
    'setNoShowStep'
  ),

setNoShowContext: (ctx) =>
  set(
    (state) => ({ noShowFlow: { ...state.noShowFlow, ...ctx } }),
    false,
    'setNoShowContext'
  ),

resetNoShowFlow: () =>
  set(
    {
      noShowFlow: {
        step: 'idle',
        appointmentId: null,
        documentId: null,
        originalContent: null,
      },
    },
    false,
    'resetNoShowFlow'
  ),
```

### Done criteria
- `pnpm build` slaagt
- `useCortexStore(s => s.noShowFlow)` geeft `{ step: 'idle', ... }` terug
- `useCortexStore(s => s.setNoShowStep)('waiting_cancel')` werkt zonder error

---

## NS.E2.S2 — Nudge Rule 1: Declarabiliteitscheck

**Bestand:** `lib/cortex/nudge.ts`

### Wijziging — rule toevoegen aan `PROTOCOL_RULES`

Voeg toe **vóór** de bestaande wondzorg-regel (hogere priority verdient eerste positie):

```typescript
{
  id: 'noshow-declarabel-check',
  name: 'No Show declarabiliteitscheck',
  trigger: {
    intent: 'register_no_show',
    conditions: [], // Geen aanvullende condities — altijd triggeren bij no-show
  },
  suggestion: {
    intent: 'cancel_appointment',
    message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?',
    prefillEntities: (_source) => ({}), // Geen prefill nodig — context wordt via API opgehaald
  },
  priority: 'high',
  enabled: true,
  expiresAfterMs: DEFAULT_EXPIRY_MS,
},
```

**Let op:** Er is geen `protocol` metadata voor deze rule — dat is optioneel en ontbreekt hier bewust (het gaat om een inkoopverplichting, geen klinisch protocol).

### Done criteria
- `evaluateNudge({ intent: 'register_no_show', actionId: 'test', entities: {}, content: '' })` geeft een array terug met één nudge
- De nudge heeft `priority: 'high'` en `suggestion.intent: 'cancel_appointment'`
- De message bevat "declarabel"

---

## NS.E2.S3 — Hoofd-chat flow uitbreiden met nudge trigger

**Bestand:** `components/cortex/chat/chat-panel.tsx`

### Context

De nudge voor no-show moet triggeren ná de AI-response, niet ná een chain action. Dat betekent: uitbreiding van de `onDone` callback in de `ChatInput` `onSend` handler.

Momenteel (vereenvoudigd):
```typescript
onDone: () => {
  setStreaming(false);
  const parsed = parseActionFromResponse(accumulatedContent);
  if (parsed.action) {
    updateLastMessage(...);
    setPendingAction(parsed.action);
  }
}
```

### Stap 1 — Store actions ophalen

Voeg bovenaan `ChatPanel()` toe:
```typescript
const noShowFlow = useCortexStore((s) => s.noShowFlow);
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
const setNoShowContext = useCortexStore((s) => s.setNoShowContext);
```

### Stap 2 — Nudge trigger in `onDone`

Voeg toe in de `onDone` callback, **ná** het bestaande `setPendingAction` blok:

```typescript
// No-show flow: trigger nudge na register_no_show intent
if (parsed.action?.intent === 'register_no_show' && isFeatureEnabled('CORTEX_NUDGE')) {
  const suggestions = evaluateNudge({
    intent: 'register_no_show',
    actionId: crypto.randomUUID(),
    entities: parsed.action.entities,
    content: message, // originele user input
  });

  suggestions.forEach((suggestion) => {
    addChatMessage({
      type: 'nudge',
      content: suggestion.suggestion.message,
      nudge: suggestion,
    });
  });
}
```

### Stap 3 — `handleAcceptNudge` uitbreiden met no-show logica

De bestaande `handleAcceptNudge` doet: `acceptSuggestion` → `routeIntentToArtifact` → `openArtifact`. We voegen een **vroege exit** toe voor no-show nudges die een aparte flow hebben.

Voeg toe aan het begin van `handleAcceptNudge`, vóór de bestaande `routeIntentToArtifact` aanroep:

```typescript
const handleAcceptNudge = useCallback(async (
  suggestionId: string,
  suggestion: ChatMessageType['nudge']
) => {
  acceptSuggestion(suggestionId);

  if (!suggestion) return;

  // --- No-show flow: stap 2 → stap 3 ---
  if (suggestion.trigger.intent === 'register_no_show') {
    await handleNoShowCancelStep(suggestion);
    return; // Vroege exit — geen generieke artifact routing
  }

  // Bestaande generieke flow (voor alle andere nudges)
  const artifact = routeIntentToArtifact(
    suggestion.suggestion.intent,
    suggestion.suggestion.entities,
    0.9
  );
  if (artifact) {
    openArtifact({ type: artifact.type, prefill: artifact.prefill, title: artifact.title });
  }
}, [acceptSuggestion, openArtifact, setNoShowStep, setNoShowContext, addChatMessage, activePatient]);
```

**Let op:** `handleAcceptNudge` is nu `async`. Controleer of de prop-definitie in `NudgeChatMessage` dit ondersteunt — zo niet, pas de prop type aan.

### Stap 4 — `handleNoShowCancelStep` functie

Voeg toe als aparte `useCallback` in `ChatPanel`:

```typescript
const handleNoShowCancelStep = useCallback(async (suggestion: NudgeSuggestion) => {
  setNoShowStep('waiting_cancel');

  // Voeg verwerking chat message toe
  addChatMessage({ type: 'assistant', content: 'Bezig met annuleren...' });

  try {
    // Annuleer de afspraak
    const cancelRes = await fetch('/api/cortex/noshow/cancel', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        appointmentId: 'mock-appt-noshow-001', // mock voor nu
        patientId: activePatient?.id ?? 'demo-patient-001',
      }),
    });

    if (!cancelRes.ok) throw new Error('Cancel mislukt');

    // Check op concept brief
    const patientId = activePatient?.id ?? 'demo-patient-001';
    const ctxRes = await fetch(`/api/cortex/noshow/context?patientId=${patientId}`);
    const ctx = await ctxRes.json();

    if (ctx.hasConceptBrief) {
      setNoShowContext({
        documentId: ctx.document.id,
        originalContent: ctx.document.content,
      });
      setNoShowStep('waiting_brief');

      // Construeer nudge 2 handmatig (niet via evaluateNudge)
      const briefNudge: NudgeSuggestion = {
        id: `nudge-noshow-brief-${Date.now()}`,
        trigger: {
          actionId: 'noshow-cancel-done',
          intent: 'cancel_appointment',
          entities: {},
        },
        suggestion: {
          intent: 'register_no_show', // gebruikt als signaal voor brief-stap
          entities: {},
          message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
          rationale: 'noshow-brief-check', // gebruikt als discriminator in handleAcceptNudge stap 5
        },
        status: 'pending',
        priority: 'high',
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
        createdAt: new Date(),
      };

      addChatMessage({ type: 'nudge', content: briefNudge.suggestion.message, nudge: briefNudge });
    } else {
      setNoShowStep('done');
      addChatMessage({
        type: 'assistant',
        content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.',
      });
    }
  } catch {
    setNoShowStep('idle');
    addChatMessage({
      type: 'error',
      content: 'Er is iets misgegaan bij het annuleren. Probeer het opnieuw.',
    });
  }
}, [activePatient, setNoShowStep, setNoShowContext, addChatMessage]);
```

### Stap 5 — `handleAcceptNudge` uitbreiden voor brief-stap

In de bestaande `handleAcceptNudge`, voeg een tweede no-show check toe vóór de generieke flow:

```typescript
// --- No-show flow: stap 4 → brief openen ---
if (suggestion.suggestion.rationale === 'noshow-brief-check') {
  await handleNoShowRescriptStep();
  return;
}
```

Voeg `handleNoShowRescriptStep` toe als aparte `useCallback` (zie NS.E3.S4 voor de rescript API, die hier aangeroepen wordt). De volledige implementatie staat in NS.E5.S1.

### Done criteria
- Typen "patiënt niet verschenen" → AI antwoordt → nudge 1 verschijnt in chat
- Klikken `[Ja]` op nudge 1 → cancel API aangeroepen → nudge 2 verschijnt
- Klikken `[Ja]` op nudge 2 → rescript API aangeroepen → artifact opent (NS.E4)
- Klikken `[Nee]` op nudge 1 → `dismissSuggestion` → nudge verdwijnt, geen verdere actie
- `noShowFlow.step` doorloopt correct: `idle` → `waiting_cancel` → `waiting_brief` → `brief_open`

---

## Validatie na NS.E2

```bash
pnpm build
pnpm lint
```

**Visuele check:**
1. Typ `"patiënt niet verschenen"` in de chat
2. Wacht op AI response
3. Verwacht: nudge bubble met "Ik zie een declarabel consult..." en `[Ja, annuleer]` / `[Nee]` knoppen
4. Browser console toont: `[ChatPanel] Nudge suggestions (chat-based): 1`
