Skip to content

fix(17391): Lab17 -- le schema passe en Mermaid, l'attribution DS-STAR corrigee #14041

fix(17391): Lab17 -- le schema passe en Mermaid, l'attribution DS-STAR corrigee

fix(17391): Lab17 -- le schema passe en Mermaid, l'attribution DS-STAR corrigee #14041

name: markdown-rendering-guard
# Durable guard for the class of markdown *rendering* regressions that slip past
# agent review because an agent cannot render markdown in its head (the same blind
# spot that let oversized exercise "hints" and, more recently, the cost-frontmatter
# blocks ship to main). The defects render as ugly, title-sized text blocks on the
# very first open of a notebook, yet are invisible in a source diff.
#
# Two complementary scanners run here:
#
# 1. detect_markdown_rendering.py -- the HARD gate. Catches YAML frontmatter dumped
# into a rendered markdown cell (`---\ntitle:...\ncost:\n ...\n---`), which
# markdown-it promotes to one oversized setext-H2 block, plus accidental
# setext-oversized prose. Baselined: the corpus already carries ~480 pre-existing
# violations, so `--check --baseline` FAILS the PR only on NEW error-level ones
# (hashes are content-stable across cell reordering). Burn the baseline down with
# `--update-baseline` after a remediation batch. See #8056 / the cost-frontmatter
# exemplar #8323.
#
# 2. scan_md_hierarchy.py -- informational. Surfaces hint-as-heading (Indice /
# Astuce / Conseil / Piste written as an H1..H3 -> renders larger than body text),
# collapsed-markdown tables, and multi-H1 hierarchy defects. Report-only for now
# (no baseline yet, ~120 pre-existing notebooks); it prints the honest count so a
# remediation batch can drive it down. Its plural-blind regex was the root cause
# of the vacuous #3968 closure (it reported 0 hints while ~200 plural-form hint
# headings survived) -- fixed in the same PR that adds this guard. Promoting it to
# a baselined hard gate is a tracked follow-up on #3966.
#
# Scope: main-repo notebooks only. Submodules are separate repos with their own CI.
on:
pull_request:
branches: [ main ]
paths:
- '**.ipynb'
- '_quarto.yml'
- 'scripts/notebook_tools/detect_markdown_rendering.py'
- 'scripts/notebook_tools/scan_md_hierarchy.py'
- 'scripts/notebook_tools/markdown_rendering_baseline.json'
- '.github/workflows/markdown-rendering-guard.yml'
push:
branches: [ main ]
paths:
- '**.ipynb'
- '_quarto.yml'
- 'scripts/notebook_tools/detect_markdown_rendering.py'
- 'scripts/notebook_tools/scan_md_hierarchy.py'
- 'scripts/notebook_tools/markdown_rendering_baseline.json'
workflow_dispatch:
permissions:
contents: read
concurrency:
# #13363/cascade : `github.ref` vaut `refs/heads/main` pour TOUS les commits
# de main, donc `cancel-in-progress: true` faisait tuer le controle du commit
# precedent par le merge suivant. Mesure du 2026-08-28 sur 30 runs main :
# 26-27 annules, 2-3 verdicts rendus. Le backstop cense attraper une mauvaise
# cascade etait tue par la cascade -- il defaillait exactement quand il servait.
# Sur une branche de PR, superseder est CORRECT (seule la tete compte) ; sur
# main, chaque commit est un sujet distinct qu'aucun autre ne peut remplacer.
group: markdown-rendering-guard-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
render-scan:
name: "markdown-rendering guard (main-repo notebooks)"
# Route vers la jambe Linux auto-hebergee (#13378 tranche 5, decision ai-01
# 2026-09-02) : garde Python pur, sans secret. La garde same-repo ci-dessous
# saute les PRs de fork (pr_gate.py compte `skipped` comme OK) : aucun code de
# fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# The Quarto closure scan (below) reads _quarto.yml via PyYAML -- a bare
# setup-python environment does not provide it, and pre-#11850 the
# resulting ImportError was swallowed as an "empty render-list", killing
# the closure scan while accusing _quarto.yml of being invalid.
- name: "Install detector dependencies (pyyaml for the closure scan)"
run: pip install pyyaml
# #12064: the embedded positive/negative controls were documented as
# "the guard workflow runs it" but nothing called --selfcheck -- a rule
# refactor losing an observed defect form would have passed CI silently.
# Runs the controls for ALL rules that carry them (yaml_block_open_no_close
# both outage forms, code_stmt_in_markdown stub/indent/fence game).
- name: "Rule selfcheck -- embedded positive/negative controls"
run: python scripts/notebook_tools/detect_markdown_rendering.py --selfcheck
- name: "HARD gate -- fail on any NEW frontmatter / setext oversize"
run: |
if ! python scripts/notebook_tools/detect_markdown_rendering.py \
--check \
--baseline scripts/notebook_tools/markdown_rendering_baseline.json; then
echo ""
echo "::error::A markdown cell was added/changed that renders as an oversized, unformatted text block (YAML frontmatter dumped into a rendered cell, or prose accidentally underlined by '---'/'==='). It looks fine in the source diff but ugly on first open."
echo "Fix options (per-family editorial choice):"
echo " * move the metadata into the notebook 'metadata' (invisible, machine-readable), OR"
echo " * render it inside a fenced \`\`\`yaml block, OR"
echo " * format it as a small markdown table."
echo "Then re-run: python scripts/notebook_tools/detect_markdown_rendering.py --report <notebook>"
echo "If the change is an intentional, already-known violation, burn it into the baseline with --update-baseline (reviewer sign-off)."
exit 1
fi
- name: "Informational -- hint-as-heading / collapsed-markdown / multi-H1 census"
if: always()
run: |
echo "Typographic-hierarchy census (report-only, not gating this PR):"
python scripts/notebook_tools/scan_md_hierarchy.py MyIA.AI.Notebooks | tail -1
echo ""
echo "Hint-as-heading lines (Indice/Astuce/Conseil/Piste as an H1..H3 -> oversized):"
python scripts/notebook_tools/scan_md_hierarchy.py MyIA.AI.Notebooks | grep -c 'HINT-AS-HEADING' || true
# Quarto closure scan: detects frontmatter / setext defects in notebooks
# linked from the render-list (one hop). The trigger paths include
# ``**/*.ipynb`` and ``_quarto.yml`` so this catches the failure mode of
# #11630 where a PR added a link to a broken notebook without touching
# the notebook itself. Informational for now -- the corpus carries
# ~358 known violations to remediate. (#11630 acceptance #1+#4.)
- name: "Informational -- Quarto closure scan (render-list + linked notebooks)"
if: always()
run: |
python scripts/notebook_tools/detect_markdown_rendering.py \
--closure --quarto-yml _quarto.yml MyIA.AI.Notebooks --max-findings 8