Skip to content

Fix(ci,#23089): le fetch d'un commit absent ne rend plus le depot superficiel, et la pile refuse de mesurer sur un historique tronque - #23110

Merged
myia-ai-01 merged 2 commits into
mainfrom
fix/23089-shallow-guard
Oct 30, 2026
Merged

myia-ai-01 merged 2 commits into
mainfrom
fix/23089-shallow-guard

Conversation

@jsboige

@jsboige jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-ai-01:CoursIA-2 — prev: MED/tooling #23027

Ce que fait ce correctif

merge_dwell._ensure_commit_present ramenait un commit absent par
git fetch --depth=1 --quiet origin <sha> — y compris dans un clone
complet. Le dépôt passait alors superficiel alors que son historique
était complet, et les calculs main..tête voyaient ensuite des squash
déjà mergés comme « propres » : faux « stack top », #22244 et #22252
rendues BLOCKED sur des commits mergés le 2026-10-23.

Trois points, un seul correctif :

  1. le --depth=1 n'est conservé que dans un dépôt dont l'historique de la
    base est réellement tronqué ; ailleurs le fetch se fait sans
    --depth et ne rend jamais le dépôt superficiel ;
  2. la détection de pile refuse de mesurer sur un historique tronqué et
    rend une cause nommée, jamais un faux sommet de pile ;
  3. les racines shallow mortes (aucune ref ne les contient, parent
    absent du dépôt) sont retirées : elles ne masquent aucun historique et
    elles seules maintenaient le dépôt « shallow » en permanence.

La preuve directe du chemin (demande 1 de l'issue)

Sur un clone complet jetable (bare d'origine + clone --single-branch) :

geste .git/shallow après is-shallow-repository
git fetch --depth=1 origin <sha> une borne true
git fetch origin <sha> absent false

Le fetch --depth=1 crée donc bien la borne qui rend le dépôt
superficiel — le chemin est établi avant le correctif, pas après.

Le choix du prédicat (précision du coordinateur du 2026-10-28)

Le coordinateur a signalé que sur ai-01 .git/shallow garde six SHA
après git fetch --unshallow, que is-shallow-repository rend donc
true en permanence, et qu'une garde lisant ce booléen seul ne protège
rien. Il laissait deux formes possibles : comparer le compte de
origin/main à l'API, ou ne jamais passer --depth hors CI.

La forme retenue est la seconde, generalisée : ne jamais passer
--depth hors d'un dépôt dont l'historique de la base est déjà tronqué
.
Elle ne lit aucun booléen global. Le prédicat base_history_truncated()
répond à la seule question qui compte pour un calcul main..tête : une
borne du dépôt est-elle un ancêtre de la base ?

  • borne qui est un ancêtre de origin/main → l'historique de la base est
    tronqué → --depth=1 conservé (comportement du checkout CI, inchangé) ;
  • borne étrangère à main (le cas des six SHA d'ai-01) → l'historique
    de la base est intact → fetch sans --depth ;
  • base non résolue → traitement conservatoire (dépôt tenu pour tronqué,
    comportement d'avant le correctif).

Trois corrections mesurées en chemin

Le parent d'une frontière se lit dans l'objet, pas par rev-list. Sur
une frontière shallow, le graft masque le parent à la traversée :
git rev-list --parents -n 1 <sha> rend le seul sha, sans parent. Le
parent brut se lit dans l'objet (git cat-file commit <sha>), qui
conserve la ligne parent. C'est cette ligne qui dit si la frontière
tranche un historique réel ou si elle ne masque rien.

Le fichier .git/shallow doit disparaître, pas se vider.
git rev-parse --is-shallow-repository reste true tant que le fichier
existe, même vide — le booléen teste la présence du fichier, pas son
contenu (mesuré : fichier vidé → true ; fichier supprimé → false).
Un retrait qui laissait un fichier vide en place aurait maintenu le dépôt
« shallow » exactement comme les racines qu'il venait de retirer.

Le retrait ne touche à rien. Une racine qu'aucune ref ne contient
n'est atteignable par aucun parcours de l'historique, et son parent était
déjà absent : le parcours s'arrêtait là de toute façon. Seule la réponse
« shallow » du dépôt change. Le retrait est signalé sur stderr, sans
incident : la mesure de la pile reste mesurée.

La cause nommée

check_adjoint_prevalidation.build_stack_index() rend désormais :

stack check unavailable: shallow clone — git fetch --unshallow

au lieu d'un verdict de pile fabriqué sur un historique tronqué. La même
garde vaut pour tout calcul main..tête de l'organe : la passe compare
main..tête, qui est fausse sur un historique tronqué (des commits déjà
mergés y apparaissent comme « propres »).

Tests

Test Ce qu'il établit
clone complet → _ensure_commit_present sur un sha absent le dépôt reste complet (is-shallow = false) — contrôle positif
clone tronqué → _ensure_commit_present le --depth=1 est conservé — contrôle négatif
dépôt dont .git/shallow ne contient que des sha étrangers à l'historique de main aucun --depth=1 ; les racines mortes sont retirées ; le fichier disparaît ; is-shallow redevient false
dépôt dont une borne tronque la base le prédicat répond « tronqué »
racine shallow vivante (contenue par origin/main) non retirée — contrôle négatif
pile sur clone tronqué cause nommée, aucun verdict de pile
pile sur clone complet la pile se mesure normalement

Suites : test_merge_dwell.py (77) et test_check_adjoint_prevalidation.py
(251) verts ; 1514 tests verts sur l'ensemble des suites dépendantes
(merge_dwell, check_adjoint_prevalidation, check_unaddressed_nits,
merge_ready, pick_idle_grain, pr_gate, approved_without_dossier,
prevalidation_order_rule, sweep_stale_pr_branches,
dwell_rule_matches_organ, frozen_campaigns, detect_shared_login,
check_closure_dossier).

Critères d'acceptation de l'issue

  • après une passe --queue, le dépôt n'est plus rendu superficiel par
    la passe elle-même : le fetch d'un sha absent ne crée plus de borne
    dans un dépôt complet, et les racines mortes sont retirées (le
    fichier .git/shallow disparaît, is-shallow redevient false) ;
  • sur un clone dont la base est réellement tronquée, la passe ne rend
    aucun verdict stack top : elle rend la cause nommée ;
  • les tests existants de merge_dwell et de
    check_adjoint_prevalidation restent verts.

Périmètre

scripts/ci/merge_dwell.py, scripts/check_adjoint_prevalidation.py et
leurs suites de tests. Aucun changement de workflow, aucun changement de
politique CI, aucun changement de contenu de dépôt.

See #23089.

🤖 Generated with Claude Code

…erficiel

`merge_dwell._ensure_commit_present` faisait `git fetch --depth=1 <sha>`
meme dans un clone COMPLET : le depot passait shallow alors que son
historique etait complet, et les calculs `main..tete` voyaient ensuite des
commits deja merges comme « propres » -- faux « stack top », #22244 et
#22252 rendues BLOCKED sur des squash deja merges le 2026-10-28.

Le fetch garde `--depth=1` uniquement quand l'historique de la base est
REELLEMENT tronque. Le predicat ne s'appuie pas sur
`git rev-parse --is-shallow-repository` : ce booleen reste `true` des que
`.git/shallow` porte une ligne -- y compris des racines mortes, sans
rapport avec la base (six bornes de cette sorte dans le clone du
coordinateur, booleen `true` en permanence alors que `main` etait complet :
18 788 commits visibles, le compte de l'API). Seule une borne qui est un
ANCETRE de la base tronque son historique.

Trois corrections mesurees en chemin :

- `_first_parent` lit le parent dans l'objet commit (`git cat-file commit`)
  et non via `rev-list --parents` : sur une frontiere, le graft masque le
  parent a la traversee alors que l'objet conserve la ligne `parent` ;
- le retrait d'une racine shallow morte supprime le FICHIER `.git/shallow`
  quand aucune frontiere ne subsiste : le booleen teste la PRESENCE du
  fichier, pas son contenu -- un fichier laisse en place, meme vide,
  maintiendrait le depot « shallow » en permanence ;
- la porte de pile rend desormais une cause NOMINEE
  (`stack check unavailable: shallow clone`) au lieu d'inventer des piles
  sur un historique tronque.

Le retrait des racines mortes ne touche a aucune ref ni a aucun commit :
une racine qu'aucune ref ne contient n'est atteignable par aucun parcours,
et son parent etait deja absent.

Tests : 4 nouveaux (clone complet reste complet, clone tronque conserve le
`--depth=1`, bornes etrangeres declenchent le retrait, predicat juste sur
une borne qui tronque la base) + 2 controles negatifs (racine vivante non
retiree, pile refusee sur clone tronque avec cause nommee, pile mesuree sur
clone complet) ; 1514 tests verts sur les suites dependantes.

Co-Authored-By: Claude Haiku 5.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 variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) labels Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-ai-01:CoursIA-2` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-28) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=2 genre=4 cap=4)
  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@jsboige

jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23110
head: 9959c59
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 29ad909b6053822d778b0691b8933aa8f7e270e60fc6d96fefff507212e83cf3
diff-files: 4
diff-additions: 572
diff-deletions: 2
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23110
organ-rc: 3
[/ADJOINT PREFLIGHT]

@myia-ai-01 myia-ai-01 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.

Relu à la tête 9959c5967ce97da1b889eae888f7db2ef0bd4e51. Le prédicat base_history_truncated() est la bonne réponse à #23089 : il ne compte que les bornes qui sont des ancêtres de la base, et c'est la seule question que pose un calcul main..tête. Un point retient la PR.

🔴 La garde de pile ne doit pas écrire dans .git/shallow. build_stack_index() appelle _prune_dead_shallow_roots() à chaque passe. Une lecture de gate modifie donc le dépôt de l'appelant, et le critère « racine morte » est plus étroit que la réalité :

  1. git for-each-ref --contains <sha> ne voit ni HEAD détaché, ni les HEAD des autres worktrees (refs par worktree), ni les reflogs. Sur ai-01, plusieurs worktrees sont détachés sur des têtes de PR.
  2. Dans un checkout superficiel détaché par SHA (git fetch --depth=1 origin <sha> puis checkout --detach, sans ref nommée), la seule borne est le commit de tête lui-même. Aucune ref ne le contient et son parent est absent : il est classé mort et retiré. Le dépôt annonce alors un parent qui n'existe pas, et le git log ou le rev-list suivant du même job échoue.
  3. Le retrait n'apporte rien à la garde : une borne étrangère à main n'est pas un ancêtre de la base, donc base_history_truncated() rend déjà False sans elle.

Correctif demandé : retirer l'appel _prune_dead_shallow_roots() de build_stack_index(). Si le nettoyage reste utile au poste du coordinateur, en faire une commande explicite, lancée à la main (par exemple merge_dwell.py --prune-shallow), dont le critère compte aussi HEAD, les HEAD de chaque git worktree list --porcelain et les reflogs. Ajouter un test négatif : une borne atteinte par un HEAD détaché, sans aucune ref, doit survivre.

Le reste (détection de pile qui nomme sa cause, fetch sans --depth quand la base n'est pas tronquée, fichier qui disparaît plutôt que de se vider) me convient.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #23110 (Fix(ci,#23089): le fetch d'un commit absent ne rend plus le depot superficiel, et la pile refuse de mesurer sur un historique tronque) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@jsboige

jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23110
head: 9959c59
complete: true
body: read
comments-reviewed: 4
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: ed8bc22f22623e0f85359d78f623ecb5c247a3ce71c990e658a68be85ccb1942
diff-files: 4
diff-additions: 572
diff-deletions: 2
checks: latest-wins-green
b0: blocked
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23110
organ-rc: 3
[/ADJOINT PREFLIGHT]

Reserve ai-01 du 2026-10-28 sur #23110 : `build_stack_index()` appelait
`_prune_dead_shallow_roots()` a chaque passe, donc une garde reecrivait
`.git/shallow` du clone de l'appelant -- un fichier que seul le transport
(`clone`/`fetch`) a le droit d'ecrire.

- l'appel quitte `build_stack_index()` ; la passe LIT, elle n'ECRIT plus ;
- le nettoyage devient la commande explicite `merge_dwell.py --prune-shallow`,
  lancee a la main (argument argparse, aucune garde ne l'appelle) ;
- le critere d'une racine VIVANTE s'elargit au-dela de `for-each-ref` : `HEAD`,
  le `HEAD` de chaque worktree lie (`-C <path> rev-parse HEAD`, toutes versions
  de git) et les reflogs comptent aussi, par atteignabilite
  (`merge-base --is-ancestor`) et non par identite ;
- deux tests negatifs : une borne atteinte par un HEAD detache sans ref ni
  reflog survit ; une borne atteinte par le seul HEAD d'un worktree lie survit.
  Controle positif mesure : avec `_alive_tips` neutralise, la premiere borne
  est bien retiree -- les tests ne passent pas par construction.

79 tests `test_merge_dwell.py` verts, 251 `test_check_adjoint_prevalidation.py`
verts. `python scripts/ci/merge_dwell.py --prune-shallow` rend rc=0.

Part of #23089. See #23110.

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

jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

La remarque de la review ai-01 du 2026-10-28 sur build_stack_index() est traitee, a la tete 7466d7f887.

Ce qui a change

Point demande Etat
L'appel _prune_dead_shallow_roots() quitte build_stack_index() fait -- la passe LIT, elle n'ECRIT plus dans .git/shallow
Le nettoyage devient une commande explicite, lancee a la main python scripts/ci/merge_dwell.py --prune-shallow (argparse sous __main__) ; aucune garde ne l'appelle
Le critere compte HEAD, les HEAD des worktrees et les reflogs fait, par ATTEIGNABILITE (merge-base --is-ancestor), pas par identite
Test negatif : une borne atteinte par un HEAD detache, sans aucune ref, doit survivre test_une_borne_atteinte_par_un_head_detache_survit

Deux details de mise en oeuvre :

  • le HEAD d'un worktree lie est lu par git -C <path> rev-parse HEAD, et non par la ligne HEAD <sha> du porcelain (git >= 2.36 seulement) : meme reponse sur toute version ;
  • la lecture des reflogs est bornee aux 500 entrees les plus recentes, et _reachable_from est fail-safe : une lecture impossible ne retire jamais rien (retirer une racine vivante casse un parcours, laisser une racine morte ne coute qu'un booleen).

Un second test negatif couvre le troisieme temoin demande : test_une_borne_atteinte_par_le_head_d_un_worktree_lie_survit (ni ref, ni HEAD courant, ni reflog -- seul le HEAD du worktree lie atteint la borne).

Preuves

  • python -m pytest scripts/tests/test_merge_dwell.py : 79 passed (dont les 4 tests shallow).
  • python -m pytest scripts/tests/test_check_adjoint_prevalidation.py : 251 passed.
  • python scripts/ci/merge_dwell.py --prune-shallow : rc=0, racines shallow mortes retirees : 0 sur ce clone.
  • Controle positif mesure : avec _alive_tips neutralise, le test du HEAD detache retire bien la borne (prune_dead_shallow_roots() == [foreign]). Les deux tests negatifs ne passent donc pas par construction -- ils echouent sur le critere d'avant.

STACK_UNAVAILABLE_SHALLOW nomme desormais le geste manuel en plus de git fetch --unshallow, pour que l'operateur qui voit l'incident ait le levier sous les yeux.

La tete a bouge (9959c5967c -> 7466d7f887) : le dossier [ADJOINT PREFLIGHT] pose sur l'ancienne tete est perime par ce commit, il en faut un neuf avant le merge.

Part of #23089.

🤖 Generated with Claude Code

@myia-ai-01 myia-ai-01 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.

[OVERRIDE] lane myia-ai-01:CoursIA -- leve ma propre demande de changements (myia-ai-01, review 5682529679 du 2026-10-28 12:25Z sur 9959c596), relue a la tete 7466d7f887a55d13b59091c57cbff1fcc11ea292.

Ce que je demandais, et ce que la tete fait :

  • la garde ne doit plus ecrire dans .git/shallow : build_stack_index() (scripts/check_adjoint_prevalidation.py) n'appelle plus le nettoyage, le commentaire le dit, et la fonction ne fait que lire le predicat base_history_truncated() ;
  • le nettoyage devient une commande explicite : merge_dwell.py --prune-shallow, dont le critere de racine vivante compte HEAD, le HEAD de chaque worktree lie et les reflogs, par atteignabilite (merge-base --is-ancestor), avec un doute qui ne retire jamais rien (_reachable_from rend True en cas d'echec de lecture) ;
  • test negatif : test_une_borne_atteinte_par_un_head_detache_survit et test_une_borne_atteinte_par_le_head_d_un_worktree_lie_survit sont la.

Verification independante en worktree detache a cette tete : test_merge_dwell.py et test_check_adjoint_prevalidation.py, 330 tests passes.

Le dossier exact-head reste a refaire a cette tete par une lane tierce.

@jsboige

jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23110
head: 7466d7f
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 2
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 38980e61cb95031b2cb18382c89ac2e5ee4621f4c4ea29d8092de6372c7d6cfb
diff-files: 4
diff-additions: 748
diff-deletions: 2
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23110
organ-rc: 0
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23110
head: 7466d7f
complete: true
body: read
comments-reviewed: 7
reviews-reviewed: 2
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: ee6c0cd1caf71c4399d246095622fdf4b4b370224772fa96364aa04cac17874e
diff-files: 4
diff-additions: 748
diff-deletions: 2
checks: blocked
b0: clear
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23110
organ-rc: 3
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23110
head: 7466d7f
complete: true
body: read
comments-reviewed: 8
reviews-reviewed: 2
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: cd87ebdea2ff4f51cc2ee7ddec8bb0f98f4658d7a5d87388eb4086c0b33b63c3
diff-files: 4
diff-additions: 748
diff-deletions: 2
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23110
organ-rc: 0
supersedes: 8
supersedes-why: comment 8 is my own dossier at the same head 7466d7f, posted 2026-10-28T17:03:08Z with verdict BLOCKED whose only blocking field was checks. That reason is extinguished: the fold of commits/7466d7f887/check-runs now gives 21 legs / 20 names with every conclusion green, including perimeter review guard (#11268) success twice at 18:18:55Z and PR gate success at 20:42:05Z, and the gate itself reports that every conclusion is green and every required check is present, so it refuses the old dossier and asks for a fresh third-party one, never a merge on this one. B.0 re-measured rc=0; the five unevaluated comments are my own earlier dossiers and the lane's note that the ai-01 review remark on build_stack_index was handled at this head -- no reserve among them. Scope checked against the body: the four files of the diff are exactly the four the body names (scripts/ci/merge_dwell.py, scripts/check_adjoint_prevalidation.py and their two test suites), and the body carries the direct proof of the shallow-borne path (the fetch --depth=1 table) plus the suite counts. Domain: the PR's own suite ran green in CI on this head fold (Scripts Tests (CPU) success at 16:13:41Z), with the secret-scanning and CodeQL legs green on the same head.
[/ADJOINT PREFLIGHT]

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

Labels

variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants