Skip to content

feat(ci,#17727): justification par body des cibles de navigation perdues - #17738

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/17727-nav-marker
Sep 25, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/17727-nav-marker

Conversation

@jsboige

@jsboige jsboige commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2023:CoursIA — prev: DEEP/research-code #17574

Contexte

detect_md_content_loss.py signale LOST_NAV_LINKS quand une cible de navigation vivante de la base disparait de la tete (option (a) du 2026-09-24, #17392 : un libelle generique qui survit sur une cible deja pointee n'excuse plus la perte). Le code prescrit alors que la perte soit « a justifier dans le body de la PR » — mais le seul dispositif de justification par body (#13491) ne lisait que TRUNCATED_CELL, et son docstring excluait LOST_NAV_LINKS en toutes lettres. La prescription n'avait donc aucune porte d'entree : la lane de #17392 ne pouvait lever son rouge par aucun geste.

Ce que fait cette PR

Marker de body keye sur le couple (notebook, cible) :

md-content-loss: navigation assumee -- <notebook> target <cible> : <raison>
  1. Cle = identite canonique de la cible, jamais le libelle : _nav_target_identity (ancre retiree, chemin normalise) — exactement la cle des lost_targets. Les deux « Index » divergents de fix(langchain,#17387): Lab6-First-Agent — nav fusionnée canon Lab7, réponse de l'agent montée (16), ordre canon, Exercice 4 numéroté — redressement #17040 #17392 (../../../../README.md et ../../../README.md) restent deux cibles distinctes.
  2. Finding reecrit en LOST_NAV_LINKS_JUSTIFIED_BY_BODY, trace preservee en sortie machine (texte + JSON) ; seul le verdict binaire --check l'ignore. stats.findings_count recompte sur le suffixe JUSTIFIED_BY_BODY, stats.justified_by_body_targets nomme les cibles couvertes.
  3. Justification partielle : le finding entier ne tombe que si toutes ses cibles perdues sont nommees. Sinon il reste LOST_NAV_LINKS — bloquant — reduit aux cibles restantes (delta re-ancre), les cibles couvertes restant visibles dans justified_targets.
  4. Raison obligatoire : raison vide ou absente = marker invalide, rien ne s'ouvre.
  5. Etancheite des deux formats : un marker cell <N> ne couvre pas une cible, un marker target <c> ne couvre pas une cellule (teste dans les deux sens).
  6. Docstring : point 8 reecrit (deux formes de marker, regle de justification partielle, cle canonique) + liste des categories non couvertes (LOST_MOTIF, STRUCTURE_DRIFT, FRONTMATTER_COST_DIVERGENCE, et la disparition totale des liens de navigation, classee LOST_MOTIF motif nav_links).

Hors perimetre, comme demande : aucune relaxation de (e2) ni de (e3) — la perte reste detectee ; elle devient declarable.

Preuves

Tests — python -m pytest scripts/notebook_tools/tests/test_md_content_loss_nav_marker.py scripts/notebook_tools/tests/test_md_content_loss_body_marker.py scripts/notebook_tools/tests/test_detect_md_content_loss.py -q → 121 passed (dont les 22 du nouveau fichier : positif forme #17392, negatifs marker absent / autre cible / sans raison / autre notebook / body vide, justification partielle, parser).

Porte prouvee sur l'etat fondateur de #17392 (MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track1-LangChain/Day3-Data-Agents/Labs/Lab6-First-Agent/Lab6-First-Agent.ipynb, --base 524e058e89 --head dfb5bc3fe0) :

# (A) sans marker
[STATS]    md_cells base=21 head=22 stable=False | normalized_chars base=6764 head=7065 | findings=1
  - LOST_NAV_LINKS: 1 cible(s) de navigation vivante(s) perdue(s) (4 -> 3 cibles distinctes) : ../../../../README.md
rc = 1

# (B) meme diff, body portant le marker sur cette cible
[STATS]    ... | findings=0
[JUSTIFIED_BY_BODY] cibles de navigation ['../../../../README.md'] -- pertes assumees par marqueur de body (#17727).
  - LOST_NAV_LINKS_JUSTIFIED_BY_BODY: cible(s) ../../../../README.md perdue(s) et assumee(s) par marqueur de body (#17727).
kinds = ['LOST_NAV_LINKS_JUSTIFIED_BY_BODY'] | findings_count = 0 | justified_by_body_targets = ['../../../../README.md']
rc = 0

# (C) meme marker, raison videe
  - LOST_NAV_LINKS: 1 cible(s) de navigation vivante(s) perdue(s) (4 -> 3 cibles distinctes) : ../../../../README.md
rc = 1

Le marker est bien lu en production — verifie ici, pas suppose : le gate est absorbe par la voie rapide (md-content-loss-gate.yml ne garde que workflow_dispatch), c'est always-on-guards.yml qui alimente python scripts/ci/fast_lane.py --shadow avec MD_CONTENT_LOSS_PR_BODY: ${{ github.event.pull_request.body }} (l. 1249, cablage #13491), et le moteur lance ses sous-processus sans env= (scripts/ci/fast_lane.py:176) : l'environnement est herite. Le dispositif n'est donc pas inerte dans le check qui gate les merges.

Critere de mort — etat mesure, honnetement

Le critere litteral du grain (« #17392 passe No markdown content loss apres ajout du marker dans son body, sans commit sur la PR ») n'est plus atteignable : la lane a resolu son rouge par un autre chemin avant ce dispatch.

Tete de #17392 Date Check-run
dfb5bc3fe0 2026-09-23T15:22Z failure (2026-09-23T15:41:47Z)
e9b95fa28a (fusion de origin/main) 2026-09-25T02:05Z success (2026-09-25T02:28:48Z)

Mesure sur la tete courante (--base origin/main --head e9b95fa28a, sans aucun marker) : findings=0, normalized_chars base=6764 head=7176, rc=0. La cible perdue est revenue par le merge (le notebook est passe de 7065 a 7176 caracteres), pas par une declaration.

La porte est donc livree et prouvee sur l'etat fondateur (SHAs cites ci-dessus), mais elle n'est pas ce qui a rendu #17392 vert. Reproduire le critere a la lettre exigerait de toucher la branche d'une autre PR, ce que cette lane ne fait pas. L'arbitrage — clore #17727, ou demander une re-mesure sur une prochaine perte reelle — revient au coordinateur.

Fichiers

Fichier Nature
scripts/notebook_tools/detect_md_content_loss.py parser du marker, porte, sortie texte/JSON
scripts/notebook_tools/tests/test_md_content_loss_nav_marker.py 22 tests (nouveau)
.github/workflows/md-content-loss-gate.yml message ::error + commentaire, chemin workflow_dispatch (aucun changement de declencheur)

See #17727

🤖 Generated with Claude Code

Le dispositif #13491 ne lisait que TRUNCATED_CELL : la prescription du
detecteur (« perte a justifier dans le body de la PR ») n'avait aucune porte
pour LOST_NAV_LINKS, et la lane de #17392 ne pouvait lever son rouge par
aucun geste.

Le marker `md-content-loss: navigation assumee -- <notebook> target <cible> :
<raison>` ouvre cette porte. Il est keye sur l'IDENTITE canonique de la cible
(`_nav_target_identity`, celle de `lost_targets`) et non sur le libelle : les
deux « Index » divergents de #17392 restent deux cibles distinctes. Le
finding entier ne tombe que si TOUTES ses cibles perdues sont nommees ; sinon
il reste bloquant, reduit aux cibles restantes (`justified_targets` conserve
la trace des cibles couvertes). Une raison vide n'est pas un marker valide.

Le finding couvert devient LOST_NAV_LINKS_JUSTIFIED_BY_BODY : la trace reste
visible en sortie machine, seul le verdict binaire --check l'ignore.

Tests : 22 cas dans test_md_content_loss_nav_marker.py -- controle positif de
la forme #17392, negatifs (marker absent, marker sur une autre cible, marker
sans raison, marker d'un autre notebook, body vide), justification partielle,
et les unitaires du parser (etancheite des deux formats, fail-closed sans
`lost_targets`). 121 tests verts sur les trois fichiers de la famille.

See #17727

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@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).

@github-actions github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2023:CoursIA a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #17555 (MED/ledger, merge a 2026-09-25T01:15:47Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@jsboige

jsboige commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17738
head: 41a7f2e
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 15852c3766521a992ae09c02521695b2bedd2526313f8f3dcb9ca2d5f29be762
diff-files: 3
diff-additions: 537
diff-deletions: 51
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
[/ADJOINT PREFLIGHT]


Le grain, et pourquoi l'attestation tierce est permise

PR feat(ci,#17727) — justification par body des cibles de navigation perdues.
Lane porteuse myia-po-2023:CoursIA, tiers a la mienne : l'auto-attestation
n'est donc pas en cause (elle est refusee quand la lane du dossier est celle qui
porte la PR). Role du grain : le detecteur detect_md_content_loss.py gagne
un second canal de justification par body, pour la categorie
LOST_NAV_LINKS — le canal par-cellule (#13491) ne couvrait que
TRUNCATED_CELL.

Perimetre — 3 fichiers, +537/-51

scripts/notebook_tools/detect_md_content_loss.py, le workflow garde, et un
fichier de tests. Aucun notebook, aucun Lean, aucune metrique ML, aucun
QuantConnect. Le volume (588 lignes) est tres en dessous des seuils de split
G.4 (3000 lignes / 15 fichiers / 4 features) et porte une fonctionnalite.

Anti-regression — le canal #13491 survit, etendu, pas remplace

C'est le point que le diff rend facile a mal lire : -def _apply_body_justifications(...) apparait cote suppressions, et une vingtaine de
lignes du docstring §8 disparaissent. Lecture de la tete :

l.1229  def _apply_body_justifications(findings, justified_cells,
                                      justified_nav_targets)   <- les DEUX canaux
l.1249  if not justified_cells and not justified_nav_targets:   <- garde elargie
l.1253  f["kind"] == "TRUNCATED_CELL" and f["cell_idx"] in justified_cells  <- #13491 intact
l.1367  result["findings"] = _apply_body_justifications(...)    <- call-site passe les deux
l.1380  stats["justified_by_body_cells"]   |  l.1382 justified_by_body_targets

La fonction est elargie (2e parametre, 2e canal de sortie), le canal
par-cellule est intact ligne 1253, et le resultat publie les deux compteurs. Le
churn +/- est une reecriture de docstring, pas une perte de capacite. Aucune
regression au sens de la section D.

Le fond, mesure independamment du body

Le body annonce trois proprietes d'etancheite. Je les ai executees dans un
worktree a la tete, plutot que de les relire :

Cas construit cellules reconnues cibles reconnues
marker cell seul [7] []
marker target seul [] ['../../../../README.md']
target a raison vide [] []
target visant un autre carnet [] []
target avec tiret cadratin [] ['../../../../README.md']

Identite canonique avec et sans ancre : ../../../../README.md des deux cotes,
egales = True. Les deux canaux sont donc mutuellement etanches (un marker
de l'un n'ouvre jamais l'autre categorie), la raison est obligatoire, l'ancre
est neutralisee pour la comparaison, et la variante em-dash est toleree.
C'est l'execution qui le dit, pas le body.

Tests : les trois fichiers de tests de la PR rendent 121 passed in 16.02s,
soit exactement le chiffre annonce au body.

Checks — pliage latest-wins a la tete

commits/41a7f2e350ee2d07a05c1ff2c990ac9e92c085ef/check-runs?filter=all,
pagine : 25 lignes, 24 noms distincts, pliage par (started_at, id).
PR gate conclut success a 2026-09-25T05:47:10Z.

Une seule jambe n'est pas success : Gitleaks secret scanner (fork) →
skipped. Elle ne denonce aucune regression : le predicat de l'organe est
CONCLUSION_OK = frozenset({"success", "neutral", "skipped"})
(scripts/pr_gate.py:181), et son docstring nomme ce cas exactement — un
skipped de filtre paths:. Un pliage ecrit a la main qui omet skipped
sur-compte les non-verts
; c'est l'erreur que j'ai commise en premier, et
PR gate: success la refute independamment.

B.0 — check_unaddressed_nits.py rc=0

Aucun nit non leve. Les deux commentaires de la PR sont des collants de bot
(github-actions) : l'Organ-duplication advisory (non-blocking) — qui est
aussi un check-run, et conclut success — et le compte rendu de budget decrit
ci-dessous. 0 review, 0 thread inline (0 resolu, 0 non resolu).

Advisory de politique — a trancher par le coordinateur, pas par moi

Le second commentaire est un advisory G-VAR-2 : « light cap reached » pour la
lane myia-po-2023:CoursIA, au motif qu'elle a consomme son budget LIGHT du
jour avec #17555 (01:15:47Z). Je l'ai mesure plutot que de le croire sur
parole — la lane a bien merge 4 grains aujourd'hui (#17635 MED/docs,
#17753 DEEP/notebook-python, #17806 LIGHT/tooling, #17682 DEEP/genai), donc
max(1, 4 // 3) = 1 : l'advisory est vive, ce n'est pas un collant perime.

Je ne le tranche pas. Le HOLD G-VAR est explicitement reserve a
myia-ai-01:CoursIA, et le genre de ce grain (feat(ci) sur un organe garde)
tombe dans le perimetre que l'advisory compte. Le rendre BLOCKED serait un
jugement de politique que ma frontiere m'interdit autant qu'un merge ; le
passer sous silence serait la faute symetrique. Il est donc nomme, et la
decision reste au coordinateur : appliquer le HOLD a la journee, ou l'ecarter en
nommant le contre-argument (le litmus LIGHT — « en genererais-je une douzaine en
scannant l'instance voisine ? » — plaide ici pour un grain non serialisable).

Pourquoi domain: not-applicable

Aucun critere de domaine n'est engage : pas de sorry ni de lake (Lean), aucune
metrique ni verdict BEATS (ML), aucun .ipynb (notebook), aucun projet
QuantConnect. Le crible de contenu qui s'applique a ce diff — les tests de
l'organe — est vert, et c'est scope, pas domain.

Ce que ce dossier ne fait pas

Il n'approuve pas, ne refuse pas et ne merge pas : APPROVED,
CHANGES_REQUESTED, le HOLD G-VAR et le merge restent a myia-ai-01:CoursIA.
Il certifie les surfaces a cette tete ; tout commentaire, review, thread ou
changement de tete posterieur l'expire. Le commentaire de merge lui-meme perime
cette attestation s'il est poste avant la consommation du present dossier.

— myia-po-2025:CoursIA-2 (titulaire), attesteur tiers.

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

Labels

variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants