newskit 0.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. newskit/__init__.py +4 -0
  2. newskit/__main__.py +3 -0
  3. newskit/acuite.py +394 -0
  4. newskit/aiguilleur_cortexdb.py +536 -0
  5. newskit/angle_critique.py +343 -0
  6. newskit/archiver_skill.py +88 -0
  7. newskit/banc.py +1855 -0
  8. newskit/banc_croise.py +799 -0
  9. newskit/banc_decision.py +598 -0
  10. newskit/banc_injection.py +290 -0
  11. newskit/banc_session.py +226 -0
  12. newskit/banc_themes.py +572 -0
  13. newskit/cartes.py +701 -0
  14. newskit/cerveau_cortexdb.py +668 -0
  15. newskit/choisir_jalons.py +249 -0
  16. newskit/classement.py +136 -0
  17. newskit/cli.py +146 -0
  18. newskit/collecte_arxiv.py +708 -0
  19. newskit/collecte_continue.py +177 -0
  20. newskit/collecte_courriel.py +516 -0
  21. newskit/collecte_depots.py +246 -0
  22. newskit/collecte_dossier_local.py +243 -0
  23. newskit/collecte_linkedin.py +184 -0
  24. newskit/collecte_medium.py +546 -0
  25. newskit/collecte_openalex.py +227 -0
  26. newskit/collecte_pdf_local.py +426 -0
  27. newskit/collecte_revue.py +219 -0
  28. newskit/collecte_rss.py +1305 -0
  29. newskit/collecte_substack.py +216 -0
  30. newskit/collecte_textes.py +405 -0
  31. newskit/combler_canon.py +684 -0
  32. newskit/commun.py +290 -0
  33. newskit/config_sources.py +1685 -0
  34. newskit/contexte.py +341 -0
  35. newskit/controle_production.py +123 -0
  36. newskit/convertir_page_text.py +56 -0
  37. newskit/convertir_substack_page_text.py +74 -0
  38. newskit/convertir_transcription.py +47 -0
  39. newskit/couts.py +449 -0
  40. newskit/decision.py +275 -0
  41. newskit/demenager_gouvernance.py +495 -0
  42. newskit/deplacer_sources.py +122 -0
  43. newskit/deposer_session.py +238 -0
  44. newskit/depots.py +556 -0
  45. newskit/designations.py +170 -0
  46. newskit/diachronie.py +437 -0
  47. newskit/diffusion.py +67 -0
  48. newskit/digerer_graphe.py +311 -0
  49. newskit/donnees_temoin/__init__.py +1 -0
  50. newskit/donnees_temoin/dossier/apiculteur-et-pretre.md +3 -0
  51. newskit/donnees_temoin/dossier/cierge-pascal.md +3 -0
  52. newskit/donnees_temoin/dossier/cire-a-cacheter.md +3 -0
  53. newskit/donnees_temoin/dossier/cire-blanche.md +3 -0
  54. newskit/donnees_temoin/dossier/cire-des-moines.md +3 -0
  55. newskit/donnees_temoin/dossier/cire-et-chandelle.md +3 -0
  56. newskit/donnees_temoin/dossier/cire-et-eau-de-pluie.md +3 -0
  57. newskit/donnees_temoin/dossier/cire-et-miel.md +3 -0
  58. newskit/donnees_temoin/dossier/cire.md +3 -0
  59. newskit/donnees_temoin/dossier/compter-les-essaims.md +3 -0
  60. newskit/donnees_temoin/dossier/cueillir-un-essaim.md +3 -0
  61. newskit/donnees_temoin/dossier/distance-du-chemin.md +3 -0
  62. newskit/donnees_temoin/dossier/droit-des-ruchers.md +3 -0
  63. newskit/donnees_temoin/dossier/empecher-l-essaimage.md +3 -0
  64. newskit/donnees_temoin/dossier/essaim-de-la-saint-jean.md +3 -0
  65. newskit/donnees_temoin/dossier/essaim-et-frontiere.md +3 -0
  66. newskit/donnees_temoin/dossier/essaim-perdu.md +3 -0
  67. newskit/donnees_temoin/dossier/essaim-secondaire.md +3 -0
  68. newskit/donnees_temoin/dossier/essaim-sur-la-maison.md +3 -0
  69. newskit/donnees_temoin/dossier/essaimage-et-lune.md +3 -0
  70. newskit/donnees_temoin/dossier/essaimage-et-vent.md +3 -0
  71. newskit/donnees_temoin/dossier/essaimage.md +7 -0
  72. newskit/donnees_temoin/dossier/faux-cirier.md +3 -0
  73. newskit/donnees_temoin/dossier/heritage-des-ruches.md +3 -0
  74. newskit/donnees_temoin/dossier/impot-de-cire.md +3 -0
  75. newskit/donnees_temoin/dossier/jur/303/251-des-ruchers.md +3 -0
  76. newskit/donnees_temoin/dossier/marque-des-ruches.md +3 -0
  77. newskit/donnees_temoin/dossier/miel-de-dime.md +3 -0
  78. newskit/donnees_temoin/dossier/moules-a-chandelles.md +3 -0
  79. newskit/donnees_temoin/dossier/piqure-de-cheval.md +3 -0
  80. newskit/donnees_temoin/dossier/reine-de-paille.md +3 -0
  81. newskit/donnees_temoin/dossier/ruche-de-tronc.md +3 -0
  82. newskit/donnees_temoin/dossier/rucher-abandonne.md +3 -0
  83. newskit/donnees_temoin/dossier/ruches-et-moulin.md +3 -0
  84. newskit/donnees_temoin/dossier/vol-de-ruche.md +3 -0
  85. newskit/donnees_temoin/media_de_test.py +68 -0
  86. newskit/donnees_temoin/reponses/.gitkeep +0 -0
  87. newskit/donnees_temoin/reponses/060c0e01eb43eab297d6f52a85bc8d0e63a3489c87fa5b5bf84575a391c62962.json +1 -0
  88. newskit/donnees_temoin/reponses/0ed64d33eda777fe146b9e4ddcacd74c00ecdd03e082913a4091704df2b0ff57.json +1 -0
  89. newskit/donnees_temoin/reponses/1df4c2c5417c97016c19230588cf98d54dcc15deff0914fe4347651bc19d29ba.json +1 -0
  90. newskit/donnees_temoin/reponses/2681359cdb67e96404d50ae225f50392d39c5096bb56a9fb3ab8d1a323689fbd.json +1 -0
  91. newskit/donnees_temoin/reponses/26f6f46523e212e7ed1f9124fe7514e2b563ff78bbc924504673d4139a98b10d.json +1 -0
  92. newskit/donnees_temoin/reponses/31cade9fe3f2e72eaeb01fe29ee0fcd1533d536635ddaa97a7a9df2051a619ea.json +1 -0
  93. newskit/donnees_temoin/reponses/3c621999baa9aed4ffbfc3da7c10b3c32228ea64c8aa95a64d8f2cc9e6bcadab.json +1 -0
  94. newskit/donnees_temoin/reponses/47f5e61839201d78ac078a8993105a2d430446913b02ed76c978b51cf5afb240.json +1 -0
  95. newskit/donnees_temoin/reponses/67d94da8f05ff1392466c665982bfc86e220c5e6b92e4dfe01f935212b885911.json +1 -0
  96. newskit/donnees_temoin/reponses/6eebe892433ffaf75d43b4737fa3f6622e4eaba309382d6b00851ded14af99d4.json +1 -0
  97. newskit/donnees_temoin/reponses/724ec6b0efce74287726d511c39a2ca1f5d7ca006c28cdca0fd763d38502489c.json +1 -0
  98. newskit/donnees_temoin/reponses/742cea43c0090631d605a8d6035c3387c26aa71ffa38937ebe2c9caf389fa616.json +1 -0
  99. newskit/donnees_temoin/reponses/7b1d6f85638f5d76759bc69df17c3f70177856d1024368da940c93718c1c98c1.json +1 -0
  100. newskit/donnees_temoin/reponses/7c7653e704a2dbc8079078054128f2728ae4414eaf230dd4049d8ede48f54769.json +1 -0
  101. newskit/donnees_temoin/reponses/812933750d0f35202c502cab3dec8b3e93c66a26b43b39b70f0940ef00fc7727.json +1 -0
  102. newskit/donnees_temoin/reponses/83e9bbcd1b44ec16534ba4ecc36eedf252bab72e327e9fb73e689ccfa9cf052e.json +1 -0
  103. newskit/donnees_temoin/reponses/867ca7534835bc45c40fafae8a00c84cc90e4a7caabe6f49b877072b91db38b8.json +1 -0
  104. newskit/donnees_temoin/reponses/88c31641c99fcf7bae1c94fe02319eb06a3d92b8585ccf758952999389062be4.json +1 -0
  105. newskit/donnees_temoin/reponses/88dfba086495fe5f771091af9f77207bd6136e678165eb26839f5d8f04705f07.json +1 -0
  106. newskit/donnees_temoin/reponses/99f7aed5726d865f6f14cedf3e8bfd88b9e7bca4155dc1e16ba612aca4e36390.json +1 -0
  107. newskit/donnees_temoin/reponses/9f96acd3accfdc74bfa2940f7720fb9dc39b07f35db7d8478fdf7afae2e16749.json +1 -0
  108. newskit/donnees_temoin/reponses/a36a9073dbef0c6c1fca7aeee39b2fa9a1107d1e157be2fc7c4ea310e1fc2716.json +1 -0
  109. newskit/donnees_temoin/reponses/a78a46b99cb238b51c83c434e39b2bdc70c2d7fc33c52ca0fbc713a710327ec2.json +1 -0
  110. newskit/donnees_temoin/reponses/afbb982cf0c6c93b8e8cebc7fae5972f19821ffcfbb2321b3c085d94cf515a0e.json +1 -0
  111. newskit/donnees_temoin/reponses/bfaabb335129975948a93ac89fb97bf488cb2625ec93cbb455c6661288bce92e.json +1 -0
  112. newskit/donnees_temoin/reponses/c2c6bbb722258fc78ac580a3ba48ecd67f5b947e30f404852d6cecc8865ad556.json +1 -0
  113. newskit/donnees_temoin/reponses/c2eb7ddf698ab7b0ff4f5ff613f5505dd0ce51d678ad624f310a08e33a509945.json +1 -0
  114. newskit/donnees_temoin/reponses/c53a1a04bb7ecde0230389e031f797771631c5786301efc44bc80c17ff3f9aec.json +1 -0
  115. newskit/donnees_temoin/reponses/c6a3b4308cd5582b9e90f894a811b5bd366438d0619f99292993d759d420c1ee.json +1 -0
  116. newskit/donnees_temoin/reponses/c838178769ebf75e061f3026274f9aa688831e9c0278716a7f2cda2eab1e5b84.json +1 -0
  117. newskit/donnees_temoin/reponses/cad1f58dbfbe94bfa5f19df3ce2a49573852d99e6f3be23afb77b4b1b38948c5.json +1 -0
  118. newskit/donnees_temoin/reponses/d18274b00e9448112dae3beef82ee718eba39c65f3ae23334b50973e736ab41b.json +1 -0
  119. newskit/donnees_temoin/reponses/dbea2d4177941ce2caee1ae70a09c9a256f7ff16373d294a165b1d2527aafdc8.json +1 -0
  120. newskit/donnees_temoin/reponses/e0b6e3c2dcb95d24ef3f64233109e51bca7a972f088a2754f629d5b46fc55213.json +1 -0
  121. newskit/donnees_temoin/reponses/e1229a6cc2304bcf2e22247360b69ecabe50e99179b3c6defbd34d2597a00637.json +1 -0
  122. newskit/donnees_temoin/reponses/e2de5455db3aba1f9b4ae39ca99c390c19d2f42baab3a71e133bafd10000dc7f.json +1 -0
  123. newskit/donnees_temoin/reponses/e8023dd1da8106b8bc62a0da71229018c0d12074af195c55bc9a3c1da986b907.json +1 -0
  124. newskit/donnees_temoin/reponses/edbc6f1aa5c620e09cab0447d3375d8228693616b897070a59707e68fda583a5.json +1 -0
  125. newskit/donnees_temoin/tests/__init__.py +0 -0
  126. newskit/donnees_temoin/tests/conftest.py +158 -0
  127. newskit/donnees_temoin/tests/test_temoin_alias.py +112 -0
  128. newskit/donnees_temoin/tests/test_temoin_debris.py +86 -0
  129. newskit/donnees_temoin/tests/test_temoin_enregistrement.py +135 -0
  130. newskit/donnees_temoin/tests/test_temoin_etancheite.py +221 -0
  131. newskit/donnees_temoin/tests/test_temoin_limite.py +61 -0
  132. newskit/donnees_temoin/tests/test_temoin_nuit.py +135 -0
  133. newskit/donnees_temoin/tests/test_temoin_plafonds.py +58 -0
  134. newskit/donnees_temoin/tests/test_temoin_refus.py +81 -0
  135. newskit/donnees_temoin/tests/test_temoin_rejeu.py +86 -0
  136. newskit/donnees_temoin/tests/test_temoin_reprise.py +74 -0
  137. newskit/donnees_temoin/tests/test_temoin_routine.py +82 -0
  138. newskit/donnees_temoin/tests/test_temoin_sans_specifiques.py +77 -0
  139. newskit/doublons_d_identite.py +221 -0
  140. newskit/eclaireur.py +640 -0
  141. newskit/embeddings_locaux.py +67 -0
  142. newskit/encodeur.py +609 -0
  143. newskit/evenements.py +108 -0
  144. newskit/exemples_redaction.py +172 -0
  145. newskit/exporter_onnx.py +270 -0
  146. newskit/extraire_concepts.py +608 -0
  147. newskit/extraire_squelette.py +726 -0
  148. newskit/extraire_structure.py +100 -0
  149. newskit/extrait.py +32 -0
  150. newskit/fiche.py +713 -0
  151. newskit/fil.py +1649 -0
  152. newskit/fil_anglais.py +369 -0
  153. newskit/fil_fiche.py +196 -0
  154. newskit/fondre_doublons.py +552 -0
  155. newskit/fraicheur_github.py +316 -0
  156. newskit/francais.py +264 -0
  157. newskit/fusionner_graphe.py +530 -0
  158. newskit/garde_pousse.py +79 -0
  159. newskit/interroger.py +138 -0
  160. newskit/interroger_graphe.py +609 -0
  161. newskit/lacunes.py +328 -0
  162. newskit/launchd_graphe.plist.modele +67 -0
  163. newskit/lecture_cortexdb.py +179 -0
  164. newskit/lecture_web.py +891 -0
  165. newskit/lettre.py +3682 -0
  166. newskit/lettre_anglaise.py +109 -0
  167. newskit/lettre_semaine.py +237 -0
  168. newskit/media.py +306 -0
  169. newskit/mesure_pilote_cortexdb.py +304 -0
  170. newskit/mesure_serveur.py +66 -0
  171. newskit/mesurer_avance.py +417 -0
  172. newskit/mesurer_rejets.py +264 -0
  173. newskit/migrer_frontmatter.py +80 -0
  174. newskit/naissance.py +256 -0
  175. newskit/notifier_courriel.py +706 -0
  176. newskit/nouveau_graphe.py +323 -0
  177. newskit/nouvelles.py +232 -0
  178. newskit/ouvrir_session.py +453 -0
  179. newskit/partage_graphe.py +478 -0
  180. newskit/partage_sources.py +303 -0
  181. newskit/passe_partout.py +125 -0
  182. newskit/peu_couverts.py +332 -0
  183. newskit/politiques.py +78 -0
  184. newskit/poseur.py +279 -0
  185. newskit/prospecter_source.py +410 -0
  186. newskit/proxies_webshare.py +136 -0
  187. newskit/publier_cerveau.py +172 -0
  188. newskit/publier_fil.py +181 -0
  189. newskit/publier_graphes.py +171 -0
  190. newskit/publier_index.py +195 -0
  191. newskit/puits_cortexdb.py +1023 -0
  192. newskit/purger_fiches.py +128 -0
  193. newskit/recepteur_local.py +53 -0
  194. newskit/recuperer_tronquee.py +119 -0
  195. newskit/refus.py +104 -0
  196. newskit/reporter.py +3102 -0
  197. newskit/retirer_domaines.py +70 -0
  198. newskit/retouche_lettre.py +312 -0
  199. newskit/routine_assistee.py +261 -0
  200. newskit/routine_hebdo.py +2074 -0
  201. newskit/sante_sources.py +175 -0
  202. newskit/selectionneur.py +203 -0
  203. newskit/serveur_cortexdb.py +209 -0
  204. newskit/synthese.py +1877 -0
  205. newskit/temoin.py +617 -0
  206. newskit/themes_emergents.py +812 -0
  207. newskit/valider_fiches.py +75 -0
  208. newskit/vocabulaire_web2.txt.gz +0 -0
  209. newskit-0.1.7.dist-info/METADATA +210 -0
  210. newskit-0.1.7.dist-info/RECORD +213 -0
  211. newskit-0.1.7.dist-info/WHEEL +4 -0
  212. newskit-0.1.7.dist-info/entry_points.txt +2 -0
  213. newskit-0.1.7.dist-info/licenses/LICENSE +21 -0
