# Import studio → LMS — spec de mapping (L4.6·C1, #311)

> **Statut : PROPOSITION à valider par Enguerran.** Chaque choix marqué `[C1-x]`
> est une décision à confirmer. Le contrat côté studio est FIGÉ (manifeste v1,
> `docs/architecture/lms_contract_changelog.md` du dépôt microlearning) : cette
> spec ne décrit que le côté LMS.

## Principes hérités du contrat (non négociables)

1. **`contract_version` inconnu = refus.** Jamais de parsing best-effort.
2. **`notion_id` stables à vie** : c'est la clé d'idempotence. Ré-importer un
   package régénéré met à jour — ne duplique jamais, ne casse jamais les stats.
3. **Zéro timing importé** : le `reminder_plan` dit QUOI rappeler avec QUEL
   support ; le QUAND (courbe de désapprentissage) appartient au LMS et reste
   HORS lot 4.6.
4. **Les manques sont déclarés, jamais comblés** : `coverage`/`gaps` remontent
   tels quels dans le rapport d'import.

## Mapping proposé

### Notions — nouvelle table (le pivot des stats à vie)

```
notions
  id            bigint PK auto            -- id interne LMS
  studio_id     string UNIQUE NOT NULL    -- le notion_id du studio (clé d'idempotence)
  label         string
  definition    text
  aliases       json
  project_ref   string                    -- project.id du manifeste (provenance)
  created_at / updated_at
```

`[C1-1]` L'id studio est stocké dans `studio_id` (unique), l'id interne reste
un auto-increment LMS — les jointures internes ne dépendent jamais d'une clé
externe. **Upsert par `studio_id`.**

Liaisons :
- `questions.notion_id` (nullable, FK notions) — le capteur d'échec par notion.
- `notion_module` (pivot) : `notion_id`, `course_id`, `is_primary` — reflète
  `primary_module` + `module_refs` du manifeste.

### Modules → cours et sections

`[C1-2]` **Un module studio = un cours LMS** (pas une section) :
- le module porte objectifs, quiz de module, plan de rappel — la granularité
  d'inscription/suivi du LMS ;
- `display_number` + `title` → `courses.name` ; `objectives` → description ;
- les assets du module → **leçons d'une section unique** créée par l'import
  (« Contenu ») — l'ordre du manifeste fait foi (`order_no`).

Alternative écartée : tout le projet = 1 cours et modules = sections. Écartée
parce que le suivi (progression, quiz de module, cockpit) vit au niveau cours
dans le LMS — on perdrait le grain de pilotage.

### Assets → leçons

- `assets[].support_type` → `lessons.support_type_id` via le **référentiel
  #164** (clé `support_types.key`).
