
# 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 :

```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](#statut).                                                                                      |
| `date`         | Le jour de la décision, en jour calendaire ISO-8601 : `2026-06-01`. Voir [Pourquoi la date est contrainte](#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](#ce-que-touches-dsigne). |
| `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](#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

```http
GET /api/admin/decisions
```

```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

- [Vue d'ensemble du schéma](/fr/docs/schema-overview) — toutes les clés racine d'une configuration d'application
- [Configurations TypeScript](/fr/docs/configuration-typescript) — écrire `app.ts` avec l'autocomplétion
- [Tableau de bord d'administration](/fr/docs/admin-dashboard) — la console en lecture seule et son API de lecture
- [Personnalisation de la console](/fr/docs/admin-customization) — ce que la console intégrée vous laisse changer, et ce qu'elle ne vous laisse pas changer
