# FEEDBACK TO ARCHITECT — v9

> **Spec technique** : `001-histometeo-mvp.tech.v9.md`
> **Spec fonctionnelle** : `001-histometeo-mvp.md`
> **Date** : 2026-03-12
> **Tests** : 57/57 passent (`pytest tests/ -v`)
> **Fichiers modifiés** : `src/main.py`, `src/config.py`, `src/commune_service.py`, `public/app.js`, `public/index.html`, `public/style.css`, `tests/test_api.py`, `tests/test_commune_service.py`, `README.md`
> **Fichiers créés** : `src/normals_service.py`, `tests/test_normals_service.py`

---

## 1) Functional Compliance

| AC                                    | Statut | Justification                                                                                                                             |
| ------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| AC1 — Autocomplétion commune          | ✅ OK  | Fonctionnel, y compris les champs enrichis (population, surface, dept_nom, region_nom) transparents pour l'existant.                      |
| AC2 — Tableau horaire après recherche | ✅ OK  | Inchangé, testé via `test_weather_ok`.                                                                                                    |
| AC3 — Colonnes météo par heure        | ✅ OK  | Inchangé.                                                                                                                                 |
| AC4 — Fuseau Europe/Paris             | ✅ OK  | INV-2 préservé.                                                                                                                           |
| AC5 — Limite 31 jours                 | ✅ OK  | Inchangé, testé via `test_weather_period_too_long`.                                                                                       |
| AC6 — Date future refusée             | ✅ OK  | Testé via `test_weather_future_date`.                                                                                                     |
| AC7 — Date avant 1940 refusée         | ✅ OK  | Testé via `test_weather_before_1940`.                                                                                                     |
| AC8 — Messages d'erreur explicites    | ✅ OK  | Nouveaux messages pour `/api/normals` (400 + 502) conformes.                                                                              |
| AC9 — Note de transparence visible    | ✅ OK  | Section `#info` inchangée et visible.                                                                                                     |
| AC10 — Responsive mobile 360px        | ✅ OK  | Styles `.info-card`, `.commune-info-list` héritent des media queries existantes. Anomaly table dans `.table-wrapper` (scroll horizontal). |
| AC11 — Sans inscription               | ✅ OK  | INV-1 préservé, aucun stockage client/serveur.                                                                                            |

### Critères spécifiques v9

| Critère                                        | Statut             | Justification                                                                                                                                           |
| ---------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/api/normals` retourne les normales           | ✅ OK              | Route correcte, validation lat/lon/start/end, réponse conforme (elevation, reference_period, period_normals, daily_normals). Testée par 4 tests API.    |
| Bloc « Anomalie climatique » affiché           | ✅ OK              | `renderClimateNormals()` crée le tableau 4 lignes avec classes CSS `anomaly-warm/cold/wet/dry`. Conforme INV-7 (pas d'`innerHTML`).                     |
| Bloc « Informations commune » affiché          | ⚠️ OK avec réserve | Fonctionnellement correct (population, département, région, superficie, densité, altitude). **Mais positionnement DOM incorrect** (voir C1 ci-dessous). |
| `/api/resolve/{slug}` retourne champs enrichis | ✅ OK              | `resolve_slug` retourne `population`, `surface_km2`, `departement_nom`, `region_nom`. Testé via `test_resolve_slug_enriched_fields`.                    |
| INV-9 — Normales non bloquantes                | ✅ OK              | `fetchAndRenderNormals` dans un `try/catch` indépendant. En cas d'échec, le bloc normales est masqué et le bloc commune info s'affiche sans altitude.   |
| INV-10 — Enrichissement progressif             | ✅ OK              | Chaque champ commune est testé individuellement avant affichage. `renderCommuneInfo` n'affiche le bloc que si ≥ 1 champ enrichi (hors nom).             |
| Corrections R24–R26, R28 intégrées             | ✅ OK              | Voir section 2.                                                                                                                                         |
| Normales masquées en mode comparaison          | ✅ OK              | `fetchAndRenderNormals` retourne immédiatement si `comparisonMode === true`. `renderComparisonResults` masque explicitement les deux blocs.             |

---

## 2) Contract Compliance

### Scope respecté

| Fichier                                   | Autorisé | Vérifié                                                           |
| ----------------------------------------- | -------- | ----------------------------------------------------------------- |
| `src/normals_service.py` (nouveau)        | ✅       | Créé conformément                                                 |
| `src/main.py`                             | ✅       | Modifié : nouvelle route `/api/normals`, validation R28 `dept`    |
| `src/commune_service.py`                  | ✅       | Modifié : `_normalize_commune` enrichi, `resolve_slug` enrichi    |
| `src/config.py`                           | ✅       | Modifié : constantes normales + `COMMUNES_FIELDS` étendu          |
| `public/app.js`                           | ✅       | Modifié : fetch normales, rendu anomalies, rendu commune info     |
| `public/index.html`                       | ✅       | Modifié : sections `#climate-normals` et `#commune-info` ajoutées |
| `public/style.css`                        | ✅       | Modifié : `.info-card`, anomaly classes, commune-info styles      |
| `tests/test_normals_service.py` (nouveau) | ✅       | 5 tests unitaires                                                 |
| `tests/test_api.py`                       | ✅       | Tests route `/api/normals` + R24 + R28                            |
| `tests/test_commune_service.py`           | ✅       | Tests enrichissement + R25 + R26                                  |
| `README.md`                               | ⚠️       | Non listé dans le scope mais modification mineure acceptable      |

