# 18 — Applications desktop et mobiles

**Statut : fait · Phase : transverse · Mis à jour : 29/10/2026.**

Décidé le 29/10/2026. Tout site construit avec Récit doit pouvoir devenir une application de bureau
(Linux, macOS, Windows) puis mobile (Android, iOS), **sans code par site** : une coquille Tauri 2
commune, configurée par une section `app:` du `recit.config`, qui embarque le site prérendu et le
sert hors ligne. Le modèle est la mise en place de Tauri du projet `sass`
(`/home/deploy/work/sass`), reprise là où elle s'applique et retournée là où Récit diffère : `sass`
est le **client d'un serveur**, un site Récit est **un dossier de fichiers**.

## 1. Objectifs et non-objectifs

**Objectifs.**

- Un site Récit (`sites/*`, `examples/site-*`, le site d'un utilisateur) se construit en application
  native avec une commande : `recit app build`, à partir du même `dist/` que `recit-site build`.
- Hors ligne complet : pages, recherche Pagefind, polices, récits publiés (`publish:`), diagrammes.
- Une seule coquille Rust, versionnée dans un paquet publié, jamais copiée dans les sites.
- La configuration est typée dans `@recit/core` (zod, `z.infer`), comme toute section de
  `recit.config`, et présente dans le JSON Schema `config`.
- Les lots qui n'exigent pas la chaîne Rust (contrat, bundle, paquet, CLI, e2e web, docs) se font et
  se vérifient sur cette machine ; les lots natifs et mobiles sont marqués **toolchain** ou **CI**.

**Non-objectifs.**

- Pas de serveur embarqué, pas de SSR dans l'application : le mode `--experimental-ssr` du framework
  reste une affaire d'hébergement.
- Pas d'authentification, de notifications, de stockage sécurisé ni de lien profond au départ :
  `sass` en a besoin pour son SaaS, un site de contenu non. Le lien profond reste une option de
  `app:` (§4.4) pour plus tard.
- Pas de publication sur les stores dans ce plan : la question des comptes est ouverte (§10).
- `recit render` (MP4) reste une chaîne Node + Chromium, hors application.

## 2. Ce que fait `sass`, et ce qui se transfère

### 2.1 Constats, avec les fichiers

| Mécanisme                        | Où                                                                                               | Constat                                                                                                                                                                                                                                             |
| -------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tauri **2**                      | `apps/desktop/src-tauri/tauri.conf.json:2`, `Cargo.toml:16-20`                                   | `$schema …/config/2`, `tauri = "2"`, plugins `notification`, `deep-link`, `store`, `http`                                                                                                                                                           |
| Frontend **distant**             | `tauri.conf.json:6-10`                                                                           | `devUrl: http://localhost:3000`, `frontendDist: "https://app.tonsaas.com"` : l'app est un client de l'app Next déployée, qui a besoin d'un serveur (routes API, SQLite)                                                                             |
| Permissions pour pages distantes | `capabilities/default.json:6-8`, `:9-15`                                                         | `remote.urls` liste les origines qui ont le droit d'appeler les plugins ; sans cela l'app s'ouvre mais les appels sont refusés                                                                                                                      |
| Code Rust                        | `src/lib.rs`, `src/main.rs`, `build.rs`                                                          | 4 plugins, 2 commandes de démonstration (`greet`, `get_system_info`), `#[cfg_attr(mobile, tauri::mobile_entry_point)]` ; `crate-type = ["staticlib","cdylib","rlib"]` (`Cargo.toml:10`), prêt pour le mobile                                        |
| **Une coquille, N applications** | `packages/sites/scripts/app.ts:42-61`, `:63-66`, `:73`                                           | un overlay JSON par site (`productName`, `identifier`, `build.devUrl/frontendDist`, fenêtre, capabilities) écrit dans `apps/desktop/.site-configs/<site>.json` (ignoré par git, `apps/desktop/.gitignore:1`) et passé à `tauri dev\|build --config` |
| Réglages par site                | `app.ts:20-24`, `sites/engravingcol/app.json`, `sites/sass/app.json`                             | `app.json` facultatif : `productName`, `identifier`, `url` ; identifiant par défaut `com.tonsaas.<sous-domaine>` sans tirets (`:44`)                                                                                                                |
| Réutilisation du serveur de dev  | `app.ts:29-40`, `:45`                                                                            | sonde du port 3000 ; s'il répond, `beforeDevCommand: ""` et l'app se branche dessus                                                                                                                                                                 |
| Pont JavaScript                  | `packages/native/src/bridge.ts`, `useTauri.ts`, `client.ts`                                      | `isTauriEnvironment()` (`__TAURI_INTERNALS__` dans `window`), imports dynamiques des plugins avec repli web ; **utilisé par aucun écran** (`apps/web/src` n'importe pas `@saas/native`)                                                             |
| Mobile                           | `icons/android`, `icons/ios` ; `apps/docs/pages/sites/application-desktop-et-mobile.md` § Mobile | icônes générées par `tauri icon`, mais **aucune cible initialisée** (pas de `gen/`) ; Xcode et SDK Android absents de la machine de dev ; rien de vérifié                                                                                           |
| Stores                           | `apps/docs/pages/projet/questions-ouvertes.md:76-78` (Q12)                                       | qui publie, avec quel compte : non tranché ; recommandation de ne proposer le mobile qu'à la demande                                                                                                                                                |
| Lien profond                     | `apps/docs/pages/securite/authentification.md:47-49`                                             | `tonsaas://auth/callback?token=` prévu, jamais branché                                                                                                                                                                                              |
| Mise à jour, signature, CI       | —                                                                                                | aucune (pas de `.github/`, pas d'updater, pas de clé)                                                                                                                                                                                               |
| Commandes                        | `package.json:13-14`, `Makefile:38-45`                                                           | `desktop:dev`, `desktop:build`, `desktop-check` (`cargo check`)                                                                                                                                                                                     |

### 2.2 Ce qui se transfère

| Mécanisme de `sass`                                  | Dans Récit                                                                                                                   |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Tauri 2, `crate-type` et point d'entrée mobile       | **tel quel** dans la coquille de `@recit/app` (§4.3)                                                                         |
| Une coquille, N apps par overlay `--config` généré   | **tel quel**, l'overlay est généré depuis `app:` du `recit.config` (§4.4) et non depuis un `app.json`                        |
| Réutilisation d'un serveur de dev déjà lancé         | **tel quel** dans `recit app dev` (§4.7)                                                                                     |
| Icônes de toutes les plateformes par `tauri icon`    | **tel quel**, depuis l'icône du site (§4.8)                                                                                  |
| `frontendDist` distant, `remote.urls`                | **sans objet** : le bundle prérendu est embarqué, la capability ne nomme aucune origine distante                             |
| Plugins `notification`, `store`, `http`, `deep-link` | **sans objet** au départ ; un seul plugin, `opener`, pour les liens externes (§4.6) ; `deep-link` reste une option de `app:` |
| Pont JavaScript `@saas/native`                       | **sans objet** : la garde de navigation est côté Rust, **zéro code client**, `@recit/site` n'est pas touché                  |
| Makefile et scripts `desktop:*`                      | **adapté** : sous-commandes `recit app …`, et un script `app:build` dans le `package.json` des sites qui le veulent          |

## 3. Prérequis machine

La machine de travail actuelle (Debian, noyau 6.1 cloud, **sans affichage**) a node 22.22 et pnpm
11.6, et n'a ni `rustc`, `cargo`, `rustup`, ni Java, ni SDK/NDK Android, ni `adb`, ni les
bibliothèques de développement WebKitGTK ; `build-essential` est présent.

| Besoin                                                                             | Niveau             | Comment                                                                                                                        | Sert à                                   |
| ---------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| Rust stable (`rustc`, `cargo`)                                                     | utilisateur        | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh -s -- -y`, puis `rustup default stable`                       | `cargo check`, tout `tauri build`        |
| Cibles Android                                                                     | utilisateur        | `rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`                      | APP7                                     |
| Cibles iOS                                                                         | utilisateur (Mac)  | `rustup target add aarch64-apple-ios aarch64-apple-ios-sim`                                                                    | APP8                                     |
| CLI Tauri                                                                          | utilisateur (pnpm) | `@tauri-apps/cli` en dépendance de `@recit/app`, épinglé dans le catalogue de `pnpm-workspace.yaml`                            | APP1 et suivants                         |
| WebKitGTK et GTK (Linux)                                                           | **apt, sudo**      | `libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libxdo-dev pkg-config file curl wget` | APP5 sur cette machine ; sinon CI (APP6) |
| JDK 17                                                                             | apt ou utilisateur | `openjdk-17-jdk` (sudo) ou une archive Temurin dans `~/.jdks` (sans sudo), `JAVA_HOME`                                         | APP7                                     |
| SDK Android (cmdline-tools, platform-tools, platforms 34, build-tools) et NDK ≥ 26 | utilisateur        | `sdkmanager` dans `~/Android/Sdk`, `ANDROID_HOME`, `NDK_HOME`                                                                  | APP7                                     |
| Xcode complet, CocoaPods                                                           | Mac                | App Store puis `xcode-select --install`, `brew install cocoapods`                                                              | APP8                                     |
| Un affichage                                                                       | —                  | `tauri build` lie sans affichage ; `tauri dev` et un test de fumée demandent Xvfb (`xvfb-run`) ou un poste avec écran          | APP5, APP6                               |

Conséquence sur l'ordre : APP0 à APP4 ne touchent pas à Rust et s'exécutent ici ; APP5 demande
`rustup` **et** la ligne apt ; APP6 porte tout ce que la machine ne peut pas faire sur des runners
GitHub ; APP7 et APP8 sont **CI** ou poste équipé.

## 4. Architecture cible

### 4.1 Vue d'ensemble

```text
recit.config.ts ── app: ──▶ @recit/core/config  (AppConfigSchema, resolveAppConfig)   [APP0]
                                 │
recit app build ──▶ @recit/cli ──▶ @recit/app (scope:tool, Node)                      [APP2 → APP1]
                                 │   ├─ matérialise <site>/.recit/app/  (coquille + tauri.conf.json générés)
                                 │   ├─ lance `recit-site build`  →  <site>/dist/        (le bundle, inchangé)
                                 │   └─ lance `tauri build --config …`  (frontendDist = ../../dist)
                                 ▼
                     .deb / .AppImage / .dmg / .msi / .apk / .aab / .ipa
```

Trois règles de `03-packaging.md` §8 fixent la place de chaque chose :

- `@recit/app` porte `scope:tool`, comme `@recit/paged` : Node uniquement, importé par la CLI
  (`scope:distribution`) et par personne d'autre. Un outil peut dépendre des fondations ; il n'a
  besoin que de `@recit/core`. Aucune nouvelle arête dans `scripts/check-package-boundaries.js`.
- Un paquet publié n'importe jamais `@recit/site`. `@recit/app` **lance** `recit-site build` (le
  binaire est une dépendance de chaque site) et lit le `dist/` ; il n'importe rien du framework.
- Une seule stack : `@tauri-apps/cli` est la seule dépendance nouvelle, dans `@recit/app` seulement
  ; `recit` la charge à la demande comme `@recit/motion-render`
  (`packages/cli/src/commands/motion.ts:200-213`, `peerDependenciesMeta` de
  `packages/cli/package.json`). Un utilisateur qui n'en veut pas ne la télécharge pas.

### 4.2 Le paquet `@recit/app`

`packages/tools/app`, publié (ajouté à `release.projects` de `nx.json`, vérifié par
`scripts/__tests__/release-workflow.spec.ts`). Forme d'un paquet publié de `03-packaging.md` §8 :
`exports` à trois conditions (`types`, `@recit/source`, `default`), construit par `tsdown` comme
`packages/formats/motion-render`.

API, le contrat que APP2 et APP3 consomment (les types de configuration viennent de `@recit/core`) :

```ts
import type { Diagnostic } from "@recit/core";
import type { ResolvedAppConfig } from "@recit/core/config";

export type ShellTarget = "desktop" | "android" | "ios";

/** tauri.conf.json complet, objet JSON pur, testé par instantané */
export function tauriConfig(
  app: ResolvedAppConfig,
  options: { target: ShellTarget; frontendDist: string; devUrl?: string },
): Record<string, unknown>;

/** Écrit la coquille et sa configuration dans <siteDir>/.recit/app ; idempotent */
export function materializeShell(options: {
  siteDir: string;
  app: ResolvedAppConfig;
  target: ShellTarget;
  devUrl?: string;
}): Promise<{ shellDir: string; configFile: string; diagnostics: Diagnostic[] }>;

/** Lance @tauri-apps/cli dans la coquille (code de sortie) */
export function runTauri(
  shellDir: string,
  args: readonly string[],
  options?: { env?: NodeJS.ProcessEnv },
): Promise<number>;

/** Sert dist/ en résolvant les chemins comme le protocole de Tauri (§4.5) */
export function serveLikeTauri(
  distDir: string,
  options?: { port?: number },
): Promise<{ url: string; close(): Promise<void> }>;

/** Un serveur répond-il déjà sur ce port ? (sass, app.ts:29-40) */
export function isPortInUse(port: number): Promise<boolean>;
```

### 4.3 La coquille

`packages/tools/app/shell/`, copiée telle quelle dans `<site>/.recit/app/` par `materializeShell`
(dossier ignoré par git, comme `.site-configs/` de `sass` ; la ligne `.recit/` s'ajoute au
`.gitignore` racine) :

| Fichier                     | Rôle                                                                                                                                                |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cargo.toml`                | `tauri = "2"`, `tauri-plugin-opener = "2"`, `serde`, `serde_json` ; `[lib] name = "recit_app_lib"`, `crate-type = ["staticlib", "cdylib", "rlib"]`  |
| `build.rs`                  | `tauri_build::build()`                                                                                                                              |
| `src/main.rs`               | `recit_app_lib::run()` avec `windows_subsystem = "windows"` hors debug (comme `sass`)                                                               |
| `src/lib.rs`                | `#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run()` : plugin `opener`, garde `on_navigation` (§4.6), fenêtre montrée après `on_page_load` |
| `capabilities/default.json` | `windows: ["*"]`, `permissions: ["core:default", "opener:default"]`, **sans** `remote`                                                              |
| `tauri.conf.json`           | **généré** par `tauriConfig()` à chaque appel, jamais édité à la main                                                                               |
| `icons/`                    | **générés** par `tauri icon` (§4.8)                                                                                                                 |
| `.gitignore`                | `target/`, `gen/`, `icons/`                                                                                                                         |

Le dossier `target/` de Cargo vit dans `<site>/.recit/app/target` par défaut ;
`RECIT_APP_TARGET_DIR` le déplace pour partager le cache entre les sites d'un monorepo.

Le `tauri.conf.json` généré, pour un site :

```jsonc
{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "Récit Docs",
  "version": "0.93.0",                      // app.version, sinon version du package.json du site
  "identifier": "dev.recit.docs",
  "build": {
    "devUrl": "http://localhost:3001",      // recit app dev seulement
    "frontendDist": "../../dist",           // relatif à .recit/app/
    "beforeDevCommand": "",                 // recit gère le serveur de dev lui-même (§4.7)
    "beforeBuildCommand": ""                // et le build du site
  },
  "app": {
    "windows": [{ "title": "Récit Docs", "width": 1280, "height": 800, "visible": false }],
    "security": { "csp": null }
  },
  "bundle": { "active": true, "targets": "all", "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"] }
}
```

`bundle.android.minSdkVersion`, `bundle.iOS.developmentTeam`, `plugins.deep-link` et
`plugins.updater` n'apparaissent que si la section `app:` les renseigne.

### 4.4 La section `app:` du `recit.config`

Dans `packages/core/src/config/validate.ts`, à côté de `DeckConfigSchema` et `PublishConfigSchema`,
et ajoutée à `RecitConfigSchema` (`app: z.optional(AppConfigSchema)`). C'est une **section Récit** :
`defineConfig` la retire des options du site et la range sous `RECIT_CONFIG`
(`packages/core/src/config/define.ts:47-57`), donc elle n'entre pas dans `SITE_CONFIG_KEYS` et le
test `packages/integrations/site-stories/src/__tests__/site-config.spec.ts` (« Récit sections never
collide with site options ») reste vert sans changement. Le JSON Schema `config` suit tout seul
(`packages/core/src/schema/json-schema.ts:25`).

```ts
export const AppConfigSchema = z.object({
  /** Nom de l'application et de sa fenêtre. Défaut : site.title, sinon le dossier du site */
  productName: optionalString,
  /** Identifiant inversé (dev.recit.docs), figé après publication. Défaut : canonicalUrlOrigin inversé */
  identifier: optionalString,
  /** Version affichée. Défaut : version du package.json du site */
  version: optionalString,
  /** Icône source carrée, PNG ≥ 1024 px ou SVG, relative au site. Défaut : metadata.favicon */
  icon: optionalString,
  window: z.optional(
    z.object({
      width: z.optional(positiveInt),
      height: z.optional(positiveInt),
      minWidth: z.optional(positiveInt),
      minHeight: z.optional(positiveInt),
      resizable: optionalBoolean,
    }),
  ),
  /** Origines que la fenêtre a le droit de charger en plus du bundle ; tout le reste s'ouvre dans le navigateur */
  allowedOrigins: z.optional(z.array(z.string())),
  /** Schéma d'URL qui ouvre l'application (recit-docs://…) */
  deepLink: optionalString,
  android: z.optional(z.object({ minSdkVersion: z.optional(positiveInt) })),
  ios: z.optional(z.object({ developmentTeam: optionalString })),
  updater: z.optional(z.object({ endpoints: z.array(z.string()), pubkey: z.string() })),
});
export type AppConfig = z.infer<typeof AppConfigSchema>;
```

`resolveAppConfig(config, site: { dir, packageVersion })` rend un `ResolvedAppConfig` où chaque
champ a sa valeur, plus des `Diagnostic[]` (type de `@recit/core`) :

- `identifier` : au moins deux segments, chacun `[a-zA-Z][a-zA-Z0-9_]*` (règle Android, pas de tiret
  ; `sass` les supprime, `app.ts:44`). Sans valeur ni `canonicalUrlOrigin` : **erreur**, le message
  donne la règle.
- `basePath` renseigné : **erreur**. Tauri sert `frontendDist` à la racine, or le routeur
  (`createBrowserRouter`, `basename`, `packages/site/src/app/entry.client.tsx:68-70`) et Pagefind
  (`PagefindSearch.tsx:37`) attendent le préfixe. Les sites Récit n'en ont pas.
- `cdnUrl` renseigné : **erreur**, les assets viendraient du réseau.
- `search.type: "inkeep"` : avertissement, la recherche est distante.
- `app` absent : erreur de `recit app`, « ajoutez une section `app:` », avec l'exemple minimal.

### 4.5 Le bundle statique

`recit-site build` produit déjà ce qu'il faut, sans option nouvelle :

- une page par route en `<chemin>.html` (`packages/site/src/vite/prerender/worker.ts:33`),
  `index.html` pour `/`, et un fichier de 85 octets par redirection ;
- des assets référencés en **relatif** (`renderBuiltUrl` rend `{ relative: true }` sans CDN,
  `packages/site/src/vite/config.ts:196-206`) ;
- les polices embarquées (`@fontsource-variable/geist`, `packages/site/src/app/main.css:1-2`),
  Shiki, Mermaid, KaTeX dans le bundle ;
- l'index Pagefind dans `dist/pagefind/` (`prerender.ts:216-218`), chargé par import dynamique de
  `/pagefind/pagefind.js` (`PagefindSearch.tsx:36-37`) ; `pagefind.js` charge son `.wasm` et ses
  fragments relativement à lui-même ;
- le dossier `server/` supprimé après prérendu (`packages/site/src/vite/build.ts:236`).

Le protocole de Tauri (`tauri://localhost` sur macOS, Linux et iOS, `http://tauri.localhost` sur
Windows et Android) résout une requête `/a/b` en essayant `a/b`, `a/b.html`, `a/b/index.html` puis
**`index.html`** en repli : un rechargement sur une page profonde trouve son `.html`, un chemin
inconnu tombe sur la route 404 du routeur (`packages/site/src/app/notFoundRoute.tsx`), le `.wasm`
est servi avec son type MIME. `serveLikeTauri()` reproduit exactement cette chaîne pour la vérifier
en e2e **sans Rust** (APP3) et pour `recit app preview`.

Ce qui reste dans le bundle et ne sert à rien dans l'app (sitemap, `llms.txt`, copies `.md` de
`publishMarkdown`) pèse quelques centaines de kilo-octets : on ne filtre pas.

### 4.6 Liens externes et navigation

Un site Récit renvoie vers les autres sites (`header.navigation` de
`packages/tools/site-kit/src/config.tsx:100-109`), GitHub, et les liens des pages. Dans la fenêtre
native, ces navigations doivent partir dans le navigateur du système, pas remplacer l'application.
Deux mécanismes, tous deux côté Rust :

1. `tauri::Builder::on_navigation` : autorise l'origine du bundle, `devUrl` en dev et
   `app.allowedOrigins` ; pour toute autre URL, appelle `tauri_plugin_opener::open_url` et refuse la
   navigation.
2. `window.open` et `target="_blank"` ne passent pas par `on_navigation` : un script
   d'initialisation (`initialization_script`) de dix lignes remplace `window.open` par un appel à
   `plugin:opener|open_url`. À vérifier en APP5 sur les trois systèmes.

Rien n'est ajouté au framework ni aux composants : le site ne sait pas qu'il tourne dans une
application. Si un jour une page doit le savoir, `"__TAURI_INTERNALS__" in window` suffit (`sass`,
`bridge.ts:3-5`), sans dépendance.

### 4.7 La boucle de développement

`recit app dev [dir]` :

1. lit `app:` et le port du site (`port` du config, sinon celui de `recit-site dev`) ;
2. sonde le port (`isPortInUse`) ; s'il répond, réutilise le serveur, sinon lance
   `recit-site dev --port <p>` dans le dossier du site ;
3. matérialise la coquille avec `devUrl: http://localhost:<p>` et lance `tauri dev --config`.

Le serveur de dev du framework accepte tout nom d'hôte depuis le 29/10 (`allowedHosts: true`,
`packages/site/src/vite/config.ts:130`) et écoute sur toutes les interfaces
(`packages/site/src/vite/dev-server.ts:288`) : `tauri android dev` et `tauri ios dev` réécrivent
`devUrl` vers l'adresse LAN de la machine pour l'émulateur ou l'appareil, et le serveur répond. Le
HMR passe par la même origine que `devUrl`, il fonctionne dans la webview.

### 4.8 Icônes et écran de lancement

`materializeShell` lance `tauri icon <source> -o icons/` quand la source est plus récente que
`icons/icon.png`. La source est `app.icon`, sinon `metadata.favicon` du site (SVG accepté ; pour les
sites Récit c'est `brand/logo/q07-icon.svg`). Cela produit les tailles desktop, Android (`mipmap-*`)
et iOS (`AppIcon-*`), comme dans `sass` (`icons/android`, `icons/ios`).

Pas d'écran de lancement sur desktop : la fenêtre naît `visible: false` et s'affiche à
`on_page_load`, ce qui évite le blanc. Sur Android 12+, le système dérive le splash de l'icône ; sur
iOS, `gen/apple` fournit un `LaunchScreen.storyboard` par défaut : on le laisse.

### 4.9 Mise à jour et distribution

- **Desktop** : `tauri-plugin-updater` + `tauri-plugin-process`, clés `tauri signer generate`,
  `TAURI_SIGNING_PRIVATE_KEY` en secret CI, `latest.json` et les binaires sur la GitHub Release de
  l'application. Opt-in par `app.updater` ; la coquille n'ajoute les plugins que si la section est
  présente. Lot APP9.
- **Mobile** : les stores assurent la mise à jour ; l'updater est ignoré.
- **Signature** : macOS (Developer ID, notarisation) et Windows (certificat) sont des secrets CI ;
  sans eux, APP6 produit des binaires non signés, suffisants pour tester.

### 4.10 Decks et films

- Un deck publié par `publish:` est déjà dans `dist/` : il est dans l'application **sans rien
  faire**. La télécommande smartphone (`--remote`, WebSocket du serveur de dev,
  `packages/cli/src/commands/dev.ts:312`) n'a pas de serveur dans l'app : hors périmètre. La console
  présentateur ouvre une seconde fenêtre par `window.open` : traitée par le script de §4.6, à
  vérifier en APP5.
- Un film (`StoryEmbed format=motion`) se joue dans la webview par `@recit/motion-blocks`, déjà dans
  le bundle. `recit render` reste hors application.
- Un deck **seul** (`recit app build slides.md`) : la sortie de `recit build` est déjà relative
  (`base = "./"`, `packages/cli/src/render/build-deck.ts:43`), la même coquille la prend comme
  `frontendDist`. Lot APP10, optionnel, une fois la coquille stable.

## 5. Lots

Chaque lot nomme ses **fichiers possédés** (deux agents ne touchent jamais le même fichier), ce dont
il **dépend**, ce qu'il **fournit** aux autres, et la **vérification** qui le déclare fini. Les
tailles sont S, M, L. **Toolchain** : exige Rust et la ligne apt de §3 ; **CI** : s'exécute sur un
runner GitHub.

### État au 29/10/2026

Exécuté le jour même par des agents en parallèle (notes détaillées dans `journal.md`, une par lot) :

| Lot            | État                                                                                                 |
| -------------- | ---------------------------------------------------------------------------------------------------- |
| APP0           | Fait et vérifié : section `app:`, résolveur, 14 tests, golden du JSON Schema                         |
| APP1           | Fait : paquet `@recit/app`, coquille compilée ici, 94 tests                                          |
| APP2           | Fait : `recit app build\|dev\|preview\|android\|ios\|icon`, 105 tests avec APP10 et APP11a           |
| APP3           | Fait : 5 e2e sur le bundle servi comme Tauri, aucune requête externe                                 |
| APP4           | Fait : trois pages de doc, opt-in de `site-embed`, plans 03 et 06                                    |
| APP5           | Fait ici : binaires `.deb`/`.rpm`/`.AppImage`, garde de navigation, fenêtre, cache partagé           |
| APP6           | Écrit, pas encore exécuté : aucun push depuis la machine de travail                                  |
| APP7           | Fait ici sauf appareil et émulateur : `.apk` signé 21,7 Mo, `.aab` 15,2 Mo                           |
| APP8           | Écrit (code, workflow, doc), non vérifié faute de Mac                                                |
| APP9           | Fait ici : updater signé, cycle 0.1.0 → 0.1.1 prouvé en local, `app-release.yaml` écrit              |
| APP10          | Fait : deck seul, `--remote` refusé ; APP10.2 : seconde fenêtre native et `BroadcastChannel` prouvés |
| APP11a, APP11b | Faits : intégration CLI, `@recit/app`, cœur, CI, doc, d'après les constats des lots                  |

### APP0 — Le contrat de configuration (S), sans Rust

Dépend de : rien. Parallèle avec : rien, il ouvre la voie.

Fichiers possédés : `packages/core/src/config/validate.ts`, `packages/core/src/config/app.ts`
(nouveau : `resolveAppConfig`, `ResolvedAppConfig`), `packages/core/src/config/index.ts` (une ligne
d'export), `packages/core/src/__tests__/app-config.spec.ts` (nouveau), et le golden ou instantané du
JSON Schema `config` s'il en existe un.

- [ ] APP0.1 — `AppConfigSchema` et `AppConfig` de §4.4, `app: z.optional(AppConfigSchema)` dans
      `RecitConfigSchema`.
- [ ] APP0.2 — `resolveAppConfig()` avec les défauts et les diagnostics de §4.4 ; aucune exception,
      des `Diagnostic[]`.
- [ ] APP0.3 — tests : défauts, identifiant inversé depuis `canonicalUrlOrigin` (`docs.recit.dev` →
      `dev.recit.docs`), identifiant invalide, `basePath`, `cdnUrl`, section absente ;
      `defineConfig` range `app` sous `RECIT_CONFIG` et pas dans les options du site.

Fournit : `AppConfigSchema`, `AppConfig`, `ResolvedAppConfig`, `resolveAppConfig` depuis
`@recit/core/config`. Les lots suivants ne redéfinissent aucun de ces types.

Vérification :

```bash
pnpm exec vitest run packages/core packages/integrations/site-stories/src/__tests__/site-config.spec.ts
pnpm -F @recit/core typecheck
```

### APP1 — Le paquet `@recit/app` (M), sans Rust

Dépend de : APP0. Parallèle avec : APP4.

Fichiers possédés : `packages/tools/app/**` (nouveau), `pnpm-workspace.yaml` (ligne de catalogue
`@tauri-apps/cli`), `nx.json` (`release.projects`), `.nx/version-plans/version-plan-app.md`
(nouveau), `.gitignore` racine (ligne `.recit/`).

- [ ] APP1.1 — squelette sur le modèle de `packages/formats/motion-render` : `package.json`
      (`scope:tool`, `exports` à trois conditions, `tsdown`), `tsconfig.json`, `tsdown.config.ts`,
      `LICENSE.md`, `README.md` court.
- [ ] APP1.2 — `tauriConfig()` et ses instantanés : desktop, Android, iOS, avec et sans `deepLink`,
      `updater`, `devUrl`.
- [ ] APP1.3 — `shell/` (§4.3) : Rust écrit et relu, **non compilé ici** ; `materializeShell()`
      copie, génère `tauri.conf.json`, appelle `tauri icon` si la source a changé ; test sur un
      dossier temporaire (sans lancer `tauri icon` : injecté).
- [ ] APP1.4 — `serveLikeTauri()` : la chaîne de résolution de §4.5, types MIME (`.wasm`), test
      unitaire sur un `dist/` factice (`a.html`, `b/index.html`, repli `index.html`).
- [ ] APP1.5 — `runTauri()` et `isPortInUse()`.
- [ ] APP1.6 — version plan `prerelease` (§8), `release.projects`, catalogue.

Fournit : l'API de §4.2, figée.

Vérification :

```bash
pnpm exec vitest run packages/tools/app scripts/__tests__/release-workflow.spec.ts
pnpm exec nx run @recit/app:build && pnpm -F @recit/app typecheck
pnpm check && pnpm publint   # frontières, version plans, forme du paquet publié
```

### APP2 — La commande `recit app` (M), sans Rust

Dépend de : APP1. Parallèle avec : APP3.

Fichiers possédés : `packages/cli/src/commands/app.ts` (nouveau),
`packages/cli/src/__tests__/app.spec.ts` (nouveau), `packages/cli/src/index.ts` (**seulement** les
lignes d'enregistrement de la commande), `packages/cli/package.json` (`@recit/app` en peer
optionnel, comme `@recit/motion-render`).

- [ ] APP2.1 — `recit app build [dir] [--target desktop|android|ios] [--skip-site-build]` :
      `resolveAppConfig`, `recit-site build` (spawn dans le dossier du site), `materializeShell`,
      `tauri build --config`. Diagnostics imprimés comme ceux de `recit build`.
- [ ] APP2.2 — `recit app dev [dir]` (§4.7).
- [ ] APP2.3 — `recit app preview [dir] [--port]` : `serveLikeTauri(dist)`, imprime l'URL.
- [ ] APP2.4 — `recit app android <init|dev|build> [-- …]` et
      `recit app ios <init|dev|build> [-- …]` : passe-plat vers `tauri android …` et `tauri ios …`
      dans la coquille, les arguments après `--` transmis tels quels (`--aab`, `--export-method`),
      avec le message des prérequis de §3 quand l'outil manque. APP7 et APP8 n'ont donc rien à
      ajouter à la CLI.
- [ ] APP2.5 — `recit app icon [source]` : régénère les icônes.
- [ ] APP2.6 — `@recit/app` absent : message « installez @recit/app » (motif de
      `motion.ts:200-213`).
- [ ] APP2.7 — tests avec `@recit/app` injecté (comme `loadRenderer`) : enchaînement des étapes,
      options, codes de sortie.

Fournit : les noms de commandes ci-dessus, que APP4 documente.

Vérification :

```bash
pnpm exec vitest run packages/cli
pnpm exec nx run @recit/cli:build && pnpm --filter recit-example-site-embed exec recit app --help
```

### APP3 — Le bundle vérifié en e2e (S), sans Rust

Dépend de : APP1. Parallèle avec : APP2.

Fichiers possédés : `e2e/site-app-bundle.spec.ts` (nouveau), `e2e/ports.ts` (une constante
`APP_BUNDLE_PORT`, **hors** de `E2E_PORTS` dont chaque clé lance `recit-site preview`,
`playwright.config.ts:33`), `playwright.config.ts` (une entrée `webServer` de plus),
`e2e/package.json` (dépendance `@recit/app`), `e2e/support.ts` si un helper est nécessaire.

- [ ] APP3.1 — `examples/site-embed/dist` servi par `serveLikeTauri` sur `APP_BUNDLE_PORT`.
- [ ] APP3.2 — tests : la page d'accueil, une navigation client vers `/stories/lorem-ipsum`, un
      rechargement sur cette adresse profonde (fichier `.html`), une recherche Pagefind qui rend des
      résultats, `/nulle-part` qui affiche la page 404 par repli `index.html`.
- [ ] APP3.3 — **hors ligne** : toutes les requêtes de la session (`page.on("request")`) ont
      l'origine du serveur ; une seule requête externe fait échouer le test.

Vérification :

```bash
pnpm build && pnpm exec playwright test e2e/site-app-bundle.spec.ts
pnpm test:e2e:linux   # la suite complète, en conteneur, comme CI
```

### APP4 — Documentation, plans et opt-in de l'exemple (S), sans Rust

Dépend de : APP0 (la clé `app` doit exister pour le typecheck de l'exemple). Parallèle avec : APP1.
Les noms de commandes viennent de ce plan (§5, APP2), pas du code.

Fichiers possédés : `examples/site-embed/recit.config.ts` (section `app:`),
`sites/docs/pages/documentation/configuration/app.mdx`, `app-android.mdx` et `app-ios.mdx`
(nouveaux) et les lignes qui les ajoutent dans `sites/docs/sidebar.ts`,
`sites/plans/pages/03-packaging.md` (`app` dans l'arborescence §2, la liste §6 et la table des tags
§8), `sites/plans/pages/06-roadmap.md` (une ligne dans P5 renvoyant ici). `NOTICE.md` n'est pas
touché : la coquille est le gabarit de Tauri, pas du code de `sass`.

- [ ] APP4.1 — `app: { productName: "Lorem Ipsum Docs", identifier: "com.example.loremipsum" }` dans
      l'exemple ; le round-trip des récits ne lit pas la config, rien d'autre ne bouge.
- [ ] APP4.2 — la page de doc : installer `@recit/app`, la section `app:` champ par champ, les
      commandes, les prérequis desktop, ce qui marche hors ligne, les limites (`basePath`, `cdnUrl`,
      télécommande). Deux pages sœurs, `app-android.mdx` et `app-ios.mdx`, créées avec les prérequis
      de §3 et la mention « en cours » : APP7 et APP8 les remplissent sans toucher au sidebar.
- [ ] APP4.3 — plans : table des tags, roadmap, et le lien depuis `index.mdx` (fait avec ce plan).

Vérification :

```bash
pnpm check                                   # frontmatter, liens de sites, version plans
pnpm exec nx run docs:build && pnpm -F recit-example-site-embed typecheck
```

### APP5 — La coquille compilée et le premier binaire (M), **toolchain**

Dépend de : APP1, APP2. Parallèle avec : rien ; si la machine ne compile pas, APP6 le remplace et
APP5 devient la lecture des journaux du run (§6).

Fichiers possédés : `packages/tools/app/shell/**` (reprend la propriété après APP1),
`packages/tools/app/src/lib-rs.spec.ts` ou équivalent si un test lit le Rust.

- [ ] APP5.1 — `cargo check` de la coquille matérialisée pour `examples/site-embed`.
- [ ] APP5.2 — `recit app build` sur Linux : `.deb` et `.AppImage` ; l'app s'ouvre (sous `xvfb-run`
      si la machine n'a pas d'écran) sur la page d'accueil, navigue, recharge, cherche.
- [ ] APP5.3 — garde de navigation (§4.6) : un lien vers GitHub ouvre le navigateur, la fenêtre
      reste sur le site ; `target="_blank"` et la console présentateur d'un deck publié.
- [ ] APP5.4 — fenêtre cachée jusqu'à `on_page_load`, titre et taille de `app:`.
- [ ] APP5.5 — `RECIT_APP_TARGET_DIR` partagé entre deux sites : le second build ne recompile pas
      Tauri.

Vérification :

```bash
cargo check --manifest-path examples/site-embed/.recit/app/Cargo.toml
pnpm --filter recit-example-site-embed exec recit app build && ls examples/site-embed/.recit/app/target/release/bundle
```

### APP6 — CI desktop sur trois systèmes (M), **CI**

Dépend de : APP2, et APP5 quand la machine compile. Parallèle avec : APP9, APP10 quand APP5 a tourné
localement.

Fichiers possédés : `.github/workflows/app.yaml` (nouveau). Rien d'autre :
`scripts/__tests__/release-workflow.spec.ts` lit déjà tout `.github/workflows` et exige chaque
`uses:` épinglé par SHA (`release-workflow.spec.ts:80-90`).

- [ ] APP6.1 — `workflow_dispatch`, matrice `ubuntu-24.04` (apt de §3), `macos-15`, `windows-2025` ;
      `dtolnay/rust-toolchain` et `Swatinem/rust-cache` épinglés ; `pnpm install`, build du site,
      `recit app build`, `actions/upload-artifact`.
- [ ] APP6.2 — un test de fumée par système : le binaire démarre et quitte (`xvfb-run` sur Linux).
- [ ] APP6.3 — secrets optionnels de signature (macOS, Windows) : le job passe sans eux.

Vérification : un run vert avec trois artefacts téléchargeables ;
`pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts`.

### APP7 — Android (L), **toolchain / CI**

Dépend de : APP6. Parallèle avec : APP8.

Fichiers possédés : `packages/tools/app/src/android.ts` (nouveau : keystore depuis l'environnement,
`gen/android/keystore.properties` jamais commité), `.github/workflows/app-android.yaml` (nouveau),
`sites/docs/pages/documentation/configuration/app-android.mdx` (créée par APP4).

- [ ] APP7.1 — `recit app android init` sur un poste équipé ou en CI
      (`android-actions/setup-android` épinglé) ; `minSdkVersion` depuis `app.android`.
- [ ] APP7.2 — `recit app android dev` contre `recit-site dev` sur le LAN (§4.7), émulateur.
- [ ] APP7.3 — `recit app android build -- --aab` signé par un keystore de test ; l'`.apk`
      s'installe et le site tourne hors ligne (mode avion).
- [ ] APP7.4 — tailles : poids de l'`.apk` d'`examples/site-embed` noté dans le journal.

Vérification : run CI vert avec `.apk` et `.aab` ; installation manuelle vérifiée une fois.

### APP8 — iOS (L), **Mac / CI**

Dépend de : APP6. Parallèle avec : APP7.

Fichiers possédés : `packages/tools/app/src/ios.ts` (nouveau : `developmentTeam` depuis `app.ios` ou
`APPLE_DEVELOPMENT_TEAM`), `.github/workflows/app-ios.yaml` (nouveau),
`sites/docs/pages/documentation/configuration/app-ios.mdx` (créée par APP4).

- [ ] APP8.1 — `recit app ios init` sur Mac ; simulateur avec `recit app ios dev`.
- [ ] APP8.2 — `recit app ios build -- --export-method app-store-connect` avec une équipe de test.
- [ ] APP8.3 — `LaunchScreen` et icônes vérifiés ; hors ligne en mode avion.

Vérification : run CI macOS vert avec un `.ipa` ; TestFlight une fois les comptes tranchés (§10).

### APP9 — Mise à jour automatique desktop (M), **toolchain**, optionnel

Dépend de : une coquille compilée (APP5 ou APP6). Parallèle avec : APP7, APP8, APP10.

Fichiers possédés : `packages/tools/app/shell/Cargo.toml` et `shell/src/lib.rs` (sections updater,
conditionnelles ; `tauriConfig()` sait déjà écrire `plugins.updater` depuis APP1.2),
`.github/workflows/app-release.yaml` (nouveau : `latest.json`, signature, GitHub Release), sans
toucher à `app.yaml`.

- [ ] APP9.1 — `tauri signer generate`, clé publique dans `app.updater.pubkey`, privée en secret.
- [ ] APP9.2 — vérification au lancement, dialogue natif, redémarrage (`tauri-plugin-process`).
- [ ] APP9.3 — un cycle complet entre deux versions de test.

Vérification : une app en version n se met à jour en n+1 depuis une GitHub Release de test.

### APP10 — Deck seul, console présentateur (M), **toolchain**, optionnel

Dépend de : une coquille compilée (APP5 ou APP6). Parallèle avec : APP7 à APP9.

Fichiers possédés : `packages/cli/src/commands/app.ts` (reprend la propriété après APP2 : branche «
fichier de récit » de `recit app build`), `packages/tools/app/src/deck.ts` (nouveau), les tests
correspondants.

- [ ] APP10.1 — `recit app build slides.md` : `recit build` puis la coquille avec `frontendDist` sur
      la sortie du deck ; `app:` lue dans le `recit.config` du deck (même schéma).
- [ ] APP10.2 — console présentateur en seconde fenêtre native.
- [ ] APP10.3 — `--remote` : diagnostic clair, « pas de télécommande dans l'application ».

Vérification : `pnpm exec vitest run packages/cli packages/tools/app` ; un deck d'`examples/` en
`.AppImage`.

## 6. Exécution multi-agent

Le plan s'exécute par plusieurs agents en parallèle sur **le même arbre de travail**, et d'autres
sessions peuvent éditer des fichiers en même temps. Règles pour chaque agent :

- **Relire avant d'écrire.** Chaque fichier est relu juste avant d'être modifié ; si son contenu a
  changé entre la lecture et l'écriture, relire et refaire la modification sur le nouveau contenu.
  Dans `journal.md` et `index.mdx`, on **ajoute** ses lignes, on ne réordonne ni ne réécrit rien, et
  on ne rétablit jamais un changement qu'on n'a pas fait.
- **Un lot, un agent, ses fichiers.** La liste « fichiers possédés » de §5 est exhaustive : un
  fichier qui manque se demande au coordinateur avant d'y toucher. Les fichiers partagés
  (`packages/cli/src/index.ts`, `nx.json`, `pnpm-workspace.yaml`, `.gitignore`, `e2e/ports.ts`,
  `playwright.config.ts`) ont chacun **un** propriétaire.
- **Jamais de git** (`commit`, `push`, `add`, `stash`, branche, worktree), jamais `pnpm fix` ni
  `oxfmt` sur tout le dépôt : les sources Markdown de `sites/*/pages` et `examples/**` en sont
  exclues et une mise en forme les casse. `pnpm check` et les vérifications de son lot seulement.
- **Une note dans `journal.md` à la fin du lot**, ajoutée à la fin du fichier sous un titre
  `### APPn — <titre> — <date>` : ce qui est fait, ce qui est mesuré (commandes et résultats), ce
  qui reste, et tout piège rencontré. Le prochain agent commence par la lire.
- **Le contrat ne bouge pas** : un lot qui a besoin d'un champ de `app:` ou d'une fonction de
  `@recit/app` qui n'existe pas l'écrit dans le journal et s'arrête, il ne modifie pas APP0 ni APP1.

Les vagues :

| Vague | Lots en parallèle | Condition d'entrée                                                           | Machine           |
| ----- | ----------------- | ---------------------------------------------------------------------------- | ----------------- |
| 0     | APP0              | —                                                                            | ici               |
| 1     | APP1, APP4        | APP0 vérifié (`vitest` core + `site-config.spec.ts`, typecheck)              | ici               |
| 2     | APP2, APP3        | APP1 vérifié                                                                 | ici               |
| 3     | APP5, ou APP6     | APP2 vérifié ; APP5 si `rustup` et la ligne apt sont là, sinon APP6 d'emblée | toolchain, ou CI  |
| 4     | APP6, APP9, APP10 | une coquille compilée (APP5 fait ici, ou le premier run vert de APP6)        | CI / toolchain    |
| 5     | APP7, APP8        | APP6 vert                                                                    | CI / poste équipé |

Si la chaîne Rust n'est pas installée à la vague 3, APP6 compile la coquille pour la première fois
sur le runner, et APP5 se réduit à lire les journaux du run et à corriger la coquille ; la vague 4
ne contient plus que APP9 et APP10.

## 7. Tests et vérification

| Niveau      | Quoi                                                                                                    | Où                                               | Rust |
| ----------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ---- |
| Unitaire    | schéma, défauts, diagnostics de `app:`                                                                  | `packages/core/src/__tests__/app-config.spec.ts` | non  |
| Unitaire    | `tauriConfig` par instantané, `materializeShell`, `serveLikeTauri`                                      | `packages/tools/app/src/__tests__/`              | non  |
| Unitaire    | enchaînement de `recit app …` avec `@recit/app` injecté                                                 | `packages/cli/src/__tests__/app.spec.ts`         | non  |
| e2e         | bundle servi comme Tauri : navigation, rechargement profond, recherche, 404, **aucune requête externe** | `e2e/site-app-bundle.spec.ts`                    | non  |
| Compilation | `cargo check` de la coquille matérialisée                                                               | APP5, puis chaque job de APP6                    | oui  |
| Fumée       | le binaire démarre et quitte, sur trois systèmes                                                        | APP6                                             | oui  |
| Manuel      | liens externes, `window.open`, mode avion sur mobile                                                    | APP5, APP7, APP8, notés dans le journal          | oui  |

`pnpm verify` reste la porte : APP0 à APP4 ne la ralentissent que de leurs tests unitaires ; aucun
lot n'ajoute Rust à `pnpm verify` ni à `build.yaml`. La compilation native vit dans `app.yaml`,
lancé à la main.

## 8. Version plan

`@recit/app` devient un paquet publié : il entre dans `release.projects` de `nx.json` (vérifié par
`scripts/__tests__/release-workflow.spec.ts`) et prend un version plan. Tous les paquets partagent
une version (`projectsRelationship: "fixed"`) ; en 0.x seul `preminor` (première release d'une
phase) ou `prerelease` est accepté (`CONTRIBUTING.md` §5, `scripts/check-version-plans.js`). Ce plan
ne crée pas de phase : `.nx/version-plans/version-plan-app.md` en **`prerelease`**, pour
`@recit/core` (la section `app:`), `@recit/cli` (`recit app`) et `@recit/app`, écrit par APP1. APP10
ajoute un second plan `prerelease` si APP1 est déjà publié.

## 9. Risques

| Risque                                                                                                    | Parade                                                                                                                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pagefind sous le protocole `tauri://` : `fetch` du `.wasm` et des fragments depuis un schéma personnalisé | APP3 le prouve sous `serveLikeTauri` (HTTP) ; APP5 le vérifie sous le vrai protocole sur Linux (WebKitGTK) et APP6 sur macOS (WKWebView) et Windows (WebView2) ; en dernier recours, un serveur local embarqué (`tauri-plugin-localhost`), qui coûte une dépendance |
| `window.open` et `target="_blank"` hors de `on_navigation`                                                | script d'initialisation (§4.6) ; test manuel APP5.3                                                                                                                                                                                                                 |
| Un site avec `basePath` ou `cdnUrl`                                                                       | refusé avec diagnostic dès APP0 ; un support ultérieur passerait par un build du site avec `basePath` forcé à `""`                                                                                                                                                  |
| Poids des binaires (Mermaid, Shiki, KaTeX, polices)                                                       | mesuré en APP5.2 et APP7.4, noté au journal ; les langages Shiki sont déjà une liste par site (`site-kit/src/config.tsx:111-123`)                                                                                                                                   |
| `@tauri-apps/cli` : binaire natif par plateforme, lourd                                                   | dépendance de `@recit/app` seulement, chargé à la demande par `recit` ; `pnpm-workspace.yaml` l'épingle                                                                                                                                                             |
| Cache Cargo (gigaoctets) dans chaque site                                                                 | `RECIT_APP_TARGET_DIR` (§4.3), ignoré par git                                                                                                                                                                                                                       |
| Pas d'écran, pas de Rust sur la machine de travail                                                        | §3 et l'ordre des vagues ; APP6 porte la compilation sur GitHub                                                                                                                                                                                                     |
| Les sites Récit renvoient entre eux par des URL absolues (`siteUrl`)                                      | ouverts dans le navigateur par la garde ; une version « tout en un » (plusieurs sites dans une app) n'est pas prévue                                                                                                                                                |
| Stores : identifiants figés, comptes, revues                                                              | `identifier` documenté comme définitif ; question ouverte Q2                                                                                                                                                                                                        |

## 10. Questions ouvertes

1. **Quel site en premier ?** Proposé : `examples/site-embed` pour les tests, `sites/docs` pour la
   première application distribuée (une doc hors ligne a un sens). `plans` est interne.
2. **Les stores.** Compte Apple Developer et Google Play : ceux de Récit ? Pour quels sites ? La
   recommandation de `sass` (Q12) tient : le mobile à la demande, le site est déjà responsive.
   Décider avant APP7.
3. **Le nom du paquet** : `@recit/app` (proposé) ou `@recit/native` ; le dossier généré
   `.recit/app/`.
4. **La chaîne Rust ici.** La ligne apt de §3 demande sudo : l'installer sur cette machine, ou
   réserver la compilation à la CI (APP6) ?
5. **Place dans la feuille de route.** Proposé : APP0 à APP6 pendant P5 et P6 en `prerelease` ; le
   mobile (APP7, APP8) après la 1.0 sauf demande. `06-roadmap.md` reçoit une ligne en P5 (APP4).
6. **Une app par site, ou une app Récit** qui embarque plusieurs sites ? Ce plan fait une app par
   site, comme `sass` ; l'autre forme demanderait un routeur multi-sites qui n'existe pas.
