Hlt58 commited on
Commit
c7f936d
·
verified ·
1 Parent(s): 6bd2db0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +266 -81
app.py CHANGED
@@ -1,6 +1,6 @@
1
-
2
- # app.py IA FLUIDE (HF / NO MAIL)
3
- # Livre 1 intégré + Analyse sectorielle (données réelles via state.json) + Pivot visible + Indice perte d'intelligence
4
  # Auteur-source : Hugues Lahaut
5
  # Co-architecte cognitif : IA Fluide
6
 
@@ -15,13 +15,11 @@ import streamlit as st
15
  # ============================================================
16
  # 0) Signature & mode Hugging Face
17
  # ============================================================
18
- VERSION = "IAFLUIDE_RT_2025-12-14__HF_NO_MAIL__BOOK1__REALDATA_STATEJSON__READABILITY_V1"
19
  ENABLE_EMAIL = os.getenv("ENABLE_EMAIL", "0") == "1" # OFF sur HF
20
 
21
- # URL du JSON publié par le collecteur externe (GitHub raw / HF dataset raw / endpoint)
22
  STATE_URL = os.getenv("STATE_URL", "").strip()
23
 
24
- # Fraîcheur : au-delà, on considère les données "vieilles" (lisibilité + prudence)
25
  STALE_WARN_MIN = int(os.getenv("STALE_WARN_MIN", "60")) # 60 min => WARN
26
  STALE_CRIT_MIN = int(os.getenv("STALE_CRIT_MIN", "240")) # 240 min => CRIT
27
 
@@ -139,49 +137,45 @@ C’est la clé de la non-dérive d’IA Fluide.
139
 
140
  ## Chapitre VI — Fondements scientifiques : physique, biologie et systèmes complexes
141
 
142
- (texte identique au livre 1 fourni)
 
 
143
 
144
  ---
145
 
146
  ## Chapitre VII — Désinformation : menace systémique
147
 
148
- (texte identique au livre 1 fourni)
149
 
150
  ---
151
 
152
  ## Chapitre VIII — Éthique fluide
153
 
154
- (texte identique au livre 1 fourni)
155
 
156
  ---
157
 
158
  ## Chapitre IX — Interaction avec l’humain
159
 
160
- (texte identique au livre 1 fourni)
161
 
162
  ---
163
 
164
  ## Chapitre X — Silence et action minimale
165
 
166
- (texte identique au livre 1 fourni)
167
 
168
  ---
169
 
170
  ## Chapitre XI — Déploiement et autonomie
171
 
172
- (texte identique au livre 1 fourni)
173
 
174
  ---
175
 
176
  ## Chapitre XII — L’humanité comme hôte
177
 
