Skip to content

Bloc 102 : le panneau ne répond plus à sa propre écriture - #128

Merged
magicgg91 merged 1 commit into
devfrom
claude/bloc-102-player-settings-second-path
Sep 15, 2026
Merged

magicgg91 merged 1 commit into
devfrom
claude/bloc-102-player-settings-second-path

Conversation

@magicgg91

Copy link
Copy Markdown
Owner

Second chemin d'écrasement des paramètres du joueur — celui que le Bloc 99 avait laissé debout.

1. Reproduction

Harnais qui rejoue le scénario du test reflects an external equipment-skills transfer live en boucle dans un seul processus, instrumenté (chaque écriture du stockage, chaque diffusion, chaque passage de syncFromStorage avec current.level / next.level / equal, chaque rendu du NumberStepper du niveau).

  • ~1/100 au repos. Le symptôme est exactement celui observé en CI : Niveau du joueur attendu 5, reçu 10.
  • Monté à un taux exploitable en lançant 4 processus en parallèle sur 4 cœurs — les processus se font concurrence pour l'ordonnanceur, ce qui est la variable qui compte ici (la charge CPU brute par boucles occupées, seule, ne suffisait pas : 0/60).

2. Cause exacte

Trace saine, puis trace en échec, au même point :

                                  SAINE                         EN ÉCHEC
--- transfert ---                 --- transfert ---
                                  transfer: lit stockage level=10
                                  sync: current=10 next=10 equal=false   (adopte le transfert)
save effect value.level=10        │
WRITE / broadcast                 │
--- change niveau à 5 ---         --- change niveau à 5 ---
sync: current=10 next=10 equal=true    save effect value.level=10   ← l'effet du rendu PRÉCÉDENT
render display=5 value=5               WRITE / broadcast             ← annonce l'instantané périmé
                                       sync: current=5 next=10 equal=false
                                       drop draft=5 value=10
                                       render display=10

La persistance passe par un effet passif. React le lance après le commit qui l'a programmé — et, quand une mise à jour plus récente arrive d'abord, React vide les effets passifs en attente avant de traiter cette mise à jour. L'effet écrit alors, et annonce, l'instantané qu'il avait capturé, que le panneau a déjà dépassé.

syncFromStorage relisait le stockage sur cette annonce, y trouvait l'instantané antérieur, le jugeait différent de son état courant, et l'adoptait. Les deux mises à jour étant appliquées dans le même rendu (setLevel puis le sync), le niveau 5 n'a jamais été committé : le NumberStepper a alors correctement lâché son brouillon (value 10 !== lastReported 5) et réaffiché 10.

Le Bloc 99 empêchait le panneau de répondre à sa propre écriture en rendant la comparaison égale — ce qui ne tient que tant que le stockage et le panneau sont d'accord. C'est précisément faux quand l'écriture est en retard.

3. Correctif

Le garde devient structurel plutôt que dépendant d'une égalité octet à octet : broadcasting dit si l'événement vient du panneau lui-même, et la comparaison de contenu dit si les réglages stockés diffèrent vraiment. dispatchEvent étant synchrone, tous les écouteurs — syncFromStorage compris — tournent dans cette fenêtre.

Après le correctif, le stockage se remet à jour tout seul : l'écriture périmée est ignorée, le rendu du niveau 5 est committé, et son effet de persistance écrit 5.

Mesure A/B

Même harnais non instrumenté, mêmes conditions (4 processus × 150 tours) :

Bras Reproductions
avant 7 / 600
après 0 / 600

4. Portée du défaut

Les deux autres appelants de usePersistedState (Combat, Expédition) ne s'abonnent à aucun événement et sont seuls sur leur clé : ils ne peuvent pas subir ce défaut. usePlayerSettings lit le stockage via useSyncExternalStore — il n'a aucun état local à perdre, il reflète le stockage et se corrige à la diffusion suivante.

5. Tests

  • Bloc102: ignores its own broadcast, which can announce a snapshot it has moved past — force l'entrelacement au lieu de l'attendre : l'écriture suivante du panneau dépose l'instantané précédent, ce que fait exactement un effet passif en retard. Déterministe, et échoue avec « attendu 5, reçu 10 » — le symptôme de production mot pour mot — dès que le garde est retiré.
  • Bloc102: an outside announcement that changes nothing leaves the panel alone — l'autre moitié du garde, que rien ne fixait : en supprimant la comparaison de contenu, aucun test ne rougissait, alors que sans elle deux copies montées du panneau se répondraient sans fin.

Bloc 99 vérifié intact : en remettant le v dans les réglages retournés, son test unitaire (hands back the settings alone) rougit toujours. À noter honnêtement — son test comportemental (settles in a single save) passe désormais même sans lui, parce que le garde du Bloc 102 coupe la réponse à sa propre diffusion en amont. Le v reste néanmoins porteur : il est ce qui empêche deux panneaux montés de se juger perpétuellement différents.

Validation

pnpm lint · prettier --check · tsc --noEmit · pnpm test (147 fichiers, 1342 tests) · pnpm build · playwright test (88/88).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2


Generated by Claude Code

Second chemin d'écrasement des paramètres du joueur, celui que le Bloc 99
avait laissé debout.

Cause exacte. La persistance passe par un effet passif : React le lance
après le commit qui l'a programmé, et donc — quand une mise à jour plus
récente arrive d'abord — après ce rendu plus récent aussi. L'effet écrit
alors, et annonce, l'instantané qu'il avait capturé, que le panneau a déjà
dépassé. syncFromStorage relisait le stockage sur cette annonce, y trouvait
l'instantané antérieur, le jugeait différent de son état courant et
l'adoptait : le niveau que l'utilisateur venait de taper revenait à sa
valeur précédente (5 attendu, 10 reçu).

Le Bloc 99 empêchait le panneau de répondre à sa propre écriture en
rendant la comparaison égale ; cela ne tient que tant que le stockage et
le panneau sont d'accord, ce qui est faux précisément quand l'écriture est
en retard. Le garde est donc structurel maintenant : `broadcasting` dit si
l'événement vient du panneau lui-même, la comparaison de contenu dit si
les réglages stockés diffèrent vraiment.

Mesure A/B, scénario rejoué sous contention CPU (4 processus, 4 cœurs) :
7 échecs sur 600 avant, 0 sur 600 après.

Tests. Un test qui force l'entrelacement au lieu de l'attendre — l'écriture
suivante du panneau dépose l'instantané précédent, exactement ce que fait
un effet passif en retard — et échoue avec « attendu 5, reçu 10 » sans le
correctif. Plus un test sur l'autre moitié du garde, que rien ne fixait :
sans la comparaison de contenu, deux copies montées du panneau se
répondraient sans fin.

Le correctif du Bloc 99 reste en place et couvert : le remettre en cause
fait toujours rougir son test unitaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgsDSfsCbbn8cochFGwe2
@magicgg91
magicgg91 merged commit aa3116f into dev Sep 15, 2026
2 checks passed
@magicgg91
magicgg91 deleted the claude/bloc-102-player-settings-second-path branch September 15, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants