Skip to content

Release/0.6.2 - #341

Merged
bbstudioapp merged 41 commits into
mainfrom
release/0.6.2
Aug 21, 2026
Merged

Release/0.6.2#341
bbstudioapp merged 41 commits into
mainfrom
release/0.6.2

Conversation

@bbstudioapp

Copy link
Copy Markdown
Owner

No description provided.

chore: back-merge main into develop
L'horloge de timeline est gelée pendant tout un défi (choix assumé : le
défi ne consomme pas de temps de séance). C'est pourtant elle que l'écran
de fin affichait et que `_finish` créditait au temps de jeu cumulé — un
défi de dix minutes ne rapportait donc ni point de spécialisation, ni
déblocage de coach, ni badge d'endurance, et la séance était rapportée
plus courte que ce que la joueuse venait de jouer.

`playedSeconds` porte désormais le temps réellement passé en séance,
pauses exclues. Le gel de la timeline est inchangé : il continue de
piloter le contenu.
Le générateur réserve après chaque trigger de défi une enveloppe
`breath + nominalDurationSeconds` où il ne pose aucun contenu, puisque le
défi y sera joué. Cette enveloppe était prise sur la durée demandée, et
le runtime la retirait *encore* de la durée de séance
(`_excisChallengeFromSession`) : la joueuse la payait deux fois. Une
séance « Moyenne » (1500 s) avec les 3 défis du retour utilisateur — dont
un d'endurance à 530 s — ne rendait plus que ~980 s de contenu.

`GenerationContext.challengeReserveSeconds` cumule ces fenêtres : elles
décalent d'autant la fin de la boucle main et sont retranchées de
`progress`, si bien que le contenu et la courbe de difficulté restent
ceux de la durée demandée. La séance dure donc le temps demandé plus le
temps des défis.
Les notes de version 0.6.0 annoncent les postures imposées et les pauses
scénarisées, mais leur tirage était conditionné à un toggle de la section
debug de l'écran SONS, off par défaut : la fonctionnalité était invisible
pour tout le monde depuis deux versions.

Le réglage devient une préférence utilisateur assumée (`pref.scripted_breaks`,
on par défaut, section « Déroulé de séance » du Profil) au lieu d'un flag
debug. L'ancienne clé `debug.scripted_breaks` est abandonnée plutôt que
migrée : la reprendre rendrait le off historique collant.

Les presets de debug `reset` et `mi_carriere_profondeur`, qui recopiaient les
défauts d'usine, sont réalignés sur le nouveau défaut.
…vancé

Le coach Marc a coûté un correctif en 0.6.1 : son palier de déblocage était
franchissable une seule fois, et qui l'avait dépassé avant sa sortie restait
verrouillé définitivement. Les 5 milestones `intro_posture_*` (levels 4 à 12)
sont exposées au même risque pour un profil déjà au-delà.

Ces tests tournent sur le vrai catalogue `assets/career/milestones.json` et
établissent que `level` est un plancher et non une fenêtre, que les postures
remontent en tête de file (règle overdue), et qu'il faut 3 séances
moyenne/longue — 5 courtes — pour les débloquer toutes. Ils caractérisent
aussi l'exigence de capacité de `intro_posture_on_back`
(`hold.full.streak ≥ 5`), seul palier qui attend une preuve physique.
… une pause