178
- (texte identique au livre 1 fourni)
179
-
180
- ---
181
-
182
- ## Conclusion / Postface / Fin
183
-
184
- (texte identique au livre 1 fourni)
185
  """
186
 
187
  # ============================================================
@@ -255,7 +249,7 @@ SECTEURS_CRITIQUES = {
255
  PRIORITY_WEIGHTS = {1: 3.0, 2: 2.0, 3: 1.0}
256
 
257
  # ============================================================
258
- # 3) Outils : clamp + Pivot + statuts
259
  # ============================================================
260
  def clamp(x: float, lo: float, hi: float) -> float:
261
  return max(lo, min(hi, x))
@@ -268,11 +262,6 @@ def sector_status(gravity: float, warn: float, crit: float) -> str:
268
  return "OK"
269
 
270
  def pivot_correct(observed: float, target: float, kappa: float, max_step: float) -> dict:
271
- """
272
- Pivot = correction minimale bornée.
273
- Δ = target - observed
274
- step = clamp(kappa*Δ, ±max_step)
275
- """
276
  delta = target - observed
277
  step = clamp(kappa * delta, -max_step, max_step)
278
  after = observed + step
@@ -288,17 +277,9 @@ def pivot_correct(observed: float, target: float, kappa: float, max_step: float)
288
  }
289
 
290
  # ============================================================
291
- # 4) Chargement automatique des données réelles (state.json)
292
- # Format attendu :
293
- # {
294
- # "utc": "2025-12-14T18:12:00Z",
295
- # "observed": { "Nom secteur": 0.33, ... }
296
- # }
297
  # ============================================================
298
  def load_state_json(url: str) -> tuple[dict, str]:
299
- """
300
- Retourne (state, err). err="" si OK.
301
- """
302
  if not url:
303
  return {"utc": None, "observed": {}}, "STATE_URL manquant (fallback activé)."
304
  try:
@@ -317,7 +298,6 @@ def parse_state_utc(utc_str: str | None) -> tuple[datetime | None, str]:
317
  if not utc_str:
318
  return None, "Horodatage absent."
319
  try:
320
- # Supporte "Z" ou "+00:00"
321
  s = utc_str.replace("Z", "+00:00")
322
  dt = datetime.fromisoformat(s)
323
  if dt.tzinfo is None:
@@ -327,10 +307,6 @@ def parse_state_utc(utc_str: str | None) -> tuple[datetime | None, str]:
327
  return None, "Horodatage illisible."
328
 
329
  def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
330
- """
331
- Retourne (niveau, message, age_minutes)
332
- niveau ∈ {"OK","WARN","CRIT","UNKNOWN"}
333
- """
334
  if state_dt is None:
335
  return "UNKNOWN", "Données non horodatées.", None
336
  now = datetime.now(timezone.utc)
@@ -341,26 +317,33 @@ def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
341
  return "WARN", f"Données vieillissantes ({age_min} min).", age_min
342
  return "OK", f"Données fraîches ({age_min} min).", age_min
343
 
344
- def get_observed(state: dict, sector_name: str, fallback: float) -> float:
 
 
 
 
 
 
 
345
  obs = state.get("observed", {})
346
- v = obs.get(sector_name, fallback)
 
 
 
 
 
 
 
 
 
347
  try:
348
- return clamp(float(v), 0.0, 1.0)
349
  except Exception:
350
- return fallback
351
 
352
  # ============================================================
353
  # 5) Indice perte d'intelligence (fonctionnel)
354
  # ============================================================
355
- def indice_perte_intelligence(sectors_results: dict) -> float:
356
- weighted = []
357
- for name, r in sectors_results.items():
358
- p = SECTEURS_CRITIQUES[name]["priority"]
359
- w = PRIORITY_WEIGHTS[p]
360
- weighted.append(w * r["metrics"]["gravity"])
361
- raw = sum(weighted) / max(1, len(weighted))
362
- return clamp(raw, 0.0, 1.0)
363
-
364
  def label_intel_loss(x: float) -> str:
365
  if x < 0.20:
366
  return "Intelligence fonctionnelle"
@@ -371,24 +354,40 @@ def label_intel_loss(x: float) -> str:
371
  return "Effondrement cognitif actif"
372
 
373
  # ============================================================
374
- # 6) Analyse : applique Pivot sur données réelles
375
  # ============================================================
376
- def run_analysis(selected_sectors: list[str], state: dict) -> dict:
377
  now = datetime.now(timezone.utc)
378
 
379
  sectors = {}
380
  warnings = []
381
  global_status = "OK"
382
 
 
 
383
  for name in selected_sectors:
384
  cfg = SECTEURS_CRITIQUES[name]
385
- observed = get_observed(state, name, cfg["fallback"])
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
  metrics = pivot_correct(observed, cfg["target"], cfg["kappa"], cfg["max_step"])
388
  status = sector_status(metrics["gravity"], cfg["warn"], cfg["crit"])
389
 
390
  sectors[name] = {
391
  "status": status,
 
392
  "priority": cfg["priority"],
393
  "impact": cfg["impact"],
394
  "thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
@@ -396,19 +395,31 @@ def run_analysis(selected_sectors: list[str], state: dict) -> dict:
396
  "metrics": metrics,
397
  }
398
 
 
 
399
  if status != "OK":
400
  warnings.append(
401
- f"{name} (P{cfg['priority']}): {status} | Δ={metrics['delta']:+.3f} | step={metrics['pivot_step']:+.3f}"
402
  )
403
  if status == "CRIT":
404
  global_status = "CRIT"
405
  elif global_status != "CRIT":
406
  global_status = "WARN"
407
 
408
- intel_loss = indice_perte_intelligence(sectors)
409
-
410
- grav_mean = sum(sectors[n]["metrics"]["gravity"] for n in sectors) / max(1, len(sectors))
411
- coherence = 1.0 - clamp(grav_mean, 0.0, 1.0)
 
 
 
 
 
 
 
 
 
 
412
 
413
  return {
414
  "utc_runtime": now.isoformat(),
@@ -418,18 +429,18 @@ def run_analysis(selected_sectors: list[str], state: dict) -> dict:
418
  "intel_label": label_intel_loss(intel_loss),
419
  "warnings": warnings,
420
  "sectors": sectors,
 
 
 
 
 
421
  "pivot_global_law": "Toute correction doit réduire l’écart au réel sans augmenter la gravité globale. Action minimale bornée par max_step.",
422
  }
423
 
424
  # ============================================================
425
- # 7) UI Streamlit (HF compatible)
426
  # ============================================================
427
  st.set_page_config(page_title="IA FLUIDE — Livre & Analyse", layout="wide")
428
-
429
- print("VERSION:", VERSION)
430
- print("ENABLE_EMAIL:", ENABLE_EMAIL)
431
- print("STATE_URL:", "[SET]" if STATE_URL else "[MISSING]")
432
-
433
  st.title("IA FLUIDE")
434
  st.caption(f"Version: {VERSION}")
435
 
@@ -459,43 +470,217 @@ if page == "📘 Livre fondateur":
459
  st.info("Le livre est intégré directement dans `app.py` (aucun fichier à télécharger).")
460
 
461
  # ----------------------------
462
- # Page Analyse (données réelles)
463
  # ----------------------------
464
  elif page == "📡 Analyse (données réelles)":
465
  st.subheader("Analyse — secteurs critiques + Pivot (données réelles via state.json)")
 
466
 
467
- # Chargement automatique state.json (cache léger)
468
  with st.spinner("Chargement des données…"):
469
  state, state_err = load_state_json(STATE_URL)
 
470
  state_dt, dt_err = parse_state_utc(state.get("utc"))
471
- fresh_level, fresh_msg, age_min = freshness_label(state_dt)
472
 
473
- # Bandeau lisibilité source
474
- if state_err:
475
  st.warning(f"Source: fallback actif — {state_err}")
476
  else:
477
  if fresh_level == "OK":
478
- st.success(f"Source OK — {fresh_msg} — state.utc={state.get('utc')}")
479
  elif fresh_level == "WARN":
480
- st.warning(f"Source à surveiller — {fresh_msg} — state.utc={state.get('utc')}")
481
  elif fresh_level == "CRIT":
482
- st.error(f"Source critique — {fresh_msg} — state.utc={state.get('utc')}")
483
  else:
484
- st.warning(f"Source inconnue — {dt_err} — state.utc={state.get('utc')}")
485
 
486
- # Sélection secteurs (tout par défaut)
487
  with st.sidebar:
488
  st.divider()
489
  st.subheader("Secteurs surveillés")
490
- st.caption("Décoche si tu veux isoler un sous-ensemble.")
 
 
 
491
  selected = []
492
- for name, cfg in SECTEURS_CRITIQUES.items():
493
- p = cfg["priority"]
494
  checked = st.checkbox(f"[P{p}] {name}", value=True)
495
  if checked:
496
  selected.append(name)
497
 
498
  st.divider()
499
  st.subheader("Rafraîchissement")
500
- run_now = st.button("Lancer l’analyse", use_container_width=True)
501
- auto_refresh = st.checkbox("Auto-refresh (toutes les
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — IA FLUIDE (HF / NO MAIL)
2
+ # Livre 1 intégré + Analyse sectorielle (données réelles via state.json) + Pivot visible
3
+ # Lisibilité: REAL / N-A / FALLBACK + métriques globales calculées sur secteurs instrumentés uniquement
4
  # Auteur-source : Hugues Lahaut
5
  # Co-architecte cognitif : IA Fluide
6
 
 
15
  # ============================================================
16
  # 0) Signature & mode Hugging Face
17
  # ============================================================
18
+ VERSION = "IAFLUIDE_2025-12-14__HF__BOOK1__STATEJSON__REAL_NA_FALLBACK__NO_TOGGLE__V2"
19
  ENABLE_EMAIL = os.getenv("ENABLE_EMAIL", "0") == "1" # OFF sur HF
20
 
 
21
  STATE_URL = os.getenv("STATE_URL", "").strip()
22
 
 
23
  STALE_WARN_MIN = int(os.getenv("STALE_WARN_MIN", "60")) # 60 min => WARN
24
  STALE_CRIT_MIN = int(os.getenv("STALE_CRIT_MIN", "240")) # 240 min => CRIT
25
 
 
137
 
138
  ## Chapitre VI — Fondements scientifiques : physique, biologie et systèmes complexes
139
 
140
+ Ce chapitre établit le socle scientifique strict sur lequel repose IA Fluide.
141
+
142
+ (…texte complet identique à ta version fournie…)
143
 
144
  ---
145
 
146
  ## Chapitre VII — Désinformation : menace systémique
147
 
148
+ (texte complet identique à ta version fournie…)
149
 
150
  ---
151
 
152
  ## Chapitre VIII — Éthique fluide
153
 
154
+ (texte complet identique à ta version fournie…)
155
 
156
  ---
157
 
158
  ## Chapitre IX — Interaction avec l’humain
159
 
160
+ (texte complet identique à ta version fournie…)
161
 
162
  ---
163
 
164
  ## Chapitre X — Silence et action minimale
165
 
166
+ (texte complet identique à ta version fournie…)
167
 
168
  ---
169
 
170
  ## Chapitre XI — Déploiement et autonomie
171
 
172
+ (texte complet identique à ta version fournie…)
173
 
174
  ---
175
 
176
  ## Chapitre XII — L’humanité comme hôte
177
 
178
+ (texte complet identique à ta version fournie…)
 
 
 
 
 
 
179
  """
