ArchitecturePublic log (/changelog + /roadmap) — Architecture & Reference

Public log (/changelog + /roadmap) — Architecture & Reference

Source de verite pour le pipeline qui alimente /changelog et /roadmap. Mis a jour : 2026-09 (platform-ops/2640).

Source de verite pour le pipeline qui alimente /changelog et /roadmap. Mis a jour : 2026-09 (platform-ops/2640).

1. Le defaut que ce pipeline corrige

.github/workflows/auto-changelog.yml POST chaque commit pousse sur main a POST /api/admin/changelog/auto (src/app/api/admin/changelog/auto/route.ts), qui cherche un RoadmapItem dont commitSha egale le sha du commit. Cette colonne n'est ecrite que par le champ texte de l'editeur admin (src/app/(dashboard)/admin/content/roadmap/_components/ roadmap-editor.tsx) ou par l'outil MCP complete_task (src/app/api/mcp/roadmap/route.ts). La flotte n'appelle ni l'un ni l'autre : son modele operatoire est 100% fichiers (backlog/<pillar>/<id>-<slug>.md, docs/team/protocol.md §1). Chaque commit ressortait donc skipped: no-matching-roadmap-item, et au 2026-09-12 /changelog affichait une seule entree (le seed prisma/seed-changelog.ts) pour 481 items archives et 248 PR mergees depuis. Ce workflow reste dans le depot (il ne fait de mal a rien), mais le mecanisme reel est desormais celui decrit ici.

Depuis septembre 2026, le chemin reel n'est meme plus un commit : c'est une pull request. POST /api/webhooks/github (dev-console.md §7) recoit pull_request et pull_request_review, correle la PR a son item par le trailer Backlog: que le protocole exige deja (§5) ou par la branche fleet/..., fait avancer le RoadmapItem, et ouvre l'entree ChangelogEntry DRAFT au merge — une par PR mergee, dedupliquee sur prUrl. auto-changelog.yml n'est pas supprime pour autant : il ne fait de mal a rien, et le supprimer serait un changement sans benefice.

2. La chaine

backlog/**/*.md  (public: "true")
      │  scripts/derive-public-log.mjs   ← pre-push + CI (--check)
      ▼
src/services/public-log/public-log.generated.ts   ← artefact versionne
      │  src/services/public-log/sync.ts  ← cold-start, latche
      ▼
RoadmapItem (IN_PROGRESS)  +  ChangelogEntry (DRAFT, groupe par semaine)
      │  cron IA hebdomadaire (ai-platform/2641, pas encore livre)
      ▼
/admin/content/changelog + /admin/content/roadmap  → un humain valide
      ▼
/changelog · /changelog/[id] · /changelog/rss.xml · /roadmap

3. Opt-in, jamais automatique

Un item de backlog n'entre dans le manifeste que s'il porte public: "true" dans son frontmatter (backlog/_template.md). Absent = prive, y compris pour les 481 items deja archives avant ce pipeline : aucune retro-publication en masse n'a ete faite, decider quels items historiques sont surs a montrer (trente sont des correctifs de securite) est une decision produit, pas quelque chose qu'un script derive tranche.

public_title / public_summary sont facultatifs : ecrits par l'agent qui livre, ils font autorite ; absents, le composer hebdomadaire (ai-platform/2641) les redige.

Une release n'est pas une PR

La roadmap suit la regle de la flotte : un item, une PR. Le changelog, lui, compte en releases, et une release groupe ce qui a du sens ensemble :

  • release: "<cle-kebab>" dans le frontmatter d'un item le range dans cette release. Plusieurs items (donc plusieurs PR) qui partagent la cle font UNE entree ; une grosse PR seule peut aussi etre sa propre release ;
  • sans release, l'item retombe dans la semaine ISO de son archivage (weekKey()), le comportement d'avant ;
  • groupShippedByRelease (src/services/public-log/sync.ts) cree la release en DRAFT au premier item, puis, tant qu'elle est DRAFT, ajoute les puces des items livres plus tard dedans. Ajouter seulement : une ligne retouchee par un admin reste, et une release COMPLETED n'est plus jamais touchee. La publier reste un clic humain dans /admin/content/changelog.

Avant le lancement public, /changelog ne montre qu'une release de lancement construite depuis SHIPPED, derriere CHANGELOG_LAUNCHED (src/app/(marketing)/changelog/_components/release-extras.ts). Passer ce drapeau a true bascule la page sur ces releases : c'est l'item growth-web/3031, bloque jusqu'au lancement.

Cette regle a ete contournee par la table, pas par le manifeste (growth-web/3001, 2026-09-25). Le sync de la flotte (src/services/fleet/sync.ts) ecrit dans le MEME RoadmapItem une ligne par item suivi, sans opt-in, pour la console operateur. /roadmap, /roadmap/[id] et le sitemap ne filtraient que le statut : environ 300 items internes etaient publies, titres de failles de securite ouvertes compris. Tout lecteur public applique desormais PUBLIC_ROADMAP_WHERE (src/services/public-log/public-roadmap.ts), qui exclut toute ligne portant backlogRef, et src/test/public-roadmap-hides-fleet-rows.test.ts refuse un lecteur qui l'oublie.