### Forbidden changes respectés

| Fichier interdit         | Modifié ?          |
| ------------------------ | ------------------ |
| `src/weather_service.py` | ❌ Non modifié ✅  |
| `src/cache.py`           | ❌ Non modifié ✅  |
| `docs/` (existants)      | ❌ Non modifiés ✅ |
| `.github/`               | ❌ Non modifié ✅  |
| `Dockerfile`             | ❌ Non modifié ✅  |
| `pyproject.toml`         | ❌ Non modifié ✅  |
| `public/assets/`         | ❌ Non modifié ✅  |

### Invariants préservés

| Invariant                                    | Statut                                                                                                                        |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| INV-1 — Aucune donnée utilisateur stockée    | ✅ Préservé                                                                                                                   |
| INV-2 — Fuseau Europe/Paris                  | ✅ Préservé (param `timezone` dans fetch Open-Meteo)                                                                          |
| INV-3 — Période max 31 jours                 | ✅ Préservé                                                                                                                   |
| INV-4 — Aucune clé API                       | ✅ Préservé                                                                                                                   |
| INV-5 — Interface en français                | ✅ Préservé (« Anomalie climatique », « Informations sur la commune »)                                                        |
| INV-6b — Page unique, URL = état             | ✅ Préservé                                                                                                                   |
| INV-7 — Aucun innerHTML                      | ✅ Vérifié par grep sur `public/app.js` — aucune occurrence. `createElement`, `textContent`, `replaceChildren` exclusivement. |
| INV-8 — Recherche simple indépendante        | ✅ Préservé                                                                                                                   |
| INV-9 — Normales non bloquantes (nouveau)    | ✅ Respecté                                                                                                                   |
| INV-10 — Enrichissement progressif (nouveau) | ✅ Respecté                                                                                                                   |

### Corrections feedback v8

| Correction                                     | Statut | Vérification                                                                                                    |
| ---------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------- |
| R24 — Content-type HTML sur route comparaison  | ✅ OK  | `test_seo_route_comparaison` vérifie `"text/html" in response.headers.get("content-type", "")`                  |
| R25 — `resolve_slug("invalidslug")` → `None`   | ✅ OK  | `test_resolve_slug_invalid_no_dept` vérifie le retour `None`                                                    |
| R26 — Tests `generate_slug` pour noms courants | ✅ OK  | 4 tests : Paris, Aix-en-Provence, Noisy-le-Grand, chaîne vide                                                   |
| R27 — Pseudo-code slug corrigé dans la spec    | ✅ OK  | La spec v9 documente l'algorithme correct (apostrophes → suppression, pas tiret)                                |
| R28 — Validation `dept` regex sur redirect     | ✅ OK  | Regex `^\d{2,3}$` appliquée sur `dept` et `dept2`. `test_legacy_redirect_invalid_dept` vérifie le comportement. |

---

## 3) Technical Quality

### Points positifs

