Files
mimic/tasks/todo.md
Knacky b001f57774 docs: sprint 3 wrap-up — README + CHANGELOG + lessons + plan final
- README: status bump to sprint 3, test counts refreshed (164/86/105), IPv6 note for the e2e runner
- CHANGELOG: sprint 3 entry under [Unreleased] (multi-tech model + matrix endpoint + auto-save UI); sprint 2 moved to its own [Sprint 2] section (merged 2026-05-27)
- tasks/lessons.md: 6 lessons captured (2-pass spec-review, inline summary scoping, "test in brief means test in commit" discipline, SQLite batch_alter_table, real migration round-trip, modal Apply 0 disambiguation)
- tasks/todo.md: status flipped to 🟢 SPRINT COMPLET, execution sequence ticks updated with commit hashes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 04:55:12 +02:00

291 lines
24 KiB
Markdown
Raw 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.
# Sprint 3 — MITRE matrix modal + multi-technique simulations
**Branche** : `sprint/3-mitre-matrix`
**Statut** : 🟢 SPRINT COMPLET — 105/105 sprint 3 e2e verts, code-review traité, PR prête
**Base** : `main` @ `e1d9738`
**Objectif** : remplacer la sélection MITRE mono-technique de sprint 2 par une sélection multi-techniques avec deux modes complémentaires : autocomplete (rapide) et matrice cliquable (exploration). Les techniques choisies s'affichent comme tags sur la simulation.
---
## 0. Évolution SPEC.md à acter en début de sprint
SPEC.md § Simulation dit aujourd'hui "Type d'attaque MITRE correspondant (peut être une liste de référence)" au singulier. Le team-lead met à jour cette ligne en début de sprint (pas de PR séparée) pour refléter le scope multi-techniques. Texte cible :
> Types d'attaque MITRE correspondants (multi-techniques) — sélectionnables par autocomplete OU via la matrice ATT&CK affichée en modale.
L'évolution est tracée dans CHANGELOG.md § Changed du sprint 3.
---
## 1. User stories
### US-13 — En tant que redteam, je sélectionne plusieurs techniques MITRE par simulation
**Pourquoi** : un test couvre souvent plusieurs TTPs (ex : Initial Access → Discovery → Execution). Mono-technique limite la description réelle d'un test.
**Critères d'acceptation**
- [ ] AC-13.1 : modèle `Simulation` n'a plus `mitre_technique_id` ni `mitre_technique_name` (scalaires). Remplacés par `techniques` (colonne JSON, liste d'objets `{id: str, name: str}`, défaut `[]`).
- [ ] AC-13.2 : migration Alembic `0003_simulation_techniques_array.py` :
- ajoute la colonne `techniques` (JSON)
- backfill les simulations existantes : si `mitre_technique_id` non null → `techniques = [{id, name}]`, sinon `techniques = []`
- drop les deux anciennes colonnes
- migration réversible (downgrade : prendre le premier élément, ré-injecter dans les scalaires, drop `techniques`)
- [ ] AC-13.3 : sérialisation simulation expose `techniques: [{id, name, tactics: [...]}]` — le backend enrichit chaque entrée avec ses `tactics` depuis le service MITRE au moment du serialize (snapshot d'`id`+`name` en DB, tactics dérivées au runtime depuis le bundle).
- [ ] AC-13.4 : `PATCH /api/simulations/<sid>` accepte `{technique_ids: ["T1059", "T1078"]}` (liste d'IDs string). Backend valide chaque ID contre le bundle MITRE, résout `name`, écrit `[{id, name}]` en DB. ID inconnu → 400 `{error: "unknown technique id: T9999"}`.
- [ ] AC-13.5 : la règle d'auto-transition `pending → in_progress` s'applique aussi à `technique_ids` quand la liste reçue est non vide.
### US-14 — En tant que redteam, je vois et retire les techniques d'une simulation sous forme de tags
**Pourquoi** : visualiser rapidement la couverture TTP d'un test.
**Critères d'acceptation**
- [ ] AC-14.1 : sur `SimulationFormPage`, à la place du seul `MitreTechniquePicker` du sprint 2, un composant `MitreTechniquesField` affiche :
- Liste des techniques sélectionnées sous forme de chips/tags (id + name, ex : `T1059 — Command and Scripting Interpreter`), avec un `×` cliquable pour retirer chaque technique.
- Bouton "Add technique" qui ouvre la modale matrice (US-15).
- Bouton "Quick search" qui ouvre l'autocomplete existant (réutilisation du `MitreTechniquePicker`) en mode "ajoute à la liste" (sélection = append, pas replace).
- État vide : message "No techniques selected — use the matrix or the quick search to add."
- [ ] AC-14.2 : retirer un tag (× sur le chip) déclenche un PATCH immédiat (auto-save) avec la liste mise à jour. La modale matrice (US-15) auto-save aussi via "Apply". Le picker Quick Search auto-save chaque sélection. Toast `'Techniques updated'` sur succès, toast erreur sinon. Pas de bouton Save manuel pour les techniques.
- [ ] AC-14.3 : sur `SimulationList` (table dans EngagementDetailPage), la colonne "MITRE" affiche un compteur + premier tag (ex : `T1059 +2` si 3 techniques sélectionnées). Si la liste est vide, afficher `—`.
- [ ] AC-14.4 : ordre des tags dans la simulation préservé entre lecture et écriture (pas de tri imposé côté serveur).
- [ ] AC-14.5 : tags affichés avec les couleurs/spacing DESIGN.md (`bg-primary-soft`, `text-primary-deep`, `rounded-full`, `px-md py-xxs`).
### US-15 — En tant que redteam, j'ouvre la matrice MITRE ATT&CK pour explorer et sélectionner des techniques
**Pourquoi** : l'autocomplete est efficace si on sait ce qu'on cherche ; la matrice est nécessaire pour "voir ce qui existe" et combiner par tactique.
**Critères d'acceptation**
- [ ] AC-15.1 : nouvel endpoint `GET /api/mitre/matrix` (auth, tous rôles) → tree `[{tactic_id, tactic_name, techniques: [{id, name, subtechniques: [{id, name}]}]}]`. Chaque technique top-level embarque ses sub-techniques (`T1059``[T1059.001, T1059.002, ...]`). Ordre des tactiques = ordre canonique MITRE Enterprise (Initial Access → Execution → Persistence → ... → Exfiltration → Impact). 503 si bundle non chargé.
- [ ] AC-15.2 : composant `MitreMatrixModal` :
- Modal large (≥ 1100px), scroll vertical interne.
- Layout horizontal en colonnes : 1 colonne par tactique. Header de colonne = nom de la tactique + compteur de techniques sélectionnées dans cette tactique (sub-techniques incluses).
- Chaque technique top-level = bouton/cellule cliquable. État sélectionné visible (`bg-primary` + texte blanc).
- Si la technique a des sub-techniques (`subtechniques.length > 0`), un chevron (▸/▾) précède le nom. Click sur le chevron = expand/collapse (n'affecte PAS la sélection). Click sur le label = toggle sélection de la technique top-level.
- Sub-techniques affichées en cascade indentée sous leur parent quand expand. Cliquables individuellement (toggle de la sub). État visuel distinct : `bg-primary-soft` quand sélectionnée, indent `pl-md`, font-size légèrement plus petit.
- Sélectionner une sub-technique ne sélectionne PAS le parent (les deux sont indépendants côté data). Mais le compteur de tactique somme parent + subs sélectionnées.
- Champ de recherche en haut du modal qui filtre les techniques affichées (case-insensitive sur id ET name). Quand le filtre matche une sub-technique, son parent est automatiquement expand pour la rendre visible.
- Boutons en footer : "Cancel" (ferme sans appliquer), "Apply N techniques" (compteur = total parents + subs sélectionnés).
- [ ] AC-15.3 : la modale est ouverte depuis le bouton "Add technique" de US-14. Elle reçoit en input la liste actuelle de techniques sélectionnées et travaille sur une copie locale ; "Apply" déclenche directement le PATCH (auto-save, cf AC-14.2) et ferme la modale ; "Cancel" jette le diff local.
- [ ] AC-15.4 : Escape ferme la modale (= Cancel). Click sur le backdrop = Cancel.
- [ ] AC-15.5 : a11y V1 — **scope minimal explicite** : (1) focus initial sur le champ recherche à l'ouverture, (2) Tab cycle entre les éléments focusables de la modale (wrap : dernier élément → premier), (3) Escape ferme = onCancel, (4) ARIA `role="dialog"` + `aria-labelledby` sur le titre. Full WAI-ARIA dialog conformance (live regions, focus restoration au close, screen reader announcements détaillés) **out of scope V1** — c'est une dette assumée à reprendre dans un sprint a11y dédié.
### US-16 — En tant que user (tous rôles), j'utilise les autres fonctionnalités sans régression
**Critères d'acceptation** (régression)
- [ ] AC-16.1 : workflow sprint 2 (auto-transition, transitions manuelles, RBAC SOC) inchangé — tous les ACs sprint 2 (US-7 → US-12) continuent de passer.
- [ ] AC-16.2 : l'ancien `MitreTechniquePicker` est conservé dans la base de code MAIS sa signature passe en clean rewrite (`onSelect({id, name})` au lieu de `onChange(id, name)`), wrappé par `MitreTechniquesField` en mode append.
- [ ] AC-16.3 : aucune e2e sprint 1/sprint 2 ne casse. Quelques assertions sprint 2 (US-8 et US-10) qui validaient le mono-technique sont mises à jour pour refléter la liste.
---
## 2. Brief technique — Backend Builder
**Scope strict** : `backend/`. Pas de touche au frontend, e2e, docs (team-lead).
### Livrables
**Modèle `Simulation`** (`backend/app/models/simulation.py`)
- Remplacer `mitre_technique_id`, `mitre_technique_name` (str nullable) par :
```python
techniques: Mapped[list[dict]] = mapped_column(JSON, nullable=False, default=list)
```
- Stockage : `[{"id": "T1059", "name": "Command and Scripting Interpreter"}, ...]`. Pas de `tactics` en DB (dérivé au serialize).
**Service workflow** (`backend/app/services/simulation_workflow.py`) — **mise à jour RBAC field-level OBLIGATOIRE**
- Dans le `REDTEAM_FIELDS` frozenset existant : **retirer** `"mitre_technique_id"` et `"mitre_technique_name"`, **ajouter** `"technique_ids"`.
- Sans ce changement : un user soc qui PATCH avec `{technique_ids: [...]}` reçoit un silent no-op (champ ignoré) au lieu du 403 attendu. La gate field-level RBAC pour `technique_ids` repose intégralement sur ce frozenset.
- Le `SOC_FIELDS` frozenset reste inchangé.
- Tester explicitement : `test_simulations_techniques.py` doit inclure "SOC PATCH technique_ids → 403" (cf. liste de tests plus bas).
**Migration Alembic `0003_simulation_techniques_array.py`**
- Upgrade :
1. Ajouter colonne `techniques` (JSON, nullable=True temporaire, default `'[]'`) — `op.add_column` direct OK.
2. Data migration : pour chaque ligne, si `mitre_technique_id` IS NOT NULL → set `techniques = '[{"id":"<id>","name":"<name>"}]'`, sinon `'[]'`.
3. ALTER column `techniques` → nullable=False — **OBLIGATOIRE via `op.batch_alter_table('simulations', ...)`** car SQLite ne supporte pas ALTER COLUMN nativement.
4. Drop columns `mitre_technique_id`, `mitre_technique_name` — **OBLIGATOIRE via `op.batch_alter_table('simulations', ...)`** (même raison : SQLite ne supporte pas DROP COLUMN hors batch mode).
- Downgrade : symétrique avec les MÊMES guards batch_alter_table pour les étapes ALTER/DROP. Recrée les 2 colonnes, prend le premier élément de `techniques` si non vide, drop `techniques`.
- Pattern à suivre : la migration `0002_add_simulations.py` (sprint 2) — vérifier le style batch_alter_table déjà en place.
**Serializer** (`backend/app/serializers.py`)
- `serialize_simulation(sim)` :
- Avant retour, enrichir chaque tag avec `tactics` depuis `mitre_svc.get_tactics(id)`. Si la technique a été retirée du bundle MITRE entre-temps, `tactics = []` (gracieux).
- `commands` reste tel quel (text brut, inchangé sprint 2).
**Service MITRE** (`backend/app/services/mitre.py`)
- Étendre l'index avec un dict `tactics_by_technique: dict[str, list[str]]` pour lookup O(1) au serialize.
- Nouvelle fonction `get_tactics(technique_id: str) -> list[str]`.
- Nouvelle fonction `lookup_name(technique_id: str) -> str | None` — utilisée par l'endpoint PATCH pour résoudre le name côté serveur (le client n'envoie que les IDs).
- Nouvelle fonction `get_matrix() -> list[dict]` :
```json
[
{"tactic_id": "TA0001", "tactic_name": "Initial Access",
"techniques": [
{"id": "T1078", "name": "Valid Accounts",
"subtechniques": [{"id": "T1078.001", "name": "Default Accounts"}, ...]},
...
]},
...
]
```
Sub-techniques embarquées sous chaque parent (relation STIX `subtechnique-of` dans le bundle). Si la technique n'en a pas, `subtechniques: []`.
Ordre des tactiques : canonical MITRE Enterprise order (12 tactics). Lecture depuis les objets STIX `x-mitre-tactic` ordonnés par `x_mitre_shortname` natif OU constante module-level hardcodée si plus simple.
Ordre des techniques au sein d'une tactique : alphabétique par `name` (déterministe, lisible).
**API** (`backend/app/api/simulations.py`)
- `GET /api/mitre/matrix` — nouvel endpoint, 200 + tree, 503 si bundle absent.
- `PATCH /api/simulations/<sid>` : le payload accepte maintenant `technique_ids: list[str]` à la place de `mitre_technique_id` + `mitre_technique_name`. Validation : tous les IDs doivent exister dans le bundle (400 sinon), `name` snapshot servi par `lookup_name`. Pas de rétrocompat avec les anciens champs scalaires (clean break — pas d'utilisateur externe).
- **Dedup serveur** : avant écriture en DB, dédupliquer la liste `technique_ids` en préservant l'ordre (`list(dict.fromkeys(technique_ids))`). Le client peut envoyer accidentellement des doublons (race UI ou bug), le serveur ne doit jamais persister deux fois la même technique.
- Auto-transition (AC-13.5) : un `technique_ids` non vide (≥1 élément) compte comme redteam-side filled, déclenche `pending → in_progress`. Liste vide = pas de trigger.
**Tests pytest**
- `test_simulations_techniques.py` (nouveau) :
- Création + PATCH `technique_ids` → simulation a la bonne liste, sérialisation expose `techniques` avec `tactics`.
- PATCH avec ID inconnu → 400.
- Auto-transition sur `technique_ids` non vide.
- Retirer toutes les techniques (`technique_ids: []`) → pas de trigger d'auto-transition (cohérent avec règle "valeur vide").
- **Dedup** : PATCH avec `technique_ids: ["T1059", "T1078", "T1059"]` → DB ne stocke que 2 entrées, ordre préservé (`T1059` en premier).
- `test_mitre.py` (existant) — ajouter :
- `get_matrix()` renvoie les bonnes tactiques dans le bon ordre.
- `lookup_name(unknown)` → None.
- `get_tactics(known)` → liste correcte (≥1 tactique).
- `test_simulations_crud.py` + `test_simulations_patch.py` + `test_simulations_workflow.py` (existants) — adapter toute assertion qui touchait `mitre_technique_id` / `mitre_technique_name`.
- Migration : test que les anciennes simulations en `pending` avec un id mono-tech sont upgradées en `techniques: [{id, name}]` (fixture inline ou test direct sur Alembic).
**Quality bar** : ruff + mypy clean, tous les tests existants + nouveaux verts.
---
## 3. Brief technique — Frontend Builder
**Scope strict** : `frontend/` uniquement.
**Note process (lesson learned sprint 2)** : avant de marquer la tâche terminée, lance le dev server et screenshot (a) la matrice modale ouverte avec ≥3 techniques sélectionnées et (b) la simulation form avec ≥2 tags affichés. Joins-les à ton summary final.
### Livrables
**Types** (`frontend/src/api/types.ts`)
- `MitreTechnique`: `{id: string, name: string, tactics: string[]}` (déjà existant pour le picker — réutiliser, ajouter `tactics` si manquant).
- Ajouter `MitreTactic`: `{tactic_id: string, tactic_name: string, techniques: MitreMatrixTechnique[]}` avec `MitreMatrixTechnique = {id: string, name: string, subtechniques: {id: string, name: string}[]}`.
- `Simulation.techniques: MitreTechnique[]` à la place de `mitre_technique_id` + `mitre_technique_name`. PATCH payload : `{technique_ids: string[]}`.
**API client** (`frontend/src/api/mitre.ts`)
- `searchMitreTechniques(q)` — existant, garder.
- `getMitreMatrix()` — nouveau, GET `/api/mitre/matrix`.
**Hooks** (`frontend/src/hooks/useMitre.ts`)
- `useMitreSearch(q, enabled)` — existant, garder.
- `useMitreMatrix(enabled)` — nouveau hook TanStack Query, `staleTime: Infinity` (la matrice ne change qu'avec `make update-mitre` + redémarrage).
**Composants**
- **`MitreTechniqueTag.tsx`** (nouveau) : chip affichant `{id} — {name}` avec un bouton `×`. Props : `technique: MitreTechnique`, `onRemove: () => void`, `disabled?: boolean`.
- **`MitreTechniquesField.tsx`** (nouveau, dans `frontend/src/components/`) : conteneur qui orchestre la sélection multi-tech avec **auto-save** (PATCH déclenché par chaque add/remove/Apply).
- Props : `value: MitreTechnique[]`, `simulationId: number`, `disabled?: boolean`. (Pas de `onChange` du parent — le composant fait son propre PATCH via `useUpdateSimulation`.)
- UI : liste de `<MitreTechniqueTag>` + 2 boutons "Add technique" (ouvre matrix) et "Quick search" (ouvre/toggle picker autocomplete inline).
- Dédup : si l'utilisateur essaye d'ajouter une technique déjà présente, no-op silencieux (pas de PATCH non plus).
- Auto-save : chaque mutation (× sur tag, Apply matrice, sélection Quick Search) déclenche `useUpdateSimulation` avec `{technique_ids: [...]}`. Toast succès `'Techniques updated'`, toast erreur sinon. Pendant le PATCH : disable l'interaction (les × deviennent grisés, les boutons disabled).
- **`MitreMatrixModal.tsx`** (nouveau) : modale matrice avec sub-techniques expand/collapse.
- Props : `isOpen: boolean`, `initialSelection: MitreTechnique[]`, `onApply: (selection: MitreTechnique[]) => void`, `onCancel: () => void`.
- État local : (a) copie de `initialSelection` mutée par les toggles, (b) `expandedTechniques: Set<string>` pour les IDs parents dépliés.
- Layout : flex horizontal scrollable, 1 colonne par tactique. Largeur fixe 220px par colonne pour cohérence visuelle.
- Chevron `▸/▾` à gauche du nom des techniques qui ont des sub-techniques (`subtechniques.length > 0`). Click chevron = toggle expand (mute le set `expandedTechniques`), ne modifie PAS la sélection.
- Click sur le label d'une technique top-level = toggle sa sélection (le chevron ne se déclenche pas dans ce cas — séparer les zones cliquables).
- Sub-techniques rendues en cascade indentée sous leur parent quand expand : `pl-md text-[12px] bg-cloud rounded` (vs parent `text-[14px]`). Cliquables individuellement, sélection indépendante du parent.
- Compteur header de tactique = nombre de techniques **parents + subs** sélectionnées dans cette tactique.
- Champ recherche en haut : filtre case-insensitive sur id ET name. Une sub-technique matchée force l'expand de son parent (modifie automatiquement `expandedTechniques`).
- Modale : `position: fixed`, backdrop `bg-ink/60`, container `bg-canvas rounded-xl shadow-elevated max-w-[95vw] max-h-[85vh] overflow-hidden`.
- Footer : "Cancel" (jette les changements locaux + ferme), "Apply N techniques" (compteur total ; click → onApply renvoie la liste complète, parent fait le PATCH auto-save US-14.2).
- Focus trap (scope minimal V1, cf AC-15.5) :
- `useEffect` au mount → `searchInputRef.current?.focus()`.
- `onKeyDown` au niveau du container modale :
- `Tab` sans shift sur le dernier élément focusable → `preventDefault()` + focus le premier.
- `Shift+Tab` sur le premier → `preventDefault()` + focus le dernier.
- Récupérer la liste des focusables via `container.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])')`, ignorer ceux `disabled` ou `hidden`.
- Pas de focus restoration ni de live region — out of scope V1.
- Pas de dépendance npm.
- Escape → onCancel. Click backdrop → onCancel.
- **`MitreTechniquePicker.tsx`** (existant, sprint 2) : clean rewrite de la signature.
- Avant : `onChange(id: string | null, name: string | null)` qui remplaçait la valeur.
- Après : `onSelect({id, name})` — un seul match sélectionné, le parent (MitreTechniquesField) gère l'append + le dédup.
- Plus de prop `techniqueId`/`techniqueName` en entrée (le picker est désormais un sélecteur "one-shot" qui se réinitialise après chaque sélection).
**Pages**
- **`SimulationFormPage.tsx`** : remplacer le `<MitreTechniquePicker>` standalone par un `<MitreTechniquesField simulationId={sim.id}>`. Le state `rt.techniques` disparait du form (les techniques ont leur propre cycle de save via le champ lui-même — auto-save). Le bouton "Save Red Team" continue de batcher tous les AUTRES champs (name, description, commands, etc.) mais ne touche pas aux techniques. Affichage read-only (rôle SOC) : afficher les tags sans `×`, boutons Add/Quick Search masqués (`disabled` prop).
- **`SimulationList.tsx`** : colonne MITRE — afficher `techniques[0]?.id + (techniques.length > 1 ? ` +${techniques.length - 1}` : '')`. Si `techniques` est vide, afficher ``.
**Tests Vitest**
- `MitreTechniqueTag.test.tsx` — render id+name, click × appelle onRemove.
- `MitreTechniquesField.test.tsx` — affiche tags, "Add technique" ouvre le modal matrix, "Quick search" ouvre le picker, dédup silencieuse, remove via × appelle onChange avec liste mise à jour.
- `MitreMatrixModal.test.tsx` — render colonnes par tactique, click toggle sélection, Apply renvoie liste, Cancel jette, Escape ferme, search filtre.
- Adapter `MitreTechniquePicker.test.tsx` (sprint 2) à la nouvelle signature `onSelect`.
- Adapter `SimulationFormPage.test.tsx` (sprint 2) — assertions sur `techniques` array au lieu de scalaire.
**Quality bar** : typecheck + lint + vitest clean.
### Règles
- Lit le summary backend EN PREMIER.
- Pas d'invention d'endpoints — `GET /api/mitre/matrix` est le seul nouveau, déjà spec'd.
- Réutiliser `LoadingState`, `ErrorState`, `ConfirmDialog`, `useToast`, action bar pattern (sprint 2) existants.
- Respect DESIGN.md tokens (palette + spacing). Tags = `bg-primary-soft text-primary-deep rounded-full px-md py-xxs gap-xxs text-[14px]`.
- Pas de nouvelle dépendance npm sans escalade au team-lead.
---
## 4. Brief — Test verifier
E2e Playwright. Un fichier par US :
- `us13-multi-techniques.spec.ts` — AC-13.1 → AC-13.5 (focus API + données)
- `us14-techniques-tags.spec.ts` — AC-14.1 → AC-14.5 (UI tags + remove)
- `us15-mitre-matrix-modal.spec.ts` — AC-15.1 → AC-15.5 (modal interaction + a11y)
- `us16-regression-sprint2.spec.ts` — re-exécuter les ACs critiques sprint 2 (auto-transition US-8, workflow US-11, SOC restrictions US-9) avec le nouveau modèle.
Mettre à jour les e2e sprint 2 qui assertaient `mitre_technique_id` / `mitre_technique_name` scalaires (US-8, US-10 selon le grep).
---
## 5. Definition of Done — Sprint 3
- [ ] Tous les AC US-13 → US-16 passent.
- [ ] Backend tests verts (`pytest -q`). Ruff + mypy clean.
- [ ] Frontend tests verts (`npm run test -- --run`). Typecheck + lint clean.
- [ ] E2e Playwright suite verte (sprint 1 + 2 + 3).
- [ ] Migration Alembic testée upgrade + downgrade.
- [ ] SPEC.md mis à jour (multi-techniques acté).
- [ ] README.md mis à jour (mention matrice + multi-tech dans la description workflow).
- [ ] CHANGELOG.md sprint 3 entry sous [Unreleased].
- [ ] Code-reviewer sans BLOCKER.
- [ ] **Frontend-builder a screenshot la matrice modale + la simulation form avec tags AVANT de marquer la tâche terminée (lesson learned sprint 2).**
- [ ] PR ouverte + récap synthétique team-lead.
---
## 6. Décisions arrêtées (utilisateur 2026-05-27)
1. **Storage multi-tech** : colonne JSON `[{id, name}]` (KISS, pattern `commands` sprint 2).
2. **Sub-techniques dans la matrice** : OUI, affichées avec expand/collapse par technique parent. Sub-techniques sont aussi accessibles via Quick Search en plus.
3. **API shape** : `PATCH` reçoit `{technique_ids: ["T1059", "T1059.001", ...]}` — IDs uniquement (parents et subs au même niveau dans la liste). Backend résout names depuis le bundle.
4. **Rétrocompat** : migration backfill `[{id, name}]` depuis les scalaires. Pas de rétrocompat API.
5. **MitreTechniquePicker** : clean rewrite de la signature (`onSelect({id, name})`).
6. **Matrix layout** : colonnes par tactique, 220px fixe, scroll horizontal global.
7. **Apply de la modale matrice** : auto-save immédiat (PATCH déclenché par `MitreTechniquesField` quand le modal renvoie sa liste via `onApply`). Add/remove via tag × ou Quick Search aussi auto-save.
8. **Sprint 4 framing** (anticipation, NE PAS implémenter dans sprint 3) : Dark mode (toggle + tokens dark + persistence) + Hygiène process UI (`design-reviewer` agent + screenshot mandatory dans brief frontend-builder). Connecteur C2 reporté au-delà. Les builders sprint 3 N'ajoutent PAS de tokens dark, N'invoquent PAS le design-reviewer (qui n'existe pas encore). Seule la lesson `screenshots mandatory` est déjà appliquée en sprint 3 dans le brief frontend (§3).
---
## 7. Plan d'exécution
1. ✅ User a validé les 8 décisions §6 (2026-05-27).
2. ✅ Team-lead a mis à jour SPEC.md (§0).
3. ✅ Spec-reviewer : APPROVED WITH NOTES après 2 passes (5 items au total, tous traités).
4. ✅ Backend-builder : commits `b5ea292` + `673b25e` (model + migration + matrix endpoint + 503 unloaded, 162 passing).
5. ✅ Frontend-builder : commit `771483f` (MitreTechniquesField + MitreMatrixModal + tags + auto-save + screenshots, 84 passing).
6. ✅ Code-reviewer : APPROVED WITH NITS (2 MINORs + 4 NITs).
7. ✅ Post-review fixes : `4596f09` + `393b6ed` backend (164 passing) + `39f4076` frontend (86 passing).
8. ✅ Test-verifier : commit `df8a6b6` (105/106 sprint 3 e2e verts, 1 pré-existant sprint 1 — DB pollution, non-régression).
9. 🟡 Team-lead : récap + PR en cours.
4. 🔵 Backend-builder : modèle + migration + endpoints + tests.
5. 🔵 Frontend-builder : composants + page update + tests Vitest. Screenshots obligatoires avant "done".
6. 🔵 Code-reviewer : LSP-first.
7. 🔵 Test-verifier : e2e US-13 → US-16 + adaptation sprint 2.
8. 🟢 Team-lead : PR + récap.