# 02 — Architecture cible

**Statut : accepté · Phase : transverse · Mis à jour : 25/10/2026.**

## 1. Vue d'ensemble

```
                      ┌──────────────────────── @recit/core ────────────────────────┐
 story.md / .mdx  ──► │ scan ─► frontmatter ─► split en scènes ─► directives ─► AST │
 recit.config.ts  ──► │ defineConfig / mergeConfig ─► thème normalisé (tokens HSL)  │
                      │ rules (densité, accessibilité) ─► diagnostics               │
                      └──────────────────────────────┬──────────────────────────────┘
                                                     │ Story { meta, settings, scenes[], diagnostics }
     ┌──────────────┬──────────────┬───────────────┼──────────────┬──────────────┬──────────────┐
     ▼              ▼              ▼               ▼              ▼              ▼              │
 @recit/deck   @recit/motion   @recit/site-stories   @recit/report  @recit/toon    @recit/book        │
 slides,       Story ─►        routes,         sections,      cases, bulles, chapitres, notes,  │
 présentateur  handover ─►     embeds MDX,     :value,        lecteur scroll, renvois, index    │
 HTML / PDF    Remotion MP4    Pagefind        ::chart        tranches, PDF  PDF / EPUB / web   │
     │                                              │                              │            │
     └──────────── PDF, par la CLI ─────────────────┴──────────────────────────────┘            │
                              ▼                                                                 │
     @recit/paged (Node, CLI seulement : runner Chromium, Paged.js pour book)                   │
                                     @recit/data (sources, agrégations, graphiques SVG) ◄───────┘
          @recit/theme (tokens d'une seule teinte, CSS de chaque format, thèmes motion et lettrage)
          @recit/cli : recit dev | build | export [--to pdf|html|epub|images|ics|rss]
                       | render | convert | validate | mcp (liste unique : 03-packaging.md §6)
```

## 2. Le modèle `Story`

On généralise `CompiledDeck` (`presentation-engine/packages/core/src/types`). Les champs propres à
un format sont sous une clé dédiée. Le cœur ne connaît aucun format.

```ts
type StoryFormat =
  | "deck"
  | "motion"
  | "docs"
  | "report"
  | "toon"
  | "book"
  | "roadmap"
  | "calendar"
  | "stories"; // specs : 05-video.md, 08-formats.md ; puis glossary, directory, atlas dans leur phase (règle B1)

type Story = {
  version: 1;
  format: StoryFormat; // frontmatter `format:`, défaut "deck"
  meta: { title: string; description?: string; author?: string; lang: string };
  settings: ResolvedConfig; // recit.config.ts + frontmatter, après mergeConfig
  design: { width: number; height: number; aspectRatio: string };
  themeCss: string;
  scenes: Scene[];
  diagnostics: Diagnostic[];
  summary: { totalDurationMs: number; formattedTotalDuration: string };
};

type Scene = {
  id: string; // stable : `id=` explicite, sinon s1, s2… (comme le handover emaki)
  index: number;
  source: { file: string; line: number; endLine: number }; // pour les erreurs et la réécriture
  attrs: Record<string, string>; // attributs du séparateur : layout, type, hold, dur, cols…
  html: string; // rendu HTML sanitizé (deck, docs)
  mdast?: Root; // conservé pour les formats qui réinterprètent (motion)
  steps: number;
  notesHtml?: string;
  durationMs?: number;
  density: DensityReport;
  accessibility: A11yReport;
};
```

- `compileDeck` devient `compileStory`. `compileDeck` reste un alias pendant la migration.
- Le **deck** utilise `scene.html`, `steps` et `notesHtml`, comme aujourd'hui.
- Le **motion** relit `scene.mdast` et `scene.attrs` pour produire un handover emaki (section 4).
- Les composants MDX restent compilés côté CLI ou Vite, un module par scène
  (`<file>.recit-scene-N.jsx`, ex-`presentation-slide-N.jsx`, voir 6.2).

## 2 bis. Entrées : Markdown, JSON et langues

`compileStory` accepte un `.md`/`.mdx` **ou** un `.json` typé (`StorySource`), complété par des
compagnons JSON et des variantes de langue (`example.fr.md`, `example.fr.json`). Le Markdown est
d'abord converti en `StorySource`, puis tout suit le même chemin. La spécification complète (schémas
zod, JSON Schema publiés, fusion par `id` de scène, résolution des langues et repli) est dans
`09-langues-et-json.md`.

```
example.md ─┐                      example.json (sidecar) ─┐
example.fr.md ─► markdownToSource ─► StorySource ◄─ fusion ◄─ example.fr.json
example.json (story) ────────────────┘      │
                                            ▼
                             compileStory(source, { locale }) ─► Story ─► format
```

## 3. Syntaxe commune

La syntaxe de presentation-engine est la référence. Elle est déjà documentée dans
`presentation-engine/docs/pages/documentation/writing`.

```md
---
title: Lorem ipsum
format: deck # deck | motion | docs…
aspectRatio: 16:9
lang: en
---

--- layout=cover id=open

# Lorem ipsum

### Dolor sit amet

:::notes
Speaker notes.
:::

--- layout=grid cols=3

:::card

### Lorem

:::
```

| Élément              | Syntaxe                                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Métadonnées du récit | frontmatter YAML                                                                                                                     |
| Séparateur de scène  | `---` seul, ou `--- clé=valeur …` (attributs de scène)                                                                               |
| Blocs                | directives `:::card`, `:::callout`, `:::step`, `:::left` / `:::right`, `:::col`, `:::code-group`, `:::component{name=…}`, `:::notes` |
| Inline               | `:badge[…]{type=…}`, `::image{src alt fit}`, `:kbd[…]`                                                                               |
| MDX                  | `.mdx` : imports ESM et composants React                                                                                             |

**Registre des directives (règle B4, `06-roadmap.md`).** Le cœur ne garde que les **noms** des
directives et des attributs réservés. Le plugin de chaque bloc vit dans la fondation qui le porte :
`::chart` dans `@recit/data`, `::map` dans `@recit/geo`, `:term` (P14, plan 12) et le bloc
` ```pixel ` dans `@recit/components`. Attributs réservés : `type=`, `entry=`, `end`, `prompt=`,
`cast=`, `beat=`, `set=`, `shot=`, `pose=`, `expression=`, `camera=`, `background` (jamais `bg`). Le
mot `kind` est réservé au discriminant des documents JSON (`09-langues-et-json.md` §2.2) : aucun
réglage ni attribut ne s'appelle `kind`. Les réglages du frontmatter sont **à plat**
(`08-formats.md`) ; la forme imbriquée n'existe que dans `recit.config`.

**Décision à valider : les directives de scène d'emaki.** `emaki/PLAN.md` (phase 4) propose
`<!-- type: stat, id: proof, hold: 1 -->`, invisible sur GitHub. Récit retient les **attributs du
séparateur** (`--- type=stat id=proof hold=1`) comme forme canonique. Le commentaire HTML est
accepté comme alias en première ligne de scène, et le parseur fusionne les deux. Une seule forme est
émise lors de la réécriture : la canonique.

## 4. Pont deck ↔ motion

Une même source peut être rendue en deck **et** en vidéo (`recit build` et `recit render`). On passe
d'un layout de scène à un bloc emaki par un **tableau fermé**. Une forme sans correspondance produit
une erreur avec numéro de ligne, jamais une devinette, selon la règle emaki « aucune métrique
inventée ».

| Scène Récit                                                       | Bloc emaki     | Props                                                                                                             |
| ----------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------- |
| `layout=cover` (H1 et H3 ou paragraphe)                           | `title`        | H1 → `text`, sous-titre → `kicker`                                                                                |
| `layout=section` (H1 ou H2)                                       | `chapter`      | titre → `title`, `number` via attribut ou préfixe `01 ·`                                                          |
| `layout=quote`, ou paragraphe seul                                | `statement`    | texte → `text`, **gras** → `emphasis[]`                                                                           |
| titre et liste à puces                                            | `list`         | titre → `title`, puces → `items[]`                                                                                |
| titre et tableau `label \| before \| after`                       | `compare-bars` | lignes → `rows[]`, `unit` via attribut                                                                            |
| `type=stat` explicite                                             | `stat`         | H1 → `value`, paragraphe → `label`, _italique_ → `caption`                                                        |
| `type=ui-mock` / `type=ui-scene`                                  | idem           | bloc ` ```yaml ` = props                                                                                          |
| tout bloc ` ```yaml ` ou ` ```json ` avec `type=`                 | `type`         | props du bloc (échappatoire universelle)                                                                          |
| `grid`, `split`, `image-*`, `code`, Mermaid, KaTeX, composant MDX | —              | **erreur** en v1 (`motion: layout "grid" has no block`). Rendu en image fixe en v2 (section 5.3 de `05-video.md`) |

`:::notes` devient la **voix off** ou le sous-titre de la scène (champ à ajouter au schéma emaki).
Les `:::step` deviennent des temps d'apparition (`hold`).

## 5. Configuration : un seul `recit.config.ts`

**`recit.config.ts` est le seul fichier de configuration d'un projet Récit.** Il remplace :

- `recit.config.ts` / `recit.config.tsx` (configuration du site du framework de sites) ;
- `presentation.config.ts` (configuration des decks de presentation-engine).

Un projet sans site (un deck seul, lancé avec `recit dev slides.md`) n'y met que les sections de
format. Un site y ajoute les options du framework de sites habituelles, au même niveau.

```ts
import { defineConfig } from "recit/config"; // réexporte @recit/core/config

export default defineConfig({
  // — options du framework de sites (ex-recit.config.ts), facultatives —
  site: { title: "Lorem", logo: { src: { light: "/logo-light.svg", dark: "/logo-dark.svg" } } },
  navigation: [
    /* … */
  ],
  redirects: [
    /* … */
  ],

  // — options Récit communes —
  theme: {
    colors: { primary: "#7c3aed" },
    radius: "0.75rem",
    font: {
      /* … */
    },
  },
  footer: { left: "{title}", right: "{page}/{total}", hideOn: ["cover"] },
  components: "./components/index.ts",
  publish: { dir: "./stories", routePrefix: "/present" }, // récits publiés par le site (ex-`stories:`)

  // — options propres à chaque format, facultatives —
  deck: { transition: "slide", presenter: true },
  motion: { theme: "warm-editorial", wordsPerSecond: 2.6, aspect: "9:16" },
  report: { pdf: { page: { size: "A4" } } },
  calendar: { timezone: "Europe/Paris" },
  // stories: { … }, réglages du format stories (la section du site s'appelle `publish:`)
  // terms: { … }, glossaire partagé, à la phase du plan 12 (P14)

  // — langues : fonctionnalité optionnelle (09-langues-et-json.md §4) —
  // i18n: { defaultLocale: "en", locales: ["en", "fr", "ja", "ar", "sw"] },
});
```

Comment ça marche :

- **`defineConfig`** valide tout le fichier avec zod 4 et sépare les deux parties :
  - les options du framework de sites sont passées telles quelles au chargeur du framework de sites.
    Elles sont validées par le framework de sites lui-même, qui retire les clés inconnues : les
    sections Récit doivent donc être extraites avant ;
  - les sections Récit configurent `recitPlugin` et `recitVitePlugin`, qui sont **injectés
    automatiquement** dans `plugins`. Plus de double déclaration (plugin Vite + plugin le framework
    de sites) : cela remplace le patch « clé `stories` » prévu en 6.2.
- **Chargement** : le cœur ne connaît que `recit.config.*` (liste en dur dans
  `packages/site/src/config/loader.ts`). La liste contient `recit.config.{ts,tsx,js,jsx,mjs}`.
- **Compatibilité** : un `recit.config.ts` existant continue de fonctionner, avec un avertissement
  qui invite à le renommer. `presentation.config.ts` est lu avec le même avertissement, jusqu'à la
  0.2.
- `recit dev | build` (CLI) lit le même fichier qu'un site : un deck a le même rendu en CLI et dans
  la doc.
- Le schéma est en **zod 4** (catalogue `4.6.5`, `zod/mini`), fait en P3 dans
  `packages/core/src/config/validate.ts` (presentation-engine validait à la main). Le JSON Schema de
  `recit.config` est publié avec les autres (`recit.dev/schema/v1/config.json`).