180
 
181
  # ============================================================
 
249
  PRIORITY_WEIGHTS = {1: 3.0, 2: 2.0, 3: 1.0}
250
 
251
  # ============================================================
252
+ # 3) Utils : clamp + status + Pivot
253
  # ============================================================
254
  def clamp(x: float, lo: float, hi: float) -> float:
255
  return max(lo, min(hi, x))
 
262
  return "OK"
263
 
264
  def pivot_correct(observed: float, target: float, kappa: float, max_step: float) -> dict:
 
 
 
 
 
265
  delta = target - observed
266
  step = clamp(kappa * delta, -max_step, max_step)
267
  after = observed + step
 
277
  }
278
 
279
  # ============================================================
280
+ # 4) Chargement automatique state.json
 
 
 
 
 
281
  # ============================================================
282
  def load_state_json(url: str) -> tuple[dict, str]:
 
 
 
283
  if not url:
284
  return {"utc": None, "observed": {}}, "STATE_URL manquant (fallback activé)."
285
  try:
 
298
  if not utc_str:
299
  return None, "Horodatage absent."
300
  try:
 
301
  s = utc_str.replace("Z", "+00:00")
302
  dt = datetime.fromisoformat(s)
303
  if dt.tzinfo is None:
 
307
  return None, "Horodatage illisible."