- `[C1-3]` **Type inconnu = ligne en échec DANS LE RAPPORT, jamais de création
  silencieuse de type** — la promesse F1 (« l'admin crée un type sans dev »)
  reste un geste d'admin, pas un effet de bord d'import.
- Fichiers du package copiés dans le storage LMS ; chemins relatifs du
  manifeste résolus depuis la racine du package.

### Quiz et questions

- `quiz[].scope=module` → quiz rattaché au cours correspondant.
- `questions[].notion_id` → `questions.notion_id` (via `notions.studio_id`).
- `[C1-4]` Les questions sont upsertées par leur `id` studio (colonne
  `studio_id` ajoutée à `questions`, nullable — les questions créées à la main
  dans le LMS n'en ont pas).

### Remédiation

- `remediation[]` (kits validés jury uniquement, garanti par le studio) →
  table `remediation_kits` : `notion_id`, `state`, `volets` (json), `path`
  d'origine. Simple stockage exposable ; la mécanique de remédiation est hors
  lot.

### Reminder plan

- `modules[].reminder_plan` → colonne `courses.reminder_plan` (json, tel quel).
- Verrou anti-timing : l'import REFUSE un package dont le `reminder_plan`
  contient un champ de calendrier (delay, date, J+…) — testé.

### Fil d'Ariane (synergie #313)

`[C1-5]` Le rôle pédagogique (`Exposition → Récupération active →
Consolidation → Synthèse → Évaluation`) devient une colonne du référentiel
`support_types.pedagogical_role` (défaut par type), avec dérogation
`lessons.pedagogical_role` (nullable, prioritaire). L'import pose la
dérogation si le manifeste la fournit un jour (champ optionnel v1.x).

**Étape 1 — gestion manuelle (#313, livré).** Précision d'Enguerran du 12/08 :
à terme cette donnée **vient du microlearning**, qui connaît le rôle de chaque
support dans son déroulé. En attendant le contrat, le rôle se gère à la main :
défaut par type (écran #164) + dérogation par leçon. La dérogation n'est pas un
pis-aller — c'est exactement le point d'entrée que l'import utilisera.

L'énumération est déclarée dans `App\Support\Content\PedagogicalRole` et **nulle
part ailleurs** : la validation lit `keys()`, les écrans reçoivent `options()`,
et la priorité « dérogation > défaut du type > rien » est tranchée par le seul
`PedagogicalRole::forLesson()`.

#### Rôle par défaut des 7 types système — **proposition à valider**

| Type (`key`) | Famille | Rôle proposé | Pourquoi |
|---|---|---|---|
| `video` | video | **Exposition** | Le support qui porte le déroulé complet de la notion : c'est par lui que l'apprenant la découvre. |
| `article` | page | **Consolidation** | Reprend une notion déjà exposée en y ajoutant exemples et détails — on approfondit, on ne découvre pas. |
| `podcast` | audio | **Consolidation** | Même notion, autre modalité, écoutée en différé : ré-encodage plutôt que première rencontre. |
| `fiche` | document | **Synthèse** | « L'essentiel du module », condensé et fait pour être relu. |
| `infographie` | document | **Synthèse** | Même fonction que la fiche en une image : la vue d'ensemble, pas le détail. |
| `reel` | video | **Récupération active** | Format court diffusé *après* le module, dont la fonction est de faire ressurgir le point clé (réactivation espacée), pas de l'enseigner. |
| `quiz` | page | **Évaluation** | Le seul support qui mesure. |

Deux points à trancher explicitement :

1. **`reel`** est le moins assuré. Le rôle proposé suppose un reel de
   *réactivation après coup* ; utilisé comme accroche **avant** le module, son
   rôle est *Exposition*. Le défaut par type ne peut pas trancher les deux —
   c'est un cas d'école pour la dérogation par leçon.
2. **`podcast`** peut être un support d'**Exposition** dans une formation
   entièrement audio. Le mapping retenu est celui du déroulé standard du
   pipeline (vidéo d'abord).

À noter : *Récupération active* désigne stricto sensu un support qui fait
**rappeler** activement à l'apprenant (effet test). Un quiz intermédiaire de
réactivation relève donc de ce rôle, alors que le quiz de fin relève de
l'Évaluation. Le référentiel ne portant qu'un seul type `quiz`, le défaut est
*Évaluation* et un quiz de réactivation se règle par dérogation.

Un type créé par un admin naît **sans rôle** (`NULL`) : rien n'est deviné, et
un support sans rôle n'affiche aucune étiquette côté apprenant (CA FRONT#26).

## Ce que l'import NE fait PAS (périmètre C2)

Publication automatique (les cours importés naissent **brouillon** — synergie
EA-059), affectation d'apprenants, exécution du plan de rappel, écrasement de
contenus modifiés à la main côté LMS sans le signaler (le rapport liste les
champs divergents avant écrasement `[C1-6]` — stratégie à confirmer :
« l'import fait foi » vs « le LMS fait foi », proposition : **l'import fait
foi sur le contenu, le LMS fait foi sur l'organisation** — catégorie,
affectations, publication).

## Décisions attendues d'Enguerran

| Réf | Question | Proposition |
|---|---|---|
| C1-1 | Clé d'idempotence | `studio_id` unique + id interne LMS |
| C1-2 | Grain module | 1 module = 1 cours |
| C1-3 | Type de support inconnu | échec rapporté, jamais créé |
| C1-4 | Upsert questions | par `studio_id` nullable |
| C1-5 | Fil d'Ariane | défaut par type + dérogation par leçon |
| C1-6 | Conflit contenu modifié à la main | import=contenu, LMS=organisation |