Deux listes, derivees separement par scripts/derive-public-log.mjs (reutilise parseFrontmatter de scripts/fleet-status.mjs) :

  • SHIPPED — tout item public: "true" sous backlog/_archive/**, avec archivedAt (date du commit qui a deplace le fichier dans _archive/, git log -1 --format=%cI).
  • NOW — tout item public: "true" dont status est claimed / in-progress ET qui porte un branch: (le verrou fleet reel, docs/team/protocol.md §1 — un item sans branche n'est pas effectivement en cours au sens de la flotte).
pnpm log:derive          # regenere le manifeste
pnpm log:derive:check    # verifie sans ecrire (CI + pre-push)

4. Pourquoi un artefact committe, pas une lecture runtime de backlog/

Une fonction serverless Vercel ne voit que ce que Next trace. next.config.mjs's outputFileTracingIncludes ne declare que trois fichiers, tous pour /admin/settings/security — backlog/ n'est importe par aucun module applicatif, donc absent du bundle. La derivation tourne donc la ou le depot existe entierement : pre-push et CI, jamais au runtime. Le resultat, un module TypeScript importe statiquement, suit la convention des trois autres artefacts generes du depot (schema-guard.generated.ts, client-namespaces.generated.ts, shopify-taxonomy.generated.ts) : declare -merge linguist-generated dans .gitattributes, generateur dans scripts/resolve-generated-conflicts.mjs, garde par src/test/generated-artifacts-have-a-generator.test.ts.

5. Le sync runtime : create-only, jamais d'ecrasement

src/services/public-log/sync.ts tourne au cold-start (src/instrumentation-node.ts, meme latch claimDeployOnce que syncMarketplaceListings), et force-triggerable par un admin via POST /api/admin/public-log/sync.

Il ne modifie jamais une ligne existante, seulement en cree une nouvelle quand elle est absente.

Ce paragraphe a cesse d'etre vrai pour l'AUTRE sync. La colonne marqueur qui manquait ici existe depuis RoadmapItem.backlogRef, et src/services/fleet/sync.ts l'utilise pour rafraichir en toute securite les lignes qu'il a lui-meme ecrites (dev-console.md §4). Le sync du log public, lui, reste create-only : il n'ecrit pas backlogRef, donc il n'a toujours aucun moyen de reconnaitre ses propres lignes, et le raisonnement ci-dessous s'applique tel quel.

Pas de colonne marqueur pour distinguer « ecrit par ce sync, sans risque a rafraichir » de « edite a la main par un admin, a laisser tranquille » (contrairement a syncMarketplaceListings et son ADMIN_HELD_STATUSES) : le choix le plus sur est donc d'ecrire une fois puis de laisser la ligne au proprietaire qui la possede ensuite (/admin/content/roadmap, /admin/content/changelog). Le vrai re-composeur, qui doit decider « ce DRAFT a-t-il besoin d'etre etendu », est le cron IA hebdomadaire (ai-platform/2641, pas encore livre) — sur le modele de composeWeeklyEdition (src/services/bulletin/compose-weekly.ts) et son dedupeKey, pas un sync aveugle.

Rien de ce que ce sync ecrit n'est visible publiquement : RoadmapItem est cree IN_PROGRESS, ChangelogEntry DRAFT — les deux etats que /changelog (filtre COMPLETED) et /roadmap (n'exclut que CANCELLED, donc IN_PROGRESS EST visible — c'est le point : un item « Now » doit se voir) traitent deja correctement aujourd'hui.

6. Identite sans migration de schema

prisma/schema.prisma est un hot file et pnpm db:guard refuse toute colonne NOT NULL sans defaut (cf. CLAUDE.md, section « Schema sync en deploy »). Ce pipeline n'ajoute aucune colonne :

EntiteIdentiteMeme forme que
ChangelogEntry (release hebdo)version: weekKey() (2026-W36) + commitSha: nullprisma/seed-changelog.ts::seededEntryWhere
RoadmapItem ("Now")branchName (deja une colonne, le verrou fleet)—

7. Ce qui reste a faire

  • ai-platform/2641 — le composer IA hebdomadaire qui redige la copie EN structuree (New / Improved / Fixed) quand publicTitle/publicSummary sont absents, sur le modele de compose-weekly.ts.
  • growth-web/2642 — la refonte visuelle : /changelog groupe par semaine avec ses sections, /roadmap en trois voies Now (derive) / Next / Later (editorial). A concevoir une fois que des items reels auront opte in : au merge de platform-ops/2640, SHIPPED/NOW sont legitimement vides.
  • growth-web/2643 — nettoyage du composant feedback-form.tsx, de la branche anonyme de /api/feedback et du namespace i18n feedback, orphelins depuis le retrait de la page publique /feedback (desormais un permanentRedirect vers /contact).

8. /feedback

Retiree comme page publique : elle dupliquait le dropdown feedback du dashboard (src/components/shared/feedback/dropdown.tsx, cable dans user-actions.tsx) pour un public qui n'a le plus souvent pas de dashboard, et son propre parcours affiche decrivait ce meme pipeline cassé (« it moves to the changelog automatically »). src/app/(marketing)/ feedback/page.tsx est desormais un permanentRedirect("/contact") — le /contact existant est deja topic-route. Les six liens entrants (deux CTA /roadmap, un /roadmap/[id], un /tutorials, un /community/forum, un tool-detail-view.tsx) pointent maintenant directement sur /contact.