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

24 KiB
Raw Blame History

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 :
    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_nameOBLIGATOIRE 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] :
    [
      {"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.
  10. 🔵 Backend-builder : modèle + migration + endpoints + tests.
  11. 🔵 Frontend-builder : composants + page update + tests Vitest. Screenshots obligatoires avant "done".
  12. 🔵 Code-reviewer : LSP-first.
  13. 🔵 Test-verifier : e2e US-13 → US-16 + adaptation sprint 2.
  14. 🟢 Team-lead : PR + récap.