La sortie d'une pause scénarisée qui impose une nouvelle posture arme un gate
de mise en place : `_checkSteps` ne consomme plus de step et `_onTick` gèle
l'horloge jusqu'à la validation. Trois chemins traversaient cette fenêtre sans
jamais passer par `_exitBreak`, donc sans lever le gate — il survivait au saut
de timeline et le gel délibéré devenait définitif (ticker actif, plus rien qui
avance, jusqu'au timeout de 90 s) :

- « je peux pas » pendant la pause ou pendant l'attente de mise en place — le
  bouton n'est masqué par aucun des deux états ;
- « Utilise-moi » et le retry milestone, via `requestUpgrade` (qui remettait
  déjà l'état de pause à zéro, mais pas le gate) ;
- la régénération post-défi, par symétrie.

Le bouton reste actif dans les deux états : c'est la seule sortie visible en
séance, et une pause de 60 à 120 s suivie d'une attente n'est pas un moment où
la priver serait défendable. Le flow d'échec annule la pause au lieu de la
subir — sans appliquer sa posture, dont l'annonce ne sera pas prononcée.

Corrige aussi le retour d'arrière-plan pendant une pause : `resume()` relançait
l'audio d'effort coupé par l'entrée dans la pause. Chemin atteignable par tout
le monde, une notification suffit.

Les 4 tests runtime ajoutés sont rouges sans ces correctifs.
`challenges` était découpée sur le curseur de file, qui avance aussi
quand un défi est écarté faute de place, alors que
`challengeTriggerTimes` ne reçoit une entrée que pour les défis
insérés. Le contrôleur indexe les deux listes par la même position :
dès qu'un défi était écarté, `_updateChallengePhase` sortait des
bornes dans le ticker principal, où une exception ne s'auto-annule
pas — gel de séance ou plantage.

Le générateur ne livre plus que les défis réellement insérés, et la
boucle d'armement se borne à la plus courte des deux listes.

Cas structurel : les trigger times sont répartis à 20/50/80 % du
budget sans tenir compte de la taille du défi, donc un défi de 530 s
tiré en dernier n'a jamais la place — d'où le désalignement.
Un défi ne s'arme que si l'horloge traverse sa fenêtre
`[trigger, trigger + 13 s)` pendant qu'un tick tourne. La reprise
après un fail saute à la prochaine section : quand le fail tombait
pile dans cette fenêtre, le saut passait par-dessus et le défi
disparaissait sans un mot — N défis demandés au menu, N-1 joués.

Le saut de reprise est désormais borné à l'ouverture de la fenêtre du
prochain défi encore armable ; quand elle est déjà ouverte, on ne
saute pas du tout (le défi s'arme au tick suivant et gèle l'horloge
lui-même).
Le défi ne prend rien à la séance, il s'y ajoute : une « Moyenne » à
3 défis annonçait « 25 min » pour ~35 min vécues. La durée affichée
sous le sélecteur porte désormais la mention quand les défis sont
activés — « 25 min + le temps des défis » — dans les 4 langues.

La mention va sur la ligne pleine largeur et non dans les cartes du
sélecteur : à 5 cartes sur un écran de 360 dp il reste ~42 dp de
texte par carte, où « Herausforderungen » ne rentre pas.
Le gel de mise en place était un drapeau armé à la sortie d'une pause et
levé par un seul chemin nominal. Quatre chemins distincts l'ont laissé
orphelin derrière eux, et chaque correctif ciblé n'a jamais rien dit du
suivant : on cesse de fermer des chemins.

`PostureGate` porte désormais ce qui *justifie* le gel — identité de la
timeline, tête de lecture, décalage d'horloge, numéro de flow d'échec — et
`awaitingPostureReady` rejoue cette justification à chaque lecture. Les
trois premières composantes sont les seules variables qui portent la
progression d'une séance : rebattre la timeline sans en toucher aucune est
impossible. Un chemin qui la rebat n'a donc rien à lever, aujourd'hui comme
demain — les trois levées explicites de la passe 2 sont retirées.

Une garde centrale enterre à chaque battement une ancre dont la
justification est tombée (timeout de sécurité compris), pour qu'un gel
enterré ne ressuscite pas si sa justification redevenait vraie.

Ferme au passage le quatrième chemin, jamais corrigé : un défi armé au même
tick qu'un ordre de posture n'empile plus son bandeau sur lui, et sa clôture
ne laisse plus la séance gelée.
Les horaires de pause et de défi étaient calculés par deux fonctions
indépendantes qui visaient toutes deux le milieu de la séance. Le trou
d'effort de la pause avalait le trigger du défi médian, que la boucle de
génération réémettait à `break.endTime` : l'ordre de se mettre en position
et l'annonce du défi tombaient au même instant.

Le planificateur de pauses reçoit les créneaux de défi et cède le passage —
les horaires de défi ne bougent pas. Sans défi, les horaires nominaux sont
inchangés.

Balayage sur le générateur réel (8 durées × 4 comptes de défis × 60 graines,
1920 générations) : 175 collisions avant, 0 après.
`_excisChallengeFromSession` reconstruit la séance sans passer `breaks:` ni
`initialPose:` — le défaut par défaut de `Session.breaks` est `const []`,
donc toute la mise en scène restante disparaissait dès qu'un défi avait eu
lieu, sur la fonctionnalité qu'on est en train d'allumer. La liste garde sa
longueur (`_nextBreakIndex` pointe dedans) et ses horaires suivent le même
décalage que les steps.
…éance

`stop()` et `_finish()` remettaient la timeline à zéro mais laissaient courir
le `Timer` de sécurité du gel de posture : ~90 s plus tard, il réveillait un
contrôleur devenu inactif ou l'écran de fin. Il n'y a pas de gel à lever —
il est tombé de lui-même — juste une ressource à libérer, aux trois bornes
de vie d'une séance (`start` le faisait déjà). Entre elles, c'est la garde du
battement qui s'en charge.
Le contrôle de place était devenu auto-contradictoire : depuis que
l'enveloppe d'un défi est reportée sur `genUntil`, insérer un défi ne
coûte plus de contenu, donc `remaining = genUntil - time` est invariant
sous l'insertion — le test réclamait une place que l'insertion crée
elle-même. Un défi de 530 s tiré en dernier n'entrait jamais : une
séance sur trois perdait son plus gros défi, sans un mot.

Contrepartie assumée et annoncée : la séance dure la durée choisie plus
le temps des défis. Mesuré sur 200 tirages, une « Bâclée » (360 s
nominal, 456 s générés sans défi) avec le trio du retour utilisateur
passe de 516 s à 1059 s — 17,6 min médianes. Le taux d'insertion est de
100 % sur 4800 générations croisant 10 niveaux, 4 paliers et l'ordre de
tirage.
Puisque tous les défis entrent désormais, la joueuse doit savoir au
moment où elle active le réglage que sa séance dépassera le format
choisi. La note complète le texte du réglage dans les 4 langues.

La mention portée par la ligne de durée tenait sur deux lignes avec un
libellé d'exemple mais pas avec les vrais : « Zeit für
Herausforderungen » poussait la carte à trois lignes sur les 5 paliers
allemands. Formulation allemande resserrée, et le test porte désormais
sur les libellés réels de tous les paliers — dont « Überraschung », qui
n'est pas un nombre.
La borne défensive de la boucle d'armement empêche le plantage si
`challenges` et `challengeTriggerTimes` divergeaient à nouveau, mais
elle abandonnait le défi orphelin en silence complet : personne ne
l'aurait su avant qu'une joueuse ne le signale — ce qui vient
d'arriver.

Une ligne, une seule fois par séance : la boucle tourne à chaque tick.
… les stats

Décision produit : l'horloge qui décide de la fin de séance ignore le
temps passé en défi — la joueuse joue la durée de contenu qu'elle a
choisie — tandis que le chiffre rendu en fin de séance et le temps
crédité aux statistiques comptent tout, défis compris.

C'est ce que fait `297abe4` au milieu d'un lot de correctifs ; ce test
énonce l'invariant pour lui-même. Il casse sur les deux neutralisations
(crédit ramené sur l'horloge gelée : 7 s au lieu de 21 ; panneau de fin
recâblé sur `elapsedSeconds`).

Le panneau de fin est privé à `session_screen.dart` et une séance ne
tourne pas sous horloge simulée : à défaut de lire le chiffre rendu, la
clause d'affichage verrouille la ligne qui l'alimente.
…le levait

La garde ne cherchait que la chaîne `confirmPostureReady`. Tous les
fichiers du contrôleur sont des `part of` : écrire `_postureGate = null`
depuis n'importe lequel d'entre eux est valide et passait au travers sans
un mot — exactement le motif que ces passes ont servi à éliminer.

Elle scanne désormais les écritures du champ dans toute sa library privée
et exige que l'ensemble des membres qui y touchent soit exactement les six
sites légitimes : naissance de l'ancre, validation joueuse, ramasse-miettes
du battement, et les trois bornes de vie d'une séance.
La clause d'identité protège contre le remplacement de la séance, pas
contre une mutation en place de ses listes. Aucun code ne le fait
aujourd'hui, rien ne l'empêche demain et rien ne le détecterait : le
noter là où la décision vit plutôt que de laisser « par définition »
porter plus qu'il ne prouve.
L'ampleur d'un défi « durée » vaut `comfort × kChallengeOverloadFactor` et
n'avait aucun plafond — contrairement aux défis de vitesse (bornés par le
BPM maximum du moteur) et de profondeur (bornés par le nombre de crans).
Or `comfort` ne fait que monter avec la pratique : sur le profil du retour
utilisateur 0.6.1 une Bâclée annoncée 6 minutes en durait 15, et rien ne
bornait la suite (73 minutes à `comfort = 3000`).

Le plafond est dérivé du palier : la part `kChallengesShareOfFormat` de la
durée annoncée, divisée par le nombre d'événements que le palier planifie.
Comme un palier ne demande jamais plus de défis qu'il ne planifie
d'événements, la somme des défis d'une séance ne peut plus dépasser la
moitié de la durée annoncée, quel que soit le format et quel que soit
l'état du catalogue de milestones.

Le tutoriel (5 s) et les défis exploratoires (5-30 s) passent très en
dessous du plus petit plafond (180 s) : le plafond ne mord que sur les
axes durée d'un profil déjà entraîné. Tronquer la cible tronque aussi la
valeur créditée au profil, qui passe alors sous le `comfort` — la
régulation y est neutre, ni le record ni la cible ne bougent. La mesure
n'est pas faussée, elle est rendue optionnelle : « JE TIENS ENCORE »
reste ouverte pour la reprendre, sur décision de la joueuse.

Le paramètre est requis plutôt que défaillant : un appelant qui l'oublie
ne doit pas retomber silencieusement sur l'ancien comportement sans borne.
… redit le temps

Le sous-titre annonçait « un défi vers 60 % de la séance » alors que le
palier en pose jusqu'à quatre, répartis entre 20 % et 80 % de la séance.
Faux dans les quatre langues, et faux juste à côté d'une phrase qu'on
venait de rendre exacte. Le nombre annoncé est désormais celui des
paliers, avec une garde qui casse si un palier planifie plus d'événements
que les textes n'en annoncent.

La mention courte allemande avait perdu le mot « Zeit » en se resserrant :
« 12 Min. + Herausforderungen » se lit comme une énumération — durée et
défis compris — c'est-à-dire l'inverse de ce qu'elle veut dire, quand le
français, l'anglais et l'espagnol gardent tous les trois le temps.

Aucune formulation gardant `Herausforderung` en entier ne tient : mesurées
aux 5 paliers × 4 largeurs, « Herausforderungszeit », « Zeit für
Herausforderungen » et « zzgl. Herausforderungen » débordent toutes à
trois lignes — le mot seul occupe déjà une ligne complète. « Challenge-Zeit »
tient partout et garde les deux notions ; « Challenge » se lit sans effort
comme le synonyme du toggle affiché juste au-dessus.

Le test de mise en page porte maintenant sur les quatre largeurs : le
resserrement d'un texte allemand se relit sur toute la gamme visée, pas
seulement sur l'écran le plus étroit.
…le source

La clause d'affichage se verrouillait sur la ligne qui alimente le panneau
de fin, faute de pouvoir lire le chiffre rendu : le panneau est privé à
`session_screen.dart` et `Stopwatch` n'est pas simulé par `flutter_test`.
Elle se lit maintenant sur le texte que l'écran affiche vraiment.

Deux points rendaient le montage possible. `debugFinishSuccess()` clôt une
séance par arithmétique en passant par le même chemin de fin que le jeu
réel, et un bouton de debug l'appelle déjà depuis l'écran. Et une boucle
qui avance les deux horloges ensemble — le temps du mur sous `runAsync`
pour le `Stopwatch`, l'horloge simulée par `pump` pour le ticker — fait
tourner une séance dans un test de widget.

Le défi, lui, se joue en temps réel : c'est le gel de la timeline pendant
le défi qui fait diverger les deux horloges, et sans divergence les deux
câblages rendraient le même chiffre. Le test coûte 16 s et casse bien sur
la neutralisation : « Durée : 7 s » au lieu des 15 s jouées.
Intègre le lot « durée de séance faussée par les défis » (13 commits) avant
que celui-ci ne parte vers develop, pour que la demande de fusion des postures
soit propre une fois la première intégrée.

Deux conflits, résolus en gardant de chaque côté ce dont il a raison.

session_controller_challenge.dart — bloc de reconstruction après un défi :
`initialPose` et `breaks` viennent des postures (sans ce report, `Session.breaks`
retombe à `const []` et la mise en scène restante disparaît dès qu'un défi se
termine). Le commentaire vient des défis : celui des postures affirmait que
l'excision retire « 2 steps (breath + défi) », faux depuis la refonte des défis
en streaming où un seul step tombe dans la timeline.

career_session_generator.dart — appel à `_computeBreakTriggerTimes` : les deux
paramètres survivent, `genUntil: ctx.genUntil` (accesseur dérivé) **et**
`challengeWindows`. À ce point du code la réserve de défis vaut encore 0, donc
`ctx.genUntil` égale la valeur qu'utilisait la branche postures — pas de
changement de sens. `_estimatedChallengeDuration` gardant sa signature, les
fenêtres de défi héritent d'office de la durée désormais plafonnée par format.

Vérifié sur l'arbre fusionné : 978 tests verts (818 base + 29 postures +
131 défis, égalité exacte : aucun test perdu), `flutter analyze` propre, format
inchangé, l10n générés conformes à `gen-l10n`. Collisions pauses/défis : 0 sur
4800 générations à défis courts et 0 sur les formats du catalogue, hors le seul
couple `longue` × 4 défis au plafond (4 cas sur 500) — identiques avant fusion.
Durées avec défis : 10,8 min en express, 39,7 min en moyenne.
…l'app

