Enregistrements de décision
Une personne qui hérite d'une application en production peut en lire chaque propriété sans jamais savoir pourquoi l'une d'elles est ainsi. Pourquoi Postgres plutôt que SQLite par défaut ? Pourquoi la colonne de marge est-elle masquée à l'atelier ? Un fichier de configuration répond quoi, un diff répond ce qui a changé — aucun des deux ne dit à quoi cela servait.
Alors la justification part ailleurs, là où la configuration ne voyage pas : un wiki, une page Notion, un fil de discussion, quelqu'un qui est parti. Elle dérive en une version ou deux, et l'application lui survit.
Le tableau decisions place ces enregistrements à côté de la configuration qu'ils ont décidée, dans le même fichier, sous la même relecture, livrés dans le même binaire :
decisions:
- id: ADR-002
title: SQLite sur le PC de l'atelier
status: superseded
date: '2025-11-04'
deciders:
- Thomas
supersededBy: ADR-007
touches:
- engine › DATABASE_URL
context: Deux personnes utilisent l'application, qui tourne sur la machine de l'atelier.
decision: Pas de DATABASE_URL. L'application garde son fichier SQLite à côté du binaire.
consequences: La sauvegarde est celle que quelqu'un pense à copier. Aucun second service à faire tourner.
- id: ADR-007
title: Postgres pour l'instance partagée
status: accepted
date: '2026-06-01'
deciders:
- Léa Fontaine
- Thomas
supersedes: ADR-002
touches:
- engine › DATABASE_URL
- tables › quotes
context: Quatorze comptes écrivent dans l'application, deux agents la lisent, et l'export nocturne verrouille le fichier une minute durant.
decision: DATABASE_URL pointe vers un Postgres infogéré en eu-west-3. Le binaire est inchangé ; les migrations tournent au démarrage comme avant.
consequences: Les sauvegardes sont celles de l'hébergeur. Le PC de l'atelier n'héberge plus rien.Propriétés d'un enregistrement
| Propriété | Description |
|---|---|
id |
Identifiant du registre, unique dans le tableau. Libre : ADR-007, DEC-083, RFC-12 sont tous acceptés. |
title |
Une ligne disant ce qui a été décidé. |
status |
accepted, proposed ou superseded. Voir Statut. |
date |
Le jour de la décision, en jour calendaire ISO-8601 : 2026-06-01. Voir Pourquoi la date est contrainte. |
deciders |
Qui a décidé — une entrée de tableau par personne, jamais une phrase. |
touches |
Ce sur quoi portait la décision, en texte d'affichage. Jamais résolu contre la configuration. Voir Ce que touches désigne. |
context |
La situation qui a imposé un choix. |
decision |
Ce qui a été choisi, énoncé à la voix active. |
consequences |
Ce qui en découle, en bien comme en mal. |
supersedes |
L'id de la décision que celle-ci remplace. Voir Remplacement. |
supersededBy |
L'id de la décision qui a remplacé celle-ci. Exige status: superseded. |
context, decision et consequences sont les trois parties rédigées d'un ADR classique de Nygard. La forme est délibérément celle que tout outil d'ADR emploie déjà : une équipe qui arrive avec un dossier docs/adr/ transcrit au lieu de traduire.
Le tableau est entièrement optionnel. Une application qui ne déclare aucune decisions n'est en faute nulle part : son registre est simplement vide.
Statut
status est ce qui fait d'un tas d'enregistrements un registre :
| Valeur | Sens |
|---|---|
accepted |
La décision tient, et la configuration la reflète. |
proposed |
Encore en cours. Un enregistrement qu'on peut lire et contester avant qu'il ne fasse loi. |
superseded |
Remplacée par une décision ultérieure — ou simplement abandonnée. |
Une décision remplacée reste une décision. La supprimer supprime la raison d'être de celle qui la remplace : le registre la garde plutôt que de l'élaguer.
Ce que touches désigne
touches nomme ce sur quoi portait une décision, tel que l'auteur l'a écrit : engine › DATABASE_URL, tables › quotes, forms › quote-request, ou un simple auth. C'est du texte d'affichage libre, et rien ne le déréférence.
Il n'est délibérément jamais résolu contre la configuration, et c'est là la décision de conception centrale du registre, non un oubli. Une décision survit nécessairement à ce qu'elle a décidé : l'enregistrement ci-dessus documente l'ère SQLite d'une application qui tourne désormais sur Postgres, et un enregistrement remplacé nomme une configuration disparue par définition. Une règle de référence croisée refuserait ici un démarrage parce que le registre a été honnête sur sa propre histoire, et vous apprendrait à supprimer l'enregistrement plutôt qu'à le garder.
Une entrée touches qui ne nomme rien d'existant dans votre configuration démarre donc, et fait l'aller-retour à l'identique. Le › est un séparateur que la surface de lecture affiche, pas un chemin que le schéma découpe — employez le vocabulaire que votre équipe emploie déjà.
Remplacement
supersedes et supersededBy sont tous deux écrits à la main. Ni l'un ni l'autre n'est dérivé, parce qu'app.ts est un fichier que des humains lisent et qu'un champ dérivé afficherait à l'écran une ligne qui n'est dans aucun fichier.
Le prix des deux extrémités écrites, c'est qu'elles peuvent se contredire — et un lien unilatéral affiche une puce de filiation sur un écran et un tiret sur l'autre. La paire est donc vérifiée au décodage, et ces six formes sont refusées au démarrage plutôt que rendues de travers :
| Refusé | Pourquoi |
|---|---|
Deux enregistrements partageant un id |
Tout lien de filiation se résout par id : un doublon nomme deux décisions. |
supersedes ou supersededBy nommant l'id de l'enregistrement |
Une décision ne peut pas remplacer la décision qu'elle est. |
Un lien nommant un id que le registre ne déclare pas |
Une référence croisée qui ne mène nulle part. |
A.supersededBy: B sans B.supersedes: A |
Les deux extrémités sont écrites : les deux doivent concorder. |
supersededBy présent avec un status autre que superseded |
Le statut et la filiation se contredisent. |
Un cycle de remplacement, A → B → A |
Une chaîne qui se referme n'a ni première ni dernière décision. |
La réciproque de la cinquième ligne est autorisée et laissée telle quelle : status: superseded sans supersededBy est l'enregistrement honnête d'une décision abandonnée, ce qui n'est pas la même chose qu'une décision remplacée.
Pourquoi la date est contrainte
id accepte n'importe quelle chaîne non vide, et date non. Ce n'est pas une incohérence.
La seule propriété dont le registre a besoin d'un id, c'est qu'un lien de filiation puisse le résoudre. Contraindre un motif comme ADR-\d{3} refuserait DEC-001, RFC-12 et GD-092 — des conventions que des équipes emploient réellement — au nom d'un style maison.
Une date, elle, a un lecteur. Un registre s'ordonne du plus récent au plus ancien en comparant les chaînes brutes, ce qui est chronologique pour un jour calendaire ISO-8601 et silencieusement faux pour tout le reste : 01/06/2026 se range à côté de 01/02/2025, la page s'affiche, répond toujours 200, et ment sur la décision la plus récente. date doit donc être AAAA-MM-JJ, et toute autre forme est refusée au démarrage.
C'est un jour calendaire et non un instant, délibérément : un instant fait l'aller-retour par un fuseau horaire qui peut déplacer le jour.
Lire le registre
GET /api/admin/decisions{
"decisions": [
{
"id": "ADR-007",
"title": "Postgres pour l'instance partagée",
"status": "accepted",
"date": "2026-06-01",
"deciders": ["Léa Fontaine", "Thomas"],
"touches": ["engine › DATABASE_URL", "tables › quotes"],
"context": "Quatorze comptes écrivent dans l'application, deux agents la lisent, et l'export nocturne verrouille le fichier une minute durant.",
"decision": "DATABASE_URL pointe vers un Postgres infogéré en eu-west-3. Le binaire est inchangé ; les migrations tournent au démarrage comme avant.",
"consequences": "Les sauvegardes sont celles de l'hébergeur. Le PC de l'atelier n'héberge plus rien.",
"supersedes": "ADR-002"
}
],
"total": 2,
"accepted": 1,
"proposed": 0,
"superseded": 1
}Les enregistrements reviennent dans l'ordre où la configuration les énonce. L'ordre de lecture est un choix qui appartient à la surface qui le fait ; trier ici ferait dépendre la réponse d'une décision dont l'API n'a pas été informée, et l'ordre propre au fichier serait ensuite irrécupérable.
Les quatre compteurs sont des frères et sœurs plats de decisions, et non un objet totals imbriqué : une tuile de tableau de bord peut ainsi lier accepted directement.
Une application sans registre répond 200 avec une liste vide et quatre zéros — jamais 404. Ne déclarer aucune décision n'est pas une erreur, et un 404-quand-absent serait indiscernable d'une route jamais montée.
La réponse porte Cache-Control: no-store. Le registre est une fonction de la configuration qui tourne à cet instant, et une copie en cache répondrait à « qu'a-t-on décidé ? » par ce qu'un déploiement précédent déclarait. Un rechargement de configuration est reflété sans redémarrage, pour la même raison.
L'endpoint est réservé à l'administration. Un membre et un appelant anonyme reçoivent tous deux 404 — jamais 403, qui apprendrait à l'appelant que l'endpoint existe. C'est la règle anti-énumération que suit chaque lecture d'administration.
Rien ne modifie une décision
Il n'existe aucun endpoint d'écriture, aucun brouillon, aucune affordance d'édition. Le registre est de la configuration, et la configuration s'édite dans votre fichier de configuration : le produit auto-hébergé est configuration-en-code uniquement, et l'Espace d'administration est une console de données opérationnelles en lecture seule. Ajouter une décision, c'est ajouter une entrée à decisions[] puis livrer, exactement comme toute autre modification de l'application.
C'est tout l'intérêt de mettre ces enregistrements ici plutôt que dans un wiki. Ils sont versionnés avec la configuration, relus avec elle et déployés avec elle — la justification et ce qu'elle a décidé ne peuvent donc jamais diverger.
Pages associées
- Vue d'ensemble du schéma — toutes les clés racine d'une configuration d'application
- Configurations TypeScript — écrire
app.tsavec l'autocomplétion - Tableau de bord d'administration — la console en lecture seule et son API de lecture
- Personnalisation de la console — ce que la console intégrée vous laisse changer, et ce qu'elle ne vous laisse pas changer
Dernière mise à jour 23 septembre 2026
Cette documentation a été rédigée avec de l'IA : des erreurs ou du contenu obsolète sont donc possibles. Sovrium est en bêta. Les contributions et corrections sont les bienvenues.