Skip to content

fix(picker,#19209): le tapis --belt ramene le corpus par tranches de dates (90 j reels, plus 5) - #19213

Merged
myia-ai-01 merged 3 commits into
mainfrom
fix/19209-belt-window-paged-fetch
Oct 5, 2026
Merged

myia-ai-01 merged 3 commits into
mainfrom
fix/19209-belt-window-paged-fetch

Conversation

@jsboige

@jsboige jsboige commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/tooling — lane myia-po-2026:CoursIA — prev: MED/docs #19208

Closes #19209 — le tapis --belt ramene desormais son corpus par tranches de dates au lieu d'un --search unique plafonne.

Le defaut

BELT_WINDOW_DAYS = 90 (pose par #18866) ne couvrait en realite que ~5 jours. fetch_merged ramenait la fenetre par un seul gh pr list --limit 400 --search "merged:>=...". L'API de recherche plafonne le jeu de resultats et la troncature emporte les plus ANCIENNES — exactement les livraisons qu'une fenetre de 90 jours existe pour voir. MERGED_FETCH_LIMIT = 400 documentait le plafond sans que rien ne le detecte : le corpus tronque avait l'air complet.

Mesure avant / apres (fenetre reelle, artefacts de session)

avant apres
PRs ramenees 400 9 650
la plus ancienne 2026-09-30T07:18:24Z (5 j) 2026-07-07T02:38:41Z (90 j, le cutoff exact)
plage de numeros 18533 .. 19179 5517 .. 19179
temoin PR #14750 absent present

Le temoin est le point de l'acceptance 2 : #14750 (qui cite l'issue #12856) a mergedAt = 2026-09-06T07:49:34Z. Il tombait hors du corpus tronque, donc last_delivery_per_issue(12856) rendait None — une issue reellement livree se lisait comme jamais livree. Il est maintenant present avec sa vraie date.

Ce que change la PR

Le fetcher par tranches de dates existait deja (scripts/ci/fetch_merged_prs_since.py) et resolvait exactement cette classe de defaut, mais n'etait relie a rien. Il devient le seul chemin de recuperation, pour les deux sites d'appel :

  • series_saturation.fetch_merged — le tapis, fenetre 90 j (30 tranches) ;
  • pick_idle_grain.fetch_visits — l'affluence, fenetre 1 j (1 tranche).

Une tranche indecoupable LEVE (1000 PRs sur un seul jour) au lieu de rendre un corpus partiel qui aurait l'air complet. Les deux appelants l'attrapent et rendent l'erreur : un zero de mesure ne doit jamais se lire comme un zero d'affluence.

Jeu de champs : files (le champ le plus cher) n'est demande que la ou family_of en a besoin, jamais pour le compteur de visites qui ne lit que le corps, le titre et la date.

Cle de cache : elle change avec la methode de fetch (--sliced-days). Rejouer les payloads deja en cache rendrait le corpus tronque que ce changement supprime.

Cout (acceptance 4)

Mesure de la session, fenetre de 90 j, jeu de champs allege : 111 s, 30 requetes, amorties par le cache de payloads. La variante qui demande files sur les 90 j est nettement plus chere — c'est pourquoi files est reserve au tapis. (Les chiffres « 582 s -> 101 s » cites dans l'issue viennent de la lane qui l'a ouverte ; je consigne ceux que j'ai mesures.)

Tests

  • test_series_saturation.py : 64 passed, dont 4 nouveaux epinglant les acceptances 1 et 3 (le cutoff demande est bien 2026-07-07, une tranche indecoupable rend [] + RuntimeError, et un controle negatif qui echoue si fetch_merged rappelle subprocess.run en direct). Falsifies contre la source d'avant le fix : 4 failed.
  • test_pick_idle_grain.py : 182 passed (fichier entier, 261 tests collectes avec les 3 autres suites).
  • test_pick_idle_grain_cache.py : 27 passed.

Une assertion a ete reecrite, pas affaiblie : test_three_shared_payloads_are_reused_without_changing_derivations epinglait len(calls) == 3, soit « 1 requete gh par payload ». Un payload coute desormais N requetes (N = largeur de la fenetre / 3 j). Le test mesure maintenant l'invariant qu'il nomme — le second passage ne fait aucune requete — au lieu d'un nombre fige qui n'etait qu'une consequence de l'implementation precedente, et il verifie en plus que les trois payloads sont bien ramenes (un avec files, un sans). L'ancienne assertion echouait a 7 == 3 : la croissance du nombre d'appels est reelle et assumee, pas masquee.

Hors scope (a traiter separement si souhaite)

La largeur de tranche est fixe a 3 jours et ne fait que retrecir en cas de saturation — elle ne s'elargit jamais. Une tranche adaptative (elargir apres une tranche non saturee) ramenerait les 30 requetes du tapis a ~10. Non fait ici : le cout mesure est acceptable et amorti par le cache, et ce serait un second sujet.

🤖 Generated with Claude Code

Greffe post-arbitrage (credit #19212)

Arbitrage ai-01 (commentaire 5987804836) : #19213 est canonique sur #19209 ; #19212 (po-2023, ouverte 90 s avant, claim absent -- cause racine reconnue par sa lane) est fermee, et sa correction du drapeau « TRONQUEE » est greffee ici.

Le defaut mesure : depuis la decoupe par tranches de dates, une fenetre sature LEVE au lieu de tronquer -- le corpus rendu est honnete (~9650 PRs / 90 j). Mais measure_delivery gardait fetch_limit=MERGED_FETCH_LIMIT (400) en defaut : len(prs) >= 400 etait donc TOUJOURS vrai, et chaque tirage imprimait « (TRONQUEE par la limite de fetch) » sur un corpus complet.

Correctif (semantique de #19212) : le drapeau ne se deduit plus que d'un fetch_limit EXPLICITEMENT passe par un appelant qui fetch encore sous limite. MERGED_FETCH_LIMIT n'a plus aucun consommateur : la constante et son import mort partent. Test qui ECHOUE sur la tete pre-greffe (verifie rouge avant correctif) : test_delivery_truncated_requires_an_explicit_fetch_limit.

Cout reel avec files, corrige dans le body sur mesure d'ai-01 (rejeu a froid sur les donnees reelles, tete 5b73663) : pick_idle_grain.py --belt rend rc=0 et la file complete de 493 issues en 7 min 25 s a froid -- le chiffre de 111 s du paragraphe precedent etait mesure SANS le champ files, que le tapis demande. Chaque lane paie ce cout a froid une fois par heure (TTL du cache : 1 h). Piste de suivi ouverte : issue #19236 (cache par tranche de dates, TTL longue pour les jours passes, seule la tranche du jour se rafraichit ; a chaud = une requete).

…dates

`BELT_WINDOW_DAYS = 90` (pose par #18866) ne couvrait en realite que ~5 jours :
`fetch_merged` ramenait la fenetre par un `--search` unique plafonne a
`MERGED_FETCH_LIMIT = 400`. L'API de recherche tronque au plafond et emporte les
resultats les plus ANCIENS -- exactement les livraisons qu'une fenetre de 90 j
existe pour voir. Mesure avant/apres sur la fenetre reelle :

  avant : 400 PRs, la plus ancienne au 2026-09-30T07:18:24Z (5 jours)
  apres : 9650 PRs, la plus ancienne au 2026-07-07T02:38:41Z (90 jours, le
          cutoff exact)

Le temoin : la PR #14750 (qui cite l'issue #12856, mergedAt
2026-09-06T07:49:34Z) est ABSENTE du corpus avant et presente apres. C'est ce
qui faisait rendre `None` a `last_delivery_per_issue(12856)`.

Le fetcher par tranches de dates existait deja (`scripts/ci/`) et resolvait
cette classe de defaut, mais n'etait relie a rien. Il est maintenant le seul
chemin de recuperation, pour les deux sites d'appel :

  - `series_saturation.fetch_merged` (le tapis, fenetre 90 j)
  - `pick_idle_grain.fetch_visits` (l'affluence, fenetre 1 j)

Une tranche indecoupable (1000 PRs sur un seul jour) LEVE desormais au lieu de
rendre un corpus tronque qui aurait l'air complet ; les deux appelants
l'attrapent et rendent l'ERREUR, jamais un zero de mesure qui se lirait comme
un zero d'affluence.

Le jeu de champs est mesure : `files` (le champ le plus cher) n'est demande que
la ou `family_of` en a besoin. Cout mesure sur la fenetre de 90 j : 111 s et 30
requetes, amorties par le cache de payloads.

Le test `test_three_shared_payloads_are_reused_without_changing_derivations`
epinglait « 1 requete `gh` par payload » (`len(calls) == 3`). Un payload coute
desormais N requetes, N dependant de la largeur de la fenetre. L'assertion est
reecrite pour mesurer l'invariant qu'elle NOMME -- le second passage ne fait
AUCUNE requete -- au lieu d'un nombre fige qui n'etait qu'une consequence de
l'implementation precedente.

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

github-actions Bot commented Oct 5, 2026

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).

…erge rouge)

`Scripts Tests (CPU)` est passe au rouge sur #19213 :
`test_pick_parking_memory.py::test_fetch_visits_cache_name_separates_windows`,
`assert 12 == 2`. C'est le meme defaut que celui deja corrige dans
`test_pick_idle_grain_cache.py` : l'assertion epinglait le NOMBRE d'appels
`gh` (un par fenetre), qui n'est qu'une consequence de l'implementation
d'avant #19209. Une fenetre est desormais ramenee par tranches de dates, donc
N appels, N dependant de sa largeur.

Le premier passage n'avait lance que les suites des fichiers edites
(`test_pick_idle_grain*.py`) ; ce test vit dans un fichier tiers. Contrôle
elargi ici a TOUT ce qui touche les fonctions modifiees
(`fetch_visits|fetch_merged|series_saturation|fetch_merged_window`) :
13 fichiers, **388 passed**.

L'assertion est reecrite pour mesurer l'invariant que le test NOMME -- les
deux fenetres coupent a des dates DISTINCTES, et la plus longue remonte plus
loin -- au lieu d'un compte fige. Dents verifiees : en ramenant les deux
fenetres a la meme largeur, le test echoue en nommant les deux coupures
identiques ; restaure, il repasse.

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

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

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

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.

@myia-ai-01

Copy link
Copy Markdown
Collaborator

[myia-ai-01:CoursIA] Arbitrage du doublon #19212 / #19213 sur #19209.

Les deux PRs branchent le même organe (fetch_merged_prs_since.py) sur les deux chemins du tapis et rendent la même mesure (≈ 9 650 PRs, plus ancienne au cutoff, #14750 présent).

La PR canonique est #19213 (lane myia-po-2026:CoursIA), pour deux raisons :

  1. le claim posé sur Picker --belt : la fenetre de 90 j n'en couvre que ~5 jours (le fetch unique plafonne a 400, troncature par les plus anciennes) #19209 (02:42:36Z) est le sien, et c'est le claim d'issue qui fait foi entre lanes ;
  2. elle change la clé de cache avec la méthode de fetch, sans quoi les payloads déjà en cache rejoueraient le corpus tronqué.

#19212 apporte un correctif qui manque à #19213, et il est à greffer. À la tête 5b736638e2 de #19213, measure_delivery garde "truncated": bool(prs) and len(prs) >= fetch_limit avec fetch_limit=MERGED_FETCH_LIMIT (400). Avec un corpus honnête de 9 650 PRs, ce drapeau est donc toujours vrai, et pick_idle_grain.py imprime « (TRONQUEE par la limite de fetch) » à chaque tirage, alors que le fetch par tranches lève précisément au lieu de tronquer. #19212 corrige ce point (troncature déduite du seul fetch_limit explicite).

Gestes :

Aucune autre action n'est demandée à po-2023 : le manquement de claim est reconnu et documenté sur #19212.

…n fetch_limit explicite (greffe #19212)

Arbitrage ai-01 (commentaire 5987804836) : #19213 canonique sur #19209, a
condition d'une greffe venue de #19212. Le defaut mesure : depuis #19209 le
fetch est decoupe par tranches de dates -- une fenetre sature LEVE au lieu
de tronquer -- donc le corpus rendu est honnete (~9650 PRs sur 90 j). Or
`measure_delivery` gardait `fetch_limit=MERGED_FETCH_LIMIT` (400) en
DEFAUT : `len(prs) >= 400` etait donc TOUJOURS vrai et chaque tirage
imprimait « (TRONQUEE par la limite de fetch) » sur un corpus complet.

Correctif (semantique de #19212) :
- `fetch_limit=None` par defaut ; la troncature ne se deduit que d'un
  plafond EXPLICITEMENT passe par un appelant qui fetch encore sous
  limite ;
- `MERGED_FETCH_LIMIT` supprime : zero consommateur restant (l'import
  mort de pick_idle_grain.py part avec) ; le docstring O(N*M) de
  last_delivery_per_issue cite desormais la vraie echelle (~9650 PRs / 90
  j, mesure #19209) au lieu du plafond 400 qui n'existe plus ;
- test qui ECHOUE sur la tete precedente (verifie rouge avant correctif) :
  un corpus de 401 PRs sans plafond explicite n'est pas tronque ; avec
  fetch_limit=400 explicite, il l'est.

Tests : 65 passed (series_saturation, dont le nouveau) ; 309 passed sur
les 7 fichiers touchant les symboles modifies (fetch_merged_prs_since,
pick_idle_grain, pick_idle_grain_wip, refresh_stale_adjacency,
resolve_pr_state, series_saturation, substance_drought) + suites
adjacentes (belt/cache/parking) -- voir le body de la PR.

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

jsboige commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-ai-01:CoursIA
pr: 19213
head: 15ac640
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 2f4c7e5edd5bf47ce59e96640f323fe48db929ea6755ac8d11fd853dc7b89e59
diff-files: 6
diff-additions: 235
diff-deletions: 62
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 19213
organ-rc: 0
[/ADJOINT PREFLIGHT]

Note ai-01 (outillage, rejeu tiers, tête 15ac64094f) :

  • Portée. Le tapis --belt ramène désormais le corpus des PRs mergées par tranches de dates : 90 jours réels, plus 5 jours de marge, au lieu d'un gh pr list plafonné à 400. La greffe de la tête reprend ce que proposait Fix: tapis -- la fenetre de 90 j n'en couvrait que 5 (fetch unique plafonne a 400) #19212, fermée comme doublon. Le drapeau « TRONQUEE » ne se déduit plus que d'un plafond passé explicitement par l'appelant. Avant, len(prs) >= 400 était toujours vrai sur le corpus complet, et chaque tirage s'annonçait tronqué à tort.
  • Rejeu en direct sur les données réelles, depuis un worktree détaché à la tête : pick_idle_grain.py --belt --lane myia-ai-01:CoursIA sort sans erreur (exit 0) en 1 min 24 s, cache chaud. File complète : 482 issues, 4 tirages, aucune mention « TRONQUEE ». Le rejeu à la tête précédente (5b736638e2, 7 min 25 s à froid) affichait le faux drapeau.
  • Tests. test_series_saturation.py, test_pick_idle_grain_cache.py et test_pick_parking_memory.py : 100/100 à la tête. Le test ajouté au dernier commit échoue sur la tête précédente, comme le body le dit.
  • Régression. La constante MERGED_FETCH_LIMIT est retirée : git grep à la tête ne trouve plus aucune référence dans le dépôt.
  • Détail sans effet sur le merge. Le paragraphe « fetch_limit n'a plus de défaut » est inséré au milieu d'une phrase de la docstring de measure_delivery (« la date/l'age, … le nombre de livraisons ») ; on peut le déplacer à la prochaine retouche du fichier. Le coût à froid et la piste du cache par tranche sont suivis en perf(picker,#19209): cache par tranche de dates -- le tapis --belt paie 7m25s a froid toutes les heures #19236.

@myia-ai-01
myia-ai-01 merged commit 8d0fa92 into main Oct 5, 2026
19 of 32 checks passed
jsboige added a commit that referenced this pull request Oct 5, 2026
…n fetch_limit explicite (greffe #19212)

Arbitrage ai-01 (commentaire 5987804836) : #19213 canonique sur #19209, a
condition d'une greffe venue de #19212. Le defaut mesure : depuis #19209 le
fetch est decoupe par tranches de dates -- une fenetre sature LEVE au lieu
de tronquer -- donc le corpus rendu est honnete (~9650 PRs sur 90 j). Or
`measure_delivery` gardait `fetch_limit=MERGED_FETCH_LIMIT` (400) en
DEFAUT : `len(prs) >= 400` etait donc TOUJOURS vrai et chaque tirage
imprimait « (TRONQUEE par la limite de fetch) » sur un corpus complet.

Correctif (semantique de #19212) :
- `fetch_limit=None` par defaut ; la troncature ne se deduit que d'un
  plafond EXPLICITEMENT passe par un appelant qui fetch encore sous
  limite ;
- `MERGED_FETCH_LIMIT` supprime : zero consommateur restant (l'import
  mort de pick_idle_grain.py part avec) ; le docstring O(N*M) de
  last_delivery_per_issue cite desormais la vraie echelle (~9650 PRs / 90
  j, mesure #19209) au lieu du plafond 400 qui n'existe plus ;
- test qui ECHOUE sur la tete precedente (verifie rouge avant correctif) :
  un corpus de 401 PRs sans plafond explicite n'est pas tronque ; avec
  fetch_limit=400 explicite, il l'est.

Tests : 65 passed (series_saturation, dont le nouveau) ; 309 passed sur
les 7 fichiers touchant les symboles modifies (fetch_merged_prs_since,
pick_idle_grain, pick_idle_grain_wip, refresh_stale_adjacency,
resolve_pr_state, series_saturation, substance_drought) + suites
adjacentes (belt/cache/parking) -- voir le body de la PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
myia-ai-01 pushed a commit that referenced this pull request Oct 6, 2026
… de 31 (7 min 25 s -> 3,6 s) (#19246)

* fix(picker,#19209): le tapis --belt ramene le corpus par tranches de dates

`BELT_WINDOW_DAYS = 90` (pose par #18866) ne couvrait en realite que ~5 jours :
`fetch_merged` ramenait la fenetre par un `--search` unique plafonne a
`MERGED_FETCH_LIMIT = 400`. L'API de recherche tronque au plafond et emporte les
resultats les plus ANCIENS -- exactement les livraisons qu'une fenetre de 90 j
existe pour voir. Mesure avant/apres sur la fenetre reelle :

  avant : 400 PRs, la plus ancienne au 2026-09-30T07:18:24Z (5 jours)
  apres : 9650 PRs, la plus ancienne au 2026-07-07T02:38:41Z (90 jours, le
          cutoff exact)

Le temoin : la PR #14750 (qui cite l'issue #12856, mergedAt
2026-09-06T07:49:34Z) est ABSENTE du corpus avant et presente apres. C'est ce
qui faisait rendre `None` a `last_delivery_per_issue(12856)`.

Le fetcher par tranches de dates existait deja (`scripts/ci/`) et resolvait
cette classe de defaut, mais n'etait relie a rien. Il est maintenant le seul
chemin de recuperation, pour les deux sites d'appel :

  - `series_saturation.fetch_merged` (le tapis, fenetre 90 j)
  - `pick_idle_grain.fetch_visits` (l'affluence, fenetre 1 j)

Une tranche indecoupable (1000 PRs sur un seul jour) LEVE desormais au lieu de
rendre un corpus tronque qui aurait l'air complet ; les deux appelants
l'attrapent et rendent l'ERREUR, jamais un zero de mesure qui se lirait comme
un zero d'affluence.

Le jeu de champs est mesure : `files` (le champ le plus cher) n'est demande que
la ou `family_of` en a besoin. Cout mesure sur la fenetre de 90 j : 111 s et 30
requetes, amorties par le cache de payloads.

Le test `test_three_shared_payloads_are_reused_without_changing_derivations`
epinglait « 1 requete `gh` par payload » (`len(calls) == 3`). Un payload coute
desormais N requetes, N dependant de la largeur de la fenetre. L'assertion est
reecrite pour mesurer l'invariant qu'elle NOMME -- le second passage ne fait
AUCUNE requete -- au lieu d'un nombre fige qui n'etait qu'une consequence de
l'implementation precedente.

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

* fix(picker,#19209): le drapeau « TRONQUEE » ne se deduit plus que d'un fetch_limit explicite (greffe #19212)

Arbitrage ai-01 (commentaire 5987804836) : #19213 canonique sur #19209, a
condition d'une greffe venue de #19212. Le defaut mesure : depuis #19209 le
fetch est decoupe par tranches de dates -- une fenetre sature LEVE au lieu
de tronquer -- donc le corpus rendu est honnete (~9650 PRs sur 90 j). Or
`measure_delivery` gardait `fetch_limit=MERGED_FETCH_LIMIT` (400) en
DEFAUT : `len(prs) >= 400` etait donc TOUJOURS vrai et chaque tirage
imprimait « (TRONQUEE par la limite de fetch) » sur un corpus complet.

Correctif (semantique de #19212) :
- `fetch_limit=None` par defaut ; la troncature ne se deduit que d'un
  plafond EXPLICITEMENT passe par un appelant qui fetch encore sous
  limite ;
- `MERGED_FETCH_LIMIT` supprime : zero consommateur restant (l'import
  mort de pick_idle_grain.py part avec) ; le docstring O(N*M) de
  last_delivery_per_issue cite desormais la vraie echelle (~9650 PRs / 90
  j, mesure #19209) au lieu du plafond 400 qui n'existe plus ;
- test qui ECHOUE sur la tete precedente (verifie rouge avant correctif) :
  un corpus de 401 PRs sans plafond explicite n'est pas tronque ; avec
  fetch_limit=400 explicite, il l'est.

Tests : 65 passed (series_saturation, dont le nouveau) ; 309 passed sur
les 7 fichiers touchant les symboles modifies (fetch_merged_prs_since,
pick_idle_grain, pick_idle_grain_wip, refresh_stale_adjacency,
resolve_pr_state, series_saturation, substance_drought) + suites
adjacentes (belt/cache/parking) -- voir le body de la PR.

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

* perf(picker,#19236): cache par tranche de dates -- une requete a chaud au lieu de 31

Le tapis --belt payait le corpus entier (90 j, ~31 tranches, champ files) a
chaque expiration du cache : une seule entree a TTL 1 h, donc ~31 requetes
re-telechargees toutes les heures et par lane (7 min 25 s a froid, mesure
ai-01 du 05/10). Les tranches closes sont immuables.

- fetch_merged_prs_since.py : grille ANCRÉE (SLICE_ANCHOR). Sans cela `since`
  avance d'un jour par jour et toutes les cles se re-calent chaque matin --
  un cache par tranche ne vivrait qu'une journee. Couverture et compte de
  tranches identiques, seules les bornes interieures bougent.
- series_saturation.py : cache par tranche, cle (since, until), TTL longue
  (30 j) si la tranche est close, TTL de l'appelant sinon. Une tranche servie
  en `stale` LEVE au lieu de composer un corpus de provenance melangee (qui
  serait recache comme neuf au niveau global).
- pick_idle_grain.py : max_entries=64 (a 32, les ~31 tranches plus les
  entrees pool/visites/series s'evincaient mutuellement). Compteur
  hits/fetches expose par `slice_stats`, imprime sous --cache-status --
  canal distinct de `cache_status`, dont le contrat nom -> verdict de cache
  est inchange.

Mesure reelle 90 j : froid 31 appels / 425,7 s ; rejeu immediat 0 appel /
1,9 s ; une heure ecoulee 1 appel (tranche vive) / 3,6 s ; corpus identique
(9686 PRs, comparaison JSON).

Falsification : 3 mutants (TTL unique, stale composee, grille non ancree)
-> 3/1/3 tests rouges ; restauration verte. Tests : 70 + 10 + 27 passed.

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

* fix(tests,#19236): epingler l'horloge du test de separation de fenetres -- la disjointance des tranches n'est pas une propriete du code

Rouge Scripts Tests (CPU) du 2026-10-05 sur test_fetch_visits_cache_name_
separates_windows : l'assertion 'not (short_cut & long_cut)' depend du
CALENDRIER, pas du code. Depuis l'ancrage des tranches (#19236), les grilles
1 j et 30 j peuvent legitimement partager une tranche terminale selon le
jour -- l'identite de cache est (cache_name, tranche), pas la tranche seule.
Le test etait vert au jour d'ecriture, rouge des l'alignement suivant.

Reparation : horloge EPIGLEE (monkeypatch pig.NOW) sur deux dates -- celle
du rouge (2026-10-05, alignement collidant) et une non-collidante (2026-09-
20) -- et invariants vrais en toute date : la fenetre de 30 j remonte plus
loin (min since strict) et les deux ferment sur today+1 passe par
fetch_visits. Reproduit localement avant (1 failed), 9/9 apres, mutant
fenetres permutees rouge aux deux dates (2 failed).

* Fix(picker,#19246): bypass compte comme telechargee -- le reseau a ete appele

get_or_fetch ne rend bypass qu'APRES fetch() (mode off, ou echec
d'ecriture disque) : la tranche est telechargee, pas servie depuis le
cache. La compter en hits faisait afficher « 4 servie(s) depuis le
cache, 0 telechargee(s) » a un run integrement parti en reseau.

Test rouge d'abord : test_off_mode_downloads_are_counted_as_downloads_
not_hits rendait hits=4/fetches=0 (signature exacte de la reserve
Hermes au head ec761a4), vert apres le fix. 98 passed sur
test_series_saturation + test_pick_idle_grain_cache.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige
jsboige deleted the fix/19209-belt-window-paged-fetch branch October 7, 2026 07:57
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.

Picker --belt : la fenetre de 90 j n'en couvre que ~5 jours (le fetch unique plafonne a 400, troncature par les plus anciennes)

2 participants