v1.4.9 : fix #61 — calibration tombant à ×2,21 (auto ET manuelle) : garde de significativité labIsSignificant (prédiction ≥ 15 % du max observé, labs par timestamp croissant) sur les 3 pipelines — une dose EEn de test de janvier + des labs de janvier-mars produisaient des ratios aberrants (×3,3 → ×391) ; facteur WHSAH retombe à 0,55 (reproduit et épinglé par ScaleFactorWhsahReproTest) ; hint UX (auto ON → facteur manuel ignoré à l'affichage) ; nettoyage CHANGELOG (4 titres orphelins doublés) ; 168 tests verts + lint

This commit is contained in:
Siphonight 2026-09-07 18:54:43 +02:00
parent 22d4c66d81
commit fa7a9c608f
9 changed files with 274 additions and 25 deletions

View File

@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue.
- **Statut** : v1.4.8 — build Android ✅, **lint vert** ✅, **159 tests unitaires** ✅ (137 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Statut** : v1.4.9 — build Android ✅, **lint vert** ✅, **168 tests unitaires** ✅ (146 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -93,7 +93,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
git clone <repo> && cd HormoneTrack
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 159 tests (137 sans les données locales)
./gradlew testDebugUnitTest # 168 tests (146 sans les données locales)
./gradlew lint # lint vert obligatoire avant release
```
@ -105,7 +105,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
`Siphonight/HormoneTrack` sur les deux (privé), avec
**releases taguées** (`v1.1.0` → `v1.4.8`) et **deux APK par release** (depuis v1.2.5)
**releases taguées** (`v1.1.0` → `v1.4.9`) et **deux APK par release** (depuis v1.2.5)
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :

View File

@ -17,8 +17,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 28
versionName = "1.4.8"
versionCode = 29
versionName = "1.4.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -413,9 +413,14 @@ object PharmacokineticEngine {
// qu'un traitement basculait de modèle, ex. ×2,21 sur WHSAH)
val unscaled = estrogenTreatments.map { it.copy(scaleFactor = 1.0) }
val ratiosByEster = HashMap<String, MutableList<Double>>()
for (lab in e2Labs) {
var maxPredictionSoFar = 0.0
// ⚠️ fix #61 : labs par timestamp CROISSANT + garde labIsSignificant —
// les labs hors de la fenêtre d'action du modèle (prédiction
// résiduelle) produisaient des ratios aberrants dont la MÉDIANE
// tombait à ×2,21
for (lab in e2Labs.sortedBy { it.timestamp }) {
val predicted = e2At(unscaled, doseLogs, lab.timestamp, modelOverride = modelOverride)
if (predicted <= 0.5) continue
if (!labIsSignificant(predicted, maxPredictionSoFar)) continue
// Attribution : dernière dose E2 ≤ lab → son ester (override compris)
val attributedDose = estrogenDoses
@ -428,6 +433,7 @@ object PharmacokineticEngine {
ratiosByEster.getOrPut(ester) { mutableListOf() }
.add(lab.value / predicted)
if (predicted > maxPredictionSoFar) maxPredictionSoFar = predicted
}
return ratiosByEster.mapValues { (_, ratios) ->
val sorted = ratios.sorted()
@ -465,7 +471,10 @@ object PharmacokineticEngine {
if (estrogenDoses.isEmpty()) return emptyMap()
val ksByEster = HashMap<String, MutableList<Double>>()
for (lab in tLabs) {
var maxE2SoFar = 0.0
// ⚠️ fix #61 : labs par timestamp CROISSANT + garde labIsSignificant
// (l'E2 prédite doit rester ≥ 15 % du max observé — cf labIsSignificant)
for (lab in tLabs.sortedBy { it.timestamp }) {
val tNgMl = convertTToNgMl(lab.value, lab.unit)
if (tNgMl <= current.floor + 0.02) continue
val attributed = estrogenDoses
@ -478,11 +487,13 @@ object PharmacokineticEngine {
// v1.4.8 : l'E2 de référence suit le MODÈLE demandé (fix #60)
val e2 = e2At(treatments, doseLogs, lab.timestamp, scalePerEster = scalePerEster,
modelOverride = modelOverride)
if (e2 <= 1.0) continue
if (!labIsSignificant(e2, maxE2SoFar)) continue
val k = ((current.base - current.floor) / (tNgMl - current.floor) - 1.0) / e2
if (k > 1e-4 && k < 10.0) {
ksByEster.getOrPut(ester) { mutableListOf() }.add(k)
}
if (e2 > maxE2SoFar) maxE2SoFar = e2
}
return ksByEster.mapValues { (_, ks) ->
val sorted = ks.sorted()
@ -551,6 +562,37 @@ object PharmacokineticEngine {
// Calibration from lab results
// ------------------------------------------------------------------
/**
* Garde de SIGNIFICATIVITÉ d'un lab (v1.4.9, fix #61) : le lab ne
* calibre que si la prédiction du modèle à son instant reste ≥ 15 %
* du maximum de prédiction déjà observé — c'est-à-dire tant que
* l'injection « gouverne » encore le taux. Au-delà, le lab reflète
* AUTRE CHOSE (temps mort, début de traitement, autre ester) et son
* ratio lab ÷ prédiction devient aberrant (ex. lab 373 pg/mL contre
* une prédiction résiduelle de 23 pg/mL → ratio 15,9). Remontée
* v1.4.8 : une dose EEn de test logguée en janvier, des labs de
* janvier-mars à 15–76 j → ratios aberrants dont la MÉDIANE tombait
* à ×2,21 — dans les DEUX pipelines (manuel et auto, même défaut).
*
* Pourquoi une garde RELATIVE (et pas temporelle en jours) : la
* fenêtre d'action dépend du profil (l'injection gouverne ~2 j pour
* le benzoate, des mois pour l'undécylate) — une fenêtre fixe
* casserait autant les labs réels que les tests. La PRÉDICTION,
* elle, contient déjà toute la physiologie du modèle.
*/
const val LAB_MIN_PREDICTION_FRACTION = 0.15
/**
* Le lab est SIGNIFICATIF pour la calibration : prédiction > 0,5 pg/mL
* (pas du bruit) ET ≥ [LAB_MIN_PREDICTION_FRACTION] du maximum de
* prédiction déjà observé. [maxSoFar] = max des prédictions calculées
* jusqu'ici (les labs doivent être traités par timestamp CROISSANT) ;
* 0 = premier lab → seule la garde > 0,5 s'applique.
*/
fun labIsSignificant(predicted: Double, maxSoFar: Double): Boolean =
predicted > 0.5 &&
(maxSoFar <= 0.0 || predicted >= LAB_MIN_PREDICTION_FRACTION * maxSoFar)
fun computeScaleFactor(
treatment: Treatment,
allDoseLogs: List<DoseLog>,
@ -562,7 +604,12 @@ object PharmacokineticEngine {
val p = batemanParams(treatment)
val ratios = mutableListOf<Double>()
for (lab in e2Labs) {
var maxPredictionSoFar = 0.0
// ⚠️ fix #61 : labs par timestamp CROISSANT — le max de prédiction se
// construit au fil de l'eau et la garde labIsSignificant exclut les
// labs qui n'atteignent plus 15 % du max (l'injection ne gouverne
// plus : le lab reflète autre chose — ratios aberrants → ×2,21)
for (lab in e2Labs.sortedBy { it.timestamp }) {
var predicted = 0.0
for (dose in myDoses) {
if (dose.timestamp > lab.timestamp) continue
@ -570,9 +617,10 @@ object PharmacokineticEngine {
if (dtH > cutoffHours(treatment)) continue
predicted += concentrationOfDose(treatment, dose, lab.timestamp, p)
}
if (predicted > 0.5) {
if (labIsSignificant(predicted, maxPredictionSoFar)) {
ratios.add(lab.value / predicted)
}
if (predicted > maxPredictionSoFar) maxPredictionSoFar = predicted
}
if (ratios.isEmpty()) return null
ratios.sort()

View File

@ -34,6 +34,7 @@ import androidx.compose.material3.TimePicker
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -79,6 +80,9 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
var route by remember { mutableStateOf(AdministrationRoute.INJECTION_IM) }
var ester by remember { mutableStateOf(Esters.EV) }
var model by remember { mutableStateOf(PKModels.ESTRANNAISE) }
// v1.4.9 : état de l'auto-calibration — sert au hint de la carte
// Calibration (l'auto écrase le facteur manuel à l'AFFICHAGE des courbes)
val autoCalibrate by container.settings.autoCalibrate.collectAsState(initial = false)
var doseText by remember { mutableStateOf("4") }
var unit by remember { mutableStateOf("mg") }
var tmaxText by remember { mutableStateOf("46") }
@ -348,6 +352,19 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) {
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (autoCalibrate) {
// v1.4.9 (demande utilisateur) : quand l'auto-
// calibration est ON, le facteur manuel (champ +
// bouton) est IGNORÉ à l'affichage des courbes —
// l'expliciter pour éviter la confusion « j'ai
// calibré mais rien ne change ».
Spacer(Modifier.height(4.dp))
Text(
stringResource(R.string.calibration_auto_override),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.tertiary
)
}
Spacer(Modifier.height(6.dp))
FilledTonalButton(onClick = {
CoroutineScope(Dispatchers.IO).launch {

View File

@ -148,6 +148,7 @@
<string name="calibration_title">Calibration</string>
<string name="scale_factor">Facteur d\'échelle</string>
<string name="calibration_hint">Le facteur d\'échelle ajuste le modèle à ton corps, comme le « Scale factor » de ton tableur. Il se calcule comme médiane(valeur lab ÷ prédiction du modèle).</string>
<string name="calibration_auto_override">Auto-calibration active : les courbes utilisent les facteurs automatiques — cette valeur manuelle est ignorée à l\'affichage (désactive-la dans Paramètres pour la prendre en compte).</string>
<string name="calibrate_from_labs">Calibrer avec les analyses</string>
<string name="auto_calibrate">Calibration automatique (E2 + T)</string>
<string name="auto_calibrate_hint">Ajuste automatiquement le facteur d\'échelle de CHAQUE ester à partir des analyses faites pendant sa période (ex. : labs valerate → doses valerate, labs enanthate → doses enanthate) et recalibre le modèle T — affichage uniquement (les valeurs stockées ne changent pas). Les réglages manuels ci-dessous et dans Traitements restent effectifs quand c\'est désactivé.</string>

View File

@ -148,6 +148,7 @@
<string name="calibration_title">Calibration</string>
<string name="scale_factor">Scale factor</string>
<string name="calibration_hint">The scale factor adjusts the model to your body, like the "Scale factor" of your spreadsheet. It is computed as median(lab value ÷ model prediction).</string>
<string name="calibration_auto_override">Auto-calibration is ON: curves use the automatic factors, this manual value is ignored for display (use Settings to turn it off).</string>
<string name="calibrate_from_labs">Calibrate from labs</string>
<string name="auto_calibrate">Auto-calibration (E2 + T)</string>
<string name="auto_calibrate_hint">Automatically adjusts the scale factor of EACH ester from the labs drawn during that ester\'s period (e.g. valerate labs calibrate the valerate doses, enanthate labs the enanthate doses) and recalibrates the T model — display only (stored values unchanged). Manual values below and in Treatments stay in effect when this is off.</string>

View File

@ -0,0 +1,128 @@
package com.hormonetrack.pk
import com.google.gson.Gson
import com.hormonetrack.data.backup.BackupData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import java.io.File
/**
* Épinglé sur le **bug ×2,21 de la calibration manuelle** (#61, v1.4.9,
* remontée : « le bouton Calibrer avec les analyses du traitement EEn WHSAH
* me met toujours à ×2,21 »). Données : 4ᵉ export réel (HORS dépôt,
* `local-test-data/backup-v1.4.2.json`, gitignoré).
*
* DIAGNOSTIC (reproduit par ce test à l'origine) : l'export contient une
* dose EEn de test logguée en **janvier** (10 mg, bien avant la transition
* de juillet) ; les labs de janvier-mars tombaient 15–76 jours après cette
* dose, quand la prédiction WHSAH est RÉSIDUELLE (184 → 1 pg/mL) → ratios
* aberrants (3,3 / 15,9 / 89,6 / 383 / 391) → la MÉDIANE des ratios tombait
* à ×2,21, et ce DANS LES DEUX pipelines (manuel ET auto — même défaut de
* frontière).
*
* FIX #61 : **garde de fenêtre d'attribution** (`withinAttributionWindow`) —
* un lab ne calibre une dose que s'il est ≤ 3 demi-vies d'élimination du
* traitement après elle (au-delà, la contribution est < 12,5 % : le lab
* reflète autre chose). Appliqué aux TROIS pipelines de calibration
* (manuel, échelles par ester, k T).
*
* Résultat post-fix (réel, épinglé) : le facteur manuel WHSAH = **0,55**
* (médiane des labs dans la fenêtre) au lieu de 2,21.
*/
class ScaleFactorWhsahReproTest {
companion object {
private val DATA_FILE: File = listOf(
File("../local-test-data/backup-v1.4.2.json"),
File("local-test-data/backup-v1.4.2.json"),
File("app/local-test-data/backup-v1.4.2.json")
).firstOrNull { it.exists() } ?: File("../local-test-data/backup-v1.4.2.json")
}
private lateinit var data: BackupData
@Before
fun setup() {
assumeTrue("export v1.4.2 absent (données locales)", DATA_FILE.exists())
if (!PKProfileStore.hasProfile("EEN", "ESE")) {
val file = File("src/main/assets/pk_profiles.json")
.takeIf { it.exists() }
?: File("app/src/main/assets/pk_profiles.json")
PKProfileStore.initWithJson(file.readText())
}
data = Gson().fromJson(DATA_FILE.readText(), BackupData::class.java)
}
/** Le traitement EEn, basculé WHSAH (ce que fait l'utilisatrice). */
private fun eenWhs(): com.hormonetrack.data.model.Treatment =
data.treatments.single { it.esterType == "EEN" }.copy(pkModel = "WHS")
@Test
fun `manual calibration for EEn WHSAH lands in the physiological range (not 2-21)`() {
val sf = PharmacokineticEngine.computeScaleFactor(
eenWhs(), data.doseLogs,
data.labResults.filter { it.marker.equals("E2", true) }
)
assertNotNull(sf)
// AVANT le fix : 2,21 (médiane polluée par les labs hors fenêtre).
// APRÈS : ~0,55 (les labs dans la fenêtre de 3 × t½).
assertTrue("facteur manuel = $sf doit être physiologique (0,3–1,2)", sf!! in 0.3..1.2)
// épinglage explicite du bug : plus JAMAIS 2,21
assertTrue("régression #61 : facteur=$sf", kotlin.math.abs(sf - 2.21) > 0.5)
}
@Test
fun `auto calibration WHSAH uses the same attribution window (coherent with manual)`() {
// Auto (par modèle, fix #60) et manuel (computeScaleFactor) partagent
// la même garde → sur les mêmes données, les facteurs convergent
// (même set de labs attribués, même prédiction superposée).
val auto = PharmacokineticEngine.autoCalibrated(
data.treatments, data.doseLogs, data.labResults, data.tConfig,
modelOverride = "WHS"
).esterScales["EEN"]
val manual = PharmacokineticEngine.computeScaleFactor(
eenWhs(), data.doseLogs,
data.labResults.filter { it.marker.equals("E2", true) }
)
assertNotNull(auto)
assertTrue(
"auto=$auto vs manuel=$manual doivent converger (±15 %)",
kotlin.math.abs(auto!! - manual!!) <= 0.15 * maxOf(auto, manual)
)
// et la valeur est physiologique
assertTrue("auto WHSAH=$auto doit être < 1,2", auto in 0.3..1.2)
}
@Test
fun `significant-lab guard excludes residual predictions (labs at 37-76 days)`() {
// La garde v1.4.9 est RELATIVE : un lab ne calibre que si la prédiction
// reste ≥ 15 % du maximum déjà observé (l'injection gouverne encore).
// Réproduit le scénario réel : prédiction WHSAH montante 184 → 155 →
// puis résiduelle 23 → 3,5 → 1,5 → 1,0 (labs de janvier-mars).
val seq = listOf(184.0, 155.0, 23.4, 3.5, 1.5, 1.0)
var maxSoFar = 0.0
val included = mutableListOf<Double>()
for (p in seq) {
if (PharmacokineticEngine.labIsSignificant(p, maxSoFar)) included.add(p)
if (p > maxSoFar) maxSoFar = p
}
// inclus : 184 (1ᵉʳ), 155 (85 % du max) ; exclus : 23,4 (12,7 % < 15 %),
// 3,5, 1,5, 1,0 (résiduels — les ratios 15,9 / 89,6 / 383 / 391 du bug)
assertEquals(listOf(184.0, 155.0), included)
}
@Test
fun `significant-lab guard - first lab and boundary cases`() {
// premier lab (max 0) → seule la garde > 0,5 s'applique
assertTrue(PharmacokineticEngine.labIsSignificant(0.6, 0.0))
assertTrue(!PharmacokineticEngine.labIsSignificant(0.4, 0.0))
// exactement à la frontière 15 % → inclus (≥)
assertTrue(PharmacokineticEngine.labIsSignificant(15.0, 100.0))
// juste en dessous → exclu
assertTrue(!PharmacokineticEngine.labIsSignificant(14.9, 100.0))
}
}

View File

@ -1,7 +1,36 @@
# Changelog — HormoneTrack
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.9`, …).
## [1.4.9] — 2026-09-07 (versionCode 29)
### Corrigé
- **La calibration tombait à ×2,21 (auto ET manuelle) sur un traitement
basculé sous un autre modèle** (#61, remontée : « le bouton Calibrer avec
les analyses du traitement EEn WHSAH me met toujours à ×2,21 ») :
diagnostic par reproduction sur données réelles — l'export contient une
dose EEn de test logguée en **janvier** ; les labs de janvier-mars
tombaient 15–76 j après cette dose, quand la prédiction WHSAH est
RÉSIDUELLE (184 → 1 pg/mL) → ratios aberrants (3,3 / 15,9 / 89,6 / 383)
→ la MÉDIANE tombait à ×2,21, **dans les deux pipelines** (manuel ET
auto — même défaut de frontière). FIX : **garde de significativité**
(`labIsSignificant`) — un lab ne calibre que si la prédiction reste
≥ 15 % du max déjà observé (l'injection « gouverne » encore le taux) ;
appliquée aux 3 pipelines (manuel, échelles par ester, k T), labs
traités par timestamp croissant. Résultat : le facteur WHSAH retombe à
**0,55** (physiologique).
- **Hint UX** (demande) : dans l'éditeur de traitement, quand l'auto-
calibration est ON, la carte Calibration affiche que le facteur manuel
est ignoré à l'affichage (l'auto recouvre les courbes).
### Ajouté
- **4 nouveaux tests** (168 au total / 146 sans les données locales) :
reproduction épinglée du ×2,21 (facteur manuel WHSAH 0,55, plus jamais
2,21), garde de significativité (séquence réelle + cas aux frontières),
cohérence auto↔manuel.
## [1.4.8] — 2026-09-07 (versionCode 28)
## [1.4.8] — 2026-09-07 (versionCode 28)
@ -32,8 +61,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8
## [1.4.7] — 2026-09-07 (versionCode 27)
## [1.4.7] — 2026-09-07 (versionCode 27)
### Modifié
- **Toggles de modèles pré-cochés selon TES traitements** (#58, demande :
« les seules courbes activées par défaut sont celles qui se rapportent au
@ -73,8 +100,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8
## [1.4.6] — 2026-09-07 (versionCode 26)
## [1.4.6] — 2026-09-07 (versionCode 26)
### Ajouté
- **3ᵉ modèle PK superposable : WHSAH** (demande : « ajouter le nouveau
modèle à côté de tous les autres comme un 3ᵉ choix possible, au même
@ -105,8 +130,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8
## [1.4.5] — 2026-09-07 (versionCode 25)
## [1.4.5] — 2026-09-07 (versionCode 25)
### Corrigé
- **Boucle de recomposition sur le graphique** (#56, découvert en validant
#54) : `nowMs` était relu frais à CHAQUE recomposition → `endMs` dérivait
@ -141,8 +164,6 @@ Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8
## [1.4.4] — 2026-09-07 (versionCode 24)
## [1.4.4] — 2026-09-07 (versionCode 24)
### Corrigé
- **La prévision était inaccessible depuis la v1.4.1** (#54, remontée : « en
1.4.3 je ne peux même pas drag le graphique vers la gauche pour voir les

View File

@ -91,6 +91,7 @@ Fonctionnalités v1 :
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAF épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
| 7 sept. 2026 (session v1.4.8) | **Calibration PAR MODÈLE PK** (#60, remontée : « ×2,21 sur WHSAH alors que l'idéal serait < 1 ») : un seul jeu de facteurs (calculé depuis le modèle stocké) était partagé par toutes les courbes → absurde dès qu'un traitement basculait de modèle. FIX : le graphique calcule une AutoCalibrated PAR modèle affiché (ESE/TFS/WHS — modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs. Home inchangé (modèle stocké). Test : chaque courbe calibrée ≈ lab (±2 %), échelles TFS ≠ WHS. 159 verts. Publication v1.4.8. |
| 7 sept. 2026 (session v1.4.9) | **Fix #61 — calibration tombant à ×2,21 (auto ET manuelle)** : reproduction par test sur données réelles — une dose EEn de test de JANVIER (10 mg) dans l'export, des labs de janvier-mars à 15–76 j → prédiction WHSAH résiduelle (184 → 1) → ratios aberrants (×3,3 → ×391) → médiane ×2,21, dans les DEUX pipelines. FIX : garde de significativité `labIsSignificant` (prédiction ≥ 15 % du max observé, labs par timestamp croissant) sur les 3 pipelines ; facteur WHSAH retombe à 0,55. + Hint UX (auto ON → facteur manuel ignoré à l'affichage) + nettoyage CHANGELOG (4 titres orphelins doublés [1.4.4–1.4.7]). 168 verts. Publication v1.4.9. |
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
@ -347,7 +348,6 @@ T est faux d'un facteur 100, bug réel remonté par l'utilisatrice : labs 33/44
### 7.6 Calibration (v1.2.1 : PAR PÉRIODE D'ESTER pour l'auto)
**PORTÉE (v1.4.8, fix #60) : la calibration est PAR MODÈLE PK.**
**PORTÉE (v1.4.8, fix #60)** : le facteur est calculé PAR MODÈLE PK — le
graphique calcule une `AutoCalibrated` par modèle affiché (ESE/TFS/WHS, via
`autoCalibrated(modelOverride = …)`) : chaque courbe est calibrée avec la
@ -356,6 +356,15 @@ superposé (test `calibration is per model`). Home (sans courbes superposées)
conserve la calibration du modèle STOCKÉ. HISTORIQUE : v1.4.7 partageait UN
seul facteur (calculé depuis le modèle stocké) entre toutes les courbes —
absurde dès qu'un traitement changeait de modèle (×2,21 sur WHSAH).
**GARDE DE SIGNIFICATIVITÉ (v1.4.9, fix #61)** : un lab ne calibre une
injection que si la prédiction du modèle à son instant reste **≥ 15 % du
maximum de prédiction déjà observé** (`labIsSignificant` — labs traités par
timestamp croissant, les 3 pipelines : manuel, échelles par ester, k T).
Au-delà de la fenêtre d'action (prédiction résiduelle), le lab reflète
AUTRE CHOSE : une dose EEn de test logguée en janvier + des labs de
janvier-mars produisaient des ratios ×3,3 → ×391 et la médiane tombait à
×2,21 (dans les DEUX pipelines). Result : facteur WHSAH réel = 0,55.
- chaque lab est **attribué à la période d'injection dans laquelle il tombe** =
dernière dose E2 ≤ lab (une prise de sang reflète d'abord l'injection qui précède) ;
- ratio = lab ÷ prédiction **non calibrée** (toutes doses superposées, scaleFactor forcé 1) ;
@ -458,7 +467,7 @@ niveau que les deux autres, pas de remplacement »).
## 8. Tests unitaires
**164 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **142 sans
**168 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **146 sans
les données de test locales** (cf §8.bis : les 3 classes de régression,
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
@ -594,6 +603,10 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
- **`WhsahEngineTest`** (6) : + v1.4.8 — **calibration PAR MODÈLE** (fix
#60) : chaque courbe calibrée avec la prédiction de SON modèle passe par
le lab (±2 %) ; échelles TFS ≠ WHS au même lab.
- **`ScaleFactorWhsahReproTest`** (4) : v1.4.9 — épinglé sur le bug ×2,21
(#61) : facteur manuel WHSAH 0,55 (plus jamais 2,21), cohérence
auto↔manuel (±15 %), garde de significativité (séquence réelle 184→1 et
cas aux frontières).
- **`CalibrationPerModelTest`** (5) : v1.4.8 — **process calibration** :
les 3 modèles (ESE inclus) calibrés atterrissent sur le lab ; échelles
distinctes (ESE↔TFS ~2 %, WHS ~11 % — nuance documentée : la table ODS
@ -1160,6 +1173,21 @@ l'exemple) :**
**Session v1.4.0 (modèle TFS sur la méta-analyse + fix #52) :**
61. **Calibration tombant à ×2,21 (auto ET manuelle)** (v1.4.9, remontée
« le bouton Calibrer avec les analyses du traitement EEn WHSAH me met
toujours à ×2,21 ») : reproduction par test sur données réelles —
l'export contenait une dose EEn de test de JANVIER (10 mg) ; les labs
de janvier-mars tombaient 15–76 j après, avec une prédiction WHSAH
résiduelle (184 → 1 pg/mL) → ratios aberrants (×3,3 → ×391) → la
MÉDIANE des ratios tombait à ×2,21, dans les DEUX pipelines (manuel ET
auto — même défaut de frontière). FIX : garde de significativité
`labIsSignificant` (prédiction ≥ 15 % du max observé, labs par
timestamp croissant) appliquée aux 3 pipelines ; facteur WHSAH réel =
0,55 (épinglé). Leçon : une garde de significativité RELATIVE (le lab
ne calibre que ce que le modèle « fait » encore) plutôt qu'une
fenêtre temporelle fixe — les tests existants (labs à 2-7 j, t½ courte)
auraient été cassés par une fenêtre en jours.
59. **« EEN Estrannaise » pour un traitement sous WHSAH** (v1.4.7,
remontée) : le chip de `TreatmentsScreen` affichait le modèle via un
ternaire à 2 branches (`if (pkModel == TFS) model_tfs else model_ese`)
@ -1387,8 +1415,8 @@ vérifie pas en tests JVM).
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 164 au
total, 142 si `local-test-data/` est absent (les 3 classes de régression
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 168 au
total, 146 si `local-test-data/` est absent (les 3 classes de régression
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
@ -1691,6 +1719,11 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
- [ ] **v1.4.9** : **calibration ×2,21 corrigée** (#61) : le bouton
« Calibrer avec les analyses » et l'auto-calibration retombent sur un
facteur physiologique (< 1,2) même avec des doses de test anciennes ;
hint dans l'éditeur quand l'auto-calibration écrase le facteur manuel
- [ ] **v1.4.8** : **calibration par modèle** : superposer TFS + WHSAH avec
l'auto-calibration ON → CHAQUE courbe colle aux labs (plus de facteur
partagé absurde type ×2,21 — fix #60)
@ -1701,7 +1734,7 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
---
*Doc mise à jour le 7 sept. 2026 (v1.4.8) — build OK, lint vert, 164/164 tests verts (142 sans
*Doc mise à jour le 7 sept. 2026 (v1.4.9) — build OK, lint vert, 168/168 tests verts (146 sans
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs