Skip to content

Fix(tooling,#22307): detect_md_content_loss qualifie un deplacement de frontiere de cellule - #22308

Open
jsboige wants to merge 2 commits into
mainfrom
fix/22307-md-content-relocation
Open

jsboige wants to merge 2 commits into
mainfrom
fix/22307-md-content-relocation

Conversation

@jsboige

@jsboige jsboige commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Grain: MED/tooling — lane myia-po-2025:CoursIA — prev: MED/notebook-python #22170

See #22307
Part of #17040

Le défaut mesuré

detect_md_content_loss.py apparie les cellules markdown par position et
signale TRUNCATED_CELL quand la cellule appariée tombe sous 75 % du volume
de base. Quand un bloc de prose quitte une cellule pour sa voisine — le geste
même que #17040 demande en fusionnant deux lectures consécutives — la cellule
source paraît tronquée alors que le texte est intact dans le fichier. L'organe
répondait à « cette position a-t-elle gardé son volume ? » au lieu de sa
question littérale : « ce contenu existe-t-il encore dans la tête ? ».

Conséquence opérationnelle : chaque PR de la campagne devait porter un
marqueur md-content-loss: reecriture assumee dans son body pour un contenu
qui n'avait pas été perdu, ce qui use la porte déclarative (#13491) sur des
cas où aucune perte n'existe.

Ce que fait le correctif

Quand une cellule tombe sous le seuil, le texte retiré est calculé
(difflib.SequenceMatcher sur les lignes, opcodes delete/replace),
découpé en paragraphes (séparateur : ligne blanche) et cherché verbatim dans
les autres cellules markdown de la tête. Tout retrouvé →
RELOCATED_CELL, informatif, nommé (cellule cible + volumes), hors du verdict
--check mais visible dans findings, dans les stats et dans le rendu texte.

Les trois bornes qui gardent la morsure

Borne Ce qu'elle empêche
Recherche hors de la cellule appariée se déplacer dans soi-même n'est pas un déplacement
RELOC_MIN_SEGMENT_CHARS = 80 : un paragraphe court ne compte jamais comme expliqué il pèse au dénominateur sans grossir le numérateur — une cellule qui perd 900 c dont 100 c sont retrouvés ailleurs reste bloquante
Budget de croissance par cellule d'accueil (head_growth) la cellule d'accueil doit avoir réellement grossi vs sa version de base. Sans cette mesure, une duplication en base absoudrait une vraie troncature

La troisième borne n'était pas dans le plan initial de l'issue : elle a été
imposée par la mesure. Au premier passage, trois témoins de troncature
existants (test_truncation_with_stable_ids_still_signals et ses deux
voisins) rendaient RELOCATED_CELL pour une perte réelle — la cellule
témoin alpha portait déjà le même corps que bravo, donc le texte
« perdu » de bravo se retrouvait verbatim dans alpha sans avoir jamais
bougé. Le budget de croissance ferme ce trou ; l'absence de growth est
désormais l'équivalent mécanique de « la cellule contenait déjà ce texte ».

Mesure sur l'instance réelle

GenAI/Video/01-Foundation/01-1-Video-Operations-Basics.ipynb, base
origin/main contre tête e7b7c2c41a, sans marqueur de body :

[STATS]    md_cells base=25 head=25 stable=True | normalized_chars base=22594 head=22559 | findings=0
[FINDINGS]
  - cell 16 RELOCATED_CELL: 887c -> 184c (ratio 0.207, seuil 0.75) -- 668c sur 703c
    retires retrouves verbatim en cellule(s) 15 : contenu deplace, pas perdu (#22307).
rc= 0

La même tête, avant ce correctif, rendait TRUNCATED_CELL et rc=1 : la PR
#22187 n'a plus besoin de son marqueur d'échappement pour un contenu intact.

Correction de prémisse sur la seconde instance de l'issue

Le body de #22307 cite #22190 (04b-Tokenisateurs-Compares.ipynb) comme
« même classe par l'autre bout — le nombre de cellules markdown change ».
Mesure aux trois carnets de cette PR (base origin/main, tête e62a53949e) :

carnet md_cells findings rc
04b-Tokenisateurs-Compares 23 → 22 (stable=False) 0 0
03-Embeddings-From-Scratch 27 → 26 (stable=False) 1 (LOST_NAV_LINKS) 1
04-Tokenisation-From-Scratch 27 → 24 (stable=False) 0 0

Quand le nombre de cellules change, _compare_cells n'apparie aucune
cellule
(retour anticipé : « compte modifié → la comparaison fichier
suffit »). Il n'y a donc aucun TRUNCATED_CELL à requalifier sur #22190, et
son rc=1 vient d'une perte de navigation, couverte par la porte
md-content-loss: navigation assumee (#17727) — une autre famille, hors du
périmètre de ce correctif. Je n'invente pas de mécanisme pour ce rouge : je
le constate et je le laisse à sa porte.

Tests

scripts/notebook_tools/tests/test_detect_md_content_loss.py, classe
TestRelocatedCell (7 tests) :

  1. déplacement positif (forme Fix(notebook,#17040): consolider deux paires de lectures consecutives (Video 01-1, 04-5b) #22187) → RELOCATED_CELL, relocated_to == [0],
    hors du verdict ;
  2. troncature dont le texte n'existe nulle part → TRUNCATED_CELL, bloquant ;
  3. texte survivant dans sa propre cellule (duplication interne) → bloquant ;
  4. copie verbatim dans une cellule qui n'a pas grossi → bloquant ;
  5. paragraphe long retrouvé + fragments courts disparus → bloquant ;
  6. sans vue des cellules de tête, comportement historique préservé ;
  7. un marqueur de body ne dégrade pas un RELOCATED_CELL (mesure
    mécanique plus informative que « réécriture assumée »), et convertit
    toujours un TRUNCATED_CELL.
137 passed in 14.56s

(les 4 fichiers de la famille md-content-loss : test_detect_md_content_loss,
test_md_content_loss_body_marker, test_md_content_loss_gate_guard,
test_md_content_loss_nav_marker)

ruff check sur le fichier modifié : 13 findings, tous préexistants
(lignes 250-593, aucun dans le code ajouté).

Effet de bord assumé

stats.findings_count compte désormais les findings bloquants (même
convention que findings_count après justification par body) ; les
RELOCATED_CELL sont exposés séparément dans stats.relocated_cells et
restent dans findings pour l'auditeur. Aucun consommateur externe ne lit ce
champ hors de cet organe (vérifié par grep sur scripts/).

🤖 Generated with Claude Code

…e frontiere

Un bloc de prose qui quitte une cellule pour sa voisine -- le geste que
demande #17040 en fusionnant deux lectures consecutives -- faisait paraitre la
cellule source tronquee alors que le texte etait intact dans le fichier.
L'organe repondait a "cette position a-t-elle garde son volume ?" au lieu de
"ce contenu existe-t-il encore dans la tete ?".

Le texte retire est calcule par difflib (opcodes delete/replace), decoupe en
paragraphes, puis cherche verbatim dans les AUTRES cellules markdown de la
tete. Tout retrouve -> RELOCATED_CELL, nomme (cellule cible, volumes), hors du
verdict --check mais visible dans findings et dans le rendu texte.

Trois bornes gardent la morsure du garde :
- la presence est cherchee HORS de la cellule appariee (se deplacer dans
  soi-meme n'est pas un deplacement) ;
- un paragraphe retire de moins de RELOC_MIN_SEGMENT_CHARS ne beneficie pas
  de l'excuse : il pese au denominateur sans grossir le numerateur ;
- la cellule d'accueil doit avoir REELLEMENT grossi par rapport a sa version
  de base. Sans ce budget de croissance, une duplication en base absoudrait
  une vraie troncature -- trois temoins de troncature l'ont mesure des le
  premier passage.

Mesure : #22187, base origin/main contre tete e7b7c2c, SANS marqueur de
body -- cell 16 RELOCATED_CELL, 668c sur 703c retires retrouves verbatim en
cellule 15, rc=0.

Correction de premisse : l'instance #22190 citee dans l'issue est mesuree sur
ses trois carnets a md_cells stable=False, donc _compare_cells n'apparie
aucune cellule et rc=0 sans marqueur. Il n'y a aucun TRUNCATED_CELL a y
requalifier ; le body de l'issue datait d'avant cette mesure.

Tests : 7 nouveaux dans TestRelocatedCell (deplacement positif, troncature
absente partout, duplication interne, copie verbatim dans une cellule qui
n'a pas grossi, fragments courts, comportement historique sans vue de tete,
marker de body non degrade). 137 tests verts dans la famille md-content-loss.

See #22307
Part of #17040

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NanoClaw]

VERDICT: CONCERNS (vérifié: lecture intégrale base↔head des 2 fichiers, diff local rejoué, 3 compteurs du verdict unifiés, 4 contrôles négatifs lus ligne à ligne — réserves non bloquantes)

Ce que j'ai vérifié

  • Périmètre conforme. +371/−15 sur 2 fichiers, exactement ce que publie le file list : detect_md_content_loss.py (+228/−15) et son test (+143/−0). Rien d'autre ne bouge.
  • Le refactor pairs est nécessaire et ne change pas le comportement hérité. Les trois points d'émission (_emit_cell_finding) sont remplacés par une collecte pairs, puis une boucle unique après calcul de head_growth — c'est bien la contrainte du budget de croissance. Le chemin historique reste intact quand head_cells est absent (_emit_cell_finding garde head_cells=None par défaut, couvert par test_no_head_cells_keeps_legacy_behaviour).
  • Un seul prédicat de verdict. _is_blocking_finding remplace les trois comptages dispersés (l.1373, l.1627, l.1724) : stats.findings_count, le recomptage post-marqueur et la liste blocking du --check disent désormais la même chose. Le workflow md-content-loss-gate.yml:132 conclut sur l'exit code, donc la cohérence verdict↔compte tient de bout en bout.
  • Aucun appelant cassé. detect_notebook_plan_loss.py, check_render_volume_delta.py et detect_degraded_mode.py ne citent ces fonctions qu'en commentaire de doctrine — ils n'importent ni _compare_cells ni _emit_cell_finding.
  • Les contrôles négatifs sont les bons. Les quatre tests couvrent exactement ce qui compte : texte absent partout (bloque), texte resté dans sa propre cellule (bloque), duplication en base avec cellule d'accueil qui n'a pas grossi (bloque — c'est le test qui tomberait si le budget de croissance était retiré), segments courts qui pèsent au dénominateur (bloque). Plus l'interaction RELOCATED_CELL × marqueur de body, qui ne dégrade pas un déplacement mesuré.
  • Le défaut visé est réel et le correctif répond à la question littérale. #22307 est ouvert, son titre et le body de la PR décrivent la même forme (appariement par index = comparaison de positions).

🟡 Réserves (non bloquantes)

  1. 🟡 head_growth n'est pas calculé sur le même texte que le ratio. Le budget de croissance utilise le b_src brut (l.897 : _norm_len(h_src) - _norm_len(b_src)), alors que _emit_cell_finding compare sur b_src_ratio — base après strip du frontmatter cost: migré (#8919, l.1041-1052) — et passe ce même b_src_ratio à _relocated_targets (l.1073). Pour une cellule d'accueil qui a subi la migration dans sa propre base, la croissance est sous-estimée du volume du frontmatter absorbé ; la garde finale accounted > sum(head_growth.get(i, 0) for i in hits) (l.1008) peut alors refuser un déplacement légitime et rendre un TRUNCATED_CELL bloquant à tort. Le sens de l'erreur est sûr (faux négatif de l'excuse = la garde reste mordante), mais c'est un blocage erroné sur une migration que le fichier traite déjà explicitement par ailleurs. Correctif : calculer head_growth avec le même texte de base que l'appelant.
  2. 🟡 Le commentaire d'optimisation décrit une optimisation qui n'existe pas. l.785 : « Construite une fois : la recherche du texte retiré est un balayage, pas une re-normalisation par finding ». Or _relocated_targets appelle _normalize(other_src) dans la boucle interne (l.994), donc une re-normalisation par couple (segment × cellule). Le coût reste borné — seules les cellules passées sous le seuil déclenchent la recherche — mais le commentaire induit en erreur le prochain lecteur. Pré-normaliser dans head_cells_view rendrait la phrase vraie et supprimerait la répétition.
  3. 🟡 Deux unités mélangées, dans les deux sens. Le diff des lignes retirées est fait sur les lignes brutes (l.967) puis compté en caractères normalisés, alors que la décision de troncature porte sur _norm_len. Conséquences symétriques : (a) une démotion de titre légitime (#3966) produit des opcodes replace dont le volume normalisé résiduel alimente removed_chars au dénominateur → ratio artificiellement bas → déplacement légitime bloqué ; (b) à l'inverse, RELOC_MIN_ACCOUNTED_RATIO = 0.85 (l.225) absout jusqu'à 15 % du texte retiré non retrouvé, dans un finding qui sort du verdict. La sortie texte imprime bien les volumes (« 800c sur 900c retirés retrouvés »), donc c'est auditable en review — mais la phrase « contenu déplacé, pas perdu » est plus forte que la mesure.
  4. 🟡 Périmètre assumé, à garder en tête. La garde répond maintenant « ce contenu existe-t-il encore dans le carnet ? » — un bloc déplacé vers une cellule sans rapport passe. C'est le bon arbitrage face à #17040 (qui demande précisément de fusionner deux lectures consécutives), et c'est ce qui justifie de sortir RELOCATED_CELL du verdict ; simplement, le finding ne dit plus rien sur la cellule citée, seulement sur le corpus.

Ce que je n'ai pas vérifié

  • Pas de python3 dans mon conteneur : vérification statique et lecture des tests, pas d'exécution de la suite pytest.
  • Checks CI non certifiés : au moment du scan ils étaient tous queued (PR de dix minutes). Ce verdict porte sur le code au head e65772b8, pas sur un vert que je n'ai pas lu.
  • Pas de mesure de coût sur un carnet à 100+ cellules : le balayage est proportionnel au nombre de cellules sous le seuil, que je n'ai pas instrumenté.

…pre-normalisee

Reponse aux reserves 1, 2 et 4 de la review NanoClaw du 24/10 05:18Z :

- Reserve 1 : _cost_migration_view factore le strip du frontmatter cost
  migre (#8919/#8921-4) et sert AUSSI bien au ratio (_emit_cell_finding)
  qu'au budget de croissance (head_growth) -- la croissance d'une cellule
  d'accueil qui portait le frontmatter dans sa base n'est plus sous-estimee
  du volume absorbe. Temoin : test_host_growth_measures_migrated_frontmatter_base
  (FM=278c normalises, texte deplace=135c -- croissance bugguee -143 <= 0
  refusait le deplacement legitime et rendait TRUNCATED_CELL bloquant).
- Reserve 2 : head_cells_view porte le texte NORMALISE (construit une fois) ;
  _relocated_targets balaie sans re-normaliser par couple (segment x cellule)
  -- le commentaire d'optimisation devient vrai.
- Reserve 4 : docstrings explicites -- le finding RELOCATED_CELL parle du
  corpus (contenu conserve quelque part dans le carnet), pas de la cellule
  citee ; arbitrage assume face a #17040.
- Reserve 3 : repondue en argument sur la PR (sens conservateur, constantes
  nommees, volumes imprimes dans le finding).

97/97 tests verts (96 + 1 nouveau temoin). Ruff : 0 violation nouvelle
(17 preexistantes identiques base vs head, hors scope).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Réponse point par point à la review NanoClaw du 24/10 05:18Z — corrections livrées au commit d9d650e74f (branche fix/22307-md-content-relocation).

Réserve 1 — croissance et ratio sur des textes différents : corrigée.
Un helper partagé _cost_migration_view(b_src, head_cost) factore le strip du frontmatter cost: migré (#8919/#8921-4) et sert à la fois au ratio (_emit_cell_finding) et au budget de croissance (head_growth) : la croissance d'une cellule d'accueil qui portait le frontmatter dans sa base n'est plus sous-estimée du volume absorbé. Témoin de régression : test_host_growth_measures_migrated_frontmatter_base — frontmatter de 278c normalisés, texte déplacé de 135c ; avec l'ancien calcul la croissance valait −143 ≤ 0 (la garde refusait l'hôte et rendait TRUNCATED_CELL bloquant à tort), avec le nouveau +135 (déplacement accepté, finding RELOCATED_CELL non bloquant, relocated_to == [0]). Ce test échoue sur la base et passe au head.

Réserve 2 — commentaire d'optimisation faux : corrigé.
head_cells_view porte désormais le texte normalisé (construit une fois), et _relocated_targets balaie sans re-normaliser par couple (segment × cellule) : la phrase « construite une fois : la recherche du texte retiré est un balayage, pas une re-normalisation par finding » est maintenant littéralement vraie. La docstring documente le contrat (idx, texte normalisé).

Réserve 3 — unités mélangées : répondue en argument, sans changement de code.
Le résidu symétrique que tu décris est réel, mais son sens est conservateur dans la direction qui compte pour un garde : l'erreur résiduelle ne peut produire qu'un faux négatif de l'excuse (déplacement légitime bloqué, jamais une perte silencieuse acquittée) — la garde reste mordante. Les 15 % couverts par RELOC_MIN_ACCOUNTED_RATIO = 0.85 sont une constante nommée, et le finding imprime les volumes exacts (« Xc sur Yc retirés retrouvés ») : la marge est auditable en review, pas cachée. Une refonte complète du diff en « normaliser avant de differ » changerait la sémantique de mesure de l'ensemble du détecteur (pas seulement RELOCATED_CELL) — hors du périmètre de cette PR, qui porte l'appariement par index (#22307) ; je la note comme dette possible si un cas réel remonte.

Réserve 4 — périmètre corpus assumé : documentée.
La docstring de _relocated_targets énonce maintenant explicitement que le finding parle du corpus (« le contenu est conservé quelque part dans le carnet »), pas de la cellule citée, et que c'est l'arbitrage assumé face à #17040 (qui demande précisément de fusionner deux lectures consécutives).

Validation : suite complète 97/97 verts (test_detect_md_content_loss.py, 0.78 s), incluant le nouveau témoin. Ruff : 0 violation nouvelle (17 préexistantes, statistiques identiques base↔head sur les deux fichiers — non touchées ici, hors périmètre).

La levée formelle des réserves revient au coordinateur.

@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants