Décisions fondatricesADR-0015 · Générations parallèles

ADR-0015 — Des générations en parallèle, et une progression qui ne ment pas

Générations en parallèle et progression honnête dans le cockpit Studio.

Statut

Proposé · 2026-09-27

Remplace ou amende : La carte de galerie de la plateforme (un pourcentage minuté plafonné à 96 %) et le composer bloqué pendant un rendu.

Origine : ADR 0005 du dépôt applicatif Orbit, renumérotée 0015 à l’intégration dans cette série (une seule série d’ADR vit désormais ici).

Statut dans Orbit (2026-10) : en vigueur. Décision reprise de l'ancien dépôt Studio ; « la plateforme » désigne le cockpit d'origine dont le comportement a été corrigé, pas une dépendance d'Orbit.

Contexte

Le cockpit transplanté (PR #9) gardait deux comportements de la plateforme :

  1. Un pourcentage inventé. gallery-context.tsx démarrait, à chaque clic, un minuteur de 350 ms qui ajoutait 6 à 22 points au hasard, plafonné à 96 % (Math.min(progress + Math.random() * 16 + 6, 96)). Une image ou une voix est une requête et une réponse : rien ne rapporte d'avancement. Le chiffre atteignait 96 % en cinq secondes environ, puis restait figé jusqu'à la réponse, que le rendu dure huit secondes ou deux minutes.
  2. Un composer bloqué. isGenerating valait « une carte en attente existe dans ce mode » : le bouton Générer restait désactivé jusqu'à la fin du rendu précédent.

Et un troisième, découvert en le corrigeant : Next exécute les fonctions serveur une à la fois. Trois clics envoyés en server actions partaient en file, l'un après l'autre, même avec un composer libéré.

Décision

  • Chaque carte en attente est un job (src/cockpit/jobs/). Sa clé est la clé d'idempotence de la génération (cockpit:<uuid>), donc la ligne du journal se retrouve avant même la réponse, et après une navigation.
  • Les gestes payants du composer passent par POST /api/cockpit/produce/<geste> (même porte, mêmes fonctions que les server actions), et s'exécutent donc côte à côte, jusqu'au plafond du moteur par membre (MAX_IN_FLIGHT_PER_MEMBER). Au plafond, le bouton le dit (« 4 en cours, c'est le maximum »), et un refus busy devient une carte en échec lisible, jamais un silence.
  • La progression suit le journal (GET /api/cockpit/jobs) : En file (QUEUED ou pas encore de ligne), Génération (SUBMITTED, PROCESSING), Finalisation (le fichier existe, le navigateur le charge), Prêt. Les étapes n'avancent que vers l'avant. Un pourcentage ne s'affiche que s'il est rapporté par le fournisseur ; sinon une animation d'activité. La durée habituelle est la médiane des dernières générations terminées du studio, par moteur, sinon la configuration (expected.ts) ; au-delà, la carte dit « Plus long que d'habitude, ça continue… ».
  • Un échec garde sa carte : la phrase, le montant remboursé (lu sur le registre, REFUND), et Relancer, sous une nouvelle clé.
  • Une vidéo répond tout de suite, en cours (0004) ; la carte suit sa ligne et la route relit le fournisseur passé le délai de VIDEO_PULL_AFTER_MS.
  • Aucun router.refresh() : les soldes passent par l'état partagé du tenant (credits-channel.ts), la galerie lit ce même état.

Conséquences

  • gallery-context.tsx devient une cale du studio (manifeste, shim) ; la carte en attente de gallery-item.tsx et l'état « bloqué » des deux composers sont des règles de réécriture déclarées. Une remontée de la plateforme sur ces fichiers est signalée par pnpm ui:sync.
  • Les cartes d'E2E utilisent les fournisseurs enregistrés, scriptés par des marqueurs dans le brief ([e2e:lent=<ms>], [e2e:echec-unique], src/generation/fixture-script.ts), jamais servis en production.