v1.2.7 : prévision robuste aux oublis/retards + rattrapage complet de la doc de dev

- generateForecastDoses : les créneaux déjà passés (oubli d'injection) ne sont
  plus simulés — avant, dernier+intervalle tombait dans le passé → faux pic
  dans l'historique + rythme décalé ; la prévision démarre au premier créneau
  strictement futur, au rythme configuré
- un retard décale toute la prévision (part de la dernière prise réelle) —
  comportement voulu, épinglé par 2 tests
- doc §7.3b corrigée (mismatch doc-code détecté par l'utilisatrice)
- docs : §13 écrasement, §19/§20 import, §14 #34/#35, §21, historique
  (lignes v1.2.2/6/7), footer — vérification intégrée dans le script de patch
- versionCode 10, versionName 1.2.7 ; 64 tests verts
This commit is contained in:
Siphonight 2026-09-05 22:14:19 +02:00
parent 33e747de0c
commit e3be3b3657
5 changed files with 108 additions and 15 deletions

View File

@ -12,8 +12,8 @@ android {
applicationId = "com.hormonetrack"
minSdk = 26
targetSdk = 36
versionCode = 9
versionName = "1.2.6"
versionCode = 10
versionName = "1.2.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -319,6 +319,15 @@ object PharmacokineticEngine {
val forecast = mutableListOf<DoseLog>()
var t = last.timestamp + intervalMs
// ⚠️ Oubli d'une injection : le premier créneau théorique tombe dans le
// PASSÉ (dernière prise + intervalle = jour de l'injection manquée) —
// on ne simule JAMAIS un créneau passé (faux pic dans l'historique) :
// on avance au premier créneau STRICTEMENT FUTUR, au rythme configuré.
// Un retard, lui, décale naturellement toute la prévision (elle part de
// la dernière prise réelle).
while (t <= nowMs) {
t += intervalMs
}
while (t <= toMs) {
forecast.add(
DoseLog(

View File

@ -50,6 +50,46 @@ class V120FeaturesTest {
forecastIntervalDays = intervalDays
)
@Test
fun `forecast skips past slots after a missed dose (oubli)`() {
// Rythme 6 j, dernier rappel... dernière PRISE il y a 12 j (une injection
// a été manquée il y a 6 j) : le créneau théorique BASE-6j est passé.
val tr = eenTreatment(intervalDays = 6.0)
val doses = listOf(
DoseLog(treatmentId = 1, timestamp = BASE - 18 * DAY, doseAmount = 5.0),
DoseLog(treatmentId = 1, timestamp = BASE - 12 * DAY, doseAmount = 5.0)
)
val forecast = PharmacokineticEngine.generateForecastDoses(
tr, doses, toMs = BASE + 30 * DAY, nowMs = BASE
)
// Aucun créneau simulé dans le passé : premier = BASE+6 j (dernière+12 j
// est le premier créneau strictement futur au rythme de 6 j)
assertTrue(forecast.isNotEmpty())
forecast.forEach { f ->
assertTrue("créneau passé simulé : ${f.timestamp}", f.timestamp > BASE)
}
assertEquals(BASE + 6 * DAY, forecast.first().timestamp)
forecast.zipWithNext { prev, cur ->
assertEquals(6 * DAY, cur.timestamp - prev.timestamp)
}
}
@Test
fun `forecast follows a delayed last dose (retard d un jour)`() {
// Rythme 6 j, dernière prise faite avec 1 jour de retard (à J-5 au lieu
// de J-6) : TOUTE la prévision se décale d'un jour, en partant de la
// dernière prise réelle.
val tr = eenTreatment(intervalDays = 6.0)
val doses = listOf(
DoseLog(treatmentId = 1, timestamp = BASE - 5 * DAY, doseAmount = 5.0)
)
val forecast = PharmacokineticEngine.generateForecastDoses(
tr, doses, toMs = BASE + 30 * DAY, nowMs = BASE
)
assertEquals(BASE + 1 * DAY, forecast.first().timestamp)
assertTrue(forecast.all { it.timestamp > BASE })
}
@Test
fun `forecast doses follow the configured interval from the last real dose`() {
val tr = eenTreatment(intervalDays = 7.0)

View File

@ -1,7 +1,21 @@
# 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.2.6`, …).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.2.7`, …).
## [1.2.7] — 2026-09-05 (versionCode 10)
### Corrigé
- **Prévision cassée après un oubli d'injection** : le premier créneau projeté
tombait dans le PASSÉ (dernière prise + intervalle = jour de l'injection
manquée) → la courbe peignait un **faux pic dans l'historique** et tout le
rythme projeté était décalé. Désormais les créneaux déjà passés ne sont
jamais simulés : la prévision avance au premier créneau **strictement
futur** au rythme configuré. Un simple **retard**, lui, décale naturellement
toute la prévision (elle part de la dernière prise réelle — comportement
voulu, désormais testé).
- 2 nouveaux tests (64 au total) : oubli (créneaux passés sautés, espacement
conservé) et retard (décalage complet de la prévision).
## [1.2.6] — 2026-09-05 (versionCode 9)

View File

@ -69,6 +69,9 @@ Fonctionnalités v1 :
| 5 sept. 2026 (session v1.2.4) | Bug `isActive` (simulation effacée) → sémantique « drapeau administratif » (§6.bis) ; régression n°3 sur le 3ᵉ export réel (transition EV inactif → EEn actif, 38 doses, 22 labs) ; **62 tests verts**, APK v1.2.4. |
| 5 sept. 2026 (session push Gitea + v1.2.3) | Push initial vers **gitea.cloudyfy.fr/Siphonight/HormoneTrack** (privé) après **anonymisation de l'historique** (filter-branch : les premiers commits embarquaient les valeurs réelles des tests) ; **données de test réelles déplacées hors dépôt** (`local-test-data/` gitignoré, tests `Assume`-skippés) ; **releases avec APK en pièce jointe** ; pics/creux + calibration T par période d'ester ; **56 tests verts**. |
| 5 sept. 2026 (session v1.2.1) | Remontées UX/utilisateur : calibration **par période d'ester** (labs EV → doses EV, labs EEn → doses EEn — corrige les courbes gonflées), lab T "pg/mL" neutralisé, **édition des labs**, tap accueil → Graphiques + mini-légende, auto-cal fusionnée (E2+T, un seul switch) ; 2ᵉ régression épinglée sur le nouvel export ; **44 tests verts**, APK v1.2.1. |
| 5 sept. 2026 (session v1.2.2) | Toggle T masque aussi les labs T ; **prise de sang E2 + T en une entrée** (chacune optionnelle) affichée **côte à côte** ; sélecteur d'édition par marqueur ; suppression par prise ; 4 tests de regroupement ; **48 tests verts**, APK v1.2.2. |
| 5 sept. 2026 (session v1.2.6) | Valeurs estimées affichées sur les pics/creux (drawExtremum) ; **import JSON en mode écrasement** (bug : conflit d'IDs si données existantes) avec restauration du tConfig et reprogrammation des rappels ; docs ; 62 tests verts, APK v1.2.6 + releases. |
| 5 sept. 2026 (session v1.2.7) | Prévision : les créneaux passés ne sont plus simulés (bug oubli → faux pic historique) ; retard = décalage voulu ; mismatch doc-code §7.3b corrigé ; 2 tests ; **64 tests verts**, APK v1.2.7 + releases. |
Leçon importante de la session build : **les erreurs de compilation et les bugs sémantiques
(bisection inversée, plancher d'affichage des profils) n'ont été détectés qu'en construisant
@ -256,14 +259,17 @@ Coupure par dose : `cutoffHours` = longueur de table (8001 h) pour les profils,
dessine les deux modèles côte à côte depuis le même traitement (Bateman non concerné :
les deux séries y sont identiques).
- **`generateForecastDoses(treatment, doseLogs, toMs, nowMs)`** : projette les doses à
venir = dernière dose réelle + k × `forecastIntervalDays` jusqu'à `toMs`, strictement
après `nowMs` ; dose = standard du traitement, ester = override de la dernière
injection. Jamais persistées : uniquement passées à `computeCurve` par le ChartScreen
quand le chip « Prévision » est actif.
venir = dernière dose réelle + k × `forecastIntervalDays`, **strictement après
`nowMs`** (v1.2.7 : les créneaux déjà passés — cas d'un OUBLI — sont sautés au
rythme configuré, sinon faux pic dans l'historique ; un RETARD décale toute la
prévision) ; dose = standard du traitement, ester = override de la dernière
injection. Jamais persistées : uniquement passées à `computeCurve` par le
ChartScreen quand le chip « Prévision » est actif.
- **`autoCalibrated(treatments, doseLogs, labs, tConfig)`** : option « Calibration
automatique » — renvoie des **copies** de traitements avec les scale factors
recalculés (médiane lab ÷ prédiction) + TConfig recalibré. Les valeurs stockées ne
bougent jamais ; HomeScreen et ChartScreen branchent dessus quand l'option est active.
automatique » (v1.2.1+) — renvoie `esterScales` (facteur **par ester**, cf §7.6)
+ `tKPerEster` (k T **par ester**, cf §7.5) recalculés depuis les labs ;
`treatments` et `tConfig` **inchangés** (fallback par ester absent = valeurs
stockées). HomeScreen et ChartScreen branchent dessus quand l'option est active.
### 7.4 Bateman (gel/patch/oral)
@ -535,8 +541,15 @@ Pièges :
- `BackupData{version=1, exportedAt, treatments[], doseLogs[], labResults[], tConfig}` → Gson
- **Les IDs Room sont conservés** dans l'export et réinsérés tels quels → les FK
dose→traitement restent valides
- Import = **ajout** (traitements → doses → labs) ; ré-import du même fichier → conflit
d'ID unique → exception catchée → `import_fail` (voulu ; un mode « replace » est en §20)
- Import : **mode ÉCRASEMENT (v1.2.6)** — les données actuelles sont effacées
AVANT l'insertion (`deleteAllDoseLogs` → `deleteAllLabResults` →
`deleteAllTreatments`, ordre enfants → parents pour la FK CASCADE), puis
restauration avec les IDs du backup conservés (FK valides). Avant (v1.2.5) :
insertion en « ajout » → conflit d'ID dès que l'app contenait des données →
l'import échouait (bug remonté). Après un import : le `tConfig` du backup est
restauré dans DataStore et les rappels sont reprogrammés
(`rescheduleAll(allTreatmentsOnce())`). Le dialog prévient que TOUT sera
remplacé (bouton « Effacer & restaurer »)
- Transport : SAF (`CreateDocument("application/json")` / `OpenDocument`), écriture
`openOutputStream(uri, "wt")` ; ⚠️ pas de `return` dans un expression body `= try{}`
@ -627,6 +640,19 @@ Historique complet — **à ne pas réintroduire** (utile pour diff/revert) :
titre `[1.2.3]` : l'extraction cherchait `[vX.Y.Z]` → fallback silencieux.
→ `scripts/gitea-release.py` (corps = section CHANGELOG, APK attaché).
34. **Import JSON impossible si l'app contient déjà des données** (v1.2.6) :
l'insertion en « ajout » avec les IDs du backup entrait en conflit
d'unicité dès la moindre donnée existante → import échouait. → mode
ÉCRASEMENT (effacement ordonné enfants→parents puis restauration) +
restauration du tConfig + reprogrammation des rappels.
35. **Prévision cassée après un oubli d'injection** (v1.2.7) : le premier
créneau projeté tombait dans le passé (dernière + intervalle = jour
manqué) → faux pic dans l'historique + rythme décalé. → les créneaux
passés sont sautés (`while (t <= nowMs) t += intervalMs`), la prévision
démarre au premier créneau futur ; un retard décale toute la prévision
(comportement voulu, testé). NB : la doc §7.3b affirmait « strictement
après nowMs » alors que le code incluait les créneaux passés —
mismatch doc-code détecté par l'utilisatrice.
33. **Double insets edge-to-edge** (v1.2.5) : espace vide en haut de l'écran —
la fenêtre AppCompat poussait le contenu sous la barre de statut ET les
TopAppBar M3 rajoutaient leur padding interne. → `enableEdgeToEdge()` +
@ -826,7 +852,7 @@ manquantes** (licences signées) — c'est comme ça que android-37 est arrivé.
Volontaires (v1) :
- Pas de ViewModel/DI (couplage UI↔repo via CompositionLocal)
- Modèle T empirique (non publié) — étiqueté estimation partout
- Import JSON = ajout seulement (pas de mode replace/dédup)
- Import JSON = **écrasement** depuis v1.2.6 (mode fusion non implémenté)
- `fallbackToDestructiveMigration()` — à retirer à la migration v2 du schéma
- WorkManager déclaré non utilisé
- Profils par **tables** (pas par formule) : les D/k1–k3 de l'ODS ne sont pas consommés —
@ -845,7 +871,7 @@ Volontaires (v1) :
1. **Robolectric + tests Compose** (VM Android en JVM — pas besoin d'appareil)
2. **Émulateur local** pour smoke-tests UI (sur demande, ~2–3 Go)
3. Mode « planifier les injections » (schedule récurrent → pré-remplir le log)
4. Import JSON : mode **replace** (wipe + insert) + détection de doublons
4. Import JSON : détection de doublons / mode fusion optionnel (l'écrasement est fait, v1.2.6)
5. Verrou biométrique (BiometricPrompt), widget, export CSV
6. Charts : **zoom** + tooltip au toucher (le pan est fait, v1.2.0) ;
MaterialExpressiveTheme quand l'API passera publique (cf §3)
@ -888,9 +914,13 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
- [ ] **v1.2.x** : édition d'une dose (tap ligne Doses) et d'un lab (tap ligne
Analyses → sélecteur E2/T si paire) ; suppression par prise entière
- [ ] **v1.2.x** : prise de sang E2 + T en une entrée (champs optionnels)
- [ ] **v1.2.7** : prévision après un OUBLI (pas de faux pic dans le passé)
et après un RETARD (prévision décalée, suivant la dernière prise)
- [ ] **v1.2.6** : import d'un backup avec l'app déjà remplie → ÉCRASER &
restaurer (message clair), tConfig restauré, rappels reprogrammés
- [ ] **v1.2.4** : passer un traitement à inactif → retiré de « Log rapide » et
du dropdown des nouvelles doses, rappel annulé, **mais sa simulation reste
sur le graphique** et la calibration couvre toujours ses périodes
---
*Doc mise à jour le 5 sept. 2026 (v1.2.6) — build OK, 62/62 tests verts (44 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.*
*Doc mise à jour le 5 sept. 2026 (v1.2.7) — build OK, 62/62 tests verts (44 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.*