- **`publish:` et `stories:`** (tranché le 24/10) : la section du site qui publie les récits
  s'appelle `publish: { dir, routePrefix, decks, layout, components, loadDeckConfig }`
  (ex-`stories:` ; `decks` liste les récits hors de `dir`, comme aujourd'hui). La clé `stories:` est
  libre pour les réglages du format stories, comme `deck:` ou `motion:` pour les leurs : la règle «
  `settings` d'un récit = section de son format » vaut pour tous les formats
  (`09-langues-et-json.md` §2.2, `15-export-pdf.md` §3.2). Le renommage est fait dans le code le
  24/10 (`PublishConfigSchema` dans `packages/core/src/config/validate.ts`), **sans alias** : une
  clé de site sous `stories:` échoue à la validation, avec un message qui renvoie à `publish.<clé>`.
- Toutes les sections sont **facultatives**. Sans section de format, ce sont les valeurs par défaut.
  Sans `i18n`, le projet est monolingue (voir `09-langues-et-json.md` §4).

## 6. Intégration au le framework de sites

### 6.1 Niveau 1 : plugin (aucun patch du cœur)

C'est ce que fait déjà `@presentation/site-extension` sur le framework de sites 0.86 :

- `recitVitePlugin({ dir, decks })` : module virtuel `virtual:recit-stories`, une compilation par
  récit, des modules MDX par scène, et `?raw` des sources.
- `recitPlugin({ routePrefix: "/present", layout: "none", components })` :
  - `getRoutes()` déclare une route par récit ;
  - `getMdxComponents()` expose `<StoryEmbed slug slide />` (ex-`PresentationEmbed`) et
    `<StoryGallery />` (ex-`PresentationGallery`).
- Recherche : sous-résultats Pagefind `#slide-N`, contenu marqué `data-pagefind-body`.
- CSS : limitée par postcss à `.recit-wrapper` (ex-`.site-presentation-wrapper`). On ajoute en tête
  la déclaration d'ordre des layers `@layer properties; @layer theme, base, components, utilities;`.
  **À conserver**, c'est ce qui a réglé le conflit de styles avec le framework de sites.
- Hydratation : rendu client seulement, via `useSyncExternalStore`. C'est ce qui a corrigé l'erreur
  React #418.

À adapter au passage de 0.86 à 0.93 : API des plugins (`SitePlugin`, `getRoutes`,
`getMdxComponents`), react-router 8.4, Vite 8.1, Shiki 4.5. Le test d'intégration
`examples/site-embed` sert de garde-fou.

### 6.2 Niveau 2 : support natif (patchs du fork, plus tard)

À faire seulement après le niveau 1, et si le besoin est réel. Chaque patch est listé ici **et**
dans `UPSTREAM.md` (supprimé depuis), section « Local patches », qui fait foi.

| Patch                                                                                                                           | Pourquoi                                                                                                                                     | Fichier du framework de sites touché |
| ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| ~~Clé de config `stories: { dir, routePrefix }` dans `recit.config.tsx`~~ remplacée par `recit.config.ts` (§5) et le patch n° 3 | Éviter la double déclaration (plugin Vite + plugin du framework de sites)                                                                    | schéma de config, chargeur Vite      |
| Type de navigation `{ type: "story", slug }`                                                                                    | Une entrée de sidebar avec l'icône du format et le nombre de scènes                                                                          | schéma `navigation`                  |
| Afficher le `title` des blocs de code                                                                                           | Problème connu : Le framework de sites affiche « Code » au lieu du `title`                                                                   | plugin MDX ou Shiki                  |
| Ids de modules MDX                                                                                                              | Le plugin MDX du framework de sites intercepte nos modules de scène, d'où le contournement `.jsx`. Une option d'exclusion serait plus propre | plugin MDX                           |

## 7. Ce qui reste séparé

- **Remotion et Chrome Headless Shell** (environ 93 Mo) ne sont **jamais** des dépendances de
  `@recit/core`, `@recit/deck` ou `@recit/site-stories`. Ils vivent uniquement dans
  `@recit/motion-render`.
- **Shiki** reste côté build. L'entrée `@recit/core/browser` n'importe ni Shiki ni le pipeline Node
  (stubs `optionalDependenciesPlugin`).
- **Mermaid** est chargé côté client, à la demande.
- **KaTeX** : CSS embarquée en local, aucun CDN.
