Skip to main content
Voir en Markdown

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 :

app.yaml
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

request.http
GET /api/admin/decisions
app.json
{
  "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

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.

Construit avec Sovrium