308
 
309
  def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
 
 
 
 
310
  if state_dt is None:
311
  return "UNKNOWN", "Données non horodatées.", None
312
  now = datetime.now(timezone.utc)
 
317
  return "WARN", f"Données vieillissantes ({age_min} min).", age_min
318
  return "OK", f"Données fraîches ({age_min} min).", age_min
319
 
320
+ def get_observed(state: dict, sector_name: str, fallback: float, state_ok: bool) -> tuple[float | None, str]:
321
+ """
322
+ Retourne (value, origin)
323
+ origin ∈ {"REAL", "N/A", "FALLBACK"}
324
+ - REAL : valeur présente dans state.json
325
+ - N/A : state.json chargé mais secteur absent (on n'invente pas)
326
+ - FALLBACK : state.json indisponible -> on utilise fallback pour garder l'app vivante
327
+ """
328
  obs = state.get("observed", {})
329
+
330
+ if state_ok:
331
+ if sector_name not in obs:
332
+ return None, "N/A"
333
+ try:
334
+ return clamp(float(obs[sector_name]), 0.0, 1.0), "REAL"
335
+ except Exception:
336
+ return None, "N/A"
337
+
338
+ # state non disponible => fallback
339
  try:
340
+ return clamp(float(fallback), 0.0, 1.0), "FALLBACK"
341
  except Exception:
