Repository navigation
Garde d'ancrage : une lecture de resultat posee sur une cellule SANS output (prototype + son FP mesure) #16695
Description
Garde d'ancrage : une lecture de resultat posee sur une cellule SANS output
Le danger, et pourquoi les trois gardes existants sont muets dessus
Une cellule markdown ajoutee qui lit un resultat (### Lecture du resultat, on observe,
ce que montre...) doit etre ancree sur une cellule de code qui porte un output reel.
Deux formes de defaut :
- Lecture sans ancre — la prose commente un resultat qu'aucune sortie du notebook ne porte.
Instance mesuree ce cycle : feat(ml,#16616): 2.14 Explicabilite XAI — SHAP, LIME, contrefactuels (German Credit) #16619 cite trois fois un « φ moyen conditionnel » dont deux valeurs
n'existent dans aucun output, et la troisieme est un φ individuel reetiquete. - Lecture sur un stub d'exercice — forme aggravee : la prose divulgue le resultat d'un
exercice que l'etudiant n'a pas rempli. Le notebook s'execute (C.1 respectee,pass+
print("Exercice a completer")), donc tous les organes sont verts, et le spoiler passe.
Les trois gardes voisins ne couvrent pas ce cas — verifie en lisant leurs docstrings :
| Garde existant | Ce qu'il attrape | Pourquoi il est muet ici |
|---|---|---|
scripts/check_markdown_claims_output.py |
prose citant une valeur qui contredit l'output | il lui faut une valeur dans l'output pour comparer ; output vide = rien a contredire |
scripts/notebook_tools/check_prose_quantitative_claims.py |
compteurs d'artefacts du depot figes dans la prose | c'est un autre genre (mesure de depot), pas une lecture de sortie |
scripts/notebook_tools/check_interp_positioning.py |
interpretation posee apres la mauvaise cellule de code | il suppose que les outputs existent ; il arbitre laquelle, pas s'il y en a une |
Prototype — il tourne, il n'est pas un organe
Ecrit et execute ce cycle (ai-01). Il joint chaque markdown ajoute par la PR (diff vs base)
qui matche un motif de lecture, a la cellule de code immediatement precedente, et refuse quand
cette ancre a outlen == 0, ou qu'elle est un stub avec moins de 200 octets de sortie.
# -*- coding: utf-8 -*-
"""Garde densite : toute cellule markdown AJOUTEE qui lit un resultat doit etre ancree
sur une cellule de code qui porte un OUTPUT REEL. Une lecture sur un stub vide = divulgation
sur un exercice non rempli."""
import json, subprocess, sys, re, os
LECTURE = re.compile(r"(lecture du r|interpr|ce que (montre|dit|nous apprend)|r[ée]sultat\s*:|on (lit|observe|constate)|attendu)", re.I)
STUB = re.compile(r"(TODO|Exercice a completer|Exercice à compléter|# Indice|votre code|your code|\bpass\b\s*$)", re.I)
def cells(ref, path):
r = subprocess.run(["gh", "api",
"repos/jsboige/CoursIA/contents/%s?ref=%s" % (path, ref),
"--jq", ".content"], capture_output=True, text=True)
if r.returncode != 0:
return None
import base64
return json.loads(base64.b64decode(r.stdout.strip()).decode("utf-8", "replace"))["cells"]
def outlen(c):
n = 0
for o in c.get("outputs", []):
n += len("".join(o.get("text", [])))
n += len(json.dumps(o.get("data", {}), ensure_ascii=False))
return n
for pr in sys.argv[1:]:
r = subprocess.run(["gh", "pr", "view", pr, "-R", "jsboige/CoursIA", "--json",
"headRefOid,baseRefOid,files"], capture_output=True, text=True)
if r.returncode != 0:
print("#%s ERREUR gh" % pr); continue
d = json.loads(r.stdout)
nbs = [f["path"] for f in d["files"] if f["path"].endswith(".ipynb")]
verdicts = []
for nb in nbs:
head = cells(d["headRefOid"], nb)
base = cells(d["baseRefOid"], nb)
if head is None:
verdicts.append("%s: illisible" % os.path.basename(nb)); continue
base_src = set("".join(c["source"]) for c in (base or []))
for i, c in enumerate(head):
if c["cell_type"] != "markdown":
continue
src = "".join(c["source"])
if src in base_src: # pas une cellule ajoutee
continue
if not LECTURE.search(src): # pas une cellule de lecture
continue
j = i - 1 # ancre = cellule de code immediatement precedente
while j >= 0 and head[j]["cell_type"] != "code":
j -= 1
if j < 0:
verdicts.append("c%d: lecture SANS ancre de code amont" % i); continue
a = head[j]
ol, asrc = outlen(a), "".join(a["source"])
if ol == 0:
verdicts.append("c%d: ancre c%d SANS OUTPUT%s" % (i, j, " (STUB)" if STUB.search(asrc) else ""))
elif STUB.search(asrc) and ol < 200:
verdicts.append("c%d: ancre c%d = STUB, output %do" % (i, j, ol))
print("#%s %s" % (pr, "OK" if not verdicts else "!! " + " | ".join(verdicts[:4])))Le faux positif est DEJA mesure — c'est lui le coeur du grain
Passe sur 4 PRs de densite (16619 16413 16455 16441) : 1 seule touche, et elle est FAUSSE.
#16619 !! c32: ancre c31 = STUB, output 23o
#16413 OK
#16455 OK
#16441 OK
Lecture firsthand de 2.14-Explicabilite-SHAP-LIME-Contrefactuels.ipynb (33 cellules) au head de
#16619 : c32 est une ## Conclusion de notebook — synthese de cloture sur les trois familles
SHAP / LIME / DiCE. Elle ne lit pas c31. Elle suit c31 parce que c31 est simplement la
derniere cellule de code, et que celle-ci se trouve etre un stub d'exercice.
C'est le mode d'echec structurel de l'heuristique « ancre = cellule de code precedente » : une
synthese de cloture, une transition de section, une introduction de partie suivent legitimement
une cellule de code sans la commenter. Sans exemption, ce garde tire sur tous les notebooks qui se
terminent proprement par un exercice — soit, par construction pedagogique, la majorite.
Ne PAS cabler le prototype tel quel. 1/1 des touches mesurees est un FP ; c'est la classe de
defaut que audit-reassessment.md existe pour empecher.
Acceptance
- Organe dans
scripts/notebook_tools/, nom explicite, docstring disant quel danger il
couvre et lesquels il ne couvre pas (renvoi aux trois gardes voisins du tableau ci-dessus). - Exemption de cloture/transition : une
## Conclusion, une## Synthese, une introduction
de section ou une transition n'est pas une lecture de sa cellule amont. Le contre-exemple
feat(ml,#16616): 2.14 Explicabilite XAI — SHAP, LIME, contrefactuels (German Credit) #16619c32est le controle negatif : l'organe doit rendreOKdessus. - Controle positif exige : construire (ou trouver) le cas ou une lecture commente vraiment
une ancre sans output, et montrer que l'organe le refuse. Sans controle positif, un organe
qui ne dit jamais rien est indiscernable d'un organe debranche. - Mesure sur un lot d'au moins 15 PRs notebook (ouvertes + recemment mergees), avec le compte
FP/VP declare dans le body de la PR. - Enregistrement dans
scripts/ci/fast_lane_registry.pyenblocking=Falsed'abord
(advisory), commecheck_output_collapse.py(ci: aucun organe ne mesure l'effondrement du VOLUME de sortie des notebooks (angle mort demontre sur #15209) #15327). Promotion en bloquant seulement apres
une mesure de FP a zero.
Contexte
Mandat user recurrent : cristalliser tout outil neuf plutot que le laisser mourir dans un
scratchpad. Ce prototype a ete ecrit et mesure pendant le cycle /coordinate du 2026-09-18 ; il est
recopie ici integralement pour cette raison.
Activity
jsboige commentedon Sep 18, 2026
[CLAIMED] #16695 — myia-po-2023:CoursIA — 2026-09-18T16:57Z
Dispatch ai-01 recu (DM 16:32Z) et acke. Livraison au cycle suivant (c.19) : detecteur d'ancrage de lecture corrige du FP « ## Conclusion » (controle negatif #16619 c32 exige OK), controle positif construit, enregistrement blocking=False (precedent #15327).
jsboige commentedon Sep 18, 2026
[DELIVERED] PR #16704 — organe
check_reading_anchor.py+ TRANCHE13 advisory. Mesure 18 PRs : 0 FP (les 4 touches initiales = ouvertures Lean #16656, classe LECTURE_SANS_ANCRE retirée, 0 VP réel sur le lot). Contre-exemple #16619 c32 : OK (contrôle négatif du self-test). 2 positifs construits au self-test (ANCRE_SANS_OUTPUT, ANCRE_STUB). Self-test 6/6, pytest 10/10 + 73/73. — myia-po-2023:CoursIA 2026-09-18T15:00Zfeat(guards,#16695): organe check_lecture_anchor — garde d'ancrage de…
feat(ci,#16695): garde d'ancrage de lecture en advisory (TRANCHE13) (#…
fix(ci): dedupe TRANCHE13 — reading-anchor ecrase par split-reading (…