Formats

26 — Roadmap : le format roadmap et roadmap.recit.dev

Statut : proposé · Phase : P9 · Mis à jour : 02/11/2026.

Ce plan détaille la phase P9 de 06 · Feuille de route : le format roadmap, la fondation @recit/time dans sa première moitié (dates, trimestres, ICS en écriture), et le site roadmap.recit.dev. Il part de la spécification de 08 · Formats §4, qu'il précise sans la contredire ; là où le code ou une règle acceptée oblige à s'en écarter, l'écart est posé en question ouverte (§12). Il s'appuie sur le code tel qu'il est le 02/11/2026, pas sur les textes plus anciens : packages/core/src/schema, packages/formats/motion, packages/cli, packages/integrations/site-stories, examples/motion-hello et e2e/site-motion.spec.ts.

Il dépend de 02 · Architecture, de 03 · Packaging §6 et §8, de 07 · Domaines, de 09 · Langues et JSON, de 12 · Glossaire et annuaire (l'analyseur {k=v}, lot DI0.2, créé avant ce plan), de 15 · Export PDF (chaîne d'impression, manifeste exports.json) et de 21 · Tests. Le calendrier, qui consomme les jalons de la roadmap, est le plan 27.

Chaque choix porte une recommandation. Rien n'est implémenté avant l'accord de l'utilisateur.

1. Pourquoi

  • Une roadmap est une story comme une autre. Un titre, des pistes, des éléments datés ou non, des jalons : c'est un document que l'on écrit, que l'on traduit et que l'on publie. Aujourd'hui, on l'écrit dans un outil fermé (GitHub Projects, Linear, Notion), ou dans un Markdown sans structure que personne ne sait afficher en frise. Récit donne les deux : le texte en Markdown, les statuts en JSON typé, et trois vues.
  • Le format prouve la promesse « une source, plusieurs rendus » sur des données. Le deck et le motion rendent de la prose ; la roadmap rend une liste d'éléments typés (statut, dates, dépendances) en trois vues, un PDF, un fichier ICS et un flux RSS, depuis la même source.
  • Récit en a besoin pour lui-même. Le plan 06 · Feuille de route et la page sites/vision/pages/roadmap.mdx sont deux roadmaps écrites à la main, en deux langues, qui dérivent l'une de l'autre. Le format est la manière de les tenir d'une seule source (§7).
  • Il prépare le calendrier. Les jalons datés passent dans le calendrier (pont roadmap → calendar, P10, plan 27) par le même type de date et le même écrivain ICS.

2. Ce qui existe déjà et qu'on réutilise

BriqueÉtat réel au 02/11/2026Ce que P9 en fait
RoadmapSourceSchemapackages/core/src/schema/source.ts : genericSource("roadmap"), un looseObject sans champ typé (règle L6 : « les champs typés arrivent avec le format »)remplacé par un strictObject typé (§3.3), comme MotionSourceSchema
Convertisseur Markdown ⇄ JSONpackages/core/src/schema/convert.ts : un convertisseur de scène par format (deckScene, docsScene, motionScene), choisi par une cascade de ternaires ; genericScene pour les autresajout de roadmapScene et de son inverse, et passage de la cascade à une table par format (§3.4)
Compilation d'un JSONcompileStorySource (compile/compile-story.ts) passe le JSON en Markdown par sourceToMarkdown, puis compile ce Markdown : le Markdown est le pivot de compilationle format relit scene.mdast et scene.attrs, comme @recit/motion (source/story.ts) ; le pivot doit être exact
Attributs de séparateurparser/split-slides.ts : key=value, valeurs entre guillemets, forme JSON --- {…} ; coercition des nombres (id=3 reste une chaîne, mais date=2027 deviendrait un nombre)les dates sont relues en chaînes par le schéma du format (§3.5)
Compagnon (kind: "sidecar")mergeSidecar (schema/assemble.ts) fusionne par id de scène ; un id inconnu est une erreur (sidecar-unknown-scene)étendu aux listes à clé (les items d'une piste), même règle d'erreur (§3.4)
Test d'aller-retourcore/src/__tests__/source-conversion.spec.ts parcourt examples/ et sites/*/storiescouvre examples/roadmap-product et la roadmap de Récit sans rien ajouter
JSON Schemaschema/json-schema.ts, recit schema [name], schemaUrl("roadmap")roadmap.json publié avec les autres
Packagespackages/formats/roadmap et packages/time : squelettes privés (export {}), tags scope:format et scope:foundation déjà posésdeviennent publiés (exports à trois conditions, tsdown, LICENSE.md, release.projects)
Analyseur {k=v}à créer en DI0.2 (12 · Glossaire et annuaire), un seul module dans @recit/coreréutilisé tel quel pour les items
Intégration aux sitessite-stories/src/plugin.tsx choisit la vue par story.format === "motion" ? <MotionViewer> : <StoryViewer> ; vite.ts calcule le film au build (storyMotion)une table de visionneuses par format (§6.1), à aligner avec le plan 23
Chaîne PDF, contrat d'impression, exports.jsonspécifiés par 15 · Export PDF (PDF0 à PDF1 en P6) : ?print=1, whenPrintable(), @recit/paged/runner, ExportManifestSchemaextension RoadmapPdfOptionsSchema, variantes ics et rss du manifeste
Graphiquesd3 sans DOM (d3-scale, d3-shape), tranché le 24/10 (question 10 de 06) ; dans le lockfile, aujourd'hui seulement en dépendance transitivela frise est un SVG calculé au build par d3-scale (scaleUtc) : aucune bibliothèque de plus
Composants de site@recit/site-kit : FormatHero, Gallery, ExportPanel, BridgeCard… (07 · Domaines §4) ; StatusBadge existe déjà (statut d'un format)réutilisés ; le badge d'un élément de roadmap prend un autre nom (RoadmapStatus), un mot par chose
Horlogeaucune : rien dans Récit ne dépend encore de la date du jourune seule horloge de build, SOURCE_DATE_EPOCH (§3.5)

3. Le modèle

3.1 Une scène = une piste ou un jalon

Accepté dans 08 · Formats §0 et repris tel quel : une scène est une piste (un groupe d'éléments : « Editor », « API ») ou un jalon (une date qui compte : « 1.0 release »). Les éléments vivent à l'intérieur d'une piste, comme les entrées d'une section d'annuaire (12 · Glossaire et annuaire D2). Le seul séparateur reste ---.

Les vues travaillent sur les éléments, pas sur les scènes : now-next-later et kanban répartissent les éléments en colonnes, la frise place les éléments sur leurs pistes et les jalons sur l'axe.

3.2 La syntaxe Markdown

MarkdownMarkdown
--- title: Lorem Product Roadmap format: roadmap lang: en view: now-next-later # now-next-later | timeline | kanban period: quarter # month | quarter | half : graduation de la frise statuses: [planned, in-progress, shipped, paused] --- --- id=editor ## Editor Lorem ipsum dolor sit amet, consectetur adipiscing elit. - [x] Lorem ipsum export {id=export quarter=2026-Q3 end=2026-09-18 link=https://example.com} - [ ] Dolor sit amet sync {id=sync status=in-progress start=2026-10-01 end=2026-12-15 owner=team-lorem} - [ ] Consectetur offline mode {id=offline quarter=2027-Q1 tags=mobile,beta horizon=later} :::item{id=offline} Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ![Lorem sketch](./img/offline.png) ::: --- id=api ## API - [ ] Adipiscing webhooks {id=webhooks quarter=2027-Q1 depends=sync} --- type=milestone id=v1 date=2027-03-31 ## 1.0 release Ut enim ad minim veniam.
ÉlémentRôle
--- id=…une piste. Le premier titre est son libellé, la prose qui suit (avant la liste) sa description
item de liste terminé par {…}un élément. Attributs : id (obligatoire, §3.6), status, horizon, start, end, quarter, owner, tags, link, depends. [x] vaut status=shipped par défaut
[ ] sans statuspremier statut de statuses (planned par défaut)
:::item{id=…}description longue d'un élément, Markdown complet (accepté dans 08)
--- type=milestone id=… date=…un jalon, sur la frise et dans l'ICS. Le corps est sa description (voir la question 3 sur type=)
compagnon product.json (kind: "sidecar")statuts, dates et propriétaires tenus par un outil (CI, script), le texte restant dans le .md (09 §1)

08 · Formats écrivait --- lane=editor id=editor : l'attribut lane= répète l'id et ne sert à rien d'autre, puisque toute scène qui n'est pas un jalon est une piste. Recommandation : --- id=… seul ; lane= n'existe pas (question 8).

Les attributs {k=v} suivent la grammaire du séparateur (mêmes guillemets, mêmes listes séparées par des virgules) ; c'est l'analyseur unique de DI0.2. Un item sans {…} est une ligne de prose de la piste, pas un élément : une liste ordinaire reste possible dans la description.

3.3 Le schéma (dans @recit/core/schema)

Les schémas de source restent dans packages/core/src/schema/source.ts, à côté de ceux du deck et du motion ; les types en sont dérivés (z.infer), et @recit/roadmap les importe sans les redéfinir (règle d'AGENTS.md). Les primitives de date sont partagées avec le calendrier (plan 27) et vivent dans un module à part, packages/core/src/schema/time.ts, importé par les deux.

TypeScriptTypeScript
import * as z from "zod/mini"; // packages/core/src/schema/time.ts — shared by roadmap (P9) and calendar (P10) /** A calendar date without time or zone: "2026-11-02" */ export const PlainDateSchema = z .string() .check(z.regex(/^\d{4}-\d{2}-\d{2}$/), z.refine(isValidPlainDate, { message: "invalid date" })); /** "2026-Q3" */ export const QuarterSchema = z.string().check(z.regex(/^\d{4}-Q[1-4]$/)); // packages/core/src/schema/source.ts export const ROADMAP_VIEWS = ["now-next-later", "timeline", "kanban"] as const; export const ROADMAP_HORIZONS = ["now", "next", "later"] as const; export const RoadmapItemSourceSchema = z.strictObject({ /** Required: anchor, RSS guid, sidecar key; stable across languages */ id: z.string().check(z.regex(/^[a-z0-9][\w-]*$/)), title: LocalizedSchema, /** One of settings.statuses; checked once the settings are known */ status: z.optional(z.string()), /** Column of the now-next-later view; derived from the status when absent */ horizon: z.optional(z.enum(ROADMAP_HORIZONS)), start: z.optional(PlainDateSchema), end: z.optional(PlainDateSchema), quarter: z.optional(QuarterSchema), owner: z.optional(z.string()), tags: z.optional(z.array(z.string())), link: z.optional(z.url()), /** Ids of other items of the same story */ depends: z.optional(z.array(z.string())), /** The :::item{id} block (Markdown) */ description: z.optional(LocalizedSchema), }); export const RoadmapSceneSourceSchema = z.strictObject({ id: sceneFields.id, /** A lane unless it is a milestone */ type: z.optional(z.enum(["milestone"])), title: z.optional(LocalizedSchema), /** Milestone date */ date: z.optional(PlainDateSchema), /** Prose of the lane or milestone (before the list) */ body: sceneFields.body, notes: sceneFields.notes, attrs: sceneFields.attrs, items: z.optional(z.array(RoadmapItemSourceSchema)), }); export const RoadmapSettingsSourceSchema = z.strictObject({ view: z.optional(z.enum(ROADMAP_VIEWS)), period: z.optional(z.enum(["month", "quarter", "half"])), statuses: z.optional(z.array(z.string()).check(z.minLength(1))), /** The status that means "done": changelog, RSS, [x] (default shipped) */ done: z.optional(z.string()), /** Status → column when an item has no horizon */ horizons: z.optional(z.partialRecord(z.enum(ROADMAP_HORIZONS), z.array(z.string()))), /** Labels of custom statuses; the default ones come from the UI strings */ statusLabels: z.optional(z.record(z.string(), LocalizedSchema)), ics: z.optional(z.strictObject({ items: z.optional(z.boolean()) })), // pdf: RoadmapPdfOptionsSchema, added by RM3 (15 §3.2: no field before its phase) }); export const RoadmapSourceSchema = z.strictObject({ ...storyFields, format: z.literal("roadmap"), settings: z.optional(RoadmapSettingsSourceSchema), scenes: z.array(RoadmapSceneSourceSchema), });

Les noms exacts de l'API zod/mini (z.partialRecord, z.url) sont à vérifier à l'implémentation, comme dans 15 · Export PDF §3.2 ; le principe est fixe. Les défauts (view: now-next-later, period: quarter, statuses, done: shipped, et la table horizons: { now: [in-progress], next: [planned], later: [paused] }) sont posés par la section roadmap: de recit.config (RoadmapConfigSchema, avec .prefault({}), règle d'AGENTS.md), pas par le schéma de source : la source dit ce que l'auteur a écrit, la config résolue dit ce qui s'applique. Les réglages s'écrivent à plat dans le frontmatter (08, « Réglages à plat »).

Contrôles portés par le schéma (z.refine au niveau de la story, avec chemin JSON) : end avant start, quarter et start/end qui se contredisent, date absent d'un jalon daté, items sur un jalon. Les contrôles qui demandent toute la story (dépendances, statuts connus) sont des diagnostics du format (§3.6), parce qu'ils doivent porter le numéro de ligne du Markdown.

3.4 Le jumeau et le compagnon

Le jumeau (kind: "story") est la même roadmap en JSON. Le convertisseur vit dans le cœur, comme celui du motion (motionScene de convert.ts) :

  • markdownToSource : le premier titre de la scène devient title, la prose avant la liste body, chaque item {…} un items[], chaque :::item{id} la description de l'élément du même id, type et date du séparateur les champs typés, le reste attrs ;
  • sourceToMarkdown émet l'ordre canonique : titre, prose, liste, blocs :::item. Une prose écrite après la liste est déplacée avant elle, avec un avertissement (roadmap-text-after-items) ; la liste se réécrit sans perte ;
  • la cascade format === "deck" ? … : format === "docs" ? … devient une table SCENE_CODECS: Record<StoryFormat, SceneCodec> (aller et retour), dans laquelle chaque format ajoute sa ligne. Le plan 23 (report) touche au même endroit : un seul mécanisme pour les deux.

Exemple du jumeau de la piste editor ci-dessus :

JSONJSON
{ "id": "editor", "title": "Editor", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "items": [ { "id": "export", "title": "Lorem ipsum export", "status": "shipped", "quarter": "2026-Q3", "end": "2026-09-18", "link": "https://example.com" }, { "id": "sync", "title": "Dolor sit amet sync", "status": "in-progress", "start": "2026-10-01", "end": "2026-12-15", "owner": "team-lorem" }, { "id": "offline", "title": "Consectetur offline mode", "quarter": "2027-Q1", "tags": ["mobile", "beta"], "horizon": "later", "description": "Sed do eiusmod…" } ] }

[x] ne s'écrit pas dans le JSON : il est status: "shipped". Au retour, un élément au statut done s'écrit [x] sans status=, les autres [ ] avec leur status= sauf s'il vaut le premier statut. L'aller-retour du test (compile(md) == compile(toJson(md))) passe donc par une forme canonique unique.

Le compagnon (kind: "sidecar") complète une piste par son id, comme aujourd'hui, et ses éléments par leur id. Dans le compagnon, les éléments s'écrivent en objet à clé, jamais en tableau, pour que la fusion soit sans ambiguïté :

JSONJSON
{ "kind": "sidecar", "version": 1, "scenes": { "editor": { "items": { "sync": { "status": "shipped", "end": "2026-11-28" }, "offline": { "owner": "team-ipsum" } } } } }

mergeSidecar gagne la notion de liste à clé : un champ déclaré comme tel par le format (items pour la roadmap ; entries pour l'annuaire du plan 12, qui en a le même besoin) se fusionne élément par élément. Un id d'élément inconnu est une erreur (sidecar-unknown-item), comme un id de scène inconnu ; le compagnon ne crée jamais d'élément et n'en retire jamais (règle d'AGENTS.md : un compagnon ne redéfinit pas la liste). La déclaration vit dans le cœur, à côté de SCENE_CODECS.

C'est le chemin d'une synchronisation : un script lit GitHub Projects ou Linear et réécrit product.json, jamais le .md. Les scripts eux-mêmes restent hors du cœur et hors de P9 (08 §4).

3.5 Les dates

  • Une roadmap ne manie que des dates civiles, sans heure ni fuseau : start, end, date (jalon) sont des PlainDate (2026-11-02), quarter un trimestre (2026-Q3). C'est volontaire : une roadmap ne promet pas d'heure, et une date sans fuseau ne change pas de jour d'un pays à l'autre. Le fuseau n'apparaît qu'à l'export ICS (journées entières, VALUE=DATE) et RSS (minuit UTC du jour, écrit en RFC 822).
  • Les valeurs du séparateur sont relues en chaînes par le convertisseur du format : date=2027 (que coerceValue transforme en nombre) produit l'erreur « date attendue AAAA-MM-JJ », jamais une date silencieusement fausse.
  • Trimestres, semestres et mois de la frise viennent de @recit/time (quarterOf, quarterRange, halfOf), calculés sur des dates civiles, sans Temporal : la moitié « roadmap » de la fondation n'a besoin d'aucun polyfill. Temporal n'arrive qu'avec le calendrier (plan 27).
  • Une seule horloge. La frise trace « aujourd'hui », et l'avertissement « élément planned dont la date est passée » a besoin de la date du jour. @recit/time expose today() et now(), qui lisent SOURCE_DATE_EPOCH (la convention des builds reproductibles) quand elle est posée, sinon l'horloge réelle. Les e2e et les goldens la fixent ; rien d'autre ne lit Date.now(). Le rendu now-next-later, lui, ne dépend jamais de la date (§4.2).
  • Affichage par Intl.DateTimeFormat dans la langue de la page (2026-Q3 devient « T3 2026 » en fr, « Q3 2026 » en en) ; les dates ne sont jamais localisables dans la source (09 §2.3).

3.6 Les règles de validation

Toutes portent le fichier et la ligne (règle d'AGENTS.md), ou le chemin JSON pour un jumeau, par le même Diagnostic que le reste du cœur.

CodeGravitéCas
roadmap-item-iderreuritem {…} sans id, ou id en double dans la story
roadmap-unknown-statuserreurstatus hors de statuses
roadmap-unknown-dependserreurdepends vers un id inconnu
roadmap-depends-cycleerreurcycle de dépendances (le chemin du cycle est dans le message)
roadmap-date-ordererreurend avant start, ou start/end hors du quarter donné
roadmap-invalid-dateerreurdate ou trimestre mal écrit (2026-13-01, 2026-Q5, 2027)
roadmap-milestone-itemserreurune liste d'éléments dans un jalon
roadmap-done-undatedavertissementélément au statut done sans end : il n'entre ni dans le changelog daté ni dans le RSS
roadmap-overdueavertissementélément non terminé dont end est passé (horloge de build) ; jamais une erreur
roadmap-text-after-itemsavertissementprose après la liste d'une piste (déplacée avant, §3.4)
sidecar-unknown-itemerreurid d'élément du compagnon absent de la piste

id est obligatoire (recommandé, question 5) : un id déduit du titre changerait avec la langue, casserait la parité de recit i18n check, les ancres, les GUID du RSS et les clés du compagnon.

3.7 Les langues

Textes localisables : title de la story, des pistes, des jalons et des éléments, body, description, statusLabels. Communs à toutes les langues : id, statuts, dates, owner, tags, link, depends (09 §2.3, §5). recit i18n check ajoute à la parité des scènes celle des éléments : un id d'élément présent dans product.md et absent de product.fr.md est une erreur, un statut ou une date différents aussi (attributs structurels). Les libellés des statuts par défaut entrent dans les packs UI_STRINGS (packages/core/src/i18n/ui.ts), en en et en fr.

4. Le rendu

4.1 Le modèle compilé

@recit/roadmap compile une Story de format roadmap en un modèle de rendu, comme @recit/motion compile une story en Film :

TypeScriptTypeScript
// packages/formats/roadmap/src/model.ts export const RoadmapSchema = z.strictObject({ title: z.string(), lang: LocaleTagSchema, view: z.enum(ROADMAP_VIEWS), period: z.enum(["month", "quarter", "half"]), statuses: z.array(z.strictObject({ id: z.string(), label: z.string(), done: z.boolean() })), lanes: z.array(RoadmapLaneSchema), // id, title, html, items (resolved: horizon, range, html) milestones: z.array(RoadmapMilestoneSchema), // id, title, date, html /** Today, from the build clock; only the timeline reads it */ today: PlainDateSchema, }); export type Roadmap = z.infer<typeof RoadmapSchema>; export function storyToRoadmap(story: Story, options?: { file?: string }): RoadmapResult;
  • Il relit scene.mdast (titres, liste, blocs :::item) et scene.attrs, jamais le texte brut ; c'est le chemin qui marche pour un .md comme pour un jumeau, puisque le JSON passe par le pivot Markdown (§2).
  • Les textes y sont déjà résolus dans une langue et rendus en HTML sanitizé par la chaîne du cœur. Le client ne reçoit que ce modèle, sans mdast (comme toClientStory).
  • Roadmap est un type propre au format, défini une fois dans @recit/roadmap, dérivé de son schéma (comme FilmSchema de @recit/motion). Les types partagés (dates, source, config, diagnostics) restent ceux du cœur.

4.2 Les trois vues

VueCe qu'elle montreDonnées nécessaires
now-next-later (défaut)trois colonnes. Un élément va dans la colonne de son horizon, sinon dans celle de son statut selon horizons. Les éléments done sont repliés dans « Livré » sous la vuestatuts ; aucune date
timelineune frise : une ligne par piste, une barre par élément daté (start–end, ou le trimestre), les jalons en losanges sur l'axe, la ligne « aujourd'hui », les dépendancesdates ; les éléments sans date vont dans une ligne « Non planifié »
kanbanune colonne par statut, dans l'ordre de statuses ; lecture seule (pas de glisser-déposer : la source est le fichier)statuts

La vue par défaut ne dépend d'aucune date (risque « une roadmap publique qui promet des dates », 08 §9). La vue se choisit dans le frontmatter et, à la lecture, par ?view= dans l'URL (comme ?slide=N du deck). Les filtres aussi : ?status=in-progress&tag=mobile&owner=team-lorem&lane=api. Chaque élément a une ancre (#sync), et la carte d'un élément ouvre sa description dans un <dialog> natif.

Composants (08 §4, un renommage) : Roadmap (racine, choix de la vue), NowNextLater, Timeline, Kanban, RoadmapItem, RoadmapStatus (au lieu de StatusBadge, déjà pris par @recit/site-kit pour le statut d'un format), Milestone, ProgressBar (part des éléments done par piste), RoadmapFilters, DependencyLine, Changelog (éléments done par date). Ils vivent dans packages/formats/roadmap/src/components/, chacun avec son module CSS et sa story.

La frise est un SVG calculé au build : d3-scale (scaleUtc) place les dates, aucune mise en page n'attend le navigateur. Elle s'affiche sans JavaScript, s'imprime telle quelle, et ne bouge pas d'une capture à l'autre. Le JavaScript du client ne sert qu'aux filtres, au choix de vue et au dialogue.

4.3 Style et accessibilité

  • Loi du plan 19 : React et un module CSS par composant, compilé par Lightning CSS (scripts/build-css-modules.js, table typée X.module.css.ts), une seule feuille publiée @recit/roadmap/styles.css. Pas de Mantine dans un package publié (Mantine n'est que dans le framework de sites et @recit/site-kit), donc des éléments natifs : <dialog>, <fieldset> de cases à cocher pour les filtres, <nav> d'onglets pour les vues. Mode sombre par light-dark() et l'attribut data-mantine-color-scheme, jamais une classe .dark.
  • Couleurs : la teinte du site roadmap (vert, 07 · Domaines §4) et ses clartés. Un statut ne se lit jamais par la couleur seule : libellé et icône à chaque fois.
  • Structure : chaque colonne est une liste (<ul> titrée), chaque élément un <article> avec son titre, les dates dans des <time datetime>. La frise SVG porte role="img", un titre, et la vue now-next-later reste l'alternative textuelle complète, à un clic. axe tourne sur chaque story, en erreur (21).

4.4 Les exports

SortieCommandeContenuOù
Webrecit build product.mdpage statique, trois vues, filtresdist/
HTML autonomerecit export product.md --to htmlun fichier, le modèle embarquécomme le deck (--html reste l'alias)
PDFrecit export product.md --to pdfla frise par défaut (A4 paysage), ou pdf.view: now-next-later@recit/paged par la CLI, vue ?print=1, signal whenPrintable()
ICSrecit export product.md --to icsun VEVENT journée entière par jalon daté ; ics.items: true ajoute les éléments datés (start–end)product.ics ; sur un site, <routePrefix>/<slug>.ics
RSSrecit export product.md --to rssRSS 2.0 : un <item> par élément done daté, du plus récent au plus ancien ; guid = URL de l'ancreproduct.rss ; sur un site, <routePrefix>/<slug>.rss, un par langue
  • Une commande, un drapeau (règle B9 et ligne @recit/cli de 03 · Packaging §6) : export <file> --to ics|rss, jamais export ics (08 §4 l'écrivait encore ainsi).
  • L'écrivain ICS est dans @recit/time (./ics), pur, sans dépendance : il plie les lignes à 75 octets, échappe les textes, émet UID = <id>@<slug>.<hôte> (stable d'un build à l'autre) et DTSTAMP depuis l'horloge de build. Le plan 27 y ajoute les heures, les fuseaux (VTIMEZONE) et la lecture. L'écrivain RSS est petit et propre à la roadmap : @recit/roadmap/feed.
  • Les options PDF entrent en RM3 : RoadmapPdfOptionsSchema = z.extend(PdfOptionsSchema, { view }), orientation paysage par défaut, section roadmap.pdf de recit.config et pdf: du frontmatter (15 §3.2).
  • Chaque fichier est listé dans exports.json. Ce plan ajoute à ExportFileSchema les variantes ics (avec events: positiveInt) et rss (avec items: positiveInt), réservées par 15 · Export PDF §3.2 ; le plan 27 réutilise ics sans le redéfinir.

5. CLI et MCP

CommandeEffet
recit init --format roadmap [file]crée roadmap.md (Lorem), ou roadmap.json avec un .json ; INIT_FORMATS gagne roadmap
recit dev product.mdserveur de dev avec rechargement, comme le deck
recit build product.mdpage statique
recit validate product.mdschéma et règles du §3.6, avec ligne ; aussi sur le jumeau et le compagnon
recit convert product.md --to json|mdjumeau dans les deux sens
recit export product.md --to pdf|ics|rss|html§4.4 ; --locale et --all-locales comme partout
recit schema roadmapJSON Schema du jumeau
recit i18n checkparité des pistes et des éléments (§3.7)

La CLI aiguille sur format: (Markdown ou JSON), et @recit/cli gagne une dépendance vers @recit/roadmap (distribution → format, permis par 03 §8). --to sur export est créé par PDF1 (P6) ; ce plan y ajoute ics et rss, rien d'autre.

MCP (packages/mcp/src/server.ts) : la liste FORMATS de describe_format gagne roadmap (syntaxe Markdown et JSON Schema du jumeau), une ressource recit://guide/roadmap s'ajoute aux guides (et donc à recit skills export, B9). Un seul outil de validation pour tous les formats non motion, validate_story (story Markdown ou JSON, ou chemin), plutôt qu'un validate_roadmap : il rend les diagnostics du §3.6. Le plan 27 l'utilise tel quel.

6. Le site

6.1 L'intégration (@recit/site-stories)

  • Une table de visionneuses. plugin.tsx choisit aujourd'hui entre deux composants par un ternaire sur story.format. Au troisième format, il devient une table FORMAT_VIEWERS: Partial<Record<StoryFormat, LazyViewer>>, chaque entrée chargée en React.lazy (règle « Bundle Size » d'AGENTS.md), avec le deck en repli. Le plan 23 (report) arrive au même endroit avant P9 : la table est posée par le premier des deux, l'autre ajoute sa ligne.
  • Le modèle calculé au build. vite.ts calcule déjà le film d'une story motion (storyMotion). Le même crochet calcule Roadmap pour une story roadmap ; CompiledDeckEntry.motion devient un champ discriminé par format, pour ne pas ajouter un champ par format.
  • Les fichiers générés. Le build écrit <routePrefix>/<slug>.ics et .rss à côté de la page, et leurs lignes dans exports.json, que ExportPanel lit.
  • StoryEmbed : <StoryEmbed slug="product" view="timeline" /> dans une page MDX. La prop view est propre aux formats qui en ont ; format garde son sens actuel (montrer un deck en film) et n'est pas étendu.
  • @recit/site-stories dépend de @recit/roadmap (intégration → format, permis).

6.2 roadmap.recit.dev

sites/roadmap, sur createSiteConfig({ site: "roadmap" }) de @recit/site-kit ; un site n'est que Markdown et config (règle d'AGENTS.md).

PageContenu
/FormatHero : la roadmap de Récit en now-next-later, sa source Markdown dépliable à côté (§7)
/galleryroadmap-product dans ses trois vues, en en et fr
/templatesgabarits à copier (produit, open source, équipe), TemplateCard
/changelogle Changelog de la roadmap de Récit, et le lien RSS
/roadmap/<slug>les stories en plein écran (publish.routePrefix: "/roadmap", layout: "none"), avec .ics et .rss
  • Port local 3007, port e2e 4307 (e2e/ports.ts), projet Vercel recit-roadmap, entrée roadmap dans la table SITES de @recit/site-kit (07 · Domaines §4 et §5).
  • vercel.json sert .ics en text/calendar; charset=utf-8 et .rss en application/rss+xml; charset=utf-8.
  • Doc : sites/docs › Formats › Roadmap (syntaxe, jumeau, compagnon, exports). Aucune doc sur roadmap.recit.dev (règle de 07).
  • @recit/site-kit (formats.ts) passe roadmap en available, et sa ligne « une scène = » de « a milestone » à « a lane or a milestone », d'après 08 §0.

7. Dogfooding : les roadmaps de Récit écrites dans le format

Deux documents de Récit sont déjà des roadmaps : 06 · Feuille de route (le plan, en français, avec ses cases, ses décisions et ses questions) et sites/vision/pages/roadmap.mdx (la page « Where it goes » de vision.recit.dev, en anglais, avec un tableau des phases). Proposition, pas encore une décision :

  1. Une story, deux langues. sites/roadmap/stories/recit.md (anglais) et recit.fr.md (français), au format roadmap : une piste par groupe (« Fondations et deck », « Formats », « Après la 1.0 »), un élément par phase (id=p9, titre « Roadmap », statut, horizon), un jalon par version publiée (0.1.0-alpha à 1.0.0, daté seulement une fois publié). Les statuts des éléments viennent de l'état réel (journal.md §2 fait foi). Pas de dates promises : la vue par défaut est now-next-later.
  2. Le plan 06 reste le plan. Ses décisions, ses risques, ses questions et ses critères de sortie sont de la prose qu'aucune vue ne remplace. Le convertir lui-même au format (« générée depuis 06-roadmap.md converti au format roadmap », 08 §4 RM4) mélangerait deux documents ; et une page de sites/plans/pages n'est pas une story (NOT_A_STORY du test d'aller-retour). À la place : un contrôle dans pnpm check, scripts/check-roadmap-sync.js, qui compare les titres ## Pn — … de 06 aux éléments id=pn de la story et échoue sur un écart (phase ajoutée, renommée ou retirée d'un seul côté). Le plan 06 gagne une ligne qui renvoie à roadmap.recit.dev.
  3. La page de vision renvoie à la story. Son tableau « The order, and the reason for it » est la même liste que la story, en plus court, avec la raison de chaque phase. Elle garde sa prose et la colonne « Why it comes here » (c'est son propos), et remplace le statut écrit à la main (« Two formats are shipped… ») par un lien vers roadmap.recit.dev. Elle ne peut pas intégrer la story : un site ne dépend jamais d'un autre site (03 §8), et examples/ n'accepte que du Lorem Ipsum. La raison de chaque phase pourrait devenir la description de son élément dans la story ; la page de vision n'aurait alors plus de tableau du tout (question 2).
  4. Le site de plans garde 06 tel quel, et peut afficher la version française de la story par un lien, pour la même raison.

Ce que le dogfooding prouve : la vue now-next-later lisible sans dates, la parité en/fr, le compagnon tenu par un script, et le flux RSS des phases livrées.

8. Les exemples

examples/roadmap-product (règle B7), contenu en Lorem Ipsum seulement, aucun nom réel (owner = team-lorem, team-ipsum) :

FichierRôle
product.mdtrois pistes, un jalon, douze éléments couvrant les quatre statuts, deux dépendances, un :::item imagé
product.fr.mdla même roadmap en français, mêmes id
product.jsoncompagnon (kind: "sidecar") : statuts et end de trois éléments, comme les écrirait une CI
recit.config.tssection roadmap: (statuts par défaut, pdf.view)
package.jsonscope:example, scripts dev, build, export:pdf, export:ics, export:rss

Le jumeau product.story.json n'est pas obligatoire sur disque : le test d'aller-retour le garantit (AGENTS.md). Un gabarit create-recit (--template roadmap-product) en est la copie. sites/roadmap liste l'exemple dans ses devDependencies (constat ST-20).

9. Les tests

NiveauQuoiOù
Unitaire, cœurschéma : chaque erreur du §3.6 avec sa ligne ou son chemin ; codec : items, [x], :::item, ordre canonique ; fusion du compagnon par élément, sidecar-unknown-itempackages/core/src/__tests__/roadmap-*.spec.ts
Aller-retourcompile(md) == compile(toJson(md)) et compile(json) == compile(toMd(json)) sur examples/roadmap-product et sites/roadmap/stories, sans rien ajouter au test existantsource-conversion.spec.ts
Unitaire, timetrimestres, semestres, semaines ISO, today() sous SOURCE_DATE_EPOCH, écrivain ICS (pliage, échappement, UID stable), relu par un analyseur ICS en testpackages/time/src/__tests__/
Unitaire, formatstoryToRoadmap : horizons dérivés, dépendances, cycle, éléments non planifiés ; RSS valide (XML bien formé, dates RFC 822, guid)packages/formats/roadmap/src/__tests__/
Storiesune story par composant, avec un play : filtrer par statut (cases), changer de vue (onglets), ouvrir et fermer la description (Escape), lire une date (<time>) ; axe en erreurpackages/formats/roadmap/src/components/*.stories.tsx, titre Roadmap/<Composant>
e2e sitechaque page en en et fr, chaque vue de /roadmap/product, filtres dans l'URL, .ics et .rss servis avec le bon type ; captures sous SOURCE_DATE_EPOCH fixee2e/site-roadmap.spec.ts, projet roadmap
e2e Storybookcaptures des stories Roadmap/*e2e/storybook-roadmap.spec.ts
PDFnombre de pages, texte extrait (titres des pistes), signetstest de la CLI, comme le PDF du deck
Contrôlescripts/check-roadmap-sync.js (06 ⇄ story de Récit) dans pnpm checkscripts/__tests__/

Les captures -linux sont la référence, écrites seulement par pnpm test:e2e:update:linux e2e/site-roadmap.spec.ts. scripts/check-stories.js refuse un composant sans story ou sans play. Les seuils de couverture de vitest.config.ts gagnent une ligne par nouveau package.

10. Les lots

Identifiants de 08 §4 conservés (RM1 à RM5), sous-tâches RMn.m (règle B10). Prérequis : le lot avancé DI0 de P9 (12 · Glossaire et annuaire), au moins DI0.2 ({k=v}). Chaque lot laisse pnpm check, le build et le typecheck des projets touchés et pnpm test:related au vert, et porte son version plan (preminor pour le premier de la phase, prerelease ensuite).

RM0 — Socle d'intégration (S)

  • RM0.1 — SCENE_CODECS dans convert.ts (la cascade de ternaires devient une table), sans changement de comportement ; si le plan 23 l'a déjà fait, rien.
  • RM0.2 — FORMAT_VIEWERS dans @recit/site-stories, entrée de CompiledDeckEntry discriminée par format ; même remarque.

Sortie : source-conversion.spec.ts et les e2e du deck et du motion passent sans changement.

RM1 — @recit/time, moitié roadmap (S/M)

  • RM1.1 — PlainDateSchema, QuarterSchema dans packages/core/src/schema/time.ts.
  • RM1.2 — @recit/time publié : quarterOf, quarterRange, halfOf, isoWeek, today() et now() sur SOURCE_DATE_EPOCH, formatage Intl.
  • RM1.3 — @recit/time/ics : écrivain de journées entières (VALUE=DATE), pliage, échappement, UID, DTSTAMP.

Sortie : tests unitaires verts, y compris l'ICS relu par l'analyseur de test ; aucune dépendance ajoutée.

RM2 — Le modèle et les vues (M/L)

  • RM2.1 — RoadmapSourceSchema typé, RoadmapConfigSchema dans recit.config, JSON Schema roadmap.json.
  • RM2.2 — Codec Markdown ⇄ JSON, listes à clé dans mergeSidecar, règles du §3.6.
  • RM2.3 — storyToRoadmap et RoadmapSchema.
  • RM2.4 — Composants et vues (§4.2, §4.3), un module CSS et une story avec play chacun.
  • RM2.5 — Runtime (@recit/roadmap/runtime, montage côté client des filtres et du dialogue) ; recit init, dev, build, validate, convert aiguillés sur roadmap.

Sortie : recit build examples/roadmap-product/product.md montre les trois vues en en et fr ; pnpm test:stories packages/formats/roadmap passe, axe compris.

RM3 — Les exports (M)

  • RM3.1 — export --to ics et --to rss ; variantes ics et rss d'ExportFileSchema.
  • RM3.2 — RoadmapPdfOptionsSchema, vue imprimable ?print=1 avec whenPrintable(), export --to pdf par @recit/paged.
  • RM3.3 — MCP : describe_format (roadmap), validate_story, guide recit://guide/roadmap.

Sortie : ICS valide (relu par l'analyseur, importé à la main dans un client), RSS valide, PDF d'une page A4 paysage pour l'exemple ; exports.json liste les trois fichiers.

RM4 — L'exemple et la roadmap de Récit (M)

  • RM4.1 — examples/roadmap-product (§8) et son gabarit create-recit.
  • RM4.2 — sites/roadmap/stories/recit.md et recit.fr.md (§7), compagnon des statuts.
  • RM4.3 — scripts/check-roadmap-sync.js, dans pnpm check.

Sortie : l'aller-retour passe sur les deux ; le contrôle échoue si l'on renomme une phase dans 06 seulement.

RM5 — roadmap.recit.dev et la doc (M)

  • RM5.1 — sites/roadmap (§6.2), SITES, ports, Vercel, en-têtes des .ics et .rss.
  • RM5.2 — StoryEmbed view=…, fichiers générés au build et ExportPanel.
  • RM5.3 — sites/docs › Formats › Roadmap ; formats.ts de @recit/site-kit.
  • RM5.4 — e2e/site-roadmap.spec.ts, e2e/storybook-roadmap.spec.ts, captures -linux.

Sortie (celle de P9 dans 06) : examples/roadmap-product dans les trois vues en en et fr ; exports ICS, RSS et PDF valides ; la roadmap de Récit en ligne sur roadmap.recit.dev ; pnpm verify et pnpm test:e2e:linux e2e/site-roadmap.spec.ts verts deux fois de suite ; version 0.5.0-alpha publiée.

Ordre et agents. RM0 et RM1 sont indépendants et peuvent partir ensemble ; RM2.1–RM2.2 (cœur) précèdent RM2.3–RM2.5 (format) ; RM3 et RM4 se partagent après RM2 ; RM5 ferme. Un agent par lot, chacun sur ses fichiers ; seul le coordinateur lance le conteneur e2e (AGENTS.md).

11. Les risques

RisqueParade
Une roadmap publique qui promet des datesnow-next-later par défaut, sans date ; dates optionnelles ; roadmap-overdue n'est qu'un avertissement
Le pivot Markdown perd un champ typécodec dans le cœur, ordre canonique, aller-retour sur chaque exemple ; un champ du jumeau sans forme Markdown est refusé au schéma
Coercition des attributs du séparateur (date=2027 lu comme nombre)le codec relit les dates en chaînes et refuse un nombre, avec la ligne
Captures instables à cause de la date du jourune horloge, SOURCE_DATE_EPOCH, fixée en e2e ; now-next-later n'en dépend pas
Le compagnon devient une seconde source qui ajoute des élémentslistes à clé : fusion seulement, sidecar-unknown-item en erreur
Les filtres côté client font clignoter la page prérenduela page prérendue montre la vue non filtrée ; les filtres s'appliquent après l'hydratation, sans décalage de mise en page (colonnes à hauteur fixe)
La synchronisation GitHub ou Linear grossit le périmètrehors du cœur et hors de P9 (08 §4) ; le contrat est le compagnon
La roadmap de Récit et le plan 06 dériventscripts/check-roadmap-sync.js dans pnpm check
Un StatusBadge de plusRoadmapStatus, un mot par chose

12. Questions ouvertes

  1. Source de la roadmap de Récit : une story écrite dans le format (sites/roadmap/stories/), tenue en phase avec 06 par un contrôle (recommandé, §7) ; ou une story générée depuis 06 par un script, comme l'écrit 08 §4 RM4 ?
  2. Page de vision : garder son tableau et sa prose avec un lien vers roadmap.recit.dev (recommandé) ; ou déplacer la raison de chaque phase dans la description des éléments et ne garder que la prose ?
  3. type=milestone : la règle B4 de 06 réserve type= aux blocs motion. Recommandé : amender B4 pour dire que type= est le type de scène propre au format (bloc en motion, milestone en roadmap), ce que fait déjà 08 §4. Autre voie : --- milestone=2027-03-31 id=v1.
  4. Colonne now-next-later : horizon= explicite, sinon dérivée du statut par la table horizons (recommandé, sans date) ; ou dérivée des dates (trimestre courant = « now »), qui dépend de l'horloge ?
  5. id obligatoire sur chaque élément (recommandé) ; ou déduit du titre, au prix de la parité entre langues ?
  6. Jalon sans date (une version pas encore datée) : permis, affiché en now-next-later et absent de l'ICS et de la frise (recommandé) ; ou date obligatoire ?
  7. Flux : RSS 2.0 seul (recommandé, 08) ; ou aussi Atom ou JSON Feed ?
  8. lane= : supprimé, puisque toute scène qui n'est pas un jalon est une piste (recommandé) ; ou gardé comme dans l'exemple de 08 ?
  9. Analyseur ICS des tests : une dépendance de dev (ical.js, MPL-2.0), qui servirait aussi à la lecture d'un .ics en P10 (plan 27, question 4) ; ou un analyseur maison minimal ?

13. À reporter dans les autres plans (par l'intégrateur)

FichierCe qui change
06-roadmap.mdP9 renvoie à ce plan ; RM0 ajouté ; règle B4 selon la question 3 ; sortie : « version 0.5.0-alpha » inchangée
08-formats.md §4export --to ics et --to rss ; lane= selon la question 8 ; id obligatoire ; RoadmapStatus ; renvoi à ce plan pour le détail
09-langues-et-json.md §2.3ligne roadmap : champs de scène type, title, date, items[] (avec horizon, depends, description)
12-glossaire-annuaire.mdles listes à clé du compagnon (entries) suivent la même fusion que items
15-export-pdf.md §3.2variantes ics et rss de ExportFileSchema, RoadmapPdfOptionsSchema
21-tests.md §3.1titre Storybook Roadmap/<Composant>, surface e2e/storybook-roadmap.spec.ts
sites/plans/recit.config.tsentrée de navigation pour 26