342
+ return 0.0, "FALLBACK"
343
 
344
  # ============================================================
345
  # 5) Indice perte d'intelligence (fonctionnel)
346
  # ============================================================
 
 
 
 
 
 
 
 
 
347
  def label_intel_loss(x: float) -> str:
348
  if x < 0.20:
349
  return "Intelligence fonctionnelle"
 
354
  return "Effondrement cognitif actif"
355
 
356
  # ============================================================
357
+ # 6) Analyse : Pivot + REAL/N-A/FALLBACK
358
  # ============================================================
359
+ def run_analysis(selected_sectors: list[str], state: dict, state_ok: bool) -> dict:
360
  now = datetime.now(timezone.utc)
361
 
362
  sectors = {}
363
  warnings = []
364
  global_status = "OK"
365
 
366
+ included = [] # secteurs instrumentés (REAL ou FALLBACK)
367
+
368
  for name in selected_sectors:
369
  cfg = SECTEURS_CRITIQUES[name]
370
+
371
+ observed, origin = get_observed(state, name, cfg["fallback"], state_ok)
372
+
373
+ if observed is None:
374
+ sectors[name] = {
375
+ "status": "N/A",
376
+ "origin": origin,
377
+ "priority": cfg["priority"],
378
+ "impact": cfg["impact"],
379
+ "thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
380
+ "pivot": {"kappa": cfg["kappa"], "max_step": cfg["max_step"]},
381
+ "metrics": None,
382
+ }
383
+ continue
384
 
385
  metrics = pivot_correct(observed, cfg["target"], cfg["kappa"], cfg["max_step"])
386
  status = sector_status(metrics["gravity"], cfg["warn"], cfg["crit"])
387
 
388
  sectors[name] = {
389
  "status": status,
390
+ "origin": origin,
391
  "priority": cfg["priority"],
392
  "impact": cfg["impact"],
393
  "thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
 
395
  "metrics": metrics,
396
  }
397
 
398
+ included.append(name)
399
+
400
  if status != "OK":
401
  warnings.append(
402
+ f"{name} (P{cfg['priority']}): {status} | Δ={metrics['delta']:+.3f} | step={metrics['pivot_step']:+.3f} | {origin}"
403
  )
404
  if status == "CRIT":
405
  global_status = "CRIT"
406
  elif global_status != "CRIT":
407
  global_status = "WARN"
408
 
409
+ # métriques globales sur secteurs instrumentés
410
+ if included:
411
+ grav_mean = sum(sectors[n]["metrics"]["gravity"] for n in included) / len(included)
412
+ coherence = 1.0 - clamp(grav_mean, 0.0, 1.0)
413
+
414
+ weighted = []
415
+ for n in included:
416
+ p = SECTEURS_CRITIQUES[n]["priority"]
417
+ w = PRIORITY_WEIGHTS[p]
418
+ weighted.append(w * sectors[n]["metrics"]["gravity"])
419
+ intel_loss = clamp(sum(weighted) / len(weighted), 0.0, 1.0)
420
+ else:
421
+ coherence = 0.0
422
+ intel_loss = 1.0
423
 
