From 4a42b8742a9e0c338b047240c1c5e70a7423d59a Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 15 Sep 2026 00:07:21 +0200 Subject: [PATCH 1/4] fix(ci,#16207): restore PR-time markdown table scan without its cost pull_request trigger (paths-filtered) is back on markdown-table-guard.yml; the 2.22 Go full-tree checkout that killed it (#12817 tranche 1) is replaced by a blob:none partial clone + dynamic `git sparse-checkout add --no-cone` of the changed files only. Founding incident #16177: CODE_SPAN_PIPE merged with no review-time signal. Arbitrage consigne: re-housing in always-on-guards rejected (blast radius on the critical path). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/markdown-table-guard.yml | 49 +++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/.github/workflows/markdown-table-guard.yml b/.github/workflows/markdown-table-guard.yml index 542a1cbf3c..c91b8b0088 100644 --- a/.github/workflows/markdown-table-guard.yml +++ b/.github/workflows/markdown-table-guard.yml @@ -28,8 +28,39 @@ name: Markdown table syntax advisory # Tranche 2 #12817: the per-PR fork guard died with the pull_request trigger # (false under schedule -> job SKIPPED, organ extinguished). The nocturne # scans the last-24h window of main; labels are PR-only and noop'd at night. +# +# #16207 -- the PR-time trigger is BACK, without the cost that killed it. +# #12817 tranche 1 removed pull_request because each run cloned the full +# 2.22 Go working tree (stratification user 2026-08-23: "les jobs lourds +# devraient être payés une fois par fournée"). The founding incident that +# reopened the question: #16177 -- a CODE_SPAN_PIPE in the intro table +# merged with NO signal; the first pair of eyes on it was a post-merge +# human comment. The re-introduction keeps the stratification honest: +# - `paths` fires ONLY for the file trees this guard scans (*.ipynb / +# *.md / *README*), plus its own wiring and the two scripts it runs; +# - the checkout materializes the scan toolbox ONLY (blob:none partial +# clone), then the changed files' blobs on demand via +# `git sparse-checkout add --no-cone` -- never the full working tree. +# Marginal cost per PR: one metadata clone + the changed files' blobs + a +# pure-Python scan, on the self-hosted ephemeral leg. The nocturne is +# unchanged (it also scans only the last-24h window's changed files). +# Rejected alternative (arbitrage consigné): re-housing the scan as a leg +# of always-on-guards.yml (mutualized checkout) -- rejected for blast +# radius: coupling an advisory labeler into the critical-path gate organs +# changes their failure surface for a signal that must stay advisory. on: + pull_request: + # #16207 : le signal PR-time. Filtre paths = les seuls arbres que la + # garde scanne (elle ne scanne jamais le reste -- voir "Per-PR scope" + # ci-dessus) + son propre câblage + les deux scripts qu'elle exécute. + paths: + - '**/*.ipynb' + - '**/*.md' + - '**/*README*' + - '.github/workflows/markdown-table-guard.yml' + - 'scripts/notebook_tools/scan_md_table_syntax.py' + - 'scripts/notebook_tools/md_table_sweep_comment.py' schedule: # Nocturnal sweep -- post-merge main verification. # Tranche 1 #12817 : sortir les advisory lourds de pull_request @@ -65,11 +96,20 @@ jobs: if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository steps: - - name: Checkout PR + - name: Checkout PR (metadata + scan toolbox, never the full tree) uses: actions/checkout@v4 with: fetch-depth: 0 filter: blob:none + # #16207 : le cout qui a tue le trigger etait l'arbre de travail + # complet (2.22 Go). Ici on ne materialise QUE la boite a outils du + # scan ; les blobs des fichiers modifies arrivent plus bas, a la + # demande, via `git sparse-checkout add --no-cone` sur changed.txt. + # fetch-depth: 0 reste requis : le diff 3-points BASE...HEAD lit le + # merge-base, et le nocturne resout la fenetre 24 h par rev-list. + sparse-checkout: | + /scripts/notebook_tools/ + sparse-checkout-cone-mode: false - name: Set up Python uses: actions/setup-python@v5 @@ -136,6 +176,13 @@ jobs: # Advisory: the job ALWAYS exits 0. The actionable signal is the # LABEL decided from the JSON total below. + # #16207 : materialiser les cibles du scan -- et elles seules. Le + # checkout d'entree est sparse (boite a outils uniquement) ; le diff + # --name-only ci-dessus n'a lu que les arbres (blob:none suffit) ; + # les blobs des fichiers modifies arrivent ici via le promissor du + # clone partiel. add (pas set) : le pattern outils pose par + # actions/checkout doit survivre a l'ajout. + git sparse-checkout add --no-cone $(cat changed.txt) python scripts/notebook_tools/scan_md_table_syntax.py $(cat changed.txt) --json > payload.json || true cat payload.json From 46659a2b2df6fd1a105e70369cc68dd6ffe3a379 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 15 Sep 2026 00:12:26 +0200 Subject: [PATCH 2/4] fix(ci,#16207): label description must fit the 100-char API limit ensure_label's 422 (description too long) was swallowed by 2>/dev/null, so the markdown-table-syntax label never existed and set_label failed with "not found" on the very first PR-time run. Shortened description, stderr no longer buried. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/markdown-table-guard.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/markdown-table-guard.yml b/.github/workflows/markdown-table-guard.yml index c91b8b0088..20ddbc851e 100644 --- a/.github/workflows/markdown-table-guard.yml +++ b/.github/workflows/markdown-table-guard.yml @@ -155,7 +155,11 @@ jobs: # Label helpers (idempotent). ensure_label() { - gh label create "$1" --description "$2" --color "$3" --force 2>/dev/null || true + # #16207 : pas de 2>/dev/null -- le create a echoue en 422 + # (description > 100 car.) en silence pendant des semaines parce + # que l'echec etait enterre ; stderr reste visible dans le log, + # seul l'exit reste non bloquant (|| true, advisory). + gh label create "$1" --description "$2" --color "$3" --force || true } set_label() { gh pr edit "$PR_NUMBER" --add-label "$1" || true; } unset_label() { gh pr edit "$PR_NUMBER" --remove-label "$1" 2>/dev/null || true; } @@ -166,7 +170,10 @@ jobs: unset_label() { true; } fi - ensure_label "$LABEL_DEFECT" "PR introduces a markdown table syntax defect (COL_MISMATCH / CODE_SPAN_PIPE / MATH_SPAN_PIPE / NO_SEP / NO_BLANK_BEFORE/AFTER / ORPHAN_TABLE_ROW). Advisory, non-blocking. See #10097." "d93f0b" + # #16207 : description <= 100 caracteres (limite API GitHub -- le + # texte long d'origine rendait le create en 422 a chaque run, + # silencieusement, et le label n'a jamais existe). + ensure_label "$LABEL_DEFECT" "Table syntax defect in changed files (CODE_SPAN_PIPE, NO_SEP, ...). Advisory. See #10097." "d93f0b" if [ "$COUNT" -eq 0 ]; then echo "No in-scope files to scan." From a86198ba62a4c60e5e86567a963385acd0ee6ece Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 16 Sep 2026 23:42:57 +0200 Subject: [PATCH 3/4] fix(ci,#16207): CR 12:35Z -- lossless scan paths (NUL-safe argv), no false "Clean." on scanner failure, whitespace-filename positive control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Repond aux 3 exigences de la CR ai-01 2026-09-16 12:35Z sur #16209 : 1. Passage lossless des chemins : git diff -z + grep -z + mapfile -d '' (argv octets-exacts). L'ancien "$(cat changed.txt)" splitait chaque nom a espaces du depot ('Créateur de mail personnalisé.ipynb', 'Conférence Tech 2025', 'Correction Activités GenAI.md', ...) en argv orphelins -> le scanner rendait exit 2 ("rien a scanner") -> payload vide -> faux "Clean." + retrait du label. Reproduit localement (exit 2, payload 0 octet, TOTAL=0). 2. Payload manquant/invalide != 0 : RC explicite du scanner + garde sur le parse (case numerique). Sur panne de mesure : ::error:: + label LAISSE EN PLACE (jamais d'unset sur un etat non mesure). 3. Controle positif live : fichier "$RUNNER_TEMP/md-table controle.md" (NO_SEP) scanne a chaque run -- un split whitespace le casserait en 2 argv -> exit 2 -> controle rouge. Ne nourrit pas le label (scan separe) : il gate la fiabilite de la mesure. Coordonne avec #16266 : hunks disjoints (leur ligne de description du label est deja satisfaite sur ce head). Coordonne avec #16266 (markdown-table-guard.yml partage); verifie par lecture des 2 diffs: aucun overlap textuel. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/markdown-table-guard.yml | 76 ++++++++++++++++++---- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/.github/workflows/markdown-table-guard.yml b/.github/workflows/markdown-table-guard.yml index 20ddbc851e..9d3882a8c4 100644 --- a/.github/workflows/markdown-table-guard.yml +++ b/.github/workflows/markdown-table-guard.yml @@ -146,11 +146,16 @@ jobs: # excluded). Skip vendored / archived trees where we do not enforce # this (.lake/ Mathlib, docs/_archives/, node_modules). # 3-point diff (#10403): merge-base...HEAD -- strictly this PR's apport. - git diff --name-only --diff-filter=d "$BASE...$HEAD" \ + # CR #16207 : -z (enregistrements NUL-separes) + grep -z partout. Un + # split whitespace casserait chaque nom a espaces du depot ('Créateur + # de mail personnalisé.ipynb', 'Conférence Tech 2025', 'Correction + # Activités GenAI.md', ...) en argv orphelins -> 0 fichier scannable + # -> exit 2 avale -> faux "Clean." (voir controle positif plus bas). + git diff -z --name-only --diff-filter=d "$BASE...$HEAD" \ -- '*.ipynb' '*.md' '*README*' \ - | grep -v -E '^(\.lake/|docs/_archives/|node_modules/)' \ - | grep -v $'\r$' > changed.txt || true - COUNT=$(wc -l < changed.txt | tr -d ' ') + | grep -z -v -E '^(\.lake/|docs/_archives/|node_modules/)' \ + | grep -z -v $'\r$' > changed.txt || true + COUNT=$(tr -cd '\0' < changed.txt | wc -c | tr -d ' ') echo "Modified *.ipynb/*.md/README* in scope: $COUNT" # Label helpers (idempotent). @@ -188,15 +193,61 @@ jobs: # --name-only ci-dessus n'a lu que les arbres (blob:none suffit) ; # les blobs des fichiers modifies arrivent ici via le promissor du # clone partiel. add (pas set) : le pattern outils pose par - # actions/checkout doit survivre a l'ajout. - git sparse-checkout add --no-cone $(cat changed.txt) - python scripts/notebook_tools/scan_md_table_syntax.py $(cat changed.txt) --json > payload.json || true - cat payload.json + # actions/checkout doit survivre a l'ajout. Les PATHS sont les argv + # OCTETS-EXACTS du fichier NUL-separe (mapfile -d ''), jamais une + # resubstitution shell (CR #16207). + mapfile -d '' PATHS < changed.txt + git sparse-checkout add --no-cone "${PATHS[@]}" - TOTAL=$(python -c "import json,sys; print(json.load(open('payload.json'))['total_findings'])" 2>/dev/null || echo 0) - echo "Total table-syntax defects in changed files: $TOTAL" + # CR #16207 : le scanner rend exit 2 ("rien a scanner") quand aucun + # path argv ne nomme un fichier existant -- ce qui arrivait en + # silence apres le split whitespace. RC explicite : un echec du + # scanner = PANNE DE MESURE (total illisible), jamais un "Clean.". + python scripts/notebook_tools/scan_md_table_syntax.py --json "${PATHS[@]}" > payload.json 2> scan.err + SCAN_RC=$? + if [ "$SCAN_RC" -ne 0 ]; then + echo "::error::scan_md_table_syntax.py failed (rc=$SCAN_RC) -- $LABEL_DEFECT NOT touched" + if [ -s scan.err ]; then cat scan.err; fi + else + cat payload.json + TOTAL=$(python -c "import json,sys; print(json.load(open('payload.json'))['total_findings'])" 2>/dev/null) || true + echo "Total table-syntax defects in changed files: $TOTAL" + # Payload lisible en rc mais total non numerique = panne de + # mesure, jamais un "Clean.". + case "$TOTAL" in + ''|*[!0-9]*) echo "::error::payload unreadable (total='$TOTAL') -- $LABEL_DEFECT NOT touched"; SCAN_RC=1 ;; + esac + fi - if [ "$TOTAL" -gt 0 ]; then + # ---- Controle positif CR #16207 : nom in-scope avec espaces ---- + # Prouve a chaque run que le passage argv est lossless : le fichier + # "md-table controle.md" porte trois lignes pipe sans separateur + # (NO_SEP) et doit rendre total >= 1. Un split whitespace a + # l'ancienne casserait ce path en argv orphelins -> exit 2 -> + # controle rouge. Le controle ne nourrit PAS le label (scan separe) : + # il gate la FIABILITE de la mesure. + CONTROL="$RUNNER_TEMP/md-table controle.md" + printf '| a | b |\n| c | d |\n| e | f |\n' > "$CONTROL" || true + python scripts/notebook_tools/scan_md_table_syntax.py --json "$CONTROL" > control.json 2>/dev/null + CTRL_RC=$? + CTRL_TOTAL=0 + if [ "$CTRL_RC" -eq 0 ]; then + CTRL_TOTAL=$(python -c "import json,sys; print(json.load(open('control.json'))['total_findings'])") + fi + if [ "$CTRL_TOTAL" -lt 1 ]; then + echo "::error::positive control failed (rc=$CTRL_RC, total=$CTRL_TOTAL) -- whitespace argv is lossy, measurement unreliable" + SCAN_RC=1 + else + echo "Positive control passed (whitespace filename fed to argv: $CTRL_TOTAL NO_SEP finding)" + fi + rm -f "$CONTROL" control.json scan.err + + # Label decision. CR #16207 : jamais d'unset sur un etat non mesure + # (SCAN_RC != 0) -- le label pose par un run precedent reste en + # place, le rouge vaut mieux qu'un faux "Clean.". + if [ "$SCAN_RC" -ne 0 ]; then + echo "::warning::table-syntax scan unreliable -- leaving '$LABEL_DEFECT' label untouched" + elif [ "$TOTAL" -gt 0 ]; then set_label "$LABEL_DEFECT" if [ "$NOCTURNE" -eq 1 ]; then echo "::warning::Found $TOTAL markdown table syntax defect(s) in the last-24h window on main. See scan_md_table_syntax.py --check locally." @@ -216,7 +267,8 @@ jobs: # de tag, pas de fermeture, pas de blocage de merge -- on route, ai-01 # tranche. apply : schedule toujours ; workflow_dispatch seulement si # apply=true (defaut dry-run, le corps s'imprime pour controle). - if [ "$NOCTURNE" -eq 1 ]; then + # CR #16207 : pas de sweep sur un payload illisible (SCAN_RC != 0). + if [ "$NOCTURNE" -eq 1 ] && [ "$SCAN_RC" -eq 0 ]; then WINDOW_DESC="$(git rev-parse --short "$BASE")..$(git rev-parse --short "$HEAD") (24 h de main)" EXTRA="" if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ inputs.apply }}" = "true" ]; then From 0fbe945b4505d7cafca64a8a6c6960ea1c6fbed6 Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 16 Sep 2026 23:47:21 +0200 Subject: [PATCH 4/4] fix(ci,#16207): positive control runs on EVERY run (not only when the PR has in-scope files) Le controle positif d'abord sautait par l'early-exit COUNT==0 : sur une PR workflow-only (le cas de la PR elle-meme) il ne s'executait jamais -> la preuve live n'existait qu'en smoke local. Deplace en amont de l'early-exit, il tourne a chaque run (PR-time ET nocturne) : preuve permanente du passage argv lossless sur l'infra reelle. Drapeau SCAN_RC porte la panne de mesure (controle ou scan reel ou parse non numerique) jusqu'a la decision de label ; l'early-exit COUNT==0 est lui-meme fail-closed (unset conditionne a SCAN_RC==0). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/markdown-table-guard.yml | 64 +++++++++++++--------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/.github/workflows/markdown-table-guard.yml b/.github/workflows/markdown-table-guard.yml index 9d3882a8c4..08910f7d98 100644 --- a/.github/workflows/markdown-table-guard.yml +++ b/.github/workflows/markdown-table-guard.yml @@ -180,9 +180,39 @@ jobs: # silencieusement, et le label n'a jamais existe). ensure_label "$LABEL_DEFECT" "Table syntax defect in changed files (CODE_SPAN_PIPE, NO_SEP, ...). Advisory. See #10097." "d93f0b" + # ---- Controle positif CR #16207 : nom in-scope avec espaces ---- + # Tourne a CHAQUE run (meme sans fichier in-scope dans la PR, meme + # en nocturne) : il gate la FIABILITE de la mesure, pas le contenu. + # Le fichier "md-table controle.md" porte trois lignes pipe sans + # separateur (NO_SEP) et doit rendre total >= 1. Un split + # whitespace a l'ancienne casserait ce path en argv orphelins -> + # exit 2 -> controle rouge. Il ne nourrit jamais le label. + SCAN_RC=0 + CONTROL="$RUNNER_TEMP/md-table controle.md" + printf '| a | b |\n| c | d |\n| e | f |\n' > "$CONTROL" || true + python scripts/notebook_tools/scan_md_table_syntax.py --json "$CONTROL" > control.json 2>/dev/null + CTRL_RC=$? + CTRL_TOTAL=0 + if [ "$CTRL_RC" -eq 0 ]; then + CTRL_TOTAL=$(python -c "import json,sys; print(json.load(open('control.json'))['total_findings'])" 2>/dev/null) || true + fi + if [ "${CTRL_TOTAL:-0}" -lt 1 ]; then + echo "::error::positive control failed (rc=$CTRL_RC, total=$CTRL_TOTAL) -- whitespace argv is lossy, measurement unreliable" + SCAN_RC=1 + else + echo "Positive control passed (whitespace filename fed to argv: $CTRL_TOTAL NO_SEP finding)" + fi + rm -f "$CONTROL" control.json + if [ "$COUNT" -eq 0 ]; then echo "No in-scope files to scan." - unset_label "$LABEL_DEFECT" + # CR #16207 : meme sans fichier in-scope, un controle de mesure + # echoue (SCAN_RC != 0) interdit l'unset -- le rouge reste. + if [ "$SCAN_RC" -eq 0 ]; then + unset_label "$LABEL_DEFECT" + else + echo "::warning::measurement control failed -- leaving '$LABEL_DEFECT' label untouched" + fi exit 0 fi @@ -204,10 +234,13 @@ jobs: # silence apres le split whitespace. RC explicite : un echec du # scanner = PANNE DE MESURE (total illisible), jamais un "Clean.". python scripts/notebook_tools/scan_md_table_syntax.py --json "${PATHS[@]}" > payload.json 2> scan.err - SCAN_RC=$? - if [ "$SCAN_RC" -ne 0 ]; then - echo "::error::scan_md_table_syntax.py failed (rc=$SCAN_RC) -- $LABEL_DEFECT NOT touched" + SRC=$? + if [ "$SRC" -ne 0 ]; then + echo "::error::scan_md_table_syntax.py failed (rc=$SRC) -- $LABEL_DEFECT NOT touched" if [ -s scan.err ]; then cat scan.err; fi + SCAN_RC=1 + elif [ "$SCAN_RC" -ne 0 ]; then + echo "::error::positive control failed -- total unreliable, $LABEL_DEFECT NOT touched" else cat payload.json TOTAL=$(python -c "import json,sys; print(json.load(open('payload.json'))['total_findings'])" 2>/dev/null) || true @@ -219,29 +252,6 @@ jobs: esac fi - # ---- Controle positif CR #16207 : nom in-scope avec espaces ---- - # Prouve a chaque run que le passage argv est lossless : le fichier - # "md-table controle.md" porte trois lignes pipe sans separateur - # (NO_SEP) et doit rendre total >= 1. Un split whitespace a - # l'ancienne casserait ce path en argv orphelins -> exit 2 -> - # controle rouge. Le controle ne nourrit PAS le label (scan separe) : - # il gate la FIABILITE de la mesure. - CONTROL="$RUNNER_TEMP/md-table controle.md" - printf '| a | b |\n| c | d |\n| e | f |\n' > "$CONTROL" || true - python scripts/notebook_tools/scan_md_table_syntax.py --json "$CONTROL" > control.json 2>/dev/null - CTRL_RC=$? - CTRL_TOTAL=0 - if [ "$CTRL_RC" -eq 0 ]; then - CTRL_TOTAL=$(python -c "import json,sys; print(json.load(open('control.json'))['total_findings'])") - fi - if [ "$CTRL_TOTAL" -lt 1 ]; then - echo "::error::positive control failed (rc=$CTRL_RC, total=$CTRL_TOTAL) -- whitespace argv is lossy, measurement unreliable" - SCAN_RC=1 - else - echo "Positive control passed (whitespace filename fed to argv: $CTRL_TOTAL NO_SEP finding)" - fi - rm -f "$CONTROL" control.json scan.err - # Label decision. CR #16207 : jamais d'unset sur un etat non mesure # (SCAN_RC != 0) -- le label pose par un run precedent reste en # place, le rouge vaut mieux qu'un faux "Clean.".