# Sprint 4 — UI polish + workflow tightening + dark mode + process hygiene

**Branche** : `sprint/4-ui-polish`
**Statut** : 🟡 DRAFT — 9 décisions arrêtées (3 nouvelles 2026-05-27 + 5 sprint 4 mémoire + 1 du PR helper), spec-reviewer en validation
**Base** : `main` @ `27573f5` (sprint 3 mergé via PR #6) + `ba313a3` (carry-over SPEC sprint 3)
**Objectif** : absorber les 7 retours QA sprint 3 (UI/UX, workflow, alignement) + livrer le dark mode + durcir le process UI (design-reviewer agent + screenshots mandatory) + automatiser l'ouverture de PR. Pas de hotfix sprint 3 séparé — tout dans sprint 4 (décision user 2026-05-27).

---

## 0. SPEC.md updates

- ✅ `ba313a3` — § Simulation : "Type d'attaque MITRE correspondant (peut être une liste de référence)" → "Types d'attaque MITRE correspondants (multi-techniques) ..." (carry-over manquant de sprint 3 §0).
- 🟡 § Fonctionnement à enrichir en début de sprint 4 :
  - Préciser que "Done" est terminal : aucune édition possible sans Reopen explicite.
  - Préciser que la transition Reopen `Done → Review required` est ouverte à admin/redteam/soc.
  - Préciser que la création/avancement d'une simu fait avancer l'engagement de `planned` à `active` automatiquement (jamais l'inverse).
- 🟡 § Décisions techniques à enrichir :
  - Section "UI/UX" : convention boutons (icônes / symboles préférés aux longs libellés).
  - Section "Theming" : dark mode supporté, toggle topbar, défaut = `prefers-color-scheme` du système, persistance `localStorage`.

L'évolution est tracée dans CHANGELOG.md § Changed sprint 4.

---

## 1. User stories

### US-17 — UI polish : dédoublonnage boutons + alignement + icônes
**Pourquoi** : QA sprint 3 — `EngagementsListPage` montre 2 boutons "New engagement" + "Create engagement" qui font la même chose ; le bouton Create de `UsersAdminPage` reste mal aligné malgré le fix sprint 2.

**Critères d'acceptation**
- [ ] AC-17.1 : `EngagementsListPage` n'affiche qu'UN SEUL bouton "New engagement". Le doublon "Create engagement" est supprimé.
- [ ] AC-17.2 : convention nouveaux boutons d'action (Create / Add / Save / Delete) : icône lucide-react ou unicode + label court (≤ 8 chars), pas de phrases. Audit des boutons existants : ne refactoriser que ceux qui dépassent ce seuil, garder les "Mark for review" / "Clear all" qui sont déjà courts ou ont une sémantique sans icône évidente. Boutons à passer en icône+label : "Save Red Team" → "Save" + icône, "Save SOC" → "Save SOC" + icône, "ADD TECHNIQUE" → "+" + "Add", "QUICK SEARCH" → "🔍" + "Search".
- [ ] AC-17.3 : `UsersAdminPage` formulaire "Create account" — les 3 FormField (Username, Password, Role) ont leurs labels alignés sur la même baseline ET leurs inputs alignés sur la même baseline. Le bouton Create est aligné horizontalement avec la rangée des inputs. Pixel-perfect au niveau visuel à 1280×720.

### US-18 — Simulation `done` = read-only + Reopen
**Pourquoi** : QA sprint 3 — actuellement une simu `done` peut toujours être PATCHée, ce qui contredit le statut terminal.

**Critères d'acceptation**
- [ ] AC-18.1 : `PATCH /api/simulations/<sid>` avec status courant `done` retourne **409** `{error: "simulation is done — reopen first"}` quel que soit le rôle.
- [ ] AC-18.2 : nouvelle transition `POST /api/simulations/<sid>/transition {to: "review_required"}` quand status courant == `done` → 200, autorisée admin + redteam + soc. Met à jour `updated_at`.
- [ ] AC-18.3 : la transition `→ review_required` depuis `pending`/`in_progress` garde le comportement sprint 2 (admin/redteam only). La nouvelle règle s'ajoute SEULEMENT pour le cas `done`.
- [ ] AC-18.4 : sur `SimulationFormPage`, quand status == `done` :
  - Tous les champs (RT + SOC) sont disabled.
  - `MitreTechniquesField` en read-only (chips sans ×, input + icône matrice masqués).
  - L'action bar affiche UNIQUEMENT un bouton "Reopen" (visible admin/redteam/soc).
  - Save RT, Save SOC, Mark for review, Close, Delete sont masqués.
- [ ] AC-18.5 : click Reopen → POST transition, toast `'Simulation reopened'`, badge se met à jour, les champs redeviennent éditables selon le rôle.

### US-19 — Engagement auto-status `planned → active`
**Pourquoi** : QA sprint 3 — un engagement reste `planned` même quand ses simulations sont in_progress.

**Critères d'acceptation**
- [ ] AC-19.1 : quand une simulation transitionne vers `in_progress` (auto-transition via PATCH RT-field non vide), si son engagement parent est `planned`, l'engagement passe à `active` dans la même unité de travail DB.
- [ ] AC-19.2 : si l'engagement est déjà `active` ou `closed`, pas de changement.
- [ ] AC-19.3 : aucun retour arrière auto. La transition `closed` reste manuelle.
- [ ] AC-19.4 : le frontend invalide `["engagement", eid]` et `["engagements"]` après chaque PATCH/transition simulation pour récupérer le statut à jour.

### US-20 — Matrice MITRE : look attack.mitre.org + pas de scroll horizontal
**Pourquoi** : QA sprint 3 — la matrice actuelle a un scroll horizontal et un layout maison.

**Critères d'acceptation**
- [ ] AC-20.1 : `MitreMatrixModal` est élargi à `max-w-[98vw]`.
- [ ] AC-20.2 : layout 12 colonnes (12 tactiques Enterprise) qui tiennent SANS scroll horizontal à 1280×720 min. Largeur cellule technique ~95-110px (vs 220px actuel), font `text-[12px]`.
- [ ] AC-20.3 : couleurs cohérentes DESIGN.md ET visuellement proches de attack.mitre.org : header tactic avec fond contrasté + label uppercase tracking, techniques en cellules `bg-canvas` avec hairline border, hover `bg-fog`, sélectionnée `bg-primary` texte blanc.
- [ ] AC-20.4 : scroll vertical autorisé (`max-h-[80vh] overflow-y-auto`). Jamais de scroll horizontal.
- [ ] AC-20.5 : sub-techniques expand/collapse PRÉSERVÉ — pas de régression sprint 3 AC-15.2. Compteur "N selected" par tactique reste lisible.
- [ ] AC-20.6 : screenshot comparaison Mimic matrix vs attack.mitre.org joint au summary frontend-builder.

### US-21 — Sélection de tactique en plus des techniques
**Pourquoi** : QA sprint 3 — l'utilisateur veut tagger une simulation par TACTIQUE (ex : `TA0007 Discovery`) sans devoir choisir une technique précise.

**Critères d'acceptation**
- [ ] AC-21.1 : modèle `Simulation` gagne un champ `tactic_ids` (colonne JSON, liste de strings TA-id, défaut `[]`). Séparé de `techniques`.
- [ ] AC-21.2 : migration Alembic `0004_simulation_tactic_ids.py` — ADD COLUMN `tactic_ids` (JSON, NOT NULL, default `[]`). Pas besoin de batch pour ADD COLUMN (SQLite natif). Aucun backfill (default suffit).
- [ ] AC-21.3 : sérialisation Simulation expose `tactics: [{id, name}]` enrichi à partir de `tactic_ids` (id snapshot + name dérivé du bundle MITRE au runtime, comme pour `techniques`).
- [ ] AC-21.4 : `PATCH /api/simulations/<sid>` accepte `{tactic_ids: ["TA0007", ...]}`. Validation : chaque ID doit exister dans `_TACTIC_IDS` (mapping TA-id → short-name, cf §2 Service MITRE). Dedup serveur. ID inconnu → 400. Bundle non chargé → 503.
- [ ] AC-21.5 : `tactic_ids` est ajouté au gate SOC : `(REDTEAM_FIELDS | {"technique_ids", "tactic_ids"}) & payload.keys()`. SOC envoie → 403. Auto-transition se déclenche aussi si `tactic_ids` non vide.
- [ ] AC-21.6 : `MitreMatrixModal` — le header de chaque colonne tactique devient cliquable (toggle de la tactique elle-même). État visuel distinct des techniques sélectionnées. Compteur passe à `N+M selected` (techniques + tactique).
- [ ] AC-21.7 : `MitreTechniquesField` — tactiques sélectionnées affichées comme chips distincts (style différencié : `bg-primary text-canvas` au lieu de `bg-primary-soft text-primary-deep`). × pour retirer. Auto-save sur add/remove.

### US-22 — Refonte input MITRE dans le form
**Pourquoi** : QA sprint 3 — pattern actuel (2 boutons textuels) trop verbeux.

**Critères d'acceptation**
- [ ] AC-22.1 : sous le label "MITRE Techniques", le composant affiche :
  - Une rangée de chips (techniques + tactiques sélectionnées).
  - En dessous, une rangée `[input texte autocomplete] [icône matrice]`.
  - L'input fait l'autocomplete inline (debounce 200ms, dropdown ↑↓Enter, comme sprint 2 mais EMBARQUÉ).
  - L'icône matrice à droite ouvre `MitreMatrixModal`.
  - Aucun bouton textuel "Add Technique" ni "Quick Search".
- [ ] AC-22.2 : les chips affichent UNIQUEMENT la référence (T-id ou TA-id, ex : `T1059.001` ou `TA0007`). Le nom apparaît au survol via `title=` attribute.
- [ ] AC-22.3 : `MitreTechniquePicker` existant est intégré dans le nouveau layout comme l'autocomplete inline. Garde la signature `onSelect`.
- [ ] AC-22.4 : empty state : message court ("No techniques selected") dans la zone des chips. L'input et l'icône matrice restent visibles.
- [ ] AC-22.5 : mode read-only (SOC sur simu non-done, ou tous sur simu done) : chips sans ×, input + icône cachés.

### US-23 — Dark mode
**Pourquoi** : ergonomie demandée. Sprint 4 framing acté.

**Critères d'acceptation**
- [ ] AC-23.1 : un toggle theme dans la topbar (`Layout.tsx`), à droite du nom user. Icône lucide-react `Sun` / `Moon` / `Monitor`.
- [ ] AC-23.2 : 3 états : `light`, `dark`, `system` (auto = suit `prefers-color-scheme`). Toggle cycle entre les 3.
- [ ] AC-23.3 : persistance via `localStorage` (clé `mimic-theme`, valeur `'light'|'dark'|'system'`, défaut `'system'`).
- [ ] AC-23.4 : Tailwind `darkMode: 'class'` activé. Classe `dark` appliquée sur `<html>` selon le résolu. Tokens DESIGN.md étendus avec variantes dark (canvas, paper, ink, graphite, charcoal, etc.). Primary HP Electric Blue garde sa teinte.
- [ ] AC-23.5 : tous les composants principaux audités et utilisent les classes Tailwind `dark:bg-...` / `dark:text-...`. Pas de couleur hardcodée.
- [ ] AC-23.6 : screenshots light + dark de `EngagementsListPage`, `SimulationFormPage`, `MitreMatrixModal` ouverte. Joints au summary.

### US-24 — Process hygiene : design-reviewer agent + screenshots mandatory
**Pourquoi** : sprint 4 framing acté. Sprint 2/3 avait laissé passer des bugs visuels faute de pass design dédié.

**Critères d'acceptation**
- [ ] AC-24.1 : nouveau fichier `.claude/agents/design-reviewer.md`. Brief : revoit le diff frontend + les screenshots fournis par le frontend-builder, audit alignement / hiérarchie typo / DESIGN.md token usage / responsive sanity / cohérence visuelle. Read-only. Lance après frontend-builder, avant code-reviewer.
- [ ] AC-24.2 : `.claude/agents/frontend-builder.md` mis à jour pour rendre EXPLICITE que screenshots sont MANDATORY avant de marquer la tâche terminée (au moins 1 par feature visible / état modifié). Liste explicite des screenshots attendus dans le summary.
- [ ] AC-24.3 : workflow sprint mis à jour dans SPEC.md § Workflows : ajouter design-reviewer entre frontend-builder et code-reviewer.

### US-25 — Infra : PR helper script + Makefile target
**Pourquoi** : capitaliser le pattern Gitea API curl utilisé en sprint 3 pour automatiser les PRs.

**Critères d'acceptation**
- [ ] AC-25.1 : `scripts/open-pr.sh` (executable, `set -euo pipefail`). Lit `~/.git-credentials`. Args : `--sprint=N`, `--title="..."`, `--body=path`. Détecte la branche courante + owner/repo depuis `git remote get-url origin`. POST `/api/v1/repos/{owner}/{repo}/pulls`. Imprime PR URL.
- [ ] AC-25.2 : target Makefile `open-pr SPRINT=N TITLE="..." BODY=path` wrap le script.
- [ ] AC-25.3 : documenté dans README.md (1 paragraphe).
- [ ] AC-25.4 : team-lead utilise ce target pour ouvrir la PR sprint 4 (dogfooding).

---

## 2. Brief technique — Backend Builder

**Scope strict** : `backend/`. Pas de touche au frontend, e2e, `.claude/agents/`, `scripts/`, `Makefile`, docs.

### Livrables

**Modèle `Simulation`** — ajout uniquement :
```python
tactic_ids: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
```

**Migration Alembic `0004_simulation_tactic_ids.py`** :
- Upgrade : `op.add_column('simulations', sa.Column('tactic_ids', sa.JSON(), nullable=False, server_default=sa.text("'[]'")))`. ADD COLUMN OK sans batch sur SQLite. `server_default` règle le NOT NULL pour les lignes existantes.
- Downgrade : `with op.batch_alter_table('simulations') as batch_op: batch_op.drop_column('tactic_ids')`.
- Test : schéma post-upgrade a `tactic_ids` NOT NULL avec default `[]`.

**Serializer** : `serialize_simulation(sim)` ajoute `tactics: [{id, name}]` enrichi runtime.

**Service MITRE** :
- Sprint 3 a indexé les tactiques par **short-name** (`"initial-access"`, `"execution"`, `...`) dans `_TACTIC_ORDER` et `TACTIC_NAMES`. La SPEC et le plan sprint 4 utilisent la notation **TA-id** (`"TA0001"`, `"TA0007"`, etc.). Il faut un mapping TA-id → short-name pour valider/résoudre les `tactic_ids` reçus.
- Ajouter une constante module-level (12 entrées hardcodées, MITRE standard stable — attention, les TA-ids ne sont PAS séquentiels) :
  ```python
  _TACTIC_IDS: dict[str, str] = {
      "TA0001": "initial-access",
      "TA0002": "execution",
      "TA0003": "persistence",
      "TA0004": "privilege-escalation",
      "TA0005": "defense-evasion",
      "TA0006": "credential-access",
      "TA0007": "discovery",
      "TA0008": "lateral-movement",
      "TA0009": "collection",
      "TA0011": "command-and-control",
      "TA0010": "exfiltration",
      "TA0040": "impact",
  }
  ```
- Nouvelle fonction `lookup_tactic(tactic_id: str) -> dict | None` :
  ```python
  short = _TACTIC_IDS.get(tactic_id)
  if short is None:
      return None
  return {"id": tactic_id, "name": TACTIC_NAMES[short]}
  ```
- Nouvelle fonction `get_tactic_name(tactic_id: str) -> str | None` : pareil mais retourne juste le name.
- Validation `tactic_ids` dans `simulation_workflow.py` : un id absent de `_TACTIC_IDS` → 400 `{"error": "unknown tactic id: <id>"}`.

**Service workflow `simulation_workflow.py`** — modifications :
1. **Guard `done` (AC-18.1)** : tout en haut de `apply_patch`, AVANT le check RBAC, si `simulation.status == "done"` → 409 `{error: "simulation is done — reopen first"}`. Vaut pour TOUS les rôles, admin compris.
2. **SOC gate étendu** : `(REDTEAM_FIELDS | {"technique_ids", "tactic_ids"}) & payload.keys()`.
3. **Validation `tactic_ids`** upfront (similaire à `technique_ids`) : tous les IDs validés contre le bundle, dedup `dict.fromkeys`. Bundle non chargé → 503.
4. **Auto-transition** : ajouter le check `len(payload["tactic_ids"]) > 0` au calcul `auto_trigger`.
5. **Transition `done → review_required` (AC-18.2)** — **implémentation précise** : le dict `_ALLOWED_TRANSITIONS` actuel est keyé par target status et a déjà une entrée `"review_required"` avec from={pending, in_progress} et roles={admin, redteam}. On NE peut PAS ajouter une 2e entrée avec la même clé. À la place, dans `transition()`, AVANT le lookup dict, ajoute un cas spécial qui suit les patterns existants du fichier :
   ```python
   # transition() returns tuple[Any, int] | None — None on success, error tuple otherwise.
   # Existing functions use datetime.now(UTC) (timezone-aware, not deprecated utcnow).
   # Enum values are UPPERCASE: SimulationStatus.DONE, SimulationStatus.REVIEW_REQUIRED.
   if to_status == "review_required" and simulation.status == SimulationStatus.DONE:
       simulation.status = SimulationStatus.REVIEW_REQUIRED
       simulation.updated_at = datetime.now(UTC)
       db.session.commit()
       return None
   # ... reste de la fonction inchangée (dict lookup pour les autres cas)
   ```
   Pas de check explicite du rôle ici — `@login_required` upstream + l'enum User limité à admin/redteam/soc rendent la défense superflue (KISS). Autres transitions depuis `done` (vers `pending`, `in_progress`, `done` lui-même) → 409 via le dict lookup qui ne les couvre pas.
6. **Hook engagement auto-status (AC-19.1)** : après une transition de simu vers `in_progress` (auto OU manual), appeler une fonction `_maybe_activate_engagement(simulation)` qui, si `simulation.engagement.status == "planned"`, set `engagement.status = "active"` et `db.session.add(engagement)`. **NE PAS appeler `db.session.commit()` dans le helper** — le caller (`api/simulations.py:update_simulation`) gère le commit final, sinon double-commit.

**API `simulations.py`** :
- PATCH : le check status==done est fait dans `apply_patch` (voir au-dessus).
- Transition : accepter le nouveau cas done → review_required pour admin/redteam/soc.

**Tests pytest**
- `test_simulations_tactics.py` (nouveau) : PATCH valide, ID inconnu → 400, bundle absent → 503, dedup, auto-transition, SOC → 403.
- `test_simulations_done_readonly.py` (nouveau) : PATCH simu done → 409 (admin/redteam/soc). Reopen via transition → 200. Autres transitions depuis done → 409. Après reopen, PATCH OK.
- `test_engagement_lifecycle.py` (nouveau) : création simu → engagement reste `planned`. PATCH simu → simu in_progress + engagement active. Engagement déjà active → pas de changement. Engagement closed → pas de changement.
- Migration test : `tactic_ids` column NOT NULL après upgrade 0004 (similaire au pattern Alembic round-trip sprint 3).
- Adapter `test_simulations_crud.py`, `test_simulations_patch.py`, `test_simulations_workflow.py` si nécessaire pour les assertions sur `tactics` et la garde done.

**Quality bar** : ruff + mypy clean, tous les tests existants + nouveaux verts.

### Règles
- Pas de touche au frontend, `.claude/agents/`, `scripts/`, `Makefile`.
- Renvoyer le summary attendu (cf `.claude/agents/backend-builder.md`).

---

## 3. Brief technique — Frontend Builder

**Scope strict** : `frontend/` UNIQUEMENT.

**SCREENSHOTS MANDATORY** (lesson sprint 2/3) : à la fin de ton travail, lance le dev server et fournis ≥ 5 screenshots :
1. `EngagementsListPage` light + dark
2. `SimulationFormPage` avec ≥ 2 chips technique + ≥ 1 chip tactique light + dark
3. `MitreMatrixModal` ouverte avec sélections light + dark
4. `UsersAdminPage` form "Create account" (alignement vérifié) light + dark
5. `SimulationFormPage` status `done` (read-only + Reopen visible) light

Paths absolus dans le summary final. Si le dev server n'a pas pu tourner, dis-le EXPLICITEMENT avec les raisons techniques précises.

### Livrables

**US-17 — UI polish**
- `EngagementsListPage.tsx` : supprimer le doublon "Create engagement". Garder un seul CTA "New" + icône `+` (selon convention AC-17.2).
- `UsersAdminPage.tsx` : retravailler la grille pour pixel-perfect alignment. Choix laissé au builder (align-items: stretch + align-self, ou restructurer en 2 rangées).
- Audit boutons : refactoriser ceux qui dépassent ≤ 8 chars. Garder "Mark for review" / "Clear all" / "Reopen" sans icône si pas d'icône évidente. Boutons à passer en icône+label : "Save Red Team" → icône + "Save", "Save SOC" → icône + "Save SOC", "ADD TECHNIQUE" → "+" + "Add" (rendu obsolète par US-22), "QUICK SEARCH" → "🔍" + "Search" (rendu obsolète par US-22).

**US-18 — Done read-only + Reopen**
- `SimulationFormPage.tsx` :
  - Quand `simulation.status === 'done'` : tous champs disabled, `MitreTechniquesField disabled`, action bar montre UNIQUEMENT "Reopen" + icône (`↻`).
  - Bouton Reopen : visible admin/redteam/soc, click → `useTransitionSimulation` to `review_required`, toast.

**US-19 — Engagement auto-status (côté UI)**
- `useUpdateSimulation` et `useTransitionSimulation` : ajouter `["engagement", eid]` et `["engagements"]` aux invalidations après mutation réussie. Pas d'autre changement visuel.
- **Note (spec-reviewer Pass 3)** : `eid` n'est pas directement disponible dans la signature des hooks (qui prennent `sid`). Solution : lire `engagement_id` depuis la response simulation (le backend l'expose toujours, cf serialize_simulation sprint 2) OU le passer en arg supplémentaire au hook si plus propre. Pas un trou plan, juste à anticiper.

**US-20 — Matrice MITRE attack.mitre.org look**
- `MitreMatrixModal.tsx` overhaul :
  - `max-w-[98vw]`, `max-h-[80vh] overflow-y-auto`, JAMAIS de scroll horizontal.
  - `display: grid; grid-template-columns: repeat(12, minmax(0, 1fr))` pour répartir équitablement.
  - Cellule technique : `text-[12px]`, padding minimal, hairline border.
  - Header tactique : sticky top, fond contrasté, uppercase tracking, badge compteur à droite.
  - Sub-techniques indent `pl-[8px]`, fond `bg-cloud`.
  - Search input top inchangé.

**US-21 — Tactic selection**
- `MitreMatrixModal.tsx` : header de tactique cliquable (toggle). État visuel distinct.
- Apply renvoie `{techniques, tactics}` au parent.
- `MitreTechniquesField.tsx` : tactic chips style différencié `bg-primary text-canvas`. Auto-save.
- **PATCH combiné (spec-reviewer fix #4)** : Apply depuis la matrice → UN SEUL PATCH `{technique_ids: [...], tactic_ids: [...]}` (les 2 listes ensemble). Pas 2 PATCH séquentiels (risque de race + risque que le 2nd appel hit le guard done). Remove via × sur un tag → un PATCH avec la liste mise à jour (seulement la dimension qui change : `technique_ids` ou `tactic_ids`). Quick Search select → 1 PATCH `{technique_ids: [...]}` (le picker n'ajoute que des techniques). Toutes les mutations passent par `useUpdateSimulation` en un appel atomique.

**US-22 — Refonte input MITRE**
- `MitreTechniquesField.tsx` :
  - Layout : chips area | input autocomplete inline + icône matrice button.
  - Plus de boutons textuels "Add Technique" / "Quick Search".
  - Chips compacts (T-id ou TA-id seul, name en `title=`).
  - Empty state minimal.
- **`SimulationFormPage.tsx` — call site update (spec-reviewer fix #4)** : la signature de `MitreTechniquesField` change de `value: MitreTechnique[]` (sprint 3) à `value: {techniques: MitreTechnique[], tactics: MitreTactic[]}`. La page doit passer `value={{techniques: sim.techniques, tactics: sim.tactics}}` (le champ `sim.tactics` vient du nouveau serializer backend). TypeScript catch le miss mais flag-le explicitement pour ne pas l'oublier.

**US-23 — Dark mode**
- `Layout.tsx` : toggle theme dans la topbar. Hook `useTheme()` (localStorage + media query). 3 états avec cycle.
- `tailwind.config.ts` : `darkMode: 'class'`. Tokens étendus avec variantes dark (recommandé via CSS variables sous `.dark { ... }` dans `index.css`, comme ça les composants n'ont pas à dupliquer leurs classes).
- Audit tous les composants : aucune couleur hardcodée (pas de `bg-white`, `text-black`, `#xxxxxx` inline). Tous passent un check visuel light + dark.

### Règles
- Lit le summary backend EN PREMIER.
- Pas d'invention d'endpoints.
- Réutiliser les patterns sprint 1/2/3.
- Respect DESIGN.md tokens.
- Pas de dépendance npm sans escalade (sauf `lucide-react` autorisé).
- **Interdiction absolue de toucher `e2e/`, `backend/`, `.claude/agents/`, `scripts/`, `Makefile`.**

---

## 4. Brief — Team-lead infra (US-24 + US-25, en parallèle des builders)

**US-24 — Process hygiene**
- Créer `.claude/agents/design-reviewer.md` avec frontmatter agent (model `opus`, tools : `Read`, `Glob`, `Grep`, `Bash` lecture seule). Brief : revoit diff frontend + screenshots, audit alignement / DESIGN.md tokens / cohérence visuelle / responsive.
- Mettre à jour `.claude/agents/frontend-builder.md` : DoD strict sur les screenshots.
- Mettre à jour SPEC.md § Workflows : insérer design-reviewer entre frontend-builder et code-reviewer.

**US-25 — PR helper**
- Écrire `scripts/open-pr.sh` (cf AC-25.1).
- Target Makefile `open-pr`.
- Documenter README.md.
- Dogfood en fin de sprint.

---

## 5. Brief — Test verifier

E2e Playwright :
- `us17-ui-polish.spec.ts` — AC-17.1 (single button), AC-17.3 (alignment via locator boundingBox).
- `us18-done-readonly-reopen.spec.ts` — AC-18.1 → AC-18.5.
- `us19-engagement-auto-status.spec.ts` — AC-19.1 → AC-19.4.
- `us20-matrix-fits-modal.spec.ts` — AC-20.1, AC-20.4 (no horizontal scroll via `boundingBox`).
- `us21-tactic-selection.spec.ts` — AC-21.4 → AC-21.7.
- `us22-mitre-input-redesign.spec.ts` — AC-22.1 → AC-22.5.
- `us23-dark-mode.spec.ts` — AC-23.1 → AC-23.3.

US-24/25 non e2e (process / repo files). Couverture par dogfood (la PR sprint 4 elle-même est ouverte via `make open-pr`).

Adapter les sprint 2/3 e2e si l'audit boutons (AC-17.2) renomme certains labels.

**Spec-reviewer INFO B** : AC-22.2 change le format des chips de "T1059 — Command and Scripting Interpreter" (sprint 3) à juste "T1059" (avec name dans `title=`). Les e2e sprint 3 (notamment `us14-techniques-tags.spec.ts`) qui assertent le format complet doivent être mis à jour. Pas seulement les labels boutons.

---

## 6. Décisions arrêtées

1. **Tactic storage** : colonne JSON `tactic_ids` séparée. ✓ 2026-05-27
2. **Dark mode default** : `system` (suit `prefers-color-scheme`, fallback `light` si non détecté). ✓ 2026-05-27
3. **Matrix CSS fidelity** : look similaire qualitatif (frontend-builder itère, pas pixel-perfect). ✓ 2026-05-27
4. **Reopen target** : `done → review_required`. ✓ mémoire (sprint 4 scope)
5. **Reopen RBAC** : admin + redteam + soc. ✓ mémoire
6. **Engagement auto trigger** : `planned → active` sur 1ère simu in_progress (auto-transition ou manual). Pas de retour arrière auto. ✓ mémoire
7. **PR helper token source** : `~/.git-credentials` (parse user + token via sed, cf [[reference-gitea-pr-api]]). ✓ 2026-05-27
8. **Workflow design-reviewer** : insérée entre frontend-builder et code-reviewer, read-only. Diff frontend + screenshots. Format rapport à la code-reviewer mais focus visuel/design. ✓ mémoire
9. **Screenshots frontend-builder** : MANDATORY au sprint 4, en sortie du frontend-builder, paths absolus dans summary, refus de marquer la tâche done sans. ✓ mémoire

---

## 7. Plan d'exécution

1. ✅ Team-lead a re-appliqué le SPEC sprint 3 oublié (`ba313a3`).
2. ✅ User a validé les 4 décisions ouvertes (tactic separated, theme system, matrix qualitative, token from ~/.git-credentials). Avec les 5 acquises en mémoire (sprint 4 scope), ça fait 9 décisions arrêtées.
3. 🟡 Team-lead met à jour SPEC.md § Workflows + § Décisions techniques (§0).
4. 🟡 Spec-reviewer valide le plan vs SPEC.md (anti-trous comme à sprint 3 — RBAC field-level, batch SQLite, scope ambigu).
5. 🔵 Backend-builder : modèle + migration 0004 + workflow done-readonly/reopen + engagement auto-lifecycle + tactic_ids + tests.
6. 🔵 Frontend-builder : UI polish + done read-only + matrix overhaul + tactic selection + input redesign + dark mode + screenshots.
7. 🔵 Team-lead (US-24 + US-25 en parallèle de frontend) : design-reviewer agent + frontend-builder.md update + scripts/open-pr.sh + Makefile target.
8. 🔵 Design-reviewer (NEW STEP) : revoit diff frontend + screenshots.
9. 🔵 Code-reviewer : revoit le diff complet (LSP-first).
10. 🔵 Test-verifier : e2e US-17 → US-23.
11. 🟢 Team-lead : PR via `make open-pr` (dogfood AC-25.4) + récap.
