From 1c8352a178a0e9112ebd64e78f8da4afc75ba8ac Mon Sep 17 00:00:00 2001 From: jsboige Date: Mon, 21 Sep 2026 17:04:17 +0200 Subject: [PATCH] feat(catalog,#14831): scientific_review mesure le risque, PRODUCTION devient un tampon signe Sign-off user 2026-09-21. Trois changements de nature sur l'axe 3. 1. L'echelle mesure le RISQUE du contenu, pas la provenance de sa relecture. UNASSESSED / ESTABLISHED / ADVANCED / RESEARCH remplace UNREVIEWED / AUTHOR_REVIEWED / PEER_REVIEWED / FORMALLY_VERIFIED, qui etait inversee dans ses effets : une serie de recherche active relue par des pairs atteignait le haut de l'echelle pendant qu'un notebook de cours classique, universellement admis, restait UNREVIEWED faute de reviewer nomme. Elle reposait de plus sur le compte de `sorry`, qui ne concerne qu'une poignee de notebooks, pour piloter un axe couvrant tout le corpus. 2. L'appreciation se PERIME quand le code bouge. `scientific_review_stale` bascule quand l'empreinte du code differe du `reviewed_code_sha` enregistre ; la grade est conservee. L'empreinte exclut markdown, sorties et execution_count -- sans quoi la campagne de densification (178 notebooks en trois semaines, zero ligne de code) aurait perime tout le corpus. Empreinte de CONTENU, jamais un blob SHA : un squash la tuerait (#11919). C'est le regime d'audit permanent demande au sign-off. 3. PRODUCTION sort de aggregate_maturity(). Il ne decrit pas une propriete du fichier : il dit que le responsable pedagogique a appose son tampon. Le signal vient de la colonne Verdict de production-scope.md ; non tranche reste BETA, verdict correct puisque l'auteur enseigne lui-meme sur les beta. Le join ligne-de-decision -> chemins passe par le commentaire de groupe de la strate A. Deux autres joins ont ete mesures sur le document reel et rejetes : par libelle de serie (devine), et par repertoire de la tete de serie (sous-signe 90 chemins sur 99 -- SmartContracts s'etale sur deux sous-repertoires). Le join retenu rend 13 groupes / 99 chemins et resout les 13 lignes ; une ligne non resolue est RENDUE, jamais silencieuse. Corrige aussi deux defauts trouves en mesurant : - production-scope.md nommait `1_OpenAI_Intro.ipynb`, le fichier sur disque est `01_OpenAI_Intro.ipynb` -- la ligne GenAI Texte n'aurait signe aucun notebook. - le loader de registre exigeait `reviewer`, herite de l'ancienne semantique : 3 entrees lues sur 80, les 77 appreciations ICT jetees en silence. Pilote fondateur : ICT en `research` (appreciation nommee par le user au sign-off), les 3 entrees Sudoku historiques en `established`. Sans une seule entree appreciee, le champ rendrait 1240/1240 UNASSESSED -- indiscernable d'un mecanisme mort. See #14831 Co-Authored-By: Claude Opus 5 (1M context) --- docs/PARCOURS.md | 76 ++- .../SCIENTIFIC_REVIEW_CARD.md | 70 ++- docs/notebook-metadata/production-scope.md | 2 +- .../scientific-review-registry.md | 529 +++++++++++++++++- docs/reference/scripts-reference.md | 2 +- scripts/audit/check_lean_notebook_sorry.py | 8 +- scripts/audit/check_scientific_review.py | 80 ++- scripts/notebook_tools/generate_catalog.py | 380 ++++++++++--- .../tests/test_generate_catalog.py | 256 +++++++-- .../tests/test_production_scope.py | 278 +++++++++ 10 files changed, 1524 insertions(+), 157 deletions(-) create mode 100644 scripts/notebook_tools/tests/test_production_scope.py diff --git a/docs/PARCOURS.md b/docs/PARCOURS.md index 3a189c1415..222c8809bc 100644 --- a/docs/PARCOURS.md +++ b/docs/PARCOURS.md @@ -14,7 +14,7 @@ Le catalogue généré (`COURSE_CATALOG.generated.json`) porte aujourd'hui un ch 1. **Maturité éditoriale** — où en est la *prose pédagogique* ? (de DRAFT = jamais relu à FINAL = relu, stable, prêt à publier). 2. **Reproductibilité** — le notebook a-t-il *réellement tourné* et avec quel niveau de garantie ? (de UNTESTED = aucune cellule exécutée à REPRODUCED = ré-exécuté de bout en bout avec succès, horodaté). -3. **Revue scientifique** — la *substance technique* a-t-elle été validée et par qui ? (de UNREVIEWED = auteur seul à FORMALLY_VERIFIED = preuve formelle ou peer-review externe). +3. **Confiance scientifique** — quel *risque* la substance prend-elle sur ce qu'elle affirme ? (de UNASSESSED = pas d'appréciation portée à RESEARCH = recherche active, contenu explicitement en cours d'élaboration). Mélanger ces axes en une seule étiquette (PRODUCTION, BETA…) a trois défauts : - **Illisible** : « BETA » ne dit pas si le notebook a réellement exécuté, ni si la substance est revue — il dit juste « pas tout à fait finalisé ». @@ -49,12 +49,50 @@ Mélanger ces axes en une seule étiquette (PRODUCTION, BETA…) a trois défaut ### Axe 3 — `scientific_review` (revue scientifique) -| Valeur | Définition | Critère vérifiable | -|--------|-----------|--------------------| -| `UNREVIEWED` | Aucun passage en revue hors l'auteur | Pas de signal `scientific_reviewed_by` ni de PR/discussion de substance attachée | -| `AUTHOR_REVIEWED` | L'auteur a relu sa propre substance (cross-check, sanity-checks) | Présence d'une note « Self-review » dans le notebook OU signal PR auteur `self-reviewed` | -| `PEER_REVIEWED` | Relecture par ≥1 agent tiers du cluster ou reviewer externe | `scientific_reviewed_by` non-null ET ≠ auteur du dernier commit | -| `FORMALLY_VERIFIED` | Preuve formelle (Lean, Coq, Agda) ou benchmark reproductible validé | Présence d'un fichier `.lean` companion OU `lake build SUCCESS` daté OU benchmark QC multi-seed ≥4 seeds | +**L'axe mesure le risque du contenu, pas la provenance de sa relecture** (#14831, +sign-off user 2026-09-21). L'échelle précédente (`UNREVIEWED` → `AUTHOR_REVIEWED` → +`PEER_REVIEWED` → `FORMALLY_VERIFIED`) classait *qui avait relu, avec quelle rigueur +formelle*. Elle était **inversée dans ses effets** : une série de recherche active relue +par des pairs atteignait le haut de l'échelle, pendant qu'un notebook de cours classique, +universellement admis et sans aucun risque, restait `UNREVIEWED` faute de reviewer nommé. +Elle reposait de plus sur le compte de `sorry`, un indicateur qui ne concerne qu'une +poignée de notebooks Lean, pour piloter un axe couvrant tout le corpus. + +| Valeur | Définition | D'où elle vient | +|--------|-----------|-----------------| +| `UNASSESSED` | Aucune appréciation portée. **Ce n'est pas un mauvais score** : c'est l'absence de jugement, et c'est le défaut honnête. | défaut ; aucune entrée de registre | +| `ESTABLISHED` | Contenu communément admis et universellement pratiqué. Le notebook ne prend aucun risque sur ce qu'il affirme. | `confidence: established` au registre | +| `ADVANCED` | Protocoles plus avancés, exécutions moins contrôlées, théories récentes, interprétations discutables. Le contenu tient, mais il engage. | `confidence: advanced` | +| `RESEARCH` | Recherche active — ICT au premier chef. Le contenu est explicitement en cours d'élaboration, et le dire est la seule position honnête. | `confidence: research` | + +Une valeur non reconnue retombe sur `UNASSESSED` (**fail-CLOSED**) : c'est la propriété +qui empêche le label-gaming. + +#### L'appréciation se périme quand le code bouge + +Une appréciation porte sur ce que le notebook **calcule et affirme**. Si le calcul change, +elle ne porte plus sur ce qui est là. Le catalogue émet donc `scientific_review_stale: +true` lorsque l'empreinte du code diffère du `reviewed_code_sha` enregistré au moment de +la revue — **la grade est conservée**, seul le drapeau bascule : la perdre effacerait +l'information (« personne n'a jamais apprécié ») alors que le fait est autre (« quelqu'un +a apprécié, puis le code a bougé »). + +Cela met l'audit scientifique en **régime permanent**, ce qui est l'effet recherché : une +nouvelle revue est due, sous le protocole de [SCIENTIFIC_REVIEW_CARD.md](notebook-metadata/SCIENTIFIC_REVIEW_CARD.md). + +L'empreinte exclut délibérément trois choses, chacune pour une raison mesurée : + +- **le markdown** — la campagne de densification a modifié 178 notebooks en trois semaines + sans toucher une ligne de code ; l'inclure rétrograderait tout le corpus au premier + passage, et une rétrogradation qui frappe tout ne signale plus rien ; +- **les sorties** — une ré-exécution les change sans changer ce que le notebook affirme ; +- **`execution_count`** — pur artefact d'ordre d'exécution. + +C'est une empreinte de **contenu**, jamais un blob SHA git : un squash-merge réécrit les +blobs et tuerait l'ancre à chaque merge (#11919). + +`sorry_free` et `scientific_reviewed_by` restent rendus **comme preuves à côté** — ils ne +pilotent plus la grade. --- @@ -63,7 +101,7 @@ Mélanger ces axes en une seule étiquette (PRODUCTION, BETA…) a trois défaut Le champ `maturity` monolithique actuel reste **présent** dans `COURSE_CATALOG.generated.json` pour ne casser aucun consommateur existant (README, dashboards, scripts tiers). Il est désormais **calculé comme l'agrégat** des 3 axes, selon la règle : ``` -editorial == "FINAL" AND reproducibility in ("EXECUTED", "REPRODUCED") AND scientific_review in ("PEER_REVIEWED", "FORMALLY_VERIFIED") +production_signed (tampon du responsable pédagogique, cf docs/notebook-metadata/production-scope.md) → maturity = "PRODUCTION" editorial in ("BETA", "FINAL") AND reproducibility in ("EXECUTED", "REPRODUCED") → maturity = "BETA" @@ -75,13 +113,29 @@ sinon → maturity = "DRAFT" ``` -**Le consommateur qui veut plus de granularité** lit directement `editorial`, `reproducibility`, `scientific_review`. Celui qui veut l'ancien label lit `maturity`. Aucun breaking change. +**`PRODUCTION` ne se dérive plus d'aucune combinaison d'axes** (#14831, sign-off user +2026-09-21). Il ne décrit pas une propriété mesurable du fichier : il dit que le +responsable pédagogique a **apposé son tampon**, et juge le notebook finalisé pour être +utilisé en cours **par d'autres**. Le calculer revenait à fabriquer une signature. + +Le signal vient donc de la colonne « Verdict » de +[production-scope.md](notebook-metadata/production-scope.md), qui est la surface de +décision. Un notebook non tranché reste `BETA` — et c'est le verdict **correct**, pas un +manque : l'auteur enseigne lui-même sur les beta et les beta-teste avec ses étudiants. + +`scientific_review` reste **nécessaire mais pas suffisant** pour `PRODUCTION` : il est +exigé par le validateur de périmètre, pas par l'agrégat — un axe qui *gate* ne doit pas +être le même objet que l'axe qui *décrit*. + +**Le consommateur qui veut plus de granularité** lit directement `editorial`, +`reproducibility`, `scientific_review` (+ `scientific_review_stale`). Celui qui veut +l'ancien label lit `maturity`. Aucun breaking change. ## Statut séparé — non touché par ce schéma Le champ `status` (`READY`, `DEMO`, `RESEARCH`, `BROKEN`) reste **orthogonal** aux 3 axes maturité. Un notebook peut être : -- `editorial=FINAL` + `reproducibility=REPRODUCED` + `scientific_review=PEER_REVIEWED` + `status=BROKEN` (ex : notebook qui marchait mais dont une dépendance externe est cassée) ; -- `editorial=ALPHA` + `reproducibility=EXECUTED` + `scientific_review=AUTHOR_REVIEWED` + `status=DEMO` (ex : démo scientifique sans valeur pédagogique aboutie). +- `editorial=FINAL` + `reproducibility=REPRODUCED` + `scientific_review=ESTABLISHED` + `status=BROKEN` (ex : notebook qui marchait mais dont une dépendance externe est cassée) ; +- `editorial=ALPHA` + `reproducibility=EXECUTED` + `scientific_review=RESEARCH` + `status=DEMO` (ex : démo scientifique sans valeur pédagogique aboutie). `status` répond à « *peut-on le faire tourner en l'état ?* », les 3 axes répondent à « *où en est sa substance ?* ». Séparer les deux est une décision architecturale stable (issue #8051 acceptance critère 2). diff --git a/docs/notebook-metadata/SCIENTIFIC_REVIEW_CARD.md b/docs/notebook-metadata/SCIENTIFIC_REVIEW_CARD.md index 22673b0eea..02d2826cfd 100644 --- a/docs/notebook-metadata/SCIENTIFIC_REVIEW_CARD.md +++ b/docs/notebook-metadata/SCIENTIFIC_REVIEW_CARD.md @@ -1,8 +1,20 @@ # SCIENTIFIC_REVIEW_CARD — Template de revue scientifique -**Statut** : template canonique (c.997) +**Statut** : template canonique (c.997, refondu #14831 — sign-off user 2026-09-21) **Usage** : copié/adapté par chaque reviewer qui ajoute une entrée à [scientific-review-registry.md](scientific-review-registry.md) **Référence scope** : [scientific-review-registry.md §3.2](scientific-review-registry.md#32-portée-de-la-revue) +**Échelle** : [PARCOURS.md — axe 3](../PARCOURS.md#axe-3--scientific_review-revue-scientifique) + +> **Quand cette carte se remplit.** À la première appréciation d'un notebook, et +> **chaque fois que le catalogue rend `scientific_review_stale: true`** — c'est-à-dire +> chaque fois que le *code* du notebook a changé depuis la dernière revue. C'est un +> régime d'**audit permanent**, et c'est voulu : une appréciation porte sur ce que le +> notebook calcule, donc elle ne survit pas à un changement de calcul. +> +> Lister ce qui est dû : +> ```bash +> python scripts/audit/check_scientific_review.py --check # classe STALE_APPRECIATION +> ``` --- @@ -24,7 +36,7 @@ Cocher la portée effectivement couverte par la PR de revue (cf registre §3.2) - [ ] **correctness** — corrections de bugs d'implémentation (off-by-one, edge case) - [ ] **full** — toutes dimensions ci-dessus -> **Note promote** : tous les scopes ci-dessus promeuvent vers `AUTHOR_REVIEWED` si le reviewer == last_validator, ou `PEER_REVIEWED` si le reviewer ≠ last_validator (cf `classify_scientific_review` l.802-808). +> **La portée n'est plus ce qui promeut** (#14831). Elle dit *ce que la revue a couvert* ; l'appréciation, elle, se déclare explicitement au verdict ci-dessous. Faire dépendre la grade de la portée ou de l'identité du relecteur mesurait la provenance de la relecture, pas le risque du contenu. ## Constats @@ -34,11 +46,53 @@ Liste des findings significatifs (1 ligne chacun) : 2. `` 3. ... -## Verdict +## Verdict — l'appréciation de confiance -- [ ] **PROMOTE_AUTHOR** — la revue justifie `scientific_reviewed_by = ""` et promeut `UNREVIEWED → AUTHOR_REVIEWED` -- [ ] **PROMOTE_PEER** — la revue est par un tiers distinct, promeut `UNREVIEWED → PEER_REVIEWED` -- [ ] **DEFER** — la revue nécessite une seconde passe +**Une seule case, et elle demande un argument écrit.** La question n'est pas « qui a +relu ? » mais **« quel risque ce notebook prend-il sur ce qu'il affirme ? »**. + +- [ ] **ESTABLISHED** — contenu communément admis et universellement pratiqué. Le + notebook n'avance rien qui puisse être contesté par un lecteur compétent. +- [ ] **ADVANCED** — protocoles plus avancés, exécutions moins contrôlées, théories + récentes, interprétations discutables. Le contenu tient, mais il **engage**. +- [ ] **RESEARCH** — recherche active. Le contenu est explicitement en cours + d'élaboration, et le dire est la seule position honnête. +- [ ] **DEFER** — la revue nécessite une seconde passe. L'entrée n'est pas écrite au + registre, et le notebook reste `UNASSESSED` : **l'absence de jugement n'est pas + un mauvais score**, c'est le défaut honnête. + +**Justification (obligatoire, 2–5 lignes)** — ce qui fait pencher vers cette classe +plutôt que vers la voisine. Nommer le point le plus contestable du notebook et dire +pourquoi il tombe de ce côté : + +``` + +``` + +> **Ne pas confondre avec `PRODUCTION`.** Cette carte n'accorde aucun passage en +> production. `PRODUCTION` est le tampon du responsable pédagogique — il dit que le +> notebook est finalisé pour être utilisé en cours **par d'autres** — et il vit dans +> [production-scope.md](production-scope.md). L'appréciation scientifique en est une +> condition **nécessaire, jamais suffisante**. + +## Ancre de péremption (obligatoire) + +Sans ancre, l'appréciation est **immortelle par omission** : elle ne se périmera jamais, +quel que soit le code qui passera ensuite. Le validateur le signale +(`WARN_NO_CODE_ANCHOR`). + +```bash +python - <<'EOF' +import json, sys +sys.path.insert(0, "scripts/notebook_tools") +from generate_catalog import code_source_sha +print(code_source_sha(json.load(open("MyIA.AI.Notebooks/.ipynb", encoding="utf-8")))) +EOF +``` + +- **`reviewed_code_sha`** : `` +- **Vérifié contre** : `git rev-parse HEAD` = `` — l'arbre sur lequel la revue a + porté. Une revue menée sur une branche mesure autre chose que `main`. ## Preuves (G.1 obligatoires) @@ -54,10 +108,12 @@ Liste des findings significatifs (1 ligne chacun) : ## Signature du reviewer -- **Reviewer** : `` (DOIT permettre `AUTHOR_REVIEWED` si == last_validator, ou `PEER_REVIEWED` si ≠) +- **Reviewer** : `` — rendu comme **preuve à côté** ; depuis #14831 il ne pilote plus la grade, donc une auto-revue n'est plus disqualifiante, elle est simplement *visible* - **Date de revue** : `` (ISO 8601) - **Notes** : `` --- **Note** : ce registre est volontairement plus restrictif que `EDITORIAL_REVIEW_CARD.md` — il exige une preuve de fond technique (algo/proba/demo/correctness), pas seulement pédagogique. + +**Et il ne se clôt jamais.** Une carte remplie vaut pour l'état du code qu'elle ancre, pas pour le notebook à perpétuité. C'est la différence entre un audit ponctuel — qui vieillit en silence — et un régime permanent, qui se redéclare lui-même à chaque fois que le calcul change. diff --git a/docs/notebook-metadata/production-scope.md b/docs/notebook-metadata/production-scope.md index 94869dedb5..f2acac429f 100644 --- a/docs/notebook-metadata/production-scope.md +++ b/docs/notebook-metadata/production-scope.md @@ -42,7 +42,7 @@ Le détail par notebook suit en strate A ci-dessous : consultation, plus décisi | GenAI Image — Foundation | EPF GenAI Bachelor 3A | `01-1-OpenAI-DALL-E-3.ipynb` | 5 | | | GenAI Audio — Foundation | EPF GenAI Bachelor 3A | `01-1-OpenAI-TTS-Intro.ipynb` | 5 | | | GenAI Video — Foundation | EPF GenAI Bachelor 3A | `01-1-Video-Operations-Basics.ipynb` | 5 | | -| GenAI Texte (1-8) | EPF GenAI Bachelor 3A | `1_OpenAI_Intro.ipynb` | 8 | | +| GenAI Texte (1-8) | EPF GenAI Bachelor 3A | `01_OpenAI_Intro.ipynb` | 8 | | | Search — Part 1 Foundations | EPITA Programmation par Contraintes | `Search-01-StateSpace.ipynb` | 13 | | | Search — Part 2 CSP | EPITA Programmation par Contraintes | `CSP-1-Fundamentals.ipynb` | 9 | | | Argument Analysis | EPITA IA Symbolique | `Argument_Analysis_Toulmin_Model.ipynb` | 4 | | diff --git a/docs/notebook-metadata/scientific-review-registry.md b/docs/notebook-metadata/scientific-review-registry.md index 215f79ee93..cea1bfbda9 100644 --- a/docs/notebook-metadata/scientific-review-registry.md +++ b/docs/notebook-metadata/scientific-review-registry.md @@ -26,8 +26,17 @@ Le schéma 3-axes (c.763, [PR #8086](https://github.com/jsboige/CoursIA/pull/808 Chaque entrée du registre suit le schéma : +> **Le schéma a changé le 2026-09-21 (#14831, sign-off user).** L'axe 3 ne mesure plus +> *qui a relu, avec quelle rigueur formelle*, mais **quel risque le contenu prend sur ce +> qu'il affirme**. Deux champs entrent : `confidence` (l'appréciation elle-même) et +> `reviewed_code_sha` (l'ancre qui la fait se périmer quand le code bouge). Les champs +> historiques restent lus et rendus — comme **preuves à côté**, sans piloter la grade. +> La table de l'échelle vit dans [PARCOURS.md](../PARCOURS.md#axe-3--scientific_review-revue-scientifique). + ```yaml - notebook_path: + confidence: + reviewed_code_sha: reviewer: review_date: evidence_pr: <"#NNNN" PR GitHub> @@ -38,12 +47,45 @@ Chaque entrée du registre suit le schéma : | Champ | Type | Contrainte | Signification | |-------|------|-----------|---------------| | `notebook_path` | string | DOIT matcher exactement `path` d'une entrée du catalogue généré | Cible de la revue | -| `reviewer` | string | DOIT être ≠ `last_validator` du notebook (sinon auto-review, ignorée par `classify_scientific_review`) | Qui a relu | +| `confidence` | enum | `established` / `advanced` / `research`. Toute autre valeur retombe sur `UNASSESSED` (**fail-CLOSED**) | **L'appréciation elle-même** — le seul champ qui pilote la grade | +| `reviewed_code_sha` | string | sha256 du code au moment de la revue (cf §2.1). Absent = l'appréciation ne se périmera jamais (`WARN_NO_CODE_ANCHOR`) | Ancre de péremption | +| `reviewer` | string | Libre depuis #14831 — il ne pilote plus la grade, il est rendu comme preuve à côté | Qui a relu | | `review_date` | string | ISO 8601 `YYYY-MM-DD` | Date de la PR de revue | | `evidence_pr` | string | `#NNNN` PR GitHub mergée | Preuve vérifiable (G.1) | | `review_scope` | enum | `factual` / `algo` / `proba` / `demo` / `correctness` / `full` | Profondeur de la revue (cf §3) | | `notes` | string | Libre, 1 ligne | Contexte bref | +### 2.1 Calculer le `reviewed_code_sha` + +L'ancre est une empreinte de **contenu**, jamais un blob SHA git — un squash-merge réécrit +les blobs et la tuerait à chaque merge (#11919). Elle se calcule avec l'organe du +catalogue, jamais à la main : + +```bash +python - <<'EOF' +import json, sys +sys.path.insert(0, "scripts/notebook_tools") +from generate_catalog import code_source_sha +nb = json.load(open("MyIA.AI.Notebooks/.ipynb", encoding="utf-8")) +print(code_source_sha(nb)) +EOF +``` + +Elle ne couvre que la **source des cellules code**. Le markdown, les sorties et +`execution_count` en sont exclus délibérément : sans ces exclusions, la campagne de +densification (178 notebooks en trois semaines, sans une ligne de code touchée) aurait +périmé tout le corpus au premier passage — et une rétrogradation qui frappe tout ne +signale plus rien. + +Quand le code bouge, le catalogue émet `scientific_review_stale: true` et **conserve la +grade** : une nouvelle revue est due, sous le protocole de +[SCIENTIFIC_REVIEW_CARD.md](SCIENTIFIC_REVIEW_CARD.md). C'est le régime d'**audit +permanent** voulu par le sign-off, pas un défaut à masquer. + +Le validateur `python scripts/audit/check_scientific_review.py --check` nomme les trois +classes : `DRIFT_NOT_APPRECIATED` (câblage cassé, **erreur**), `STALE_APPRECIATION` +(revue due, **note**), `WARN_NO_CODE_ANCHOR` (appréciation immortelle par omission). + ## 3. Curation rules (HARD) ### 3.1 Éligibilité d'une revue @@ -91,7 +133,7 @@ Exit codes : ### 4.2 Consommation par `generate_catalog.py` -`classify_scientific_review()` lit `docs/notebook-metadata/scientific-review-registry.md`, extrait les entrées YAML valides, et applique `scientific_reviewed_by` au matching `notebook_path`. Sans signal, retombe sur `UNREVIEWED`. +`classify_scientific_review()` lit `docs/notebook-metadata/scientific-review-registry.md`, extrait les entrées YAML valides, et applique `confidence` + `reviewed_code_sha` au matching `notebook_path`. Sans signal, retombe sur `UNASSESSED` — l'absence d'appréciation, pas un mauvais score. ### 4.3 Préservation par `_merge_curated_fields` @@ -105,18 +147,24 @@ Les 3 entrées suivantes posent le **pilote fondateur** sur les 3 contre-exemple # Pilote fondateur c.997/c.1022 — evidence_pr pointe désormais sur des PRs MERGÉES (Tell c.745 sustained §B.2 levée) # Vérification first-hand c.1022 : `git log origin/main --oneline -- ` → PRs MERGED réelles qui touchent chaque notebook. - notebook_path: Sudoku/Sudoku-11-Choco-Csharp.ipynb + confidence: established + reviewed_code_sha: 5644b152ff6f34020c678f36fca16430e3133a9b456a2ea13c59ffce78a749cc reviewer: jsboige@gmail.com review_date: 2026-09-08 evidence_pr: "#7794" review_scope: correctness notes: "c.997/c.1022 pilote axe 3 — PR #7794 'Sudoku-11-Choco reconcile 1-50 ms claim with 728 ms cold-start output' = correctness factuel" - notebook_path: Sudoku/Sudoku-12-Z3-Csharp.ipynb + confidence: established + reviewed_code_sha: 71f76c5ba9c96c7de49b2146c598418dc97cc3e9549ce3460edba943fdba0d52 reviewer: jsboige@gmail.com review_date: 2026-09-08 evidence_pr: "#9926" review_scope: correctness notes: "c.997/c.1022 pilote axe 3 — PR #9926 'add 2 interpretation cells to Sudoku-12-Z3-Csharp' = correctness" - notebook_path: Sudoku/Sudoku-18-Comparison-Python.ipynb + confidence: established + reviewed_code_sha: e85e8279191a1db08ad9866f560d55eb7dd3e2394d7808b530dd9372304a67a8 reviewer: jsboige@gmail.com review_date: 2026-09-08 evidence_pr: "#13069" @@ -166,3 +214,480 @@ L'acceptance #14831 demande l'arbitrage entre **3 voies** : | `docs/PARCOURS.md` documente l'état retenu | **OUVERT** — après arbitrage | — | Cette PR clôture **le geste structurel** de la voie 1. L'arbitrage final entre les 3 voies reste **ouvert** dans l'issue #14831. + +## 6. Entrées pilote — échelle de confiance (#14831) + +La série ICT est de la **recherche active** : le contenu est explicitement en +cours d'élaboration, et le dire est la seule position honnête. C'est +l'appréciation posée par le responsable pédagogique lors du sign-off du +2026-09-21, inscrite ici telle quelle. + +Ces entrées ne portent pas de `reviewer` ni de `review_scope` : elles ne +consignent pas une relecture, mais une **appréciation de risque** — c'est +exactement la distinction que #14831 introduit. Leur `reviewed_code_sha` les +fera se périmer dès que le code de la série bougera. + +```yaml +- notebook_path: IIT/ICT-Series/ICT-01-PhiTrajectories-Python.ipynb + confidence: research + reviewed_code_sha: 23f43e6922845a9974c43a12179edad225c0c857829b2e1cd4fd2e88621c6d4d + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-02-SelfSortingMorphogenesis-Python.ipynb + confidence: research + reviewed_code_sha: 7b4e3a49f838065dcf6d6572c0099b5b6ba2ac7b99156d85adbd5ce293476c59 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-03-RobustnessDelayedGratification-Python.ipynb + confidence: research + reviewed_code_sha: 78cd3757aa95961fddd352904b197475c656f8fe5b7bf48190dba4c4263b528a + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-04-ChimericArraysKinAggregation-Python.ipynb + confidence: research + reviewed_code_sha: b2056a868be85763d57d99177dcd82606319d2979b5936e3ce862c66eb314e33 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-05-CausalEmergence-Python.ipynb + confidence: research + reviewed_code_sha: 18ed9781b681e3f5ff1f800dbda1894770cb7984a99da3da21f598c8d027b845 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-06-SortingToTPM-CausalEmergence-Python.ipynb + confidence: research + reviewed_code_sha: b28200ac2decbbccf0aa9dda0934f34ecd159bd7b838f60d2bcb53a54c13c3dc + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-07-ScaleFreeSignatures-Python.ipynb + confidence: research + reviewed_code_sha: e0ee9503aec8cf28f65ba4425eac82e50f7bf541405b88ebf313620f4361a8d0 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-08-AttractorLandscapesEWS-Python.ipynb + confidence: research + reviewed_code_sha: 7b421e31ec02755567525e0fecd091cc417d82e3fbe43911ab321266f9427a32 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-09-AgencyRegeneration-Python.ipynb + confidence: research + reviewed_code_sha: 30273e02afa3e60e0786487376395d32335f28775969d1bd48330e66acc3b4ee + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-10-CatastropheGrammar.ipynb + confidence: research + reviewed_code_sha: 4c33852fe79b84797c4a3b2d91f26f24a9d80e3e42ecf0ba8541073ea5b7d0ac + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-11-CausalAgencyProfiles.ipynb + confidence: research + reviewed_code_sha: bb7b1bc831b3e6e9cff5b9cbeb0a650848902702d3937456868d55ca45d2dfe1 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-12-ValenceFieldsAndAnimats.ipynb + confidence: research + reviewed_code_sha: 4bd52d81b4f2544a47c1a782de08927bb978f38ca85fa7873ddd4e978b9e8760 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-12b-LearnedValence.ipynb + confidence: research + reviewed_code_sha: fd2fcc293543c537a4e0c7477d0729eb7b8b321fa8eab194c1d8e3b424dcd926 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-12c-PregnanceAnimat.ipynb + confidence: research + reviewed_code_sha: 60af22936f3a15fc94cbeed9e864952e0026d5af20e9c3c082ec918ec237b28c + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-12d-InhibitedActionAnimat.ipynb + confidence: research + reviewed_code_sha: 44c3406ede520ac19295c8d46ac29324e1286d73141e4caf0b436360f8849a84 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-12e-Value-of-Information-Animat.ipynb + confidence: research + reviewed_code_sha: 865df6eec48b8704606ededa7b24a92bdc98da851b3f05c274a7c70aa0e186ec + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-13-AxelrodStrategicMorphodynamics.ipynb + confidence: research + reviewed_code_sha: 58e7aed69da7a3420d286898f809008774c6e82f94104fa8ce1a45260103a856 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-13b-DecroisementDynamiqueObservable.ipynb + confidence: research + reviewed_code_sha: 2469bf57512603953c28722423fed7cc4bc08d01cca1e5ac241fee7ee5fd5af0 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-14-FreeEnergySurprise.ipynb + confidence: research + reviewed_code_sha: d2326c85498fac62beffe840f44f3f2c0778e969aa77e4ab819f88930adf9a3e + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-14b-ActiveInferenceEFE.ipynb + confidence: research + reviewed_code_sha: 218e88ac3418e0b48f5a743d3e9fdaf94f4d5d735ae7ee2d817dbcb4a412b936 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15-IntegratedComplexity.ipynb + confidence: research + reviewed_code_sha: 0ae984bc61c4e62287b50339752455fa316e81435412d9409c5a1a5412c56ef5 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15b-SensitivityCanonicity.ipynb + confidence: research + reviewed_code_sha: 5880f18b92eaa0abd4b87b8e84998f62df1a7cb450b5e37bda059b1bf94af05e + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15c-MetaProxyObstruction.ipynb + confidence: research + reviewed_code_sha: 7d9c985f85328e517e3425c6b7809732c987980962853bbef3d0a84333fdb3ea + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15d-CechObstruction.ipynb + confidence: research + reviewed_code_sha: acbce2cbd8bd0cefa0ae06251f4c2062c5f9f3409db8e0f798188c48996784d2 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15e-Bridge2-RecoverabilityAgency.ipynb + confidence: research + reviewed_code_sha: 3a9b70a316b2ab08ef6dd9fece703e97df070eb74005b37dff078654cc7b2ee0 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15f-Bridge1bis-DecoupledFamily.ipynb + confidence: research + reviewed_code_sha: 6312b51ab9076ee6217c84ba2d5c8240f1f7b73b3f80dfb3dcd5902e7df552df + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15g-EmpiricalHuangExploitation.ipynb + confidence: research + reviewed_code_sha: 7d4b66eda31f97fa86c0ea89e3e22c4d23d5bb9bd2b6cdbe9cbdba7510e8235a + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15h-Bridge1bis-AsymmetricFamily.ipynb + confidence: research + reviewed_code_sha: 19af91ab897c3cbb9e12dcd23147a3b1359503657b8a44f41ff555a850b7ebe9 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15i-Bridge1bis-2DLandscape.ipynb + confidence: research + reviewed_code_sha: 668abad364ee819539cf5c2cba4f39c22958bc1233ef21a9fc6d42385ae0f201 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15j-NerveDiscriminant.ipynb + confidence: research + reviewed_code_sha: 8728a4fd458b0ebba17bd8c81f6d032e0466e8ae7e93df561a1592eee0c31196 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15k-RecollementMacroCells.ipynb + confidence: research + reviewed_code_sha: 525b3ad7fdc9037e4673a22841276405eafbdf6e4c963cde35b8da9be54b1e41 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-15l-IndependanceGenerateur.ipynb + confidence: research + reviewed_code_sha: 797597f4fac1e53b80f2a4ce2e4d71a4b098ba1b5c4eabe65498a015ec5ceafe + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-16-MDLTwoPartCode.ipynb + confidence: research + reviewed_code_sha: 205352944f1c69a1f44034825c96841b2fea714840c23234e02f4730eff9fbb7 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-17-EpsilonMachine.ipynb + confidence: research + reviewed_code_sha: 9a0d29399fcf122f0955c8f2637f3e9771bbef13982d544f6ebeb5591023ba3b + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-17b-Grokking-CompressionProgress.ipynb + confidence: research + reviewed_code_sha: c801bbda3ebfa6ff09259ed216ff8ba429552cf556670433f565fb67a76281d0 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-18-ArrowOfTimeReversibilization.ipynb + confidence: research + reviewed_code_sha: b57dff7ce2e74f362852464d172bddfad7de6a82b6cf2290a3bcd68565df7985 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-18b-ReversibilityBudget.ipynb + confidence: research + reviewed_code_sha: 40468c55eed3e0c657dc077242a7dc71ff97d50640805246e41ab2dac27992ab + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-19-EnjeuBattery.ipynb + confidence: research + reviewed_code_sha: 66d81b8d05d75489fa4faf5f1ccb7f4a880d1efb290e4c6f5c652e2560242ebe + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-19b-EnjeuBattery-Raffinement.ipynb + confidence: research + reviewed_code_sha: 1e7b15fc0d10978baea4106bfcad65eecb46aae89c14b23998759c9b16da86c2 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-20-FeatureCatastrophes.ipynb + confidence: research + reviewed_code_sha: bc3430cf2d0aade5f19f62a39424a35dd020cc0487b64d96d3eeb9f839176f48 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-21-SAETrajectoires.ipynb + confidence: research + reviewed_code_sha: cdbb163a010f2e9ddf3260e9cf86791ad48052d778f1612fec3fc0b09d7bb552 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-21b-SAECalibration.ipynb + confidence: research + reviewed_code_sha: da68c98b375c7dfdb07e2be5b563b12591b62b22635d92a033a008c67894af92 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-21c-SAECatastrophes.ipynb + confidence: research + reviewed_code_sha: a2208b38b240a6d3c2c72525d6f7e4ee5a45002701c0d17f1acf24b6e7942e3c + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-22-LLMSubstrat.ipynb + confidence: research + reviewed_code_sha: ea2c658be67e6d872e7342793f9b767e01f7752ce9ad6f768390a00eae7aa54e + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-22b-CausalInterventionEngine.ipynb + confidence: research + reviewed_code_sha: 979d9d74381da4989af304a459232c5650235e8292419ea8630131e9feb06a43 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-23-PersonaCatastrophe.ipynb + confidence: research + reviewed_code_sha: 3f8486094a815e35a0125779eccf46f747d9085f9a693d138cf868686e7d613b + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-24-WorkspaceIgnition.ipynb + confidence: research + reviewed_code_sha: 581cab3a625a3c038a4f8f4c3ba0849c800607f5ec6216cf4c4526c16d0659e5 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-25-InoculationRL.ipynb + confidence: research + reviewed_code_sha: eecbfaf077f6f766e3ecc0915257f13b5ecd44984c075d2ef4205075ceac1324 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-26-SignalingConvention.ipynb + confidence: research + reviewed_code_sha: dd2dc3f10833a140d184089bfbdbbccc03adc8b8608d72fd59e8cf29f4d779b4 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-27-SymbolInvention.ipynb + confidence: research + reviewed_code_sha: 879e61d4396fae0cf2ef78963f73d4cada62a40040be43ec1bb7ac23d57db4aa + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-28-CollectiveAdoption.ipynb + confidence: research + reviewed_code_sha: e465794a8dcc0e03586bc807a79cb2bd3dc5b7bbc081132d6b4fecd35fcda652 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-29-ConceptInoculation.ipynb + confidence: research + reviewed_code_sha: ee18f7c49dbbdabbbfb1bf606c6b08b8e48b83d0ba4a5377b30e5081d7f4a992 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-30-InhibitedInvention.ipynb + confidence: research + reviewed_code_sha: 83a03ecb4539a305dababfb9836dfcaf4794fc8e69dbf3183a2e9d590ffadebe + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-31-ContrasteTroisSubstrats.ipynb + confidence: research + reviewed_code_sha: 61f2e898186adedee66994c7e9383d437206eae139cdc334c8f25c4b9e306f09 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-32-StratificationCausaleLife.ipynb + confidence: research + reviewed_code_sha: 66dd1f440fcea61ccad93646a34fa3a0d9d1731a82f5ff1d94006155249b5601 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-33-SoupCollisions.ipynb + confidence: research + reviewed_code_sha: 2de545a916063bbc03b604f6bf3d2e5c781e058e09963c1462f975dcdee11567 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-34-BancRecollementLectures.ipynb + confidence: research + reviewed_code_sha: e08d6bc47e39527fafae610c81338058ef902ca74a1ea85f927f11c05909fda8 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-35-HumorCausalProbe-Pilot.ipynb + confidence: research + reviewed_code_sha: 837c4e864c1edb7c79312362060e03c770f048f9f0d702f8e6db6e94781017b7 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-35b-HumorCausalPairs-SAE.ipynb + confidence: research + reviewed_code_sha: 9848440cfce428e34d3542c00471197235b2db632cc893426dbbdb15d6e8e1fb + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-35c-HumorDepthProfile-SAE.ipynb + confidence: research + reviewed_code_sha: 4bfea9bf8a989868b48734958d5346c01c69918af914c04ea12b356b73b6df64 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-36-FLens-FactoredGeometry.ipynb + confidence: research + reviewed_code_sha: c1da486357cf298755b24eeff56245494aa5e0c2ca2caa11de92abb17db09112 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-37-FLens-BeliefState.ipynb + confidence: research + reviewed_code_sha: 2f70560d65c76788c5b256ffe867b2c222b498ba826c5baeffa28627d98f8f3a + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-38-SLens-SelfLocation.ipynb + confidence: research + reviewed_code_sha: e9a6b1608aa7acf747104405d4e3772918168c8e68d5bef703105dd5ef65fbc9 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-39-CompositionRegards.ipynb + confidence: research + reviewed_code_sha: 2790130a43e6f78fb127fdef5d30ea14d43137f82fd0ebf66d17e05ca5fe7491 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-40a-TriangulationCausale.ipynb + confidence: research + reviewed_code_sha: cf5b73098eb68068f39b7fb8241cfabec4971173de15cd8da1cfb73cec9ea54f + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-40b-AnalogCognitionWaves.ipynb + confidence: research + reviewed_code_sha: f505e36a85795255dbe90108564b2ce8b6ad5f9ee1fffaa300c76df12357dc5b + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-41-SAE-GeometrieFeatures.ipynb + confidence: research + reviewed_code_sha: 84dbbee96c234cd6872de20c3632bf546b9b4acfa8c19ecbe3e0c4646402e8c2 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Annexe-ProxyContextuality.ipynb + confidence: research + reviewed_code_sha: bced15d39e457a7d9b67b79c2919bc255760202145795c5fdc31326796339622 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Argumentation-BeliefTrajectories.ipynb + confidence: research + reviewed_code_sha: 276b875f9daaaba7d32edc0d159586a7fc1fae79e0df0f6ff9d1c131e71cad04 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Dissociation-PhatSelfReference.ipynb + confidence: research + reviewed_code_sha: f3217c1a39de4952490d31779116c66a52fed1cabeb967eb13623761e0c4b918 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Dissociation-SaillancePregnance.ipynb + confidence: research + reviewed_code_sha: 717102fc0849f9d5bea93677e1c231d24aa9558ff606770b175a8affcd587477 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Greffe2-EspaceAtteignable.ipynb + confidence: research + reviewed_code_sha: 78dcd615d49bea6ab90a23683f323117bb95d127c922b45f1366b4d448f15cb3 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Greffe4-VoteOnChain.ipynb + confidence: research + reviewed_code_sha: ea3e139e4c64d512b596e189ca259587939842eb40f912608bb825e45d703ac5 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Greffe5-AttributionCausale.ipynb + confidence: research + reviewed_code_sha: 7d2ccdabdcf8b3edd489ab0713a58d684c00129ac0a703d049d94b63b2854f56 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Life-SubstratCertifie.ipynb + confidence: research + reviewed_code_sha: 7fb220ae9e5e5a4cbfa098e570695ad6fcc5e1b52d8b45babf791ae7c8257d98 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-SAE-JLens-TeteATete.ipynb + confidence: research + reviewed_code_sha: cfd3b01f84cc4e455e1ff9faea8260eae4a01fad436b9fa11678f86f3ce7317f + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +- notebook_path: IIT/ICT-Series/ICT-Synthese-CrossSubstrat.ipynb + confidence: research + reviewed_code_sha: 4e7ec8a6a2239772be4441840efca20dc4bd212dfeb39051419d8c853d5fb884 + review_date: "2026-09-21" + evidence_pr: "#14831" + notes: "Recherche active — appréciation posée au sign-off du schéma." +``` diff --git a/docs/reference/scripts-reference.md b/docs/reference/scripts-reference.md index 281e1e32c2..ff0f761858 100644 --- a/docs/reference/scripts-reference.md +++ b/docs/reference/scripts-reference.md @@ -155,7 +155,7 @@ Pipeline d'audit qualité et de **matrice de coût** (EPIC #8056) + audit séman | `check_dataset_registry.py` | Cohérence registre datasets (#8055 tr.2) : verdicts `DRIFT` / `MISSING` / `CARD_REQUIRED` / `OK` (litmus : EXTRACT, pas DECIDE), sortie YAML | | `check_denominators.py` | Détecteur léger de divergence entre 3 sources (disque / forensic / catalogue), #8050 ; `--strict` → exit 1 sur drift | | `check_editorial_review.py` | Cohérence registre editorial-review (YAML ↔ catalogue) : 6 validations dont `reviewer != owner_logique`, `evidence_pr` en état `MERGED` | -| `check_lean_notebook_sorry.py` | Tally `sorry` par notebook Lean (#8051, kernel `lean4-wsl`) : strip commentaires puis compte `sorry` word-bounded — alimente l'axe scientific_review | +| `check_lean_notebook_sorry.py` | Tally `sorry` par notebook Lean (#8051, kernel `lean4-wsl`) : strip commentaires puis compte `sorry` word-bounded. **N'alimente plus l'axe `scientific_review`** depuis #14831 : l'echelle mesure le risque du contenu, pas la rigueur formelle, et le compte de `sorry` ne concerne qu'une poignee de notebooks. `sorry_free` reste rendu comme preuve a cote | | `extract_claims_vs_outputs.py` | Audit sémantique cellule-par-cellule (#8052 P0) : compare prose markdown (claims) vs sorties code (outputs) ; 5 classes de mismatch | ### Populateurs (écrivent `nb.metadata['cost']`) diff --git a/scripts/audit/check_lean_notebook_sorry.py b/scripts/audit/check_lean_notebook_sorry.py index 95da381036..225829a9af 100644 --- a/scripts/audit/check_lean_notebook_sorry.py +++ b/scripts/audit/check_lean_notebook_sorry.py @@ -3,9 +3,11 @@ """Diagnostic: per-notebook ``sorry`` tally for ``lean4-wsl`` notebooks. Context (Epic #8051 — maturity 3-axes). The third maturity axis -``scientific_review`` (UNREVIEWED / AUTHOR_REVIEWED / PEER_REVIEWED / -FORMALLY_VERIFIED) is currently non-functional for Lean notebooks: catalog -generation cannot honestly emit ``FORMALLY_VERIFIED`` because (a) cell-level +``scientific_review`` (UNASSESSED / ESTABLISHED / ADVANCED / RESEARCH depuis +#14831) ne derive plus du compte de ``sorry`` : l'echelle mesure le RISQUE du +contenu, pas la rigueur formelle de sa relecture. ``sorry_free`` reste rendu +comme preuve a cote. Ce qui suit decrit pourquoi le signal lui-meme n'est +toujours pas cable pour les notebooks Lean : (a) cell-level ``sorry`` absence does NOT imply transitive sorry-freeness (companion ``*_lean/`` lakes may carry their own debt), and (b) there is no aggregated Lean-CI sorry-count artifact for the catalog to consume. This script gathers diff --git a/scripts/audit/check_scientific_review.py b/scripts/audit/check_scientific_review.py index fa88e4dab9..fd2fff8cbd 100644 --- a/scripts/audit/check_scientific_review.py +++ b/scripts/audit/check_scientific_review.py @@ -10,14 +10,16 @@ 3. review_scope is in the valid enum {factual, algo, proba, demo, correctness, full}. 4. reviewer != last_validator of the notebook (auto-review rejection — c.997 #14831 voie 1). 5. evidence_pr (#NNNN) is in MERGED state via `gh pr view` (best-effort: skip if gh unavailable). -6. Cross-check: catalogue entries that the registry should promote (UNREVIEWED -> AUTHOR_REVIEWED/PEER_REVIEWED). +6. Cross-check: catalogue entries that the registry apprecies (UNASSESSED -> ESTABLISHED/ADVANCED/RESEARCH), + et peremption de l'appreciation quand le CODE a bouge (#14831). Exit codes: 0 = no errors (warnings allowed) 1 = errors found (DRIFT/INVALID/MISSING/etc.) — only with --check flag Anti-FP strategy (cf scientific-review-registry.md §3.3): -- AUTO_REVIEW: reviewer == last_validator du notebook (bloquant pour PEER_REVIEWED, OK pour AUTHOR_REVIEWED self-attesté). +- AUTO_REVIEW: reviewer == last_validator. Depuis #14831 le relecteur ne PILOTE plus la grade + (il est rendu comme preuve a cote) : ce n'est donc plus un motif de rejet, seulement un fait. - DRIFT_REVIEWER_ALIAS: reviewer est substring du owner_logique (heuristic best-effort). - DRIFT_PR_NOT_TOUCHING: PR diff ne touche pas le notebook path (TODO c.997+). - WARN_PR_STATE_UNKNOWN: gh CLI indisponible (CI sans auth). @@ -41,6 +43,16 @@ REVIEW_SCOPES = {"factual", "algo", "proba", "demo", "correctness", "full"} PROMOTING_SCOPES = REVIEW_SCOPES # all promote in c.997 voie 1 +# Echelle de confiance scientifique (#14831, sign-off user 2026-09-21). Ce que le +# registre a le droit d'ecrire dans `confidence` ; tout le reste retombe sur +# UNASSESSED cote catalogue (fail-CLOSED), ce qui doit se VOIR ici. +CONFIDENCE_VALUES = {"established", "advanced", "research"} +_CONFIDENCE_GRADE = { + "established": "ESTABLISHED", + "advanced": "ADVANCED", + "research": "RESEARCH", +} + def parse_registry(registry_path: Path) -> list[dict]: """Extract YAML entries from registry markdown. @@ -190,22 +202,32 @@ def check_entry(entry: dict, catalogue: list[dict], repo: str = "jsboige/CoursIA def check_promotions(entries: list[dict], catalogue: list[dict]) -> list[str]: - """Cross-check: catalogue entries that the registry should promote. - - This checks that each whitelisted notebook (with scope in PROMOTING_SCOPES - and a non-empty reviewer) has scientific_review != UNREVIEWED in the catalogue. - - If the catalogue is from BEFORE the classifier extension (c.997), it won't - have the scientific_review field — in that case, we emit an INFO note rather - than an error. + """Croise le registre et le catalogue sur l'axe 3 — nouvelle echelle (#14831). + + Trois classes, qui ne disent pas la meme chose : + + - ``DRIFT_NOT_APPRECIATED`` — le registre declare une ``confidence`` et le + catalogue rend UNASSESSED. C'est un cablage casse : le signal existe et + n'arrive pas. **Erreur.** + - ``STALE_APPRECIATION`` — le catalogue rend ``scientific_review_stale``. Le + code a bouge depuis la relecture, l'appreciation ne porte plus sur ce qui + est la, une nouvelle revue est due. C'est le regime d'audit permanent + voulu par le sign-off : **un signal de travail, pas un defaut**, donc une + note, jamais une erreur. + - ``WARN_NO_CODE_ANCHOR`` — le registre declare une ``confidence`` sans + ``reviewed_code_sha``. L'appreciation ne pourra jamais se perimer : elle + est immortelle par omission. Les entrees anterieures a #14831 sont + legitimement dans cet etat, d'ou l'avertissement plutot que l'erreur. + + Pourquoi ce recablage : la version precedente testait ``sr == "UNREVIEWED"``, + une valeur que la nouvelle echelle n'emet plus. Elle aurait rendu 0 finding + sur un corpus entierement non apprecie — et un zero d'instrument aveugle se + lit comme un feu vert. """ notes = [] cat_paths = {nb["path"]: nb for nb in catalogue} for entry in entries: nb_path = entry.get("notebook_path") - scope = entry.get("review_scope") - if scope not in PROMOTING_SCOPES: - continue nb = cat_paths.get(nb_path) if nb is None: continue @@ -213,10 +235,36 @@ def check_promotions(entries: list[dict], catalogue: list[dict]) -> list[str]: if sr is None: notes.append(f"INFO_NO_FIELD: {nb_path} -- catalogue lacks scientific_review field") continue - if sr == "UNREVIEWED": + + confidence = (entry.get("confidence") or "").strip().lower() + if confidence and confidence not in CONFIDENCE_VALUES: + notes.append( + f"INVALID_CONFIDENCE: {nb_path} -- confidence={entry.get('confidence')!r} " + f"hors de {sorted(CONFIDENCE_VALUES)} (le catalogue retombera sur UNASSESSED)" + ) + continue + + if confidence: + attendu = _CONFIDENCE_GRADE[confidence] + if sr == "UNASSESSED": + notes.append( + f"DRIFT_NOT_APPRECIATED: {nb_path} -- scientific_review=UNASSESSED " + f"alors que le registre declare confidence={confidence} (attendu {attendu})" + ) + elif sr != attendu: + notes.append( + f"DRIFT_GRADE_MISMATCH: {nb_path} -- catalogue={sr}, registre={attendu}" + ) + if not (entry.get("reviewed_code_sha") or "").strip(): + notes.append( + f"WARN_NO_CODE_ANCHOR: {nb_path} -- confidence={confidence} sans " + f"reviewed_code_sha : l'appreciation ne pourra jamais se perimer" + ) + + if nb.get("scientific_review_stale"): notes.append( - f"DRIFT_NOT_PROMOTED: {nb_path} -- scientific_review=UNREVIEWED " - f"despite registry signal (c.997 voie 1)" + f"STALE_APPRECIATION: {nb_path} -- le code a change depuis la revue " + f"(grade {sr} conservee, nouvelle revue due)" ) return notes diff --git a/scripts/notebook_tools/generate_catalog.py b/scripts/notebook_tools/generate_catalog.py index 8616e0f5bc..c0170ed185 100644 --- a/scripts/notebook_tools/generate_catalog.py +++ b/scripts/notebook_tools/generate_catalog.py @@ -843,38 +843,118 @@ def classify_reproducibility( # Axe 3 — revue scientifique. Heuristique pauvre par defaut (UNREVIEWED) : # la promotion necessite un signal catalogue (PR label, peer-review attache, etc.) # Cette fonction est un placeholder stable qui ne pretend pas deviner la revue. +# --- Axe 3 : confiance scientifique (#14831, sign-off user 2026-09-21) ------- +# +# L'ancienne echelle (UNREVIEWED / AUTHOR_REVIEWED / PEER_REVIEWED / +# FORMALLY_VERIFIED) mesurait la PROVENANCE d'une relecture : qui a relu, et +# avec quelle rigueur formelle. Elle etait *inversee* dans ses effets — une +# serie de recherche active relue par des pairs atteignait le haut de l'echelle, +# pendant qu'un notebook de cours classique, universellement admis et sans +# aucun risque, restait UNREVIEWED faute de reviewer nomme. +# +# La nouvelle echelle mesure autre chose : **notre propre appreciation du +# niveau de confiance scientifique dans le contenu**. Elle ne classe pas les +# relecteurs, elle classe le RISQUE de ce qui est enonce. +SCIENTIFIC_CONFIDENCE = { + # Rien n'a ete apprecie. Ce n'est PAS un mauvais score : c'est l'absence de + # jugement, et c'est le defaut honnete. + "UNASSESSED": 0, + # Contenu communement admis et universellement pratique. Le notebook ne + # prend aucun risque sur ce qu'il affirme. + "ESTABLISHED": 1, + # Protocoles plus avances, executions moins controlees, theories recentes, + # interpretations discutables. Le contenu tient, mais il engage. + "ADVANCED": 2, + # Recherche active — ICT au premier chef. Le contenu est explicitement en + # cours d'elaboration, et le dire est la seule position honnete. + "RESEARCH": 3, +} + +# Ce que le registre ecrit (minuscules) -> ce que le catalogue emet. +_CONFIDENCE_FROM_REGISTRY = { + "established": "ESTABLISHED", + "advanced": "ADVANCED", + "research": "RESEARCH", +} + + +def code_source_sha(notebook: dict) -> str: + """Empreinte du CODE d'un notebook — source des cellules code, rien d'autre. + + C'est l'ancre de la retrogradation : une appreciation scientifique porte sur + ce que le notebook CALCULE et AFFIRME, donc elle survit a l'enrichissement + markdown et meurt au changement de code. + + **Trois exclusions deliberees, chacune pour une raison mesuree :** + + - **le markdown** — la campagne de densification a modifie 178 notebooks en + trois semaines sans toucher une ligne de code. Une projection qui + l'inclurait retrograderait tout le corpus au premier passage, et une + retrogradation qui frappe tout ne signale plus rien. + - **les sorties** — une re-execution change les sorties sans changer ce que + le notebook affirme. Les inclure ferait de chaque passage kernel une + retrogradation. + - **`execution_count`** — pur artefact d'ordre d'execution. + + **Empreinte de CONTENU, jamais un blob SHA git** : un squash-merge reecrit + les blobs et tuerait l'ancre a chaque merge (#11919). Le contenu, lui, + traverse le squash. + """ + import hashlib + + h = hashlib.sha256() + for cell in notebook.get("cells", []): + if cell.get("cell_type") != "code": + continue + source = cell.get("source", "") + if isinstance(source, list): + source = "".join(source) + h.update(source.encode("utf-8")) + h.update(bytes([0])) # separateur sans echappement + return h.hexdigest() + + def classify_scientific_review( notebook: dict, *, + confidence: str | None = None, + reviewed_code_sha: str | None = None, scientific_reviewed_by: str | None = None, last_validator: str | None = None, sorry_free: bool = False, -) -> str: - """Axe 3 — revue scientifique (UNREVIEWED/AUTHOR_REVIEWED/PEER_REVIEWED/FORMALLY_VERIFIED). - - Cf docs/PARCOURS.md. Sans signal explicite, on retombe sur UNREVIEWED. - - **Contrat honnete pour FORMALLY_VERIFIED** (issue #8051). La promotion vers - FORMALLY_VERIFIED exige une PREUVE, pas une simple presence : - - un compagnon `.lean` EXISTANT n'est PAS une preuve — un lake compagnon - peut porter sa propre sorry-debt (9/18 notebooks lean4-wsl ont du sorry - cell-level, cf scripts/audit/check_lean_notebook_sorry.py #8351). - Detecter le compagnon et emettre FORMALLY_VERIFIED = faux claim. - - un benchmark multi-seed est une rigueur EMPIRIQUE, pas une preuve - formelle (formelle = mathematique, sans axiome admis). - Le seul signal honnete est ``sorry_free`` : un lake Lean dont le build - reussit SANS aucun ``sorry`` (artifact Lean-CI a brancher ; absent pour - l'instant, donc ``sorry_free`` default False -> FORMALLY_VERIFIED reste - inatteignable tant que l'artifact n'est pas cable). Le catalog-gen ne peut - PAS deviner la sorry-freedom ; il consomme un artifact produit en amont. +) -> dict: + """Axe 3 — confiance scientifique. Rend un DICT, plus une chaine. + + Sign-off user 2026-09-21 (#14831). Deux changements de nature : + + 1. **L'echelle mesure le risque, pas la provenance** — cf SCIENTIFIC_CONFIDENCE. + 2. **L'appreciation se perime quand le CODE bouge.** Elle porte sur ce que le + notebook calcule ; si le calcul change, l'appreciation ne porte plus sur + ce qui est la. La grade est CONSERVEE et `stale` passe a True : perdre la + grade effacerait l'information (« personne n'a jamais apprecie ») alors + que le fait est autre (« quelqu'un a apprecie, puis le code a bouge »). + C'est ce qui rend l'audit permanent, et c'est voulu. + + `sorry_free` et `scientific_reviewed_by` ne PILOTENT plus la grade — ils sont + rendus comme **preuves a cote**. Le compte de `sorry` ne concerne qu'une + poignee de notebooks : en faire l'indicateur unique d'un axe qui couvre tout + le corpus etait un defaut d'echelle. """ - if sorry_free: - return "FORMALLY_VERIFIED" - if scientific_reviewed_by and last_validator and scientific_reviewed_by != last_validator: - return "PEER_REVIEWED" - if scientific_reviewed_by and scientific_reviewed_by == last_validator: - return "AUTHOR_REVIEWED" - return "UNREVIEWED" + grade = _CONFIDENCE_FROM_REGISTRY.get((confidence or "").strip().lower(), "UNASSESSED") + current_sha = code_source_sha(notebook) + stale = bool(grade != "UNASSESSED" and reviewed_code_sha and reviewed_code_sha != current_sha) + return { + "grade": grade, + "stale": stale, + "code_sha": current_sha, + "reviewed_code_sha": reviewed_code_sha or "", + # Preuves conservees, sans effet sur la grade. + "reviewed_by": scientific_reviewed_by or "", + "peer": bool( + scientific_reviewed_by and last_validator and scientific_reviewed_by != last_validator + ), + "sorry_free": bool(sorry_free), + } # Agregat retro-compatible : ancien label monolithique `maturity` derive des 3 axes. @@ -885,18 +965,29 @@ def aggregate_maturity( scientific_review: str, *, is_template: bool = False, + production_signed: bool = False, ) -> str: - """Calcule l'ancien label `maturity` (TEMPLATE/PRODUCTION/BETA/ALPHA/DRAFT) - a partir des 3 nouveaux axes. Permet de preserver la retro-compatibilite avec - tous les consommateurs qui lisent `maturity` directement. + """Calcule le label `maturity` (TEMPLATE/PRODUCTION/BETA/ALPHA/DRAFT). + + **PRODUCTION n'est plus derive** (sign-off user 2026-09-21, #14831). + + PRODUCTION ne decrit pas une propriete mesurable du fichier : il dit que le + responsable pedagogique a appose son tampon, et juge le notebook finalise + pour etre utilise en cours **par d'autres**. Aucune combinaison d'axes ne + peut produire ce fait — le calculer revenait a fabriquer une signature. + + Le signal vient donc de `docs/notebook-metadata/production-scope.md`, dont la + colonne « Verdict » est la surface de decision. Un notebook non tranche reste + BETA, ce qui est le verdict correct : l'auteur enseigne lui-meme sur les beta + et les beta-teste avec ses etudiants. + + `scientific_review` reste **necessaire mais pas suffisant** : il est exige + par le validateur de perimetre (`check_production_scope.py`), pas par cet + agregat — un axe qui gate ne doit pas etre le meme objet que l'axe qui decrit. """ if is_template: return "TEMPLATE" - if ( - editorial == "FINAL" - and reproducibility in ("EXECUTED", "REPRODUCED") - and scientific_review in ("PEER_REVIEWED", "FORMALLY_VERIFIED") - ): + if production_signed: return "PRODUCTION" if editorial in ("BETA", "FINAL") and reproducibility in ("EXECUTED", "REPRODUCED"): return "BETA" @@ -963,20 +1054,23 @@ def analyze_notebook(nb_path: Path, pedagogical: bool, git_meta: dict | None = N # git-derived, same convention as _merge_curated_fields). rel_str_for_registry = str(rel) if hasattr(rel, "__fspath__") else str(rel) sr_registry = _load_scientific_review_registry() - sr_curated = sr_registry.get(rel_str_for_registry) or sr_registry.get(rel_str_for_registry.replace("\\", "/")) + sr_curated = sr_registry.get(rel_str_for_registry) or sr_registry.get(rel_str_for_registry.replace("\\", "/")) or {} + # `sorry_free` reste rendu comme PREUVE A COTE, il ne pilote plus la grade : + # le compte de sorry ne concerne qu'une poignee de notebooks, et en faire + # l'indicateur d'un axe qui couvre tout le corpus etait un defaut d'echelle. scientific_review = classify_scientific_review( notebook, - scientific_reviewed_by=sr_curated or gm.get("scientific_reviewed_by"), + confidence=sr_curated.get("confidence"), + reviewed_code_sha=sr_curated.get("reviewed_code_sha"), + scientific_reviewed_by=sr_curated.get("reviewer") or gm.get("scientific_reviewed_by"), last_validator=gm.get("last_validator"), - # FORMALLY_VERIFIED exige un lake Lean sorry-free PROUVE (artifact Lean-CI), - # pas une simple presence de compagnon (cf #8051, #8351). Le champ - # git_meta 'sorry_free' n'est pas encore cable -> default False honnete : - # aucun notebook ne peut etre faussement claim FORMALLY_VERIFIED. sorry_free=gm.get("sorry_free", False), ) + production_signed = production_is_signed(_production_scope(), rel_str_for_registry) maturity = aggregate_maturity( - editorial, reproducibility, scientific_review, + editorial, reproducibility, scientific_review["grade"], is_template=is_template, + production_signed=production_signed, ) return { @@ -989,7 +1083,13 @@ def analyze_notebook(nb_path: Path, pedagogical: bool, git_meta: dict | None = N "maturity": maturity, "editorial": editorial, "reproducibility": reproducibility, - "scientific_review": scientific_review, + # La grade reste une CHAINE : tous les consommateurs lisent ce champ + # tel quel. Le detail (peremption, empreintes, preuves) vit a cote, + # pour que l'ajout n'oblige personne a changer sa lecture. + "scientific_review": scientific_review["grade"], + "scientific_review_stale": scientific_review["stale"], + "scientific_review_detail": scientific_review, + "production_signed": production_signed, "last_success_sha": gm.get("last_success_sha", ""), "executed_at": gm.get("executed_at", ""), "forensic_category": gm.get("forensic_category", ""), @@ -1051,12 +1151,36 @@ def _load_main_catalog() -> dict[str, dict]: # YAML pose le signal canonique : sans signal, classify_scientific_review retombe # sur UNREVIEWED (design délibéré cf #8051). Le champ est dans CURATED_GIT_FIELDS # (l.949) donc préservable par _merge_curated_fields. -def _load_scientific_review_registry() -> dict[str, str]: +def _registry_entry_is_usable(entry) -> bool: + """Une entree de registre porte-t-elle un signal exploitable ? + + Depuis #14831, **`confidence` suffit** : une appreciation de risque n'est pas + une relecture, et les entrees qui en portent une n'ont pas de `reviewer` par + conception. Exiger `reviewer` jetait 77 entrees sur 80 (mesure du + 2026-09-21) sans rien signaler -- le registre paraissait simplement vide. + + `reviewer` reste accepte seul : les entrees anterieures a #14831 continuent + d'etre lues, et leur relecteur est rendu comme preuve a cote. + + Les entrees gabarit (``) sont ecartees dans tous les cas. + """ + if not entry or not entry.get("notebook_path"): + return False + for champ in ("notebook_path", "reviewer", "evidence_pr", "confidence"): + if "<" in (entry.get(champ) or ""): + return False + return bool(entry.get("confidence") or entry.get("reviewer")) + + +def _load_scientific_review_registry() -> dict[str, dict]: """Load the curated scientific review registry (cf scientific-review-registry.md). - Returns a dict keyed by notebook_path with reviewer as value. Empty dict if - the registry file is missing or malformed (fail-OPEN : ne lève pas, retombe - sur UNREVIEWED comme avant le câblage c.997). + Returns a dict keyed by notebook_path with the FULL entry as value (#14831, + sign-off user 2026-09-21) : l'axe a besoin de `confidence` et de + `reviewed_code_sha`, plus seulement du nom du relecteur. + + Empty dict si le registre est absent ou malforme (fail-OPEN : ne lève pas, + retombe sur UNASSESSED — l'absence d'appreciation, pas un mauvais score). """ registry_path = REPO_ROOT / "docs" / "notebook-metadata" / "scientific-review-registry.md" if not registry_path.exists(): @@ -1074,37 +1198,160 @@ def _load_scientific_review_registry() -> dict[str, str]: if not stripped or stripped.startswith("#"): continue if stripped.startswith("- "): - if ( - current - and current.get("notebook_path") - and current.get("reviewer") - # Skip template/example entries (placeholders in angle brackets) - and "<" not in current.get("notebook_path", "") - and "<" not in current.get("reviewer", "") - and "<" not in current.get("evidence_pr", "") - ): - registry[current["notebook_path"]] = current["reviewer"] + if _registry_entry_is_usable(current): + registry[current["notebook_path"]] = dict(current) kv = stripped[2:] key, _, value = kv.partition(":") current = {key.strip(): value.strip().strip('"').strip("'")} elif current is not None and ":" in stripped: key, _, value = stripped.partition(":") current[key.strip()] = value.strip().strip('"').strip("'") - if ( - current - and current.get("notebook_path") - and current.get("reviewer") - # Skip template/example entries (placeholders in angle brackets) - and "<" not in current.get("notebook_path", "") - and "<" not in current.get("reviewer", "") - and "<" not in current.get("evidence_pr", "") - ): - registry[current["notebook_path"]] = current["reviewer"] + if _registry_entry_is_usable(current): + registry[current["notebook_path"]] = dict(current) return registry except (UnicodeDecodeError, ValueError): return {} +# --- Tampon PRODUCTION (#14831, sign-off user 2026-09-21) -------------------- +# +# PRODUCTION n'est pas une propriete du fichier : c'est la signature du +# responsable pedagogique, qui juge le notebook finalise pour etre utilise en +# cours PAR D'AUTRES. L'auteur enseigne lui-meme sur les beta et les beta-teste +# avec ses etudiants -- « ca n'est pas forcement un probleme ». Un notebook non +# tranche reste donc BETA, et c'est le verdict correct, pas un manque. +# +# La surface de decision est la colonne « Verdict » du tableau « La passe par +# serie » de production-scope.md : 13 lignes, une reponse par ligne. La strate A +# en dessous ENUMERE les 99 chemins signes par ces reponses. +# +# **Le join ligne -> chemins passe par le commentaire de groupe** de la strate A +# (``), resolu via le notebook « Tete de serie ». +# Deux autres joins ont ete mesures sur le document reel et rejetes : +# - par LIBELLE de serie : aucune correspondance dans le catalogue, devine ; +# - par REPERTOIRE de la tete : sous-signe (90/99) -- SmartContracts s'etale +# sur `00-Foundations` et `01-Solidity-Foundation`. +# Le join retenu rend 13 groupes / 99 chemins, et resout 12 des 13 lignes avec +# un N conforme a la table. +PRODUCTION_SCOPE_PATH = REPO_ROOT / "docs" / "notebook-metadata" / "production-scope.md" + +# Ce qui vaut signature dans la colonne Verdict. Fail-CLOSED : tout ce qui n'est +# pas reconnu laisse la serie NON signee. Une colonne vide -- l'etat actuel des +# 13 lignes -- ne signe rien, et c'est l'etat correct tant que le user n'a pas +# repondu. +_PRODUCTION_YES = ("oui", "yes", "o") + +_SCOPE_STRATE_RE = re.compile(r"^##\s+Strate\s+([ABC])\b") +_SCOPE_GROUP_RE = re.compile(r"^$") +_SCOPE_ITEM_RE = re.compile(r"^- \[([ xX])\]\s*`([^`]+)`") + + +_PRODUCTION_SCOPE_CACHE: dict = {} + + +def _production_scope() -> dict: + """Scope memoise : sans ca le document serait relu 1124 fois.""" + if "v" not in _PRODUCTION_SCOPE_CACHE: + scope = _load_production_scope() + for warn in scope.get("unresolved", []): + print("WARN production-scope: %s" % warn, file=sys.stderr) + _PRODUCTION_SCOPE_CACHE["v"] = scope + return _PRODUCTION_SCOPE_CACHE["v"] + + +def _load_production_scope(scope_path=None) -> dict: + """Lit le tampon de production. Fail-CLOSED, et silencieux si le doc est absent. + + Rend ``{"signed": set[str], "unresolved": list[str]}`` ou ``signed`` porte des + chemins de notebook **exacts**, tels qu'ecrits dans la strate A. + + ``unresolved`` nomme les lignes qui signent mais dont la tete de serie ne + resout aucun groupe : elles ne signent alors RIEN, et l'appelant le dit. Une + ligne qui signe zero notebook en silence est la panne que ce champ existe + pour rendre impossible. + """ + path = Path(scope_path) if scope_path else PRODUCTION_SCOPE_PATH + out: dict = {"signed": set(), "unresolved": []} + if not path.exists(): + return out + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return out + + strate = group = None + groups: dict = {} + rows = [] + for line in text.splitlines(): + s = line.strip() + m = _SCOPE_STRATE_RE.match(s) + if m: + strate, group = m.group(1), None + continue + if s.startswith("## "): + strate, group = None, None + continue + m = _SCOPE_GROUP_RE.match(s) + if m: + group = m.group(1) + continue + m = _SCOPE_ITEM_RE.match(s) + if m and strate == "A": + ticked, nb = m.group(1).lower() == "x", m.group(2) + groups.setdefault(group, []).append(nb) + # Une case cochee vaut signature par elle-meme : c'est le meme geste, + # pose notebook par notebook au lieu de serie par serie. + if ticked: + out["signed"].add(nb) + continue + if s.startswith("|") and not s.startswith("|--"): + cells = [c.strip() for c in s.strip("|").split("|")] + if len(cells) == 5 and cells[0].lower() not in ("serie", "série"): + rows.append(cells) + + for serie, _cours, tete, _n, verdict in rows: + low = verdict.lower() + head = low.split("sauf")[0].strip().strip("*` ") + if head not in _PRODUCTION_YES: + continue + base = tete.strip("` ") + hits = [g for g, paths in groups.items() + if any(nb.endswith("/" + base) for nb in paths)] + if len(hits) != 1: + out["unresolved"].append( + "%s -- tete de serie %r resout %d groupe(s) de strate A" + % (serie, base, len(hits)) + ) + continue + # « oui sauf X » : une exclusion nommee l'emporte toujours sur la + # signature de sa serie -- l'inverse transformerait une reserve en + # approbation. + excluded = [] + if "sauf" in low: + rest = verdict.split("sauf", 1)[1] + excluded = [n.strip().strip("`*, ") for n in rest.replace(";", ",").split(",")] + excluded = [n for n in excluded if n] + for nb in groups[hits[0]]: + if any(nb.endswith("/" + n) or Path(nb).name == n for n in excluded): + continue + out["signed"].add(nb) + return out + + +def production_is_signed(scope: dict, notebook_path: str) -> bool: + """Le tampon couvre-t-il CE notebook ? Comparaison de chemins exacts. + + Le document ecrit ses chemins en separateurs POSIX ; l'appelant peut porter + des separateurs Windows. La normalisation est la seule tolerance accordee -- + aucune correspondance approximative, aucun repli sur le nom de fichier seul, + qui signerait des homonymes d'autres series. + """ + if not scope or not scope.get("signed"): + return False + norm = str(notebook_path).replace("\\", "/").lstrip("./") + return norm in scope["signed"] + + def _merge_curated_fields( entries: list[dict], main_catalog: dict[str, dict], @@ -1273,8 +1520,9 @@ def _apply_editorial_review_promotion(entries, registry=None): entry["maturity"] = aggregate_maturity( entry["editorial"], entry.get("reproducibility", "UNTESTED"), - entry.get("scientific_review", "UNREVIEWED"), + entry.get("scientific_review", "UNASSESSED"), is_template=False, + production_signed=bool(entry.get("production_signed")), ) promoted += 1 diff --git a/scripts/notebook_tools/tests/test_generate_catalog.py b/scripts/notebook_tools/tests/test_generate_catalog.py index a2daa78de2..e5ba658f06 100644 --- a/scripts/notebook_tools/tests/test_generate_catalog.py +++ b/scripts/notebook_tools/tests/test_generate_catalog.py @@ -620,56 +620,193 @@ def test_draft_too_many_todos(self): assert classify_maturity(nb, code_cells, "Python 3") == "DRAFT" -# --- classify_scientific_review (axe 3, #8051) --- +# --- classify_scientific_review (axe 3, #8051 puis #14831) --- class TestClassifyScientificReview: - """Axe 3 — revue scientifique. FORMALLY_VERIFIED exige une PREUVE sorry-free, - pas une simple presence de compagnon (cf #8051, #8351).""" - - def test_default_is_unreviewed(self): - # Sans aucun signal -> UNREVIEWED (jamais de claim auto). - assert classify_scientific_review({}) == "UNREVIEWED" - - def test_author_reviewed_self_validates(self): - # reviewed_by == last_validator = auteur se valide lui-meme. - assert classify_scientific_review( - {}, scientific_reviewed_by="a@b.c", last_validator="a@b.c" - ) == "AUTHOR_REVIEWED" - - def test_peer_reviewed_distinct_validator(self): - # reviewed_by != last_validator = peer review effective. - assert classify_scientific_review( - {}, scientific_reviewed_by="a@b.c", last_validator="c@d.e" - ) == "PEER_REVIEWED" - - def test_sorry_free_yields_formally_verified(self): - # Le SEUL chemin honnete vers FORMALLY_VERIFIED = preuve sorry-free. - assert classify_scientific_review({}, sorry_free=True) == "FORMALLY_VERIFIED" - - def test_sorry_free_dominates_peer(self): - # sorry_free (preuve formelle) > peer review. - assert classify_scientific_review( - {}, sorry_free=True, - scientific_reviewed_by="a@b.c", last_validator="c@d.e", - ) == "FORMALLY_VERIFIED" - - def test_companion_presence_alone_does_not_verify(self): - # REGRESSION GUARD (#8051): l'ancien code emettait FORMALLY_VERIFIED des - # qu un compagnon .lean existait (signal mort, mais piege si active). - # Un lake compagnon peut porter sa sorry-debt -> presence != preuve. - # On verifie que rien dans l'API ne permet de claim FORMALLY_VERIFIED - # sans sorry_free=True (pas de param has_lean_companion). + """Axe 3 — confiance scientifique, nouvelle echelle (sign-off user 2026-09-21). + + Ces tests epinglaient l'ancienne echelle de PROVENANCE (qui a relu, avec + quelle rigueur formelle). Ils epinglent maintenant l'echelle de RISQUE + (UNASSESSED / ESTABLISHED / ADVANCED / RESEARCH), et la peremption sur + changement de code. Aucun n'a ete supprime : chacun dit ce qui a change et + pourquoi, pour qu'une regression ne puisse pas se faire passer pour la + migration. + """ + + def test_sans_appreciation_c_est_unassessed(self): + # L'absence de jugement n'est PAS un mauvais score, et c'est le defaut. + assert classify_scientific_review({})["grade"] == "UNASSESSED" + + def test_les_trois_grades_viennent_du_registre(self): + for declared, attendu in (("established", "ESTABLISHED"), + ("advanced", "ADVANCED"), + ("research", "RESEARCH")): + got = classify_scientific_review({}, confidence=declared)["grade"] + assert got == attendu, "%s -> %s (attendu %s)" % (declared, got, attendu) + + def test_une_valeur_inconnue_ne_promeut_rien(self): + """Fail-CLOSED : un registre qui ecrit n'importe quoi ne signe rien. + + C'est la propriete qui empeche le label-gaming — une valeur non reconnue + retombe sur l'absence d'appreciation, jamais sur une grade par defaut. + """ + for bidon in ("PEER_REVIEWED", "formally_verified", "excellent", "", " "): + assert classify_scientific_review({}, confidence=bidon)["grade"] == "UNASSESSED" + + def test_le_relecteur_ne_pilote_plus_la_grade(self): + """REGRESSION GUARD (#14831) : l'ancienne echelle promouvait sur le seul + fait qu'un relecteur soit nomme et distinct de l'auteur. C'etait mesurer + la provenance, pas la confiance — et ca laissait tout le cours classique + a UNREVIEWED faute de reviewer nomme.""" + out = classify_scientific_review( + {}, scientific_reviewed_by="a@b.c", last_validator="c@d.e") + assert out["grade"] == "UNASSESSED", "un relecteur nomme ne vaut pas une appreciation" + assert out["peer"] is True, "mais le fait reste rendu comme preuve a cote" + + def test_sorry_free_est_une_preuve_pas_une_grade(self): + """REGRESSION GUARD (#14831) : `sorry_free` emettait FORMALLY_VERIFIED, + c.-a-d. qu'un indicateur ne concernant qu'une poignee de notebooks Lean + pilotait un axe couvrant tout le corpus. Il est conserve, a cote.""" + out = classify_scientific_review({}, sorry_free=True) + assert out["grade"] == "UNASSESSED" + assert out["sorry_free"] is True + + def test_la_presence_d_un_compagnon_ne_verifie_toujours_rien(self): + # REGRESSION GUARD (#8051), conserve : l'API ne doit offrir AUCUN chemin + # ou la simple presence d'un compagnon .lean ou d'un benchmark promeut. import inspect sig = inspect.signature(classify_scientific_review) - assert "sorry_free" in sig.parameters - assert "has_lean_companion" not in sig.parameters, ( - "has_lean_companion retiré : presence de compagnon != preuve sorry-free (#8051)" - ) - assert "has_multi_seed_benchmark" not in sig.parameters, ( - "has_multi_seed_benchmark retiré : rigueur empirique != preuve formelle" + assert "has_lean_companion" not in sig.parameters + assert "has_multi_seed_benchmark" not in sig.parameters + assert "confidence" in sig.parameters, "la grade vient du registre, declaree" + + # --- peremption : l'audit permanent ------------------------------------- + + def _nb(self, code_src, md="du markdown"): + return {"cells": [ + {"cell_type": "markdown", "source": md}, + {"cell_type": "code", "source": code_src, "execution_count": 1, + "outputs": [{"output_type": "stream", "text": "sortie"}]}, + ]} + + def test_le_markdown_ne_perime_pas_une_appreciation(self): + """LA propriete qui rend la retrogradation utilisable. + + La campagne de densification a modifie 178 notebooks en trois semaines + sans toucher une ligne de code. Une projection incluant le markdown + retrograderait tout le corpus au premier passage — et une retrogradation + qui frappe tout ne signale plus rien. + """ + a = self._nb("x = 1", md="intro") + b = self._nb("x = 1", md="intro tres enrichie, lectures chiffrees, etc.") + from generate_catalog import code_source_sha + assert code_source_sha(a) == code_source_sha(b) + sha = code_source_sha(a) + out = classify_scientific_review(b, confidence="established", reviewed_code_sha=sha) + assert out["stale"] is False + assert out["grade"] == "ESTABLISHED" + + def test_une_re_execution_ne_perime_pas_une_appreciation(self): + """Les sorties changent a chaque passage kernel ; les inclure ferait de + toute re-execution une retrogradation.""" + from generate_catalog import code_source_sha + a = self._nb("x = 1") + b = self._nb("x = 1") + b["cells"][1]["outputs"] = [{"output_type": "stream", "text": "AUTRE sortie"}] + b["cells"][1]["execution_count"] = 42 + assert code_source_sha(a) == code_source_sha(b) + + def test_un_changement_de_code_perime_l_appreciation(self): + """Le controle positif : sans lui, les trois tests ci-dessus seraient + satisfaits par une fonction qui ne perime JAMAIS rien.""" + from generate_catalog import code_source_sha + ancien = code_source_sha(self._nb("x = 1")) + out = classify_scientific_review( + self._nb("x = 2"), confidence="research", reviewed_code_sha=ancien) + assert out["stale"] is True + assert out["grade"] == "RESEARCH", "la grade est CONSERVEE, seule `stale` bascule" + + def test_une_grade_absente_n_est_jamais_perimee(self): + """UNASSESSED + stale n'aurait aucun sens : il n'y a rien a perimer.""" + from generate_catalog import code_source_sha + out = classify_scientific_review( + self._nb("x = 2"), reviewed_code_sha=code_source_sha(self._nb("x = 1"))) + assert out["grade"] == "UNASSESSED" + assert out["stale"] is False + + def test_sans_ancre_enregistree_rien_ne_se_perime(self): + """Les entrees de registre anterieures a #14831 n'ont pas d'ancre : elles + ne doivent pas toutes basculer perimees au premier passage.""" + out = classify_scientific_review(self._nb("x = 2"), confidence="advanced") + assert out["grade"] == "ADVANCED" + assert out["stale"] is False + + +# --- _registry_entry_is_usable (#14831) --- + +class TestRegistryEntryIsUsable: + """Quelle entree de registre porte un signal exploitable. + + **Le defaut que ces tests epinglent a reellement eu lieu**, le 2026-09-21 : + le loader exigeait `reviewer`, herite de l'ancienne semantique ou la grade + venait du relecteur. Les 77 entrees `confidence: research` de la serie ICT, + qui n'ont pas de relecteur *par conception*, etaient jetees en silence -- + 3 entrees lues sur 80, et un registre qui paraissait simplement vide. + + C'est la forme canonique du zero d'instrument aveugle : rien n'echoue, et le + resultat se lit comme « personne n'a encore apprecie ». + """ + + def test_une_confidence_suffit_sans_relecteur(self): + from generate_catalog import _registry_entry_is_usable + assert _registry_entry_is_usable( + {"notebook_path": "IIT/ICT-Series/ICT-01.ipynb", "confidence": "research"} + ) is True + + def test_un_relecteur_suffit_sans_confidence(self): + """Les entrees anterieures a #14831 continuent d'etre lues.""" + from generate_catalog import _registry_entry_is_usable + assert _registry_entry_is_usable( + {"notebook_path": "Sudoku/nb.ipynb", "reviewer": "a@b.c"} + ) is True + + def test_sans_signal_l_entree_est_ecartee(self): + from generate_catalog import _registry_entry_is_usable + assert _registry_entry_is_usable({"notebook_path": "S/nb.ipynb"}) is False + assert _registry_entry_is_usable({"confidence": "research"}) is False + assert _registry_entry_is_usable({}) is False + assert _registry_entry_is_usable(None) is False + + def test_les_gabarits_restent_ecartes(self): + """Le bloc YAML du schema est dans le meme fichier que les entrees : sans + ce filtre, `` deviendrait une entree.""" + from generate_catalog import _registry_entry_is_usable + assert _registry_entry_is_usable( + {"notebook_path": "", "confidence": "research"}) is False + assert _registry_entry_is_usable( + {"notebook_path": "S/nb.ipynb", "confidence": ""}) is False + assert _registry_entry_is_usable( + {"notebook_path": "S/nb.ipynb", "reviewer": ""}) is False + + def test_le_registre_reel_porte_les_deux_familles(self): + """Controle positif sur le fichier reel : sans lui, tout ce qui precede + serait satisfait par un loader qui ne lit aucun bloc. + + Le registre porte deux familles depuis #14831 -- des appreciations sans + relecteur, et des entrees historiques avec relecteur. Le loader doit + rendre les deux. + """ + from generate_catalog import _load_scientific_review_registry + reg = _load_scientific_review_registry() + if not reg: + import pytest + pytest.skip("registre absent de cet arbre") + avec_confidence = [v for v in reg.values() if v.get("confidence")] + assert avec_confidence, "aucune appreciation lue : le filtre les jette a nouveau" + assert any(not v.get("reviewer") for v in avec_confidence), ( + "aucune entree sans relecteur : c'est precisement la famille que le " + "filtre historique jetait" ) - # Et sans sorry_free, on retombe sur UNREVIEWED meme si on essaye. - assert classify_scientific_review({}) == "UNREVIEWED" # --- _is_exercise_stub --- @@ -1501,16 +1638,35 @@ def test_promotes_whitelisted_beta_to_final(self): # maturity recomputed from FINAL (BETA -> PRODUCTION needs peer review, # so with scientific_review=UNREVIEWED it stays BETA — aggregate logic) assert result[0]["maturity"] == aggregate_maturity( - "FINAL", "EXECUTED", "UNREVIEWED") + "FINAL", "EXECUTED", "UNASSESSED") + + def test_aucune_combinaison_d_axes_ne_fabrique_production(self): + """REGRESSION GUARD (#14831, sign-off user 2026-09-21). - def test_maturity_propagates_to_production_when_peer_reviewed(self): - """FINAL + EXECUTED + PEER_REVIEWED -> maturity PRODUCTION.""" - entries = [self._entry( - "S/nb.ipynb", scientific_review="PEER_REVIEWED")] + Ce test epinglait `FINAL + EXECUTED + PEER_REVIEWED -> PRODUCTION`. + PRODUCTION ne decrit pas une propriete du fichier : il dit que le + responsable pedagogique a appose son tampon et juge le notebook + finalise pour etre utilise en cours PAR D'AUTRES. Le deriver d'axes + mesures revenait a fabriquer une signature. + + Le meilleur etat atteignable sans tampon est donc BETA — et c'est le + verdict correct : l'auteur enseigne lui-meme sur les beta. + """ + entries = [self._entry("S/nb.ipynb", scientific_review="RESEARCH")] registry = {"S/nb.ipynb": { "reviewer": "reviewer-x", "review_scope": "full"}} result = _apply_editorial_review_promotion(entries, registry=registry) assert result[0]["editorial"] == "FINAL" + assert result[0]["maturity"] == "BETA" + + def test_le_tampon_signe_produit_production(self): + """Le controle positif du test precedent : sans lui, PRODUCTION pourrait + etre devenu inatteignable au lieu d'etre devenu signe.""" + entries = [self._entry("S/nb.ipynb", scientific_review="ESTABLISHED")] + entries[0]["production_signed"] = True + registry = {"S/nb.ipynb": { + "reviewer": "reviewer-x", "review_scope": "full"}} + result = _apply_editorial_review_promotion(entries, registry=registry) assert result[0]["maturity"] == "PRODUCTION" def test_skips_self_review(self): diff --git a/scripts/notebook_tools/tests/test_production_scope.py b/scripts/notebook_tools/tests/test_production_scope.py new file mode 100644 index 0000000000..990b346a25 --- /dev/null +++ b/scripts/notebook_tools/tests/test_production_scope.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests du tampon PRODUCTION (#14831, sign-off user 2026-09-21). + +`PRODUCTION` ne decrit pas une propriete du fichier : il dit que le responsable +pedagogique a appose son tampon, et juge le notebook finalise pour etre utilise +en cours **par d'autres**. Aucune combinaison d'axes ne peut produire ce fait. + +Ce que ces tests epinglent, par ordre de degat s'ils cassent : + +1. **Une colonne vide ne signe rien.** C'est l'etat reel du document aujourd'hui, + et c'est l'etat correct : non tranche = BETA. Un parser qui signerait par + defaut fabriquerait la signature qu'on vient de retirer de l'agregat. +2. **Une ligne qui signe mais ne resout aucun groupe est RENDUE.** C'est la panne + mesuree pendant la conception : un join par repertoire sous-signait + silencieusement (90 chemins sur 99, SmartContracts s'etalant sur deux + sous-repertoires). Une ligne qui signe zero notebook sans le dire est + indiscernable d'une ligne non repondue. +3. **Une exclusion nommee l'emporte sur la signature de sa serie.** L'inverse + transformerait une reserve en approbation. +4. **Le document reel se lit.** Les 13 lignes resolvent leur groupe, les 99 + chemins de strate A existent. Sans ce controle, le parser pourrait rendre 0 + parce que le document a change de forme, et ce zero se lirait comme + « personne n'a encore signe ». + +Run: python -m pytest scripts/notebook_tools/tests/test_production_scope.py +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from generate_catalog import ( # noqa: E402 + PRODUCTION_SCOPE_PATH, + _load_production_scope, + production_is_signed, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] + +DOC = """# Perimetre + +## La passe par serie + +| Série | Cours | Tête de série | N proposés | Verdict | +|-------|-------|---------------|------------|---------| +| Alpha | Cours A | `A-01-Tete.ipynb` | 2 | {verdict_a} | +| Beta | Cours B | `B-01-Tete.ipynb` | 2 | {verdict_b} | + +## Strate A — proposés pour signature (4) + + + +- [{tick_a1}] `MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb` +- [ ] `MyIA.AI.Notebooks/Alpha/sub/A-02-Autre.ipynb` + + + +- [ ] `MyIA.AI.Notebooks/Beta/B-01-Tete.ipynb` +- [ ] `MyIA.AI.Notebooks/Beta/B-02-Autre.ipynb` + +## Strate B — hors proposition v1 (1) + + + +- [ ] `MyIA.AI.Notebooks/Gamma/G-01.ipynb` +""" + + +def write_doc(tmp_path, verdict_a="", verdict_b="", tick_a1=" "): + p = tmp_path / "production-scope.md" + p.write_text( + DOC.format(verdict_a=verdict_a, verdict_b=verdict_b, tick_a1=tick_a1), + encoding="utf-8", + ) + return p + + +# --- 1. une colonne vide ne signe rien --------------------------------------- + +def test_une_colonne_vide_ne_signe_rien(tmp_path): + scope = _load_production_scope(write_doc(tmp_path)) + assert scope["signed"] == set() + assert scope["unresolved"] == [] + assert production_is_signed(scope, "MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb") is False + + +def test_un_verdict_non_reconnu_ne_signe_rien(tmp_path): + """Fail-CLOSED : « à voir », « peut-être », un point d'interrogation — rien + de tout cela n'est une signature.""" + for bidon in ("à voir", "peut-être", "?", "non", "TODO"): + scope = _load_production_scope(write_doc(tmp_path, verdict_a=bidon)) + assert scope["signed"] == set(), bidon + + +def test_un_document_absent_ne_leve_pas_et_ne_signe_rien(tmp_path): + scope = _load_production_scope(tmp_path / "inexistant.md") + assert scope["signed"] == set() + + +# --- le controle positif : sans lui, tout ce qui precede est satisfait par un +# parser qui ne signe JAMAIS rien -------------------------------------- + +def test_un_oui_signe_toute_la_serie(tmp_path): + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui")) + assert scope["signed"] == { + "MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb", + "MyIA.AI.Notebooks/Alpha/sub/A-02-Autre.ipynb", + } + assert production_is_signed(scope, "MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb") is True + assert production_is_signed(scope, "MyIA.AI.Notebooks/Beta/B-01-Tete.ipynb") is False + + +def test_le_groupe_porte_les_sous_repertoires(tmp_path): + """La raison d'etre du join par commentaire de groupe : `A-02-Autre.ipynb` + vit dans `Alpha/sub/`, pas dans le repertoire de la tete de serie. Un join + par repertoire l'aurait manque -- c'est la panne mesuree sur SmartContracts + (90 chemins signes sur 99).""" + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui")) + assert production_is_signed(scope, "MyIA.AI.Notebooks/Alpha/sub/A-02-Autre.ipynb") is True + + +def test_la_strate_b_n_est_jamais_signee(tmp_path): + """La strate B n'est pas soumise a decision (non tranche = BETA).""" + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui", verdict_b="oui")) + assert not any("Gamma" in p for p in scope["signed"]) + + +# --- 2. une ligne qui ne resout rien est RENDUE ------------------------------ + +def test_une_tete_de_serie_introuvable_est_rendue_pas_tue(tmp_path): + doc = write_doc(tmp_path, verdict_a="oui") + doc.write_text( + doc.read_text(encoding="utf-8").replace("`A-01-Tete.ipynb`", "`A-1-Tete.ipynb`"), + encoding="utf-8", + ) + scope = _load_production_scope(doc) + assert scope["signed"] == set(), "une ligne non resolue ne signe rien" + assert len(scope["unresolved"]) == 1, "et elle le DIT" + assert "Alpha" in scope["unresolved"][0] + + +# --- 3. une exclusion nommee l'emporte --------------------------------------- + +def test_oui_sauf_exclut_le_notebook_nomme(tmp_path): + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui sauf `A-02-Autre.ipynb`")) + assert scope["signed"] == {"MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb"} + + +def test_oui_sauf_accepte_plusieurs_exclusions(tmp_path): + scope = _load_production_scope( + write_doc(tmp_path, verdict_a="oui sauf A-01-Tete.ipynb, A-02-Autre.ipynb")) + assert scope["signed"] == set() + + +# --- la case cochee est le meme geste, pose notebook par notebook ------------ + +def test_une_case_cochee_signe_ce_notebook_seul(tmp_path): + scope = _load_production_scope(write_doc(tmp_path, tick_a1="x")) + assert scope["signed"] == {"MyIA.AI.Notebooks/Alpha/A-01-Tete.ipynb"} + + +# --- la comparaison de chemins ne se rabat pas sur le nom de fichier --------- + +def test_un_homonyme_d_une_autre_serie_n_est_pas_signe(tmp_path): + """Se rabattre sur le basename signerait les homonymes -- et les series de + ce depot en regorgent (`01-1-...`, `Search-01-...`).""" + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui")) + assert production_is_signed(scope, "MyIA.AI.Notebooks/Autre/A-01-Tete.ipynb") is False + + +def test_les_separateurs_windows_sont_normalises(tmp_path): + scope = _load_production_scope(write_doc(tmp_path, verdict_a="oui")) + assert production_is_signed(scope, "MyIA.AI.Notebooks\\Alpha\\A-01-Tete.ipynb") is True + + +# --- 4. le document REEL se lit ---------------------------------------------- + +def test_le_document_reel_se_lit_et_ne_signe_encore_rien(): + """Contrat avec le document reel, dans les deux sens. + + **Il ne signe rien** : les 13 verdicts sont vides, le user n'a pas encore + repondu, et `PRODUCTION = 0` est le verdict correct. + + **Et aucune ligne n'echoue a se resoudre** : c'est ce qui separe « personne + n'a signe » de « le parser ne trouve plus le document ». Sans cette seconde + assertion, la premiere serait satisfaite par un parser casse. + """ + if not PRODUCTION_SCOPE_PATH.exists(): + import pytest + pytest.skip("production-scope.md absent de cet arbre") + scope = _load_production_scope() + assert scope["signed"] == set(), ( + "des notebooks sont signes : mettre a jour ce test ET verifier que le " + "user a bien repondu, la signature ne se derive de rien d'autre" + ) + assert scope["unresolved"] == [], ( + "une ligne de decision ne resout plus son groupe de strate A : %s" + % scope["unresolved"] + ) + + +def test_les_treize_lignes_de_decision_resolvent_leur_groupe(): + """Controle positif du test precedent : on force les 13 verdicts a « oui » + sur une COPIE, et on verifie que la couverture est exactement la strate A. + + Sans ca, `unresolved == []` serait aussi vrai d'un parser qui ne lit aucune + ligne. C'est la lecon du zero d'instrument aveugle : un zero ne prouve rien + tant qu'on n'a pas montre que l'instrument sait rendre autre chose. + """ + import re + if not PRODUCTION_SCOPE_PATH.exists(): + import pytest + pytest.skip("production-scope.md absent de cet arbre") + text = PRODUCTION_SCOPE_PATH.read_text(encoding="utf-8") + # remplir la derniere colonne des seules lignes a 5 cellules + out = [] + for line in text.splitlines(): + s = line.strip() + if s.startswith("|") and not s.startswith("|--"): + cells = [c.strip() for c in s.strip("|").split("|")] + if len(cells) == 5 and cells[0].lower() not in ("serie", "série") and not cells[4]: + cells[4] = "oui" + line = "| " + " | ".join(cells) + " |" + out.append(line) + import tempfile + with tempfile.TemporaryDirectory() as td: + p = Path(td) / "scope.md" + p.write_text("\n".join(out), encoding="utf-8") + scope = _load_production_scope(p) + + strate_a = set() + strate = None + for line in text.splitlines(): + s = line.strip() + m = re.match(r"^##\s+Strate\s+([ABC])\b", s) + if m: + strate = m.group(1) + continue + if s.startswith("## "): + strate = None + continue + m = re.match(r"^- \[[ xX]\]\s*`([^`]+)`", s) + if m and strate == "A": + strate_a.add(m.group(1)) + + assert scope["unresolved"] == [], scope["unresolved"] + assert scope["signed"] == strate_a, ( + "un « oui » partout doit couvrir exactement la strate A : %d signes, " + "%d attendus, ecart %s" + % (len(scope["signed"]), len(strate_a), + sorted(strate_a ^ scope["signed"])[:5]) + ) + + +def test_tous_les_chemins_de_strate_a_existent_sur_disque(): + """Un chemin de strate A qui n'existe pas serait un notebook signable et + introuvable — la signature porterait sur du vide.""" + import re + if not PRODUCTION_SCOPE_PATH.exists(): + import pytest + pytest.skip("production-scope.md absent de cet arbre") + text = PRODUCTION_SCOPE_PATH.read_text(encoding="utf-8") + strate = None + absents = [] + for line in text.splitlines(): + s = line.strip() + m = re.match(r"^##\s+Strate\s+([ABC])\b", s) + if m: + strate = m.group(1) + continue + if s.startswith("## "): + strate = None + continue + m = re.match(r"^- \[[ xX]\]\s*`([^`]+)`", s) + if m and strate == "A" and not (REPO_ROOT / m.group(1)).exists(): + absents.append(m.group(1)) + assert absents == [], "chemins de strate A absents du disque : %s" % absents[:5]