Aller au contenu

ADR-0022 — Révision convenue : trancher un fil sur un changement de valeur, sans quitter la modale

Statut 🟢 Accepté — 2026-06-19 ; implémenté 2026-06-21 (backend revue.py / workflow.py / labo.py + frontend Revue.tsx ; 5 tests dans test_revue.py, suite verte)
Portée Fin de vie d'un fil de revue qui CONCLUT un changement de valeur. Modèle FilRevue, résolution (/api/demandes/{id}/fils/{fil_id}/repondre), nouvelle application (…/appliquer-revision), garde de valider, modale de revue (frontend)
Décision La résolution d'un fil porte un verdict : maintenu (rien ne change) ou révision convenue (le reviewer saisit la valeur cible, sa justification voyage avec, la demande revient en révision). Le junior applique en un clic, dans la modale même de l'hypothèse — fork du jeu + justif au niveau étude + re-soumission. La validation refuse tant qu'une révision convenue n'est pas appliquée
Réf. ADR-0008 — Revue contradictoire ancrée ; ADR-0009 — Justifications (héritage 2 niveaux) ; ADR-0021 — Critique sourcée

🎯 À retenir

Un fil pouvait se résoudre sur « validons donc à 1,5 % » sans que rien ne change : resolu ne touchait ni la valeur (restée 2 %) ni le statut de la demande, et le junior n'avait ni moment ni lieu pour appliquer le 1,5. Pire, le senior pouvait valider depuis revue_senior sans garde → figer une valeur que tout le monde avait rejetée. On donne à la résolution un verdict : maintenu ou révision convenue. La révision convenue porte la valeur cible (saisie par le reviewer) ET sa justification (la justif suit la valeur — sinon on figerait 1,5 % sous un texte qui défendait 2 %). Le junior applique dans la modale de l'hypothèse, en un clic. Mécaniquement : fork du jeu (immuable, ADR-0008) + Justification(demande, champ) au niveau étude (override, ADR-0009) + re-soumission. Un garde empêche valider tant que la révision n'est pas appliquée.

Contexte