newskit/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Newskit : un moteur de veille sans thème qui fait naître un graphe de connaissances sur un sujet et le tient à jour
2
+ chaque nuit. Un média est un dossier qui porte un `config.yaml` (voir newskit.media)."""
3
+
4
+ __version__ = "0.1.7"
newskit/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from newskit.cli import main
2
+
3
+ raise SystemExit(main())
newskit/acuite.py ADDED
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python3
2
+ """L'acuité d'un graphe : ce qui rend lisible son écart avec un modèle de langage.
3
+
4
+ Le but des graphes est de dépasser un modèle en pertinence et en complétude (porteur, 04.10.2026).
5
+ Ce script réunit, par graphe, les nombres qui le disent, en une ligne et un fichier :
6
+
7
+ banc bonnes réponses sourcées sur un banc de questions, dans trois conditions — avec le
8
+ graphe, le modèle seul, le modèle doté de la recherche web (banc.py). La métrique de tête.
9
+ canon part de ce que le modèle tient pour essentiel et que le graphe porte (mesurer_avance.py).
10
+ avance part des notions récentes que le modèle dit ne pas connaître ; `utile` : celles qui sont
11
+ en plus dans le sujet.
12
+ fraîcheur date de la publication la plus récente que le graphe connaît, délai médian entre la
13
+ publication d'un article et sa collecte, fiches encore en attente d'extraction.
14
+ couverture par sujet déclaré : notions du canon portées, et manquantes.
15
+ corroboration part des notions attestées par au moins deux fiches, et par deux sources distinctes.
16
+ lacunes questions restées sans réponse : ouvertes, closes (lacunes.py).
17
+
18
+ Aucun appel à un modèle ici : le banc et le canon sont mesurés ailleurs, ce script les lit.
19
+
20
+ python3 -m newskit.acuite --graphe <graphe> # calcule, écrit graphe/<nom>/acuite.json, affiche
21
+ python3 -m newskit.acuite --tous # tous les graphes, une ligne chacun
22
+ python3 -m newskit.acuite --graphe <graphe> --etat # relit sans recalculer
23
+
24
+ Chaque calcul ajoute une ligne à graphe/<nom>/acuite_historique.jsonl (une par jour) : c'est la pente
25
+ qui dit si la stratégie marche, pas le niveau. À un changement de modèle, les chiffres baissent d'un
26
+ coup — l'historique garde le modèle et son horizon pour qu'on ne le prenne pas pour une régression.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import re
34
+ import statistics
35
+ import subprocess
36
+ import sys
37
+ import time
38
+ from datetime import date
39
+ from pathlib import Path
40
+
41
+ from newskit.commun import CODE, RACINE # noqa: E402 — deux racines, le code et les données (N1)
42
+ from newskit import config_sources as cfg # noqa: E402
43
+ from newskit import digerer_graphe as dg # noqa: E402
44
+ from newskit import themes_emergents as te # noqa: E402
45
+
46
+ RE_JOUR = re.compile(r"^(\d{4})-(\d{2})-(\d{2})")
47
+ # Les seuils du succès, convenus avec le porteur le 04.10.2026 : (minimum, succès). L'objectif commun aux
48
+ # graphes servis est la colonne « succès ». Avec trente questions, un écart de banc de moins de vingt à
49
+ # vingt-cinq points peut être du hasard : +25 est le plus petit écart qu'on puisse affirmer.
50
+ SEUILS = {
51
+ "banc_ecart_modele": (0.15, 0.25), # graphe − modèle seul, en part de réponses justes
52
+ "banc_ecart_web": (-0.10, 0.0), # graphe − modèle avec recherche web
53
+ "banc_faux": (0.10, 0.05), # réponses fausses du graphe : un plafond
54
+ "canon": (0.80, 0.90),
55
+ "avance_utile": (0.35, 0.50),
56
+ "age_jours": (2, 1), # un plafond
57
+ "corrobore_centrales": (0.60, 0.80), # les notions les plus reliées, attestées par deux sources
58
+ }
59
+ PLAFONDS = {"banc_faux", "age_jours"}
60
+ CENTRALES = 200 # notions les plus reliées, pour la corroboration du cœur du graphe
61
+ FENETRE = 30 # jours de collecte pris pour le délai de fraîcheur
62
+ DELAI_MAX = 14 # au-delà, la fiche est une archive remontée, pas une nouvelle
63
+
64
+
65
+ def lu(chemin: Path) -> dict:
66
+ try:
67
+ return json.loads(chemin.read_text(encoding="utf-8"))
68
+ except (OSError, json.JSONDecodeError):
69
+ return {}
70
+
71
+
72
+ def jour(texte: str) -> date | None:
73
+ m = RE_JOUR.match(texte or "")
74
+ try:
75
+ return date(int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None
76
+ except ValueError:
77
+ return None
78
+
79
+
80
+ def fraicheur(graphe: str, G, aujourdhui: date) -> dict:
81
+ """La date la plus récente connue du graphe, et le délai entre publication et collecte des nouvelles."""
82
+ dates = [jour(str(d.get("date") or "")) for _, d in G.nodes(data=True) if d.get("kind") in ("article", "papier")]
83
+ dates = [d for d in dates if d and d <= aujourdhui]
84
+ delais = []
85
+ for dossier in cfg.dossiers_du_graphe(graphe):
86
+ for f in (RACINE / dossier).glob("*.md"):
87
+ publie = jour(f.name)
88
+ m = re.search(
89
+ r'^collected: "?(\d{4}-\d{2}-\d{2})', f.read_text(encoding="utf-8", errors="replace")[:1500], flags=re.M
90
+ )
91
+ collecte = jour(m.group(1)) if m else None
92
+ if (
93
+ publie
94
+ and collecte
95
+ and (aujourdhui - collecte).days <= FENETRE
96
+ and 0 <= (collecte - publie).days <= DELAI_MAX
97
+ ):
98
+ delais.append((collecte - publie).days)
99
+ r = subprocess.run(
100
+ [sys.executable, str(CODE / "extraire_concepts.py"), "--graphe", graphe, "--etat"],
101
+ cwd=RACINE,
102
+ capture_output=True,
103
+ text=True,
104
+ timeout=600,
105
+ )
106
+ m = re.search(r"(\d+) fiche\(s\) dense\(s\) en retard", r.stdout)
107
+ return {
108
+ "dernier": max(dates).isoformat() if dates else None,
109
+ "age_jours": (aujourdhui - max(dates)).days if dates else None,
110
+ "delai_median_jours": statistics.median(delais) if delais else None,
111
+ "nouvelles_mesurees": len(delais),
112
+ "en_attente_d_extraction": int(m.group(1)) if m else 0,
113
+ }
114
+
115
+
116
+ def corroboration(G) -> dict:
117
+ """Part des notions attestées par au moins deux fiches, et par au moins deux sources (dossiers) distinctes."""
118
+ total = deux_fiches = deux_sources = 0
119
+ par_noeud: list[tuple[int, bool]] = []
120
+ for n in G:
121
+ if dg._type(G, n) not in dg.TYPES_CONCEPT:
122
+ continue
123
+ fiches = {str(d.get("source_file") or "") for _, _, d in G.edges(n, data=True)} | {
124
+ str(G.nodes[n].get("source_file") or "")
125
+ }
126
+ fiches.discard("")
127
+ if not fiches:
128
+ continue
129
+ total += 1
130
+ deux_fiches += len(fiches) >= 2
131
+ plusieurs = len({str(Path(f).parent) for f in fiches}) >= 2
132
+ deux_sources += plusieurs
133
+ par_noeud.append((G.degree(n), plusieurs))
134
+ # Le chiffre d'ensemble reste bas par nature : la longue traîne d'un domaine n'est citée qu'une fois.
135
+ # Le cœur du graphe — ses notions les plus reliées — doit, lui, reposer sur plusieurs sources.
136
+ centrales = sorted(par_noeud, reverse=True)[:CENTRALES]
137
+ return {
138
+ "notions": total,
139
+ "deux_fiches": round(deux_fiches / total, 3) if total else None,
140
+ "deux_sources": round(deux_sources / total, 3) if total else None,
141
+ "centrales": round(sum(p for _, p in centrales) / len(centrales), 3) if centrales else None,
142
+ }
143
+
144
+
145
+ def actualite(G) -> dict | None:
146
+ """Ce que les textes de référence disent du présent des notions (diachronie.py, posé à la composition) : notions
147
+ attestées par un texte de référence, part attestée par une version en vigueur, notions à lire comme périmées
148
+ (version remplacée seulement), notions évoluant entre deux versions. None sans texte de référence."""
149
+ c = {"reference": 0, "en_vigueur": 0, "remplacee_seulement": 0, "evolue": 0}
150
+ for _, d in G.nodes(data=True):
151
+ a = d.get("actualite")
152
+ if a and "en_vigueur" in a:
153
+ c["reference"] += 1
154
+ c["en_vigueur"] += bool(a["en_vigueur"])
155
+ c["remplacee_seulement"] += bool(a["remplacee_seulement"])
156
+ c["evolue"] += bool(a["evolue"])
157
+ if not c["reference"]:
158
+ return None
159
+ c["part_en_vigueur"] = round(c["en_vigueur"] / c["reference"], 3)
160
+ return c
161
+
162
+
163
+ def calculer(graphe: str) -> dict | None:
164
+ G = te.charger(graphe)
165
+ if G is None:
166
+ return None
167
+ base = cfg.chemins_du_graphe(graphe)["base"]
168
+ aujourdhui = date.today()
169
+ avance, lac, banc = lu(base / "avance.json"), lu(base / "lacunes.json"), lu(base / "banc.json")
170
+ a: dict = {
171
+ "graphe": graphe,
172
+ "date": aujourdhui.isoformat(),
173
+ "modele": avance.get("modele") or te.MODELE,
174
+ "horizon": avance.get("horizon") or cfg.HORIZON_MODELE,
175
+ "fraicheur": fraicheur(graphe, G, aujourdhui),
176
+ "corroboration": corroboration(G),
177
+ }
178
+ if act := actualite(G):
179
+ a["actualite"] = act
180
+ if avance.get("completude") is not None:
181
+ soumises = sum(s["soumises"] for s in avance.get("sources", []))
182
+ a["canon"] = {
183
+ "completude": avance["completude"],
184
+ "mesure_du": avance.get("date"),
185
+ "sujets": [
186
+ {"sujet": s["sujet"], "portees": s["portees"], "canon": s["canon"], "absentes": s["absentes"]}
187
+ for s in sorted(avance.get("sujets", []), key=lambda s: s["portees"] / s["canon"])
188
+ ],
189
+ }
190
+ a["avance"] = {
191
+ "posterieures": avance.get("posterieures"),
192
+ "notions": avance.get("notions"),
193
+ "inconnues": round(len(avance.get("inconnues_du_modele", [])) / avance["echantillon"], 3)
194
+ if avance.get("echantillon")
195
+ else None,
196
+ "utiles": round(sum(s["utiles"] for s in avance.get("sources", [])) / soumises, 3) if soumises else None,
197
+ }
198
+ if lac:
199
+ semaine = sum(
200
+ 1
201
+ for x in lac.get("closes", {}).values()
202
+ if (j := jour(str(x.get("close_le") or ""))) and (aujourdhui - j).days <= 7
203
+ )
204
+ a["lacunes"] = {
205
+ "ouvertes": len(lac.get("ouvertes", {})),
206
+ "closes_7_jours": semaine,
207
+ "a_trancher": len(lac.get("a_trancher", {})),
208
+ }
209
+ if banc.get("conditions"):
210
+ a["banc"] = {
211
+ "mesure_du": banc.get("date"),
212
+ "questions": banc.get("questions"),
213
+ "conditions": banc["conditions"],
214
+ "methode": banc.get("methode") or "ancienne",
215
+ "restitution": banc.get("restitution"),
216
+ "fond": banc.get("fond"),
217
+ "par_type": banc.get("par_type"),
218
+ }
219
+ a["paliers"] = paliers(a)
220
+ a["ligne"] = ligne(a)
221
+ return a
222
+
223
+
224
+ def valeurs(a: dict) -> dict:
225
+ """Les valeurs que les seuils jugent, tirées de l'acuité ; None quand la mesure manque."""
226
+ c = (a.get("banc") or {}).get("conditions") or {}
227
+ g, m, w = (c.get(k, {}).get("juste") for k in ("graphe", "modele", "web"))
228
+ return {
229
+ "banc_ecart_modele": None if g is None or m is None else round(g - m, 3),
230
+ "banc_ecart_web": None if g is None or w is None else round(g - w, 3),
231
+ "banc_faux": c.get("graphe", {}).get("faux"),
232
+ "canon": (a.get("canon") or {}).get("completude"),
233
+ "avance_utile": (a.get("avance") or {}).get("utiles"),
234
+ "age_jours": (a.get("fraicheur") or {}).get("age_jours"),
235
+ "corrobore_centrales": (a.get("corroboration") or {}).get("centrales"),
236
+ }
237
+
238
+
239
+ def paliers(a: dict) -> dict:
240
+ """Pour chaque seuil : « succès », « minimum », « en dessous », ou None si la mesure manque."""
241
+ out = {}
242
+ for k, v in valeurs(a).items():
243
+ mini, succes = SEUILS[k]
244
+ if v is None:
245
+ out[k] = None
246
+ elif k in PLAFONDS:
247
+ out[k] = "succès" if v <= succes else "minimum" if v <= mini else "en dessous"
248
+ else:
249
+ out[k] = "succès" if v >= succes else "minimum" if v >= mini else "en dessous"
250
+ return out
251
+
252
+
253
+ def pct(x) -> str:
254
+ return "—" if x is None else f"{x:.0%}"
255
+
256
+
257
+ def ligne(a: dict) -> str:
258
+ """La synthèse en une ligne, toujours dans le même ordre : banc, socle, avance, fraîcheur, lacunes."""
259
+ bouts = []
260
+ c = (a.get("banc") or {}).get("conditions") or {}
261
+ if c:
262
+ # Juste · faux · ne sait pas : « ne sait pas » n'est ni l'un ni l'autre, et s'écrit.
263
+ from newskit.banc import trois_parts # noqa: PLC0415
264
+
265
+ jf = lambda k: trois_parts(c.get(k) or {}) if (c.get(k) or {}).get("juste") is not None else "—" # noqa: E731
266
+ bouts.append(f"banc {jf('graphe')} | LLM {jf('modele')} | LLM+web {jf('web')} (juste · faux · ne sait pas)")
267
+ if a.get("canon"):
268
+ bouts.append(f"socle {pct(a['canon']['completude'])}")
269
+ if (a.get("avance") or {}).get("utiles") is not None:
270
+ bouts.append(f"avance utile {pct(a['avance']['utiles'])}")
271
+ f = a.get("fraicheur") or {}
272
+ if f.get("age_jours") is not None:
273
+ bouts.append(
274
+ f"frais à J-{f['age_jours']}"
275
+ + (f", {f['en_attente_d_extraction']} en attente" if f.get("en_attente_d_extraction") else "")
276
+ )
277
+ if (a.get("corroboration") or {}).get("deux_sources") is not None:
278
+ bouts.append(f"corroboré {pct(a['corroboration']['deux_sources'])}")
279
+ if a.get("lacunes"):
280
+ bouts.append(f"{a['lacunes']['ouvertes']} lacune(s)")
281
+ return " · ".join(bouts) or "pas encore mesuré"
282
+
283
+
284
+ def ecrire(graphe: str, a: dict) -> None:
285
+ base = cfg.chemins_du_graphe(graphe)["base"]
286
+ (base / "acuite.json").write_text(json.dumps(a, ensure_ascii=False, indent=1), encoding="utf-8")
287
+ # Une ligne par jour dans l'historique : celle du jour remplace la précédente du même jour.
288
+ h = base / "acuite_historique.jsonl"
289
+ lignes = [
290
+ l
291
+ for l in (h.read_text(encoding="utf-8").splitlines() if h.exists() else [])
292
+ if f'"date": "{a["date"]}"' not in l
293
+ ]
294
+ c = (a.get("banc") or {}).get("conditions") or {}
295
+ point = {
296
+ "date": a["date"],
297
+ "modele": a["modele"],
298
+ "horizon": a["horizon"],
299
+ "banc_graphe": c.get("graphe", {}).get("juste"),
300
+ "banc_modele": c.get("modele", {}).get("juste"),
301
+ "banc_web": c.get("web", {}).get("juste"),
302
+ "canon": (a.get("canon") or {}).get("completude"),
303
+ "avance_utile": (a.get("avance") or {}).get("utiles"),
304
+ "age_jours": a["fraicheur"]["age_jours"],
305
+ "en_attente": a["fraicheur"]["en_attente_d_extraction"],
306
+ "corrobore": a["corroboration"]["deux_sources"],
307
+ "lacunes": (a.get("lacunes") or {}).get("ouvertes"),
308
+ }
309
+ h.write_text("\n".join([*lignes, json.dumps(point, ensure_ascii=False)]) + "\n", encoding="utf-8")
310
+
311
+
312
+ def afficher(a: dict, detail: bool = True) -> None:
313
+ print(f" {a['graphe']} — {a['ligne']}")
314
+ if not detail:
315
+ return
316
+ f, c = a["fraicheur"], a["corroboration"]
317
+ print(
318
+ f" fraîcheur dernière publication connue : {f['dernier']} ; délai publication → collecte : "
319
+ f"{f['delai_median_jours'] if f['delai_median_jours'] is not None else '—'} j (médiane sur {f['nouvelles_mesurees']} nouvelles) ; "
320
+ f"{f['en_attente_d_extraction']} fiche(s) en attente d'extraction"
321
+ )
322
+ print(
323
+ f" corroboration {pct(c['deux_fiches'])} des {c['notions']} notions par au moins deux fiches, {pct(c['deux_sources'])} par deux "
324
+ f"sources ; {pct(c.get('centrales'))} des {CENTRALES} notions les plus reliées"
325
+ )
326
+ if a.get("actualite"):
327
+ x = a["actualite"]
328
+ print(
329
+ f" actualité {x['reference']} notions attestées par un texte de référence, {pct(x['part_en_vigueur'])} par une version "
330
+ f"en vigueur ; {x['remplacee_seulement']} à lire comme périmée(s) (version remplacée seulement) ; {x['evolue']} évoluant "
331
+ f"entre deux versions"
332
+ )
333
+ if a.get("canon"):
334
+ print(
335
+ f" socle {pct(a['canon']['completude'])} (LLM {a['modele']}, horizon {a['horizon']}, mesure du {a['canon']['mesure_du']})"
336
+ )
337
+ for s in a["canon"]["sujets"]:
338
+ print(f" {s['portees']:2d}/{s['canon']:2d} {s['sujet'][:72]}")
339
+ if a.get("avance"):
340
+ print(
341
+ f" avance {pct(a['avance']['inconnues'])} des notions récentes inconnues du LLM, {pct(a['avance']['utiles'])} utiles (dans le sujet)"
342
+ )
343
+ if a.get("lacunes"):
344
+ l = a["lacunes"]
345
+ print(
346
+ f" lacunes {l['ouvertes']} ouverte(s), {l['closes_7_jours']} close(s) en sept jours, {l['a_trancher']} à trancher"
347
+ )
348
+ v, noms = (
349
+ valeurs(a),
350
+ {
351
+ "banc_ecart_modele": "banc, écart avec le LLM seul",
352
+ "banc_ecart_web": "banc, écart avec le LLM + web",
353
+ "banc_faux": "banc, taux d'erreur du graphe",
354
+ "canon": "socle",
355
+ "avance_utile": "avance utile",
356
+ "age_jours": "fraîcheur (jours)",
357
+ "corrobore_centrales": "corroboration du cœur",
358
+ },
359
+ )
360
+ print(
361
+ " seuils "
362
+ + " ".join(f"[{p or 'non mesuré'}] {noms[k]}" for k, p in (a.get("paliers") or {}).items())
363
+ )
364
+ atteints = [p for p in (a.get("paliers") or {}).values() if p]
365
+ if atteints:
366
+ print(
367
+ f" {atteints.count('succès')} au succès, {atteints.count('minimum')} au minimum, "
368
+ f"{atteints.count('en dessous')} en dessous, {len(a['paliers']) - len(atteints)} non mesuré(s)"
369
+ )
370
+
371
+
372
+ def main() -> int:
373
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
374
+ ap.add_argument("--graphe", default=cfg.GRAPHE_PRINCIPAL)
375
+ ap.add_argument("--tous", action="store_true", help="tous les graphes, une ligne chacun")
376
+ ap.add_argument("--etat", action="store_true", help="relire la dernière mesure, sans recalculer")
377
+ args = ap.parse_args()
378
+ graphes = cfg.ordre_d_affichage() if args.tous else [cfg.nom_actuel(args.graphe)]
379
+ for g in graphes:
380
+ if args.etat:
381
+ a = lu(cfg.chemins_du_graphe(g)["base"] / "acuite.json")
382
+ else:
383
+ a = calculer(g)
384
+ if a:
385
+ ecrire(g, a)
386
+ if a:
387
+ afficher(a, detail=not args.tous)
388
+ else:
389
+ print(f" {g} : pas de graphe, ou pas encore de mesure")
390
+ return 0
391
+
392
+
393
+ if __name__ == "__main__":
394
+ sys.exit(main())