Documentation Carbon
docs est un site de documentation construit, avec opinions — Fumadocs + Next.js (React 19), light-only, esthétique papier-chaud. Il n'est plus généré ; il existe et livre du contenu. Ce skill explique comment ajouter ou modifier la documentation dans ce système, en respectant son style maison, ancré dans du vrai code Carbon.
Annoncez au démarrage: "Using the carbon-docs skill — authoring docs for {topic}."
La plus grosse erreur est d'écrire une prose générique et plausible. Le comportement de Carbon est spécifique et souvent contre-intuitif (WIP est un solde GL et non une table ; payment est un champ et non une entité ; les frais généraux ne sont pas absorbés ; la cession d'immobilisation est scrapping-only). Chaque affirmation est ancrée dans la source. Voir la directive première ci-dessous — elle prime sur tout.
Ce qu'est Carbon : un système de fabrication — ERP (le bureau) + MES (l'atelier), une plateforme sur un seul modèle de données. Academy n'est pas un troisième pilier produit : elle héberge juste des vidéos de formation, pas une application SaaS qu'un client achète. Ne présentez jamais Carbon comme « ERP, MES et formation/Academy » ; le site, la méta et la copie marketing restent ERP + MES. (L'app
academypeut figurer dans un listing architecture/monorepo — c'est ok ; décrivez-la comme de l'hébergement de vidéos, pas comme un pilier.)
La directive première — ancrer tout dans la source
La source de vérité absolue est le code source réel + les DERNIÈRES migrations base de données
(packages/database/supabase/migrations/, les plus récentes par timestamp) — PAS la connaissance générale ERP/CMMS, PAS .ai/rules/ seul (c'est souvent stale).
- Vérifiez avant d'écrire. Chaque entité, valeur d'enum de statut, et transition nommées dans la doc doivent exister dans le code réel. Confirmez les chaînes exactes (
"To Ship and Invoice","Fully Depreciated"), les actions qui les pilotent (fonctions de service, routes, edge functions danspackages/database/supabase/functions/), et ce qui les poste/gère (par ex.companySettings.accountingEnabled). - Lisez la migration la plus récente, pas la première. Les timestamps les ordonnent ; une refonte 2026 peut avoir reconstruit un sous-système que le cache décrit encore de l'ancienne façon.
- Documentez uniquement les vraies features ACTIVES. Omettez les placeholders / inactifs / pas-encore-livrés (par ex. les entrées du registre d'intégration avec
active: falsecomme QuickBooks/Sage/Zapier). Ne les surfacez pas. - Quand le code et le cache divergent, le code gagne — et notez le décalage.
- Méthode : dispatcher un sous-agent de recherche par feature → retourner des faits vérifiés avec références
file:line→ puis écrire. C'est ainsi que chaque flux du Guide a été construit. Ne le sautez pas pour rien de non-trivial.
Trois surfaces (savez laquelle vous touchez)
| Surface | Chemin / route | Ce que c'est | Écrit ? |
|---|---|---|---|
| Guides | content/guides/*.mdx → /guides |
Tours narratives éditoriales, groupées en flux. Deuxième personne, avec opinions, illustrées. Le parcours + le « pourquoi ». | MDX écrit à la main |
| Référence | content/docs/**/*.mdx → /docs |
Une page par entité/concept. Scannables : tableaux, cartes, lignes de champs. Le « quoi ». | MDX écrit à la main |
| Référence API | app/api-reference/[module]/[resource] |
Docs d'endpoint PostgREST, généré au build à partir du swagger. | Généré — NE PAS éditer à la main. Éditez scripts/generate-api-docs.mjs ou le schéma swagger. |
Entreliez les surfaces et flux : un Guide pointe vers la Référence pour les détails ; la Référence pointe vers le Guide pour l'histoire. (Les chapitres du Guide ont eu 12 liens inter-flux — l'entrelacement est attendu, pas optionnel.)
Flux d'auteur (chaque changement)
- Recherche (ancrée). Un sous-agent vérifie la feature contre la source + migrations récentes. Obtenez les noms exacts table/colonne/enum/transition +
file:line. Signalez ce qu'une description générique raterait. - Choisissez la surface + le placement. Flux de Guide + frontmatter, ou dossier Référence + ordre
meta.json. - Écrivez dans la voix maison avec les vrais composants de la surface (ci-dessous). Commencez par un exemple concret.
- Vérifiez — contre le serveur dev en exécution de l'utilisateur, en lecture seule. Ils ont généralement
pnpm --filter docs devlancé. Jamaispkill/restart,next build, ourm .nextdessous — vérifiez en récupérant les pages :cd docs pnpm exec fumadocs-mdx # regen .source si une frontmatter/content change n'a pas HMR curl -sS -X GET http://localhost:3002/docs/<slug> | grep -o "your new heading" # confirmer qu'il s'est affichéUn
pnpm --filter docs buildvert (✓ Generating static pages N/N) est l'étalon-or — mais seulement dans un checkout propre / quand aucun serveur dev n'est en cours.tscest bruyant (un skew React 18/19@typesquenext.configignore) — filtrez vos propres fichiers des erreurs plutôt que d'attendre zéro.
Guides — l'architecture de flux
Frontmatter (schéma source.config.ts) :
---
title: From quote to order
description: An RFQ becomes a quote; an accepted quote becomes a sales order.
label: "(I)" # marqueur d'affichage, chiffre romain, par-flux
index: 0 # ordre DANS le flux (0,1,2…)
flow: quote-to-cash # id flux (omis → défaut "make-to-order")
flowName: Quote to cash # libellé onglet flux
flowIndex: 1 # ordre du flux dans la subnav (0 = premier)
---
- Les chapitres sont triés par
(flowIndex, index). La subnav est un commutateur de flux ; la barre latérale + sélecteur mobile + « lire suivant » sont limités au flux actif. Les 5 chapitres d'origine (order/build/plan/floor/ship) ne portent pas de champs flow et se replient dansmake-to-order(flowIndex 0) via les défauts. - Ajoutez un chapitre à un flux : même
flow/flowName/flowIndex,indexsuivant +label. - Ajoutez un flux : nouveau
flowIndex+flowName; ses chapitres commencent àindex: 0,label: "(I)". - Tous les corps de chapitre s'affichent côté serveur sur chaque page (le lecteur s'estompe, pas de nav de route), ainsi les liens vers
/guides/<slug>se résolvent. Pas de code fences dans les guides (prose + composants seulement).
Composants (components/editorial/mdx.tsx) :
<Figure illustration="flow-overview" caption="…" />— SVG du registre.illustrationDOIT être une vraie clé decomponents/editorial/illustrations.tsx(sinon elle s'affiche silencieusement comme rien). Clés valides :flow-overview, order-split, bom-tree, demand-forecast, planning-engine, shopfloor-loop, eight-d, traceability-graph, method-types, kit-vs-subassembly, reorder-policy, outside-processing, mes-station, issue-workflow, schedule-board, get-method, conversion-factor, opportunity-thread, cash-cycle, rfq-fanout, receive-bill-axes, wip-inflow, wip-to-cogs, depreciation-curve, asset-exit. Pour tout sans clé appropriée, utilisez<Screenshot>à la place — ne créez pas de clés.<Screenshot label="Sales order dashboard" caption="…" ratio="wide|tall|square" />— un emplacement pour une vraie capture Carbon. Utilisez-le quand le lecteur doit voir l'UI réelle (dashboard, un formulaire/champ, statut, board, où cliquer) — pas comme décoration. Libellisez l'écran réel, actuel + l'état avec précision (vérifiez qu'il existe) pour qu'une vraie capture puisse y tomber directement et la doc reste synchro avec le Carbon UI en direct.ratio="wide"par défaut.<Callout tone="neutral|blue|green|amber" badge="WHY BATCH" title="…">body</Callout>— le cheval de trait. Utilisez-le pour porter la vérité Carbon-spécifique qu'une description générique rate. Tons : blue = explicatif « pourquoi », green = bon à savoir/résultat, amber = mise en garde/contraste, neutral = définitionnel.<Divider />— ferme un chapitre avant sa ligne de conclusion.<Term>make to order</Term>— terme de glossaire inline : trait pointillé ; clic/tap ouvre un popover avec une définition d'une ligne ancrée + un lien optionnel « En savoir plus ». Même composant sur les deux surfaces ; les définitions vivent dansdocs/lib/glossary.ts. Voir « Entrelacement & le glossaire » ci-dessous.
Chaque titre ## devient une entrée de barre latérale — structurez les chapitres comme 3–5 sections ##.
Référence — pages d'entité
- Frontmatter :
title+descriptionseulement. - Nav = tableaux
pagesdemeta.json(ordonnés). Dossiers :content/docs/{reference,platform,integrate}/. Ordre racine danscontent/docs/meta.json; ordre d'un dossier + titre barre latérale dans son propremeta.json({ "title": "Product reference", "defaultOpen": true, "pages": [...] }). Ajoutez une page → ajoutez son slug auxpagesdu dossier. Ne listez pasindexdanspages— fumadocs traiteindex.mdxcomme l'index du dossier et le nav l'affiche comme "Overview" ; le lister duplique le titre comme un frère. Les intégrations sont leur propre section top-level (content/docs/integrations/) groupées par catégorie — documentez seulement les intégrationsactive(omettez les placeholdersactive: false+ les commentées). - Composants (
components/editorial/reference-components.tsx+components/mdx.tsx) :<Callout type="info|note|warn|warning|error|success|tip" title?>…</Callout>— type → badge+tone (info/note→NOTE/blue,warn/warning/error→HEADS UP/amber,success/tip→GOOD TO KNOW/green). Notez que c'est une API Callout différente de celle des Guides (type vs tone+badge).<Cards><Card title href icon?>…</Card></Cards>— grille de cartes-lien (navigation inter-surfaces).<EnvVars><EnvVar name type? default? required?>…</EnvVar></EnvVars>— lignes de champ/paramètre. La page des env-vars les utilise ; pour une référence de champ d'entité préférez<FieldTable>(ci-dessous).<FieldTable><Field name type? required?>desc</Field></FieldTable>(components/editorial/field-table.tsx) — la référence accordion champ/paramètre, la prise papier-chaud sur le TypeTable de Fumadocs. Utilisez-la pour une table de forme| Field | Type | Description |(champs item/job/line, routing, work-center, champs de politique reorder/shelf-life, paramètres d'intégration).type= le token de type (omis quand la table n'a pas de colonne type) ;requiredseulement quand la source le marque ; l'enfant est la description comme MDX (ainsi`code`inline, italiques,<Term>s'affichent — c'est pour ça que c'est des enfants, pas une proptype={{}}) Enregistré dans les deuxmdx.tsxeteditorial/mdx.tsx.<StatusFlow><Status name accent? branch? terminal?>meaning</Status></StatusFlow>(components/editorial/status-flow.tsx) — un widget interactif de cycle de vie (pilules sélectionnables → unCalloutde détail montrant le sens) qui remplace une table| Status | Meaning |linéaire. Enfants en ordre source (cycle de vie) ; les sens sont MDX. Drapeaux :accent= le seul jalon pivot (≤1, optionnel) ;branch= une pause temporaire retournable (Paused, On Hold, Needs Approval) ;terminal= une sortie hors-chemin (Cancelled, Voided, Lost, Expired). Utilisez-le SEULEMENT pour un cycle de vie linéaire — une matrice de comparaison 2-axes (par ex. factures vente-vs-achat) ou une petite liste enum reste un tableau markdown.<PlanBadge plan="Business" />— signale une feature payante. Gate page entière → définirplan: Businessen frontmatter ; s'affiche comme une petite pilule "Paid" inline avec le titre de la page (le libellé est fixé à "Paid" ; la valeurplannourrit seulement le tooltip au survol + la copie bannière). Gate section → déposer<PlanBadge>en corps. Features gatées =packages/ee/src/plan.ts. Ne badgez pas les libres (email, exchange-rates).- Les pages plan-gatées reçoivent aussi une barre d'annonce pleine largeur (
components/plan-banner-bar.tsx,PlanBannerBar) affichée par la layout des docs (app/docs/[[...slug]]/layoutconstruit une carte url→plan à partir du frontmatterplanet la passe) — sticky sous l'en-tête, couvrant la zone contenu. Ajouter le frontmatterplan: Businesssuffit ; le badge et la barre s'allument tous les deux. Ne posez pas à la main une bannière en MDX. - Modèle de licence — ayez ceci juste, c'était faux avant. Voir
docs/content/docs/platform/licensing.mdx(la page canonique). Éditions (packages/utils/src/types.tsenum Edition) : Community (auto-hébergé, noyau ouvert AGPLv3), Enterprise (auto-hébergé + licence commerciale), Cloud (géré). Features EE = code souspackages/eeou tout fichier.ee→ nécessitent une licence commerciale per laLICENSEdu repo.- Carbon Cloud = le SaaS hébergé à https://app.carbon.ms — PAS le déploiement
/docs/platform/.... Les recettes auto-hébergement (Docker avec Caddy, AWS avec SST) vivent sousdocs/content/docs/platform/self-hosting/. - Plans Cloud = Starter et Business seulement. L'enum
Plana aussiPartner, mais Partner est interne-only — ne le mentionnez jamais dans la doc destinée aux lecteurs. N'écrivez pas « plans Business et Partner ». - N'écrivez jamais « l'auto-hébergement n'est pas plan-gatée ». La gate runtime est Cloud-only (
packages/ee/src/plan.server.tsretourne tôt quandCarbonEdition !== Edition.Cloud), mais la licence gouverne toujours : l'auto-hébergement tourne en mode community ; les features Enterprise/EE nécessitent une licence commerciale. Présentez-le comme une frontière de licence, pas un verrouillage technique — les features ne sont pas « désactivées » en Community.
- Carbon Cloud = le SaaS hébergé à https://app.carbon.ms — PAS le déploiement
<Steps>/<Step>,<Tabs>/<Tab>(fumadocs-ui), tableaux markdown, et code fences (panneau foncé).<Term id?>…</Term>— terme de glossaire inline (trait pointillé → popover de définition). Le même composant que le Guide ; voir « Entrelacement & le glossaire » ci-dessous.
- La voix est plus technique/scannable que les Guides — champs, contraintes, tableaux — mais nomme quand même le piège et pointe vers le Guide pour la narration.
Voix maison
- Deuxième personne, concret, narratif. Ancrez dans l'exemple en cours (la commande de 90 robots humanoïdes). « Ouvrez le tableau de bord des commandes de vente. » / « Vous ne construisez pas 90 robots comme un seul travail monolithique. »
- Citez les vrais noms de statut exactement, entre guillemets :
**"To Ship and Invoice"**,**"Posted"**,**"Open"**. Les chaînes de statut vivent sur une entité spécifique — confirmer qu'une valeur appartient à l'en-tête ou la ligne (par ex."To Ship and Invoice"/"To Invoice"sontsalesOrderStatus, PAS l'enumsalesOrderLineStatusOrdered/In Progress/Completed) avant de l'attribuer, et énoncez la transition complètement (une commande à"To Ship and Invoice"bascule à"To Invoice"une fois tout expédié mais non facturé — n'implicitez pas qu'elle reste à une valeur jusqu'à fermeture complète). - Allez facile sur les tirets cadratins. Empilés, ils se lisent comme un tic — max un par paragraphe, jamais une paire tiret-parenthétique en phrase d'ouverture. Préférez un point ou une virgule ; atteignez le tiret seulement quand il vraiment bat les deux. (« Trop de tirets cadratins » est la note de copie la plus courante de la révision.) S'applique aux chaînes
caption=ettitle=aussi, pas juste la prose du corps. - Gardez les noms et chiffres de l'exemple en cours exacts. C'est la commande de vente (ne dérivez pas vers une « commande » simple quand vous instruisez le lecteur) et c'est 90 unités (pas « un robot »). Une fois que vous nommez l'entité et la quantité, restez cohérent chaque fois — la dérive est ce qui rend un tour brouillon.
- Ancrez chaque « où cliquer ». Quand vous dites au lecteur d'agir dans l'UI, copiez les vrais libellés bouton / modal / champ de la JSX (entre guillemets) et confirmez que la capacité existe exactement sur ce chemin. Une feature qui vit ailleurs n'est pas la même qu'une sur l'écran que vous décrivez — « Carbon divise la commande en trois travaux » était faux : le dialogue "Make to Order" → "Convert Line to Job" de la ligne de commande de vente crée un travail par clic (la division N-travaux-de-M est un flux séparé bulk-jobs). Vérifiez l'action, pas juste le concept.
- Les callouts portent la vérité contre-intuitive — la chose que les gens ratent. Le titre est une affirmation, le corps est le pourquoi. (« Les devis sont optionnels — l'opportunité est le fil. »)
- Expliquez le pourquoi, nommez l'erreur, pointez vers l'étape suivante. Si un paragraphe ne fait aucune de celles-ci, supprimez-le.
- Entreliez aux coutures naturelles (
[make-to-order tour](/guides/order)). - Menez, ne libellisez pas. Ne ouvrez pas une page avec un titre générique, répété — pas de
## Introductionsur les chapitres du guide (le titre du chapitre est affiché pour vous) et pas de## Why it matterssur les pages de référence. Ouvrez avec 1–2 phrases d'introduction substantives, puis allez droit aux vraies sections. Un titre identique sur chaque page est du remplissage. Les pages landing/index sont "Overview", jamais "Introduction". - Nommez les choses telles qu'elles sont maintenant. Utilisez le nom actuel d'une feature ; ne narrez jamais l'historique de renommage/suppression (« anciennement item rules », « les storage units avaient l'habitude de… »). Quand un ancien nom et le code divergent, le code gagne.
Entrelacement & le glossaire
Deux mécanismes de liaison — utilisez les deux, délibérément, chaque fois que vous touchez une page. La liaison interne se compose : une page qui lie dehors et glose son jargon vaut plus que la même prose en isolation.
- Les liens markdown portent la navigation. Liez le nom, inline, aux coutures naturelles ; jamais « cliquez ici ». Un Guide se lie à la Référence pour les champs ; la Référence se relie au Guide pour l'histoire (
[make-to-order tour](/guides/order)). Les liens inter-surfaces et inter-flux sont attendus, pas optionnels. Mais liez seulement quand le titre de la destination reprend votre texte d'ancrage. Un saut dur dont la page d'arrivée porte un H1 différent désoriente le lecteur — lier la phrase « quote to cash » à un chapitre intitulé From quote to order a été signalé comme confus. Quand l'ancre est un concept (un nom de flux, une catégorie) plutôt qu'une page littéralement appelée ainsi, glosez-la comme une<Term>(popover de définition + optionnel « En savoir plus » viahref) au lieu d'un lien nu — le lecteur obtient le sens sur place et peut quand même naviguer s'il choisit. <Term>porte les définitions. Enveloppez un terme de fabrication/Carbon qu'un lecteur peut rencontrer froid (type de méthode, système de réapprovisionnement, WIP, opération externe, kit/sous-assemblage…) pour qu'un clic donne la glose sans quitter la page.<Term>make to order</Term>slugifie le texte pour trouver l'entrée ;<Term id="make-to-order">made</Term>quand le texte d'affichage diffère du slug.- Première occurrence par page seulement — pas chaque instance. Souligner chaque « order » est du bruit.
- Les définitions sont une source unique de vérité :
docs/lib/glossary.ts(slug → { term, definition, href? }). Ajoutez l'entrée là avant d'utiliser un nouveau terme, et ancrez la définition dans la source (la directive première s'applique — valeurs enum exactes, comportement réel). Omettezhrefquand il n'y a pas encore de page dédiée (le popover affiche quand même la définition) ; le lien « En savoir plus » s'auto-cache quand il pointerait sur la page où vous êtes déjà. - Slug inconnu → s'affiche comme texte simple (jamais casse la prose) — une typo échoue proprement, pas bruyamment.
Passe d'enrichissement. Chaque fois que vous créez ou éditez une page, terminez avec une passe de liaison : enrobez le jargon de première occurrence dans <Term>, ajoutez des liens croisés markdown aux coutures, et ajoutez toute entrée glossaire manquante. Le moins cher moyen de relever la connectivité de tout le site.
Design / style
- Light-only. Palette papier-chaud : page bg
#FBFBF9/#F5F5F2; encre#262323(+rgba(38,35,35,0.x)pour faible) ; accent#1E84B0(liens) /#00B0FF(focus/brand) ; hairlines#E7E7E3/#E3E3DF. - Valeurs Tailwind arbitraires inline (
text-[15px],bg-[#FBFBF9],border-[#E7E7E3]) — cette app est autonome (PAS@carbon/react, PAS le thème ERP). Correspondez à la densité et aux couleurs du composant environnant ; n'introduisez pas une nouvelle palette. - Polices : DM Sans (corps), Fira Code (mono). Callout tone remplissages/bordures : neutral
#EFEFEB/#DADAD5, blue#DFF5FF/#A9DAF3, green#E4F8DA/#A8DB91, amber#FFF2D8/#E6CFA3.
Pièges (acquis difficilement)
- Frontmatter est YAML — ne laissez jamais un deux-points sans guillemets dans une valeur. Une valeur
title:/description:contenant:(deux-points-espace) est parsée comme un mapping imbriqué et lanceYAMLException: bad indentation of a mapping entry. Soit citez la valeur entière (description: "How it works: the short version.") soit reformulez pour abandonner le deux-points (une virgule/point est généralement mieux). Un mauvais frontmatter 500s le site entier, pas juste cette page — fumadocs charge la collection source entière au chargement du module, le symptôme est chaque page (guides inclus) retournant 500 avec le chemin.mdxoffensant dans la pile. Cela mord le plus dur lors d'un balayage em-dash : remplacer un em-dash de description avec un deux-points casse silencieusement YAML.pnpm exec fumadocs-mdxregen NE le détecte PAS (il passe) — le seul contrôle fiable est récupérer une page (n'importe laquelle) et voir 200 vs 500. Quand vous déléguez des édits de prose à des sous-agents, dites-leur explicitement : en frontmatter, n'introduisez jamais un deux-points sans guillemets. - Thème Shiki (
source.config.ts→rehypeCodeOptions.themes) : définissez les deuxlightetdarkau même"github-dark-default". Unthemesimple (ou ungithub-lightmanquant) casse le build pour tout fichiercontent/docsavec une code fence (ShikiError: Theme github-light not found). Les guides n'ont pas de fences, donc sont immunisés — utile pour isoler un build rouge à la côté Référence. - Regen
.sourceavant typecheck après tout changement frontmatter/schéma (le schéma est cuit dans.source/au temps de génération). - Les clés Figure doivent exister — voir la liste ci-dessus ; une typo s'affiche rien, silencieusement.
- Bare
{…}en MDX est une expression JS. Un token en prose comme{item.id}(par ex. inside une règle message d'exemple) casse le build avecitem is not defined. Enrobez tout littéral accolades/tokens en backticks :`{item.id}`. Writeblocs sur les fichiers existants — protection naturelle de collision quand une session parallèle co-écrit la doc. Écrivez juste le prochain gap découvert ; ne pausez pour coordonner sauf demandé.curlGET peut 405 dans ce sandbox (une requête sans-méthode se lit comme POST) — utilisez l'outil WebFetch pour les pages externes ; le registre pnpm marche bien.- Ne hand-éditez pas la référence API (générée) — changez le générateur/schéma et rebuildez.
Références de deep-dive (lisez quand vous atteignez la phase pertinente)
references/components.md— APIs de composants complets pour les deux surfaces + le registre d'illustration.references/writing-guide.md— la voix, la règle d'ancrage, exemples travaillés.references/information-architecture.md— l'IA flux et la navmeta.jsonRéférence.references/design-language.md— palette, polices, la convention Tailwind-inline.
Note :
references/scaffold.md,references/brand-integration.md, etassets/templates/*sont ère scaffolding (comment l'app a d'abord été montée, avec Geist/@carbon/react/modèles signature-touch). L'app a divergé d'eux. Les composants en direct dansdocs/components/{editorial,api}/sont la source de vérité — lisez ceux-ci, pas les modèles, en cas de doute.
Barre de vérification
Ne déclarez jamais la doc faite sans : le nouveau contenu s'affichant (dans le serveur dev en exécution de l'utilisateur, ou un pnpm --filter docs build propre), chaque lien interne se résolvant, tout nouveau <Term> entrée glossaire ancrée dans la source + ses popovers s'affichant, noms correspondant au vrai code, pas de titres générique répétés, et une relecture qui confirme que chaque page dit ce qui importe / nomme l'erreur / pointe vers l'avant. Puis enregistrez le progrès (le fichier plan .ai/plans/ s'il en existe, + mémoire). Ne tuez ni ne rebuildez sous le serveur dev en exécution de l'utilisateur.