Observé en QA (parcours junior). Un fil ancré sur « Revalorisation des salaires » converge : le senior critique (« 1,5 est plus adaptée »), le junior répond (« d'accord »), le senior résout (« validons donc à 1,5 »). Le fil passe resolu. Mais la valeur figée reste 2 %.

La vérité du code :

  • repondre_fil (résolution) fait une seule chose : fil.statut = "resolu". Aucun effet sur la valeur ni sur le statut de la demande.
  • soumettre-revue (le seul endroit qui (re)fige une valeur) exige le statut en_etude ou a_revoir. La demande est restée en revue_senior → le junior ne peut rien (409). La modale de revue est en lecture seule ; son seul bouton après résolution est « Fermer ».
  • valider passe revue_senior → validee sans aucun contrôle. Le senior peut donc, juste après « validons à 1,5 », cliquer Valider et figer 2 %.

Deux défauts en découlent :

  1. resolu est surchargé. Il signifie soit « défense acceptée, la valeur reste » (le junior a défendu, cf. test_cycle_fil_critique_defense_resolution), soit « changement convenu » (ce cas-ci). Le système ne les distingue pas, et seul le premier est sûr à valider.
  2. Danger d'intégrité d'audit. Une valeur explicitement rejetée peut être figée puis auditée puis livrée — frontalement contraire au rail ADR-0008 (« ce qui est approuvé est exactement ce qui est livré »).

Le chemin de changement existait (le senior renvoie → la demande passe a_revoir → le junior fork au Plan de travail → re-soumets), mais : (a) rien ne mène un fil résolu vers ce chemin ; (b) « renvoyer » dit rejet, alors qu'ici les deux ont convergé — le vocabulaire combat le geste, ce qui explique que le senior ait naturellement résolu.

Décision

  • D-REVCONV-1 — Résolution à deux verdicts. La résolution d'un fil (par un reviewer, senior ou externe) porte un verdict explicite : maintenu (la valeur reste — la défense du junior tient, ou aucune révision n'est nécessaire ; comportement actuel) ou révision convenue (les deux conviennent d'un changement de valeur).
  • D-REVCONV-2 — La justification voyage avec la valeur. Une révision convenue PORTE deux choses : la valeur cible (saisie par le reviewer) et la justification du reviewer (le texte de sa résolution + son referentiel_cle + sa pièce de contre-source, ADR-0021). À l'application, cette justification supplante celle du junior pour ce champ — sinon on figerait « 1,5 % » sous un texte qui argumentait « 2 % ». La justif suit la balance : c'est l'argument du reviewer qui a fait pencher le changement, c'est lui qui est figé.
  • D-REVCONV-3 — Renvoi implicite. Résoudre en révision convenue ramène la demande en a_revoir (rouvre le droit de révision du junior). Plus de geste « renvoyer » séparé à oublier ; on ne peut plus atteindre l'état contradictoire « fil résolu sur un changement, demande validable avec l'ancienne valeur ».
  • D-REVCONV-4 — Le junior applique, dans la modale, en un clic. Le reviewer propose la valeur (à la résolution) ; le junior applique (séparation des pouvoirs : l'auteur du jeu reste celui qui exécute la révision et en porte la responsabilité d'audit). L'application se fait sans quitter la modale de l'hypothèse. Mécaniquement, en une transaction : (1) fork du jeu courant avec le champ révisé (jeu immuable → nouvelle version de la lignée) ; (2) upsert de Justification(demande, champ) = texte/référentiel/pièce du reviewer (niveau étude, qui override l'hérité au snapshot — ADR-0009 D-JUST-4) ; (3) re-soumission (recalcul + re-figeage → revue_senior). Le fil est marqué appliqué.
  • D-REVCONV-5 — Garde de validation. valider refuse (409) tant qu'une révision convenue n'est pas appliquée. On ne fige jamais une valeur explicitement rejetée. C'est le filet : même si l'UI déraille, l'intégrité d'audit tient au niveau serveur.
  • D-REVCONV-6 — Périmètre v1 : champs scalaires. La saisie in-modale de la valeur cible couvre les champs scalaires (taux_*, âge de retraite) — un nombre saisi, une application en un clic. Les champs complexes (grille IFC, plafonds, table de mortalité, causes) gardent une révision textuelle (verdict + justif) et s'appliquent au Plan de travail (fork du changement structuré). On ne sur-conçoit pas un éditeur in-modale pour la grille tant que le besoin n'est pas démontré. (Anti-sur-ingénierie, posé explicitement.)

In-modale tour-par-tour (maintenant) vs lot unique (futur) — pour référence

Le jeu est atomique : un fork re-soumet le jeu entier. Cela force un choix dès que plusieurs fils convergent vers une révision en même temps :

  • In-modale tour-par-tour (retenu). On applique chaque révision dans sa modale → re-soumission → le senior re-valide → on applique la suivante. 100 % in-modale, épouse le geste de convergence, zéro navigation. Coût : une re-validation par révision (la revue itère — comportement courant et légitime des revues réelles).
  • Lot unique (différé). Un bouton « Appliquer les N révisions convenues » qui fork une fois / re-soumet une fois. Mais il est transverse aux hypothèses → il ne peut pas vivre dans la modale d'une hypothèse ; il faut un point hors-modale (en-tête de la demande). On l'introduit seulement si la friction multi-révisions se manifeste en vrai.

Décision : tour-par-tour maintenant, lot unique en évolution pré-enregistrée. La friction à surveiller : un même renvoi qui exige ≥ 3 révisions simultanées régulièrement.

Conséquences

  • Positif. Un fil convergent a enfin un moment (le verdict du reviewer) et un lieu (la modale même), sans détour par un « renvoyer » au vocabulaire hostile. La justif figée est cohérente avec la valeur figée. L'intégrité d'audit est garantie au serveur (D-REVCONV-5), pas seulement par convention.
  • Migration. Nouveaux champs sur FilRevue (verdict / valeur cible / applique) → entrée obligatoire dans _COLONNES_AJOUTEES (db.py) sous peine de 500 en prod (SQLite déployée).
  • Limite assumée (transitoire). Champs complexes non éditables in-modale (D-REVCONV-6) ; multi-révisions en tour-par-tour (lot unique différé). (Le portage de la pièce de contre-source : la justif reprenait jadis texte + référentiel mais pas le fichier — fermé par ADR-0024 : la pièce du message de résolution est désormais matérialisée dans la justification figée, tout en restant sur le fil.)

Déclencheur de réexamen (pré-enregistré)

  • Friction multi-révisions (≥ 3 révisions simultanées récurrentes) → introduire le lot unique hors-modale.
  • Besoin d'éditer un champ complexe depuis la revue (grille, plafonds) → étendre la saisie in-modale à un éditeur structuré, ou statuer que ces révisions restent au Plan de travail.
  • Le reviewer veut imposer la valeur sans passer par le junior (ex. correction triviale) → re-statuer sur la séparation des pouvoirs D-REVCONV-4.