- **Architecture clean** : `NormalsService` est un service isolé avec son propre cache, ses propres exceptions, et un client HTTP injectable — parfait pour les tests. Cohérent avec l'architecture existante (`WeatherService`, `CommuneService`).
- **Fetch en 3 chunks parallèles** : `asyncio.gather(*tasks, return_exceptions=True)` avec fallback gracieux si certains chunks échouent. Conforme à la spec.
- **Cache efficace** : clé `normals:{lat:.2f}:{lon:.2f}` avec TTL 90 jours. Un seul fetch par localisation couvrant toute l'année. Requêtes ultérieures pour la même localisation avec une période différente servies depuis le cache.
- **Robustesse données** : `_append_if_numeric` filtre les valeurs non numériques. Le min de longueurs des arrays évite les `IndexError` sur données incohérentes.
- **Frontend progressif** : le fetch des normales est non bloquant, le bloc commune info se rend même si les normales échouent (altitude masquée), chaque champ est testé avant affichage.
- **Nombre français** : `Intl.NumberFormat('fr-FR')` pour population, `oneDecimalFr` pour températures — formatage cohérent.

### Points mineurs

- **Aucune complexité inutile détectée** : le code est minimal et direct.
- **Aucune duplication détectée** : les fonctions de rendu sont factorisées (`addInfoRow`, `formatSigned`, `setAnomalyClass`).
- **Aucune dette technique nouvelle significative.**

---

## 4) Test Coverage

### Couverture conforme à la spec

| Test spécifié (spec §6)                   | Présent | Fichier                   |
| ----------------------------------------- | ------- | ------------------------- |
| `test_normals_computation_basic`          | ✅      | `test_normals_service.py` |
| `test_normals_handles_missing_data`       | ✅      | `test_normals_service.py` |
| `test_normals_caching`                    | ✅      | `test_normals_service.py` |
| `test_normals_includes_elevation`         | ✅      | `test_normals_service.py` |
| `test_normals_period_aggregation`         | ✅      | `test_normals_service.py` |
| `test_normals_api_route_ok`               | ✅      | `test_api.py`             |
| `test_normals_api_route_missing_params`   | ✅      | `test_api.py`             |
| `test_normals_api_route_invalid_coords`   | ✅      | `test_api.py`             |
| `test_normals_api_route_upstream_error`   | ✅      | `test_api.py`             |
| `test_normalize_commune_enriched`         | ✅      | `test_commune_service.py` |
| `test_normalize_commune_missing_enriched` | ✅      | `test_commune_service.py` |
| `test_resolve_slug_enriched_fields`       | ✅      | `test_commune_service.py` |
| `test_seo_route_comparaison` (R24)        | ✅      | `test_api.py`             |
| `test_resolve_slug_invalid_no_dept` (R25) | ✅      | `test_commune_service.py` |
| `test_generate_slug_paris` (R26)          | ✅      | `test_commune_service.py` |
| `test_generate_slug_aix` (R26)            | ✅      | `test_commune_service.py` |
| `test_generate_slug_noisy` (R26)          | ✅      | `test_commune_service.py` |
| `test_generate_slug_empty` (R26)          | ✅      | `test_commune_service.py` |
| `test_legacy_redirect_invalid_dept` (R28) | ✅      | `test_api.py`             |

### Edge cases couverts

- Données manquantes (arrays de longueurs inégales) : ✅ `test_normals_handles_missing_data`
- Cache hit (pas de re-fetch) : ✅ `test_normals_caching` (vérifie `client.calls == 3`)
- Champs enrichis absents : ✅ `test_normalize_commune_missing_enriched` (tous `None`)
- Slug sans département : ✅ `test_resolve_slug_invalid_no_dept` (retourne `None`)

### Edge case manquant (non bloquant)

- **Pas de test pour `get_normals` avec une période dont aucun jour n'a de données de référence** (cas `return None`). Ce cas est improbable en production (la période de référence couvre 365 jours), mais un test unitaire simple le validerait.
- **Pas de test pour le 29 février** dans les normales. La moyenne sur les années bissextiles (8 sur 30) devrait être correcte, mais un test le confirmerait.

---

## 5) UX Consistency Check

| Point                              | Constat                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bloc anomalie climatique           | Tableau lisible, colonnes claires (Métrique / Observé / Normale / Anomalie). Couleurs d'anomalie cohérentes (rouge=chaud, bleu=froid, bleu=humide, orange=sec).                                                                                                                                                                                                             |
| Bloc infos commune                 | Liste verticale propre avec labels en gras. Densité calculée automatiquement. Altitude arrondie à l'entier.                                                                                                                                                                                                                                                                 |
| Masquage progressif                | Sections masquées quand non pertinentes (comparaison, échec normales). Aucun bloc vide visible. ✅                                                                                                                                                                                                                                                                          |
| **Positionnement `#commune-info`** | **Le bloc « Informations sur la commune » apparaît après le panneau « Transparence des données », c'est-à-dire en toute fin de page. C'est incohérent avec la logique de lecture : l'utilisateur voit les résultats météo, l'anomalie climatique, le graphique, le détail horaire, puis doit scroller au-delà de la transparence pour trouver les infos commune.** Voir C1. |

---

## 6) Required Corrections

### C1 — Positionnement DOM de `#commune-info` (bloquant)

**Constat** : dans `public/index.html`, la section `#commune-info` est placée en ligne 216, **après** la section `#info` (Transparence des données, L206). Elle est le dernier élément de `<main>`.

**Spec v9, section 3.8** : « Après le résumé narratif, avant la question SEO » :

```
6. Résumé narratif de la période (existant)
7. Informations sur la commune (nouveau — #commune-info)
8. Question SEO (existant)
```

**Problème** : l'ordre DOM actuel dans `index.html` place `#seo-question` (L134) **avant** `#period-summary` (L138), ce qui rend l'instruction de la spec (« après le résumé narratif, avant la question SEO ») contradictoire avec le DOM existant sans réorganisation.

**Action requise** : le développeur doit déplacer `#commune-info` de sa position actuelle (après `#info` transparence) vers une position cohérente avec l'intention de la spec. Cependant, la réorganisation complète du DOM pour correspondre à l'ordre de la section 3.8 impacterait les sections existantes (`#seo-question`, `#period-summary`).

**→ Retour Architecte** : clarifier si la section 3.8 demande une réorganisation complète du DOM (déplacer `#seo-question` et `#period-summary` après `#results`), ou simplement le placement de `#commune-info` dans une position raisonnable proche du résumé.

**Suggestion de correction minimale** : déplacer `<section id="commune-info">` juste avant `<div id="period-links">` (après les résultats, avant la navigation de période). Cela le rapproche du contexte de résultat sans réorganiser les sections existantes.

---

## 7) Recommended Improvements (non bloquantes)

### R29 — Incohérence sémantique dans le calcul des anomalies

`computeAggregates()` retourne `tempMax = Math.max(...dailyMaxes)` (max absolu de la période) et `tempMin = Math.min(...dailyMins)` (min absolu). Côté normales, `temp_max_avg` et `temp_min_avg` sont des **moyennes** de max/min journaliers. La comparaison « absolu observé vs moyenne climatique » produit des écarts exagérés (un seul jour très chaud sur 7 suffit à gonfler l'anomalie).

L'implémentation est **conforme à la spec** (section 3.6 utilise explicitement `computeAggregates`), mais la spec elle-même produit une comparaison « pommes vs oranges ». Suggestion pour v10 : calculer la moyenne des max/min journaliers observés pour une comparaison équitable.

### R30 — DeprecationWarnings Python 3.14+

Toujours présents (héritées de R22/v8) : `asyncio.iscoroutinefunction`, `asyncio.get_event_loop_policy`, `asyncio.set_event_loop_policy`. Résolvable par un upgrade de `pytest-asyncio` et `fastapi`.

### R31 — Assets non committés

`public/assets/logo-histometeo.png` et `public/assets/favicon.png` ne sont toujours pas trackés par git (hérité de R21/v8). `git add public/assets/` au prochain commit.

### R32 — Test du 29 février dans les normales

Ajouter un test unitaire vérifiant que le jour `"02-29"` est correctement moyenné sur les 8 années bissextiles de la période 1991–2020 (et non sur 30 ans).

### R33 — Test de `get_normals` retournant `None`

Ajouter un test unitaire où le cache contient des normales mais aucun jour ne correspond à la période demandée (ex: demander le 13-13 qui n'existe pas). Vérifier le retour `None`.

---

## 🧭 Décision finale

### ⚠️ Validé avec réserves

**Justification** : l'implémentation est globalement excellente — le service des normales est propre et robuste, l'enrichissement commune est complet, le frontend respecte parfaitement INV-7/INV-9/INV-10, les 57 tests passent, les forbidden changes sont respectés, et les corrections R24–R28 sont toutes intégrées.

**Réserve unique** : le positionnement DOM de `#commune-info` (C1) nécessite une clarification architecturale sur l'ordre des sections avant correction par le développeur. Ce n'est pas un bug fonctionnel (le bloc s'affiche correctement), mais un écart de placement UX par rapport à l'intention de la spec.

---

## 🔁 Routage

**C1** → **Retour Architecte** : clarifier l'ordre DOM des sections dans la spec, puis retour Développeur pour le déplacement de `#commune-info`.