Les deux curseurs du Profil poussaient au moteur sans rien enregistrer :
perdus au redémarrage, et remplacés par les défauts plateforme dès qu'une
séance carrière rendait la main. Même défaut que celui corrigé en 0.6.1 sur
le choix de voix, sur les deux réglages voisins.

`setUserRate` / `setUserPitch` prennent le rôle que `setUserVoice` tient
face à `setVoiceByName` : seules elles mémorisent, les presets coach
continuent de passer par `setRate` / `setPitch` sans écrire. Le réglage est
global et non par langue — un nom de voix appartient à sa langue, un débit
est un confort d'écoute.

Le test glisse les vrais curseurs : 4 de ses 6 cas sont rouges sur le code
d'avant, les 2 autres tiennent l'arbitrage (défaut plateforme conservé quand
rien n'est réglé, réglage insensible au changement de langue).
L'import est l'inverse de l'export : reposer la voix d'un profil sans son
débit laissait un état vocal mixte, et un export « complet du profil
persisté » devenait faux dès que ces deux réglages ont commencé à l'être.

Le fichier de partage des voix les porte donc lui aussi. C'est une décision,
pas un effet de bord : le test de non-fuite exige de nommer chaque champ et
sa provenance, les deux nombres y sont déclarés comme venant de leur clé et
d'elle seule, et le seed les écarte de leurs défauts pour que la provenance
soit réellement éprouvée.
…a fiche

Deuxième moitié du réglage ouvert en 0.6.1 : le timbre se choisissait déjà,
la couleur vocale restait celle du JSON. Or c'est souvent elle qui cloche, et
seule l'oreille de la joueuse peut en juger — la raison même qui avait rendu
le choix de voix manuel.

Les deux curseurs sont posés **à même la fiche**, pas repliés derrière un
second geste : décision de Manu, 2026-08-18. Un bouton « vitesse et hauteur
d'origine », affiché en permanence et inerte tant que rien n'a bougé, rend au
coach son preset — sans lui, déplacer un curseur est un aller sans retour.

Côté service, l'override est résolu **une seule fois en tête** de
`applyCoachVoicePreset` : elle a quatre sorties, chacune avec son propre
couple débit/hauteur, et une résolution par sortie serait quatre occasions
d'en oublier une. Un test couvre spécifiquement la sortie anticipée (voix
mémorisée trouvée) — vérifié rouge en cassant ce site-là seul.

Coût de « visible » : la feuille gagne ~130 px, la liste de voix perd d'autant
et défile plus tôt. Les tests qui tapaient une voix passent désormais par un
défilement — tous, pas seulement les deux qui cassaient.
…oach

Sans ça, l'asymétrie était de mon fait : la voix d'un coach s'exportait et se
réimportait, la vitesse et la hauteur que je viens de rendre réglables non —
un import aurait reposé le timbre d'un profil sur la couleur locale.

Les deux nombres sont répétés sur chaque entrée de langue du coach alors
qu'ils n'en dépendent pas. C'est la règle déjà en vigueur dans ce fichier
(`platform`, `language`) : l'unité qu'on agrège est l'entrée, et une entrée
doit se lire seule une fois sortie de sa section.
Reprise telle quelle de `test/sondes-adversariales-constats` (commit 13a3b0e)
comme point de départ mesuré. Elle est VERTE 6/6 sur `origin/develop` : ce
n'est pas un test rouge à faire passer, c'est un test de *caractérisation*
qui fige le comportement actuel, défaut compris (`expect(rhythm, 0)` sur la
séance normale sous le seuil).

Chiffres relevés sur `origin/develop`, avec comfort=mid / best=throat :
- normal sr=0.50    : rhythm=0/300    hold=300/300
- normal sr=0.80    : rhythm=300/300  hold=300/300
- égalité sr=0.70   : rhythm=16/300 (5.3 %, ≈ 1/14 = la loterie de surcharge)
- Encore sr=0.50    : rhythm=300/300  hold=300/300
- Encore sr=0.80    : rhythm=300/300  hold=300/300
- Utilise-moi 0.50  : rhythm=300/300  hold=300/300

