From a1cac443bba2a21b51d1b9bc19a7a90e656b013a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 6 Oct 2026 23:40:40 -0400 Subject: [PATCH] [FIX] session: undo never moves id counters back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Undoing back before a generation restored the whole charge, its prochainsIds included: the next generation numbered its propositions 1 to 3 again, and a label of the abandoned thread such as "retained proposition 2" named another proposition. § 8.9 forbids it: a counter never goes back. A retour now restores the content of the instant and keeps each counter at the larger of its restored and current value, so the next id is fresh. Checked: 1416 node tests; the retour test is red on the previous seance.js. --- FR --- [FIX] séance : un retour ne fait jamais reculer les compteurs Défaire jusqu'avant une génération restituait la charge entière, prochainsIds compris : la génération suivante renumérotait ses propositions de 1 à 3, et un libellé du fil abandonné, comme « retenu la proposition 2 », désignait une autre proposition. Le § 8.9 l'interdit : un compteur ne recule jamais. Un retour restitue désormais le contenu de l'instant et porte chaque compteur au plus grand de sa valeur restituée et de sa valeur courante. Vérifié : 1416 épreuves node ; l'épreuve du retour est rouge sur l'ancien code. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb --- src/application/seance.js | 15 ++++++++++++++- src/application/seance.test.js | 13 +++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/application/seance.js b/src/application/seance.js index 5c05e80..b975483 100644 --- a/src/application/seance.js +++ b/src/application/seance.js @@ -416,6 +416,19 @@ export function creerSeance({ } } + // La charge restituée, ses compteurs d'identifiants portés au plus grand + // de leur valeur restituée et de leur valeur courante : un retour rend le + // contenu d'un instant, jamais un identifiant déjà donné (§ 8.9) — une + // génération après un défaire numérote au-delà des propositions défaites, + // et le retenu d'un fil abandonné garde la proposition qu'il désignait. + function avecCompteursCourants(charge, courante) { + const prochainsIds = {}; + for (const cle of ['participant', 'table', 'proposition']) { + prochainsIds[cle] = Math.max(charge.prochainsIds[cle], courante.prochainsIds[cle]); + } + return { ...charge, prochainsIds }; + } + // Un retour, sans son suivi : défaire vise le prédécesseur de l'instant // courant sur le fil, refaire le sommet de sa pile, revenir l'instant // donné ; l'entrée porte la charge restituée, son sens et sa cible. @@ -431,7 +444,7 @@ export function creerSeance({ if (cibleRefaire === null) throw new ErreurSeance('RIEN_A_REFAIRE'); cible = cibleRefaire; } - const charge = restituer(ouvert, cible); + const charge = avecCompteursCourants(restituer(ouvert, cible), ouvert.charge); if (serialiserCharge(charge) === serialiserCharge(ouvert.charge)) throw new ErreurSeance('SANS_EFFET', { sens }); const instant = sens === 'defaire' ? courant : libelleDeRevision(ouvert.journal, cible); const entree = { charge, libelle: libelleDuRetour(sens, instant), avertissements: [] }; diff --git a/src/application/seance.test.js b/src/application/seance.test.js index 7a789dd..691fc52 100644 --- a/src/application/seance.test.js +++ b/src/application/seance.test.js @@ -641,8 +641,17 @@ describe('séance : revenir, défaire, refaire (§ 8.3)', () => { [7, 'revenir', 2, 'Retour à « Ajouté Ancolie »'], ], ); - assert.deepEqual(etatDe(fs).charge, reconstruire(journal, 2)); - assert.deepEqual(reconstruire(journal, 7), reconstruire(journal, 2)); + // L'instant 2 revient avec son contenu, et ses compteurs restent où trois + // ajouts les ont portés : aucun identifiant donné ne se redonne (§ 8.9). + const instant2 = reconstruire(journal, 2); + const attendu = { ...instant2, prochainsIds: { ...instant2.prochainsIds, participant: 4 } }; + assert.deepEqual(etatDe(fs).charge, attendu); + assert.deepEqual(reconstruire(journal, 7), attendu); + await seance.executer('ajouterParticipant', { champs: { nom: 'Digitale' } }); + assert.deepEqual( + seance.charge.participants.map(({ id, nom }) => [id, nom]), + [[1, 'Ancolie'], [4, 'Digitale']], + ); }); test("refaire n'existe qu'après des retours, tant qu'aucun geste ne les suit", async () => {