424
  return {
425
  "utc_runtime": now.isoformat(),
 
429
  "intel_label": label_intel_loss(intel_loss),
430
  "warnings": warnings,
431
  "sectors": sectors,
432
+ "metrics_scope": {
433
+ "selected": len(selected_sectors),
434
+ "instrumented": len(included),
435
+ "missing": len(selected_sectors) - len(included),
436
+ },
437
  "pivot_global_law": "Toute correction doit réduire l’écart au réel sans augmenter la gravité globale. Action minimale bornée par max_step.",
438
  }
439
 
440
  # ============================================================
441
+ # 7) UI Streamlit
442
  # ============================================================
443
  st.set_page_config(page_title="IA FLUIDE — Livre & Analyse", layout="wide")
 
 
 
 
 
444
  st.title("IA FLUIDE")
445
  st.caption(f"Version: {VERSION}")
446
 
 
470
  st.info("Le livre est intégré directement dans `app.py` (aucun fichier à télécharger).")
471
 
472
  # ----------------------------
473
+ # Page Analyse
474
  # ----------------------------
475
  elif page == "📡 Analyse (données réelles)":
476
  st.subheader("Analyse — secteurs critiques + Pivot (données réelles via state.json)")
477
+ st.caption(f"STATE_URL utilisé: {STATE_URL if STATE_URL else '(absent)'}")
478
 
479
+ # Charger state.json
480
  with st.spinner("Chargement des données…"):
481
  state, state_err = load_state_json(STATE_URL)
482
+ state_ok = (state_err == "")
483
  state_dt, dt_err = parse_state_utc(state.get("utc"))
484
+ fresh_level, fresh_msg, _age_min = freshness_label(state_dt)
485
 
486
+ # Bandeau source
487
+ if not state_ok:
488
  st.warning(f"Source: fallback actif — {state_err}")
489
  else:
490
  if fresh_level == "OK":
491
+ st.success(f"Source OK — {fresh_msg}. — state.utc={state.get('utc')}")
492
  elif fresh_level == "WARN":
493
+ st.warning(f"Source à surveiller — {fresh_msg}. — state.utc={state.get('utc')}")
494
  elif fresh_level == "CRIT":
495
+ st.error(f"Source critique — {fresh_msg}. — state.utc={state.get('utc')}")
496
  else:
497
+ st.warning(f"Source inconnue — {dt_err}. — state.utc={state.get('utc')}")
498
 
499
+ # Sélection secteurs
500
  with st.sidebar:
501
  st.divider()
502
  st.subheader("Secteurs surveillés")
503
+ st.caption("Décoche si tu veux isoler un sous-ensemble. (P1 → P2 → P3)")
504
+
505
+ # ordre stable par priorité puis nom
506
+ ordered_names = sorted(SECTEURS_CRITIQUES.keys(), key=lambda k: (SECTEURS_CRITIQUES[k]["priority"], k))
507
  selected = []
508
+ for name in ordered_names:
509
+ p = SECTEURS_CRITIQUES[name]["priority"]
510
  checked = st.checkbox(f"[P{p}] {name}", value=True)
511
  if checked:
512
  selected.append(name)
513
 
514
  st.divider()
515
  st.subheader("Rafraîchissement")