Commité avant tout correctif pour que le diff suivant montre exactement quel
`expect` bascule, et pourquoi.
Réapplication sur le code d'aujourd'hui de l'intention de 411feba
(`origin/feat/depth-comfort-probe`, 101 commits de retard) : la profondeur
rythmée vise **un cran au-dessus du comfort, borné par le `best` prouvé**.

Les deux fonctions ciblées sont identiques à ce qu'elles étaient au
merge-base (39cbcaa) — vérifié : `git diff 39cbcaa..origin/develop` est vide
sur les deux fichiers. Le raisonnement d'origine tient donc tel quel.

Deux écarts assumés par rapport à 411feba :
- garde `max(comfort, …)` dans `capabilityCapFor` : un profil persisté où
  `best < comfort` verrait sinon la « sonde » RABAISSER le cap. Le `best` est
  monotone en mémoire, mais il est persisté séparément du `comfort` et
  rechargeable depuis un import — le cas n'est pas impossible.
- `maxDepthIndexForProfile` formulé en `best.round() > rounded ? rounded+1 :
  rounded` (équivalent au `min` d'origine sur des entiers, non abaissant par
  construction).

Le clamp lit `comfort` en double (le decay produit des crans fractionnaires),
d'où le `min(comfort + 1, best)` conservé là — le consommateur arrondit, et
`comfort=2.7 / best=3.0` doit donner throat, pas full.
Deux `expect` figeaient le défaut ; ils expriment maintenant la règle voulue.
Ajout des cas qui bornent le correctif — c'est là que se joue « est-ce trop
généreux ? » : un cran seulement (comfort=head/best=throat reste borné à mid,
0/300), no-op strict quand best == comfort (0/300), pas d'abaissement quand
best < comfort, et le tableau unitaire de `maxDepthIndexForProfile`.

Non-régression des tenues : deux invariants insensibles à la dérive RNG (les
compteurs bruts ne sont PAS comparables step à step — déplacer un step rythmé
fait diverger toute la séquence en aval) : la tranche throat sort toujours en
tenue, et `full` reste fermé sans `fullHold` acquise.

Discrimination vérifiée sur un worktree détaché sur origin/develop :
- avec le correctif : 11/11 verts
- sur origin/develop : 2 rouges (rhythm 0 au lieu de 300 ; maxDepthIndex 2
  au lieu de 3), les 9 autres verts — ce sont les non-régressions.
Les sondes de profondeur existantes injectent un profil figé dans le
générateur : elles prouvent que la tranche throat redevient visible, pas
que le `comfort` remonte. Ce harnais referme le circuit — profil persisté
relu par le générateur, séance rejouée dans le vrai `CapabilityTracker`,
rapport repassé par le vrai `CapabilityService.commit`.

Mesuré sur la branche : comfort=mid/best=throat remonte à throat à la 2ᵉ
séance (5 bases de graines), une chute de deux crans en 3, un tap-out
coûte un cran et la profondeur proposée redescend avec lui. Les 4 tests
sont rouges sur origin/develop, où le comfort ne bouge pas de 20 séances.
La relecture adverse l'a relevé : la reformulation n'est pas équivalente
au `min` d'origine, et l'écart tombe exactement sur le cas que l'autre
fichier du correctif protège explicitement (`best < comfort`, écrit tel
quel par l'import de profil). Sans cette note, un refactor qui « revient
à la forme littérale » réintroduit la régression.
… la 0.6.2

Huit comptes rendus produits le 2026-08-18 (tri des constats, rejeu adversarial,
correction du verrou de profondeur et sa relecture, réglage de débit et hauteur
de voix et sa relecture, mesures de repli et de remontée du confort) n'étaient
enregistrés nulle part.

Claude-Session: https://claude.ai/code/session_01L4UqzJYEndHBCS3WBayUt6
docs(analysis): consigner les analyses et relectures du 18 août avant…
Journal des vingt-six changements accumulés depuis la 0.6.1, plus le réglage
de voix par coach et le déblocage de la profondeur. Badges des huit README,
exemple de version du gabarit de remontée, liens de comparaison du journal.

1015 tests verts, flutter analyze propre, format inchangé.

Claude-Session: https://claude.ai/code/session_01L4UqzJYEndHBCS3WBayUt6
@bbstudioapp
bbstudioapp merged commit dccdfc3 into main Aug 21, 2026
5 checks passed
@bbstudioapp
bbstudioapp deleted the release/0.6.2 branch August 21, 2026 07:21
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.

1 participant