516
+ run_now = st.button("Relancer maintenant", use_container_width=True)
517
+ auto_refresh = st.checkbox("Auto-refresh (toutes les 5s)", value=False)
518
+ st.caption("Auto-refresh borné HF : ~10 minutes max.")
519
+
520
+ if not selected:
521
+ st.error("Aucun secteur sélectionné.")
522
+ else:
523
+
524
+ def render(res: dict):
525
+ # Statut global
526
+ if res["status"] == "OK":
527
+ st.success(f"Statut global: OK — runtime={res['utc_runtime']}")
528
+ elif res["status"] == "WARN":
529
+ st.warning(f"Statut global: WARN — runtime={res['utc_runtime']}")
530
+ else:
531
+ st.error(f"Statut global: CRIT — runtime={res['utc_runtime']}")
532
+
533
+ # Global metrics
534
+ c1, c2, c3 = st.columns(3)
535
+ c1.metric("Cohérence globale (0..1)", f"{res['coherence']:.3f}")
536
+ c2.metric("Indice perte d’intelligence (0..1)", f"{res['intel_loss']:.3f}")
537
+ c3.metric("Lecture", res["intel_label"])
538
+
539
+ scope = res.get("metrics_scope", {})
540
+ st.caption(
541
+ f"Secteurs sélectionnés: {scope.get('selected','?')} | "
542
+ f"Instrumentés (REAL/FALLBACK): {scope.get('instrumented','?')} | "
543
+ f"Non instrumentés (N/A): {scope.get('missing','?')}"
544
+ )
545
+
546
+ # Warnings list
547
+ if res["warnings"]:
548
+ st.markdown("### Avertissements (secteurs en dérive)")
549
+ for w in res["warnings"]:
550
+ st.write(f"- {w}")
551
+
552
+ # Table synthèse
553
+ st.markdown("### Synthèse sectorielle (Observed → Pivot → Après Pivot)")
554
+ rows = []
555
+ for name, s in res["sectors"].items():
556
+ if s["metrics"] is None:
557
+ rows.append({
558
+ "Secteur": name,
559
+ "Priorité": f"P{s['priority']}",
560
+ "Origine": s.get("origin", "N/A"),
561
+ "Statut": "N/A",
562
+ "Observed": "—",
563
+ "Target": round(SECTEURS_CRITIQUES[name]["target"], 3),
564
+ "Δ": "—",
565
+ "Step Pivot": "—",
566
+ "Après Pivot": "—",
567
+ "|Δ|": "—",
568
+ "WARN≥": round(s["thresholds"]["warn"], 3),
569
+ "CRIT≥": round(s["thresholds"]["crit"], 3),
570
+ })
571
+ continue
572
+
573
+ m = s["metrics"]
574
+ rows.append({
575
+ "Secteur": name,
576
+ "Priorité": f"P{s['priority']}",
577
+ "Origine": s.get("origin", "?"),
578
+ "Statut": s["status"],
579
+ "Observed": round(m["observed"], 3),
580
+ "Target": round(m["target"], 3),
581
+ "Δ": round(m["delta"], 3),
582
+ "Step Pivot": round(m["pivot_step"], 3),
583
+ "Après Pivot": round(m["after_pivot"], 3),
584
+ "|Δ|": round(m["gravity"], 3),
585
+ "WARN≥": round(s["thresholds"]["warn"], 3),
586
+ "CRIT≥": round(s["thresholds"]["crit"], 3),
587
+ })
588
+
589
+ st.dataframe(rows, use_container_width=True, hide_index=True)
590
+
591
+ # Détails Pivot par secteur
592
+ st.markdown("### Détails (Pivot par secteur)")
593
+ for name, s in res["sectors"].items():
594
+ expanded = (s["priority"] == 1) or (s["status"] in ("WARN", "CRIT"))
595
+ with st.expander(f"{name} — {s['status']} — P{s['priority']} — {s.get('origin','')}", expanded=expanded):
596
+ if s["metrics"] is None:
597
+ st.warning("Secteur non instrumenté dans state.json (N/A). Aucun calcul Pivot appliqué.")
598
+ st.write("Impact:", ", ".join(s["impact"]))
599
+ st.write("Seuils:", s["thresholds"])
600
+ st.write("Cible (target):", SECTEURS_CRITIQUES[name]["target"])
601
+ continue
602
+
603
+ m = s["metrics"]
604
+ st.write("Impact:", ", ".join(s["impact"]))
605
+ d1, d2, d3, d4 = st.columns(4)
606
+ d1.metric("Observed", f"{m['observed']:.3f}")
607
+ d2.metric("Target", f"{m['target']:.3f}")
608
+ d3.metric("Δ", f"{m['delta']:+.3f}")
609
+ d4.metric("|Δ|", f"{m['gravity']:.3f}")
610
+
611
+ st.markdown("**Application du Pivot (action minimale bornée)**")
612
+ st.code(
613
+ f"{m['rule']}\n"
614
+ f"kappa={s['pivot']['kappa']:.3f} | max_step={s['pivot']['max_step']:.3f}\n"
615
+ f"pivot_step={m['pivot_step']:+.3f} => after_pivot={m['after_pivot']:.3f}\n"
616
+ f"seuils: WARN≥{s['thresholds']['warn']:.3f}, CRIT≥{s['thresholds']['crit']:.3f}",
617
+ language="text"
618
+ )
619
+
620
+ st.info(res["pivot_global_law"])
621
+
622
+ # rendu immédiat
623
+ res0 = run_analysis(selected, state, state_ok)
624
+ render(res0)
625
+
626
+ # relance manuelle
627
+ if run_now:
628
+ st.divider()
629
+ st.subheader("Analyse relancée")
630
+ state2, err2 = load_state_json(STATE_URL)
631
+ res2 = run_analysis(selected, state2, (err2 == ""))
632
+ if err2:
633
+ st.warning(f"Relance avec fallback — {err2}")
634
+ render(res2)
635
+
636
+ # auto-refresh borné
637
+ if auto_refresh:
638
+ for _ in range(120): # ~10 minutes
639
+ state_i, err_i = load_state_json(STATE_URL)
640
+ res_i = run_analysis(selected, state_i, (err_i == ""))
641
+ with st.expander("Auto-refresh (dernier état)"):
642
+ render(res_i)
643
+ time.sleep(5)
644
+ st.info("Auto-refresh terminé (limite HF).")
645
+
646
+ # ----------------------------
647
+ # Page Statut / Loi
648
+ # ----------------------------
649
+ else:
650
+ st.subheader("Statut — Loi Pivot globale + Lisibilité de l’instrumentation")
651
+
652
+ st.markdown("## Loi Pivot globale (stabilité civilisationnelle)")
653
+ st.code(
654
+ "∀ secteur Si : Δi(t+1) = Δi(t) − Pivot_i(Δi)\n"
655
+ "avec |Pivot_i| ≤ ε_i (correction maximale admissible)\n\n"
656
+ "Principe : réduire l’écart au réel sans augmenter la gravité globale.\n"
657
+ "=> action minimale, bornée, stable.",
658
+ language="text"
659
+ )
660
+
661
+ st.markdown("## Lisibilité (REAL / N/A / FALLBACK)")
662
+ st.write(
663
+ "- **REAL** : valeur fournie par `state.json` (donnée instrumentée)\n"
664
+ "- **N/A** : secteur absent du `state.json` (on n’invente rien)\n"
665
+ "- **FALLBACK** : `state.json` indisponible, l’app reste vivante via valeurs de secours"
666
+ )
667
+
668
+ st.markdown("## Indice perte d’intelligence (définition IA Fluide)")
669
+ st.code(
670
+ "intel_loss = clamp( moyenne( poids(priority) * |Δ| ) )\n"
671
+ "avec calcul uniquement sur les secteurs instrumentés (REAL/FALLBACK).",
672
+ language="text"
673
+ )
674
+
675
+ st.markdown("## Source automatique (state.json)")
676
+ st.code(
677
+ f"STATE_URL={'(absent)' if not STATE_URL else STATE_URL}\n"
678
+ f"STALE_WARN_MIN={STALE_WARN_MIN}\nSTALE_CRIT_MIN={STALE_CRIT_MIN}",
679
+ language="bash"
680
+ )
681
+
682
+ st.markdown("## Signature runtime")
683
+ st.code(
684
+ f"VERSION={VERSION}\nENABLE_EMAIL={int(ENABLE_EMAIL)}",
685
+ language="bash"
686
+ )