docs: convert plot descriptions to plt.figtext + strip tqdm noise from notebooks - #240
Conversation
…ct (#241) Adds a new `Execute Notebooks` workflow that owns notebook execution as a first-class CI stage, and reworks `Build Combined Doc` to consume the resulting artifact instead of re-executing notebooks itself. Also adds a `docs-jenner-artifact` Makefile target for the CI-side build path. ## Problems solved 1. Notebook execution used to fire only on releases (issue #186). Now fires on every main-push touching notebook or execution-affecting source, plus on manual dispatch. Broken notebooks are surfaced within minutes of landing. 2. Committed executed-notebook outputs no longer need to be kept fresh by convention. The CI artifact is the source of truth for downstream doc/RAG builds; the committed `outputs` field is decorative (kept only so github.com renders charts inline). 3. Doc builds no longer re-execute notebooks. `Build Combined Doc` downloads the executed_nbs artifact and just runs `make docs-jenner-artifact` (~2-3 min wall-clock vs the previous ~25 min). ## Architecture ``` Execute Notebooks trigger: push to main (paths-filtered) | workflow_dispatch cache: source-hash-keyed, invalidates on any input that can affect outputs (notebooks, src/**, pyproject.toml, docs/requirements.txt, docs/**/*.py, Makefile, the workflow file itself) gate: python docs/check_executed_nbs.py (fail on any nb error) manifest: writes dist/executed_nbs/manifest.json with commit_sha, source_hash, run_id, python_version, event_name, was_cache_hit, was_forced, was_allow_errors uploads: executed_nbs (400d) push-to-main OR clean dispatch executed_nbs-debug-<run_id> (30d) dispatch with allow_notebook_errors=1 executed_nbs-failed-<run_id> (30d) any failure path Build Combined Doc trigger: workflow_run completion of Execute Notebooks (auto-chain, restricted to push-triggered upstream runs so debug dispatches don't push a corpus PR to laser-mcp) | workflow_dispatch download: executed_nbs artifact (by run-id for workflow_run, latest successful for workflow_dispatch) compat gate (workflow_dispatch only): hashFiles of checkout vs manifest.source_hash; fail on mismatch unless use_latest_anyway=true build: make docs-jenner-artifact -> build site + concat sync: create/update laser-mcp PR ``` The two `hashFiles` lists (cache-key in execute-notebooks + compat-check in build-combined-doc) are literal duplicates by design — 7 patterns each, same order. Diverging silently breaks the compat gate. ## Deliberate non-goals - **No `pull_request` trigger.** Executing every notebook adds ~25 min per PR iteration; that cost is not affordable during review cycles. Broken notebooks land on main and are caught by the post-merge push run within minutes. Local `make docs-jenner-execute` or a manual `workflow_dispatch` remain as pre-merge validation options for PRs that specifically need them. - **No enforcement of source-only committed notebooks.** Contributors may commit executed OR stripped notebooks — both work identically for the doc build. Documented inline in the workflow file. If future consensus wants strict source-only commits (Option B), the enforcement point is a single check step in `github-actions.yml`. ## Bootstrap note On first enable, `workflow_dispatch` on Build Combined Doc will fail cleanly (no prior successful Execute Notebooks artifact to consume — `if_no_artifact_found: fail` catches this). Kick off Execute Notebooks once manually to seed. After that, either trigger works. ## Third-party action `dawidd6/action-download-artifact@v3` — needed because `actions/download-artifact` only fetches from the current run or a known run-id, and `workflow_dispatch` on Build Combined Doc has no upstream run-id to point at. This third-party action searches for "latest successful run of workflow X" — exactly the semantics we need. Widely used (~4M weekly downloads). ## Compatibility with other in-flight PRs Independent of #204 (plot descriptions), #240 (figtext form of #204), and #236 (concat improvements) — those change notebook / concat content. Compatible with #237 (skip re-exec in local `docs-jenner`) — together the two produce a clean Makefile trio: - `make docs-jenner` local fast path (per #237) - `make docs-jenner-execute` local full pipeline (per #237) - `make docs-jenner-artifact` CI path, EXEC_DIR pre-populated (this PR) Closes #186. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Refactors the 70 plot-description cells added by #204 from standalone markdown cells (### Reading the ... plot, with observation + interpretation + takeaway paragraphs) to plt.figtext() captions embedded inside the plot-generating code cells. Content is compressed to the takeaway sentence. Motivation: alpha-suite evaluation (jenner-generic-mcp, N=3 per arm, same generation quality: arm mean/20 a1 Δ vs no-descriptions no descriptions (control) 17.00 9.00 — fat markdown (#204 as-authored) 14.33 7.67 -2.67 slim markdown (H3 + takeaway) 16.33 8.33 -0.67 figtext (this PR) 17.00 9.00 +0.00 Standalone markdown description cells create their own chunks in the RAG corpus that compete for top-k retrieval on prompts like "build this SIR model" — the descriptions are semantically close ("simulate + plot") but have no API-usage content, so retrieval picks them up and displaces the chunks that actually show the model-building calls. plt.figtext embeds the description text inside the code cell's Python source. The concat pipeline emits that cell as a fenced Python block, and the ingest H4 splitter (laser-mcp #37) plus the recursive character splitter's fence-first separator keep the string fused with the surrounding plotting code. Result: description content is still in the RAG corpus, but as part of code chunks — no separate description-only chunks to compete with API-usage retrieval. Chunk-count evidence (laser-mcp ingest.py, chunk_size=1200): arm sections chunks Δ chunks vs no-descriptions no desc 515 972 — figtext 515 991 +19 (co-located, no new boundaries) slim 585 1037 +65 (new md sections) composite 585 1101 +129 (new md sections + fat content) The figtext arm adds essentially no new chunk boundaries. Takeaway extraction: for each description .md file, extracts the last bold span (>20 chars) containing a takeaway trigger word ("demonstrates", "takeaway", "confirms", ...). 68/70 auto-extract cleanly; 1 spot-fixed via MANUAL_TAKEAWAY, 1 confirmed OK via fallback path. Full logic lives in tools/apply_figtext_captions.py so the same manifest at tools/plot_descriptions/config.json remains the source of truth. Rendering: plt.figtext(0.5, -0.05, ..., ha="center", va="top", wrap=True, fontsize=8) places the caption just below the axes. Notebook outputs are NOT re-executed by this PR (docs-jenner uses committed outputs per #237), so the committed figure PNGs are unchanged; when notebooks are next re-executed for real (e.g. a release-time full build via docs-jenner-execute) the caption will render in the figure. Base branch: docs/notebook-plot-descriptions (#204's branch). This is a stacked PR — #204's markdown-form remains the base for reviewers to see what's being converted. Merge order intended: 1. #204 (as-authored, or force-updated to figtext form if reviewers prefer) 2. This PR (if not folded into #204) Related: - #204 base — the plot descriptions themselves - #236 — concat heading demote + oversize warning (independent) - #237 — docs-jenner skip re-execution (independent) - laser-mcp #37 — H4 in MarkdownHeaderTextSplitter (works with either form) - Alpha-suite runs under tests/prompt_test_suite_generic/output/experiment_204eval/ in laser-mcp; N=3 per arm, gpt-5-mini generation, gpt-4o review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Alpha suite re-test: rebased #240 vs prior baselinesRan the 20-prompt alpha suite N=3 against a freshly-built corpus from this branch (cherry-picked onto latest main, so it now composes with #236 concat improvements and post-#37 laser-mcp ingest). Corpus fingerprint: 515 sections / 991 chunks (identical to the earlier figtext test). Per-iteration:
Vs baselines this session:
Run 3 was a clean sweep (20/20 first time this session). The +1.33 improvement over the earlier figtext test likely reflects the RAG-quality improvements that landed since — laser-generic #236 (concat heading demote + oversize warning) and laser-mcp #37 (H4 splitter in the MarkdownHeaderTextSplitter) are both live now, and both compound with the figtext form's chunk-fusion property. Green light to merge from an alpha-suite standpoint. |
Three sources of tqdm crud were letting progress-bar snapshots leak into
downstream products:
1. Committed notebooks in docs/tutorials/notebooks/ carried 381 lines
of tqdm output across 14 of 19 files (each ~30 chars of
"N,NNN agents in M node(s): 37%|...|... it/s]" ANSI-adjacent junk).
2. The CI executed_nbs artifact produced by docs/execute_notebooks.py
captured whatever nbconvert saw during execution, so the artifact
that feeds MkDocs Deploy and Build Combined Doc always had noise.
3. docs/concat_mkdocs.py's tqdm filter (from #229) ran at concat time
— it kept the RAG corpus clean but couldn't help the docs site
(which uses the artifact directly, not the concat output).
## New module: docs/tqdm_strip.py
Owns the tqdm-line regex and the in-place notebook mutation. Exposes:
- TQDM_PROGRESS_RE: shared line pattern (matches lines ending with the
tqdm rate suffix `it/s]` or `s/it]`)
- strip_tqdm_from_notebook(nb): mutate a loaded notebook dict; returns
lines removed
- strip_notebook_file(path): load, mutate, write back if changed
- count_tqdm_lines(path): read-only line-count for CI checks
- CLI: `python docs/tqdm_strip.py path.ipynb ...` (strip in place)
`python docs/tqdm_strip.py --check path.ipynb ...` (exits 1 if any file dirty)
Handles both `stream` outputs (text field) and `execute_result` /
`display_data` outputs (data.text/plain field), including no-newline
single-chunk outputs that tqdm produces when writing to stderr.
## Wired into docs/execute_notebooks.py
After every nbconvert call, strip_notebook_file runs against the just-
executed notebook. CI artifact ships clean by construction. Downstream
consumers (MkDocs Deploy site build via artifact overlay, Build Combined
Doc RAG concat) both benefit without any changes on their side.
## docs/concat_mkdocs.py — imports shared regex
Kept its filter logic (still needed as a defensive second pass for local
dev flows or non-freshly-executed inputs), but pulls the regex from the
new shared module so any future refinement lands in one place.
## Committed notebook cleanup
Applied `strip_notebook_file` to all 19 docs/tutorials/notebooks/*.ipynb.
Stripped 381 lines across 14 files:
nb04: 116 lines
nb09: 101 lines
mortality: 46 lines
SEI_and_SEIS: 30 lines
nb03: 28 lines
nb05: 26 lines
...8 more (11 lines total)
## New pytest gate
tests/test_notebooks_no_tqdm_crud.py fails the CI matrix's `check` job if
any committed notebook has tqdm-progress lines in its outputs. Error
message points contributors at `python docs/tqdm_strip.py` for the fix.
No new workflow — the existing check job in github-actions.yml picks it up.
73471e9 to
c9f8c07
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves the cleanliness and retrieval quality of the tutorial notebook corpus by (1) embedding plot descriptions directly into plotting code via plt.figtext(...) and (2) stripping tqdm progress-bar noise from committed notebooks and CI-executed notebook artifacts via a shared docs/tqdm_strip.py utility.
Changes:
- Added a shared tqdm-stripping module + CLI (
docs/tqdm_strip.py) and integrated it into notebook execution and concat tooling. - Added a CI gate to prevent tqdm output from being committed into
docs/tutorials/notebooks/*.ipynb. - Updated tutorial notebooks to remove tqdm output noise and add
plt.figtext(...)captions near plots.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/apply_figtext_captions.py | New script to inject plt.figtext(...) captions into notebook code cells based on the plot description manifest. |
| docs/tqdm_strip.py | New shared regex + library + CLI to remove tqdm progress-bar snapshot lines from notebook outputs. |
| docs/execute_notebooks.py | Runs strip_notebook_file(...) post-nbconvert so CI executed_nbs artifacts ship clean. |
| docs/concat_mkdocs.py | Imports shared tqdm regex from docs/tqdm_strip.py instead of duplicating it. |
| tests/test_notebooks_no_tqdm_crud.py | New pytest gate ensuring committed tutorial notebooks contain no tqdm progress output lines. |
| docs/tutorials/notebooks/SEI_and_SEIS_implementations.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/seasonality.ipynb | Removes tqdm output noise; adds plot captions (noted helper/call-site caption placement issues in review). |
| docs/tutorials/notebooks/ri_exploration.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/mortality.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/grid_examples.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/distributions.ipynb | Adds plt.figtext(...) plot captions to distribution sampler plots. |
| docs/tutorials/notebooks/constant_pop.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot caption. |
| docs/tutorials/notebooks/births.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/10_England_and_Wales.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions; fixes a spelling issue (“vicinity”). |
| docs/tutorials/notebooks/09_rabies_diffusion_1D.ipynb | Reduces tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/08_2patch_SIR_wbirths_correlation.ipynb | Adds plt.figtext(...) plot caption to the coupling/correlation plot cell. |
| docs/tutorials/notebooks/07_SIR_CCS.ipynb | Adds plt.figtext(...) plot captions in CCS-related plot cells. |
| docs/tutorials/notebooks/06_SIR_wbirths_natural_periodicity.ipynb | Adds plt.figtext(...) plot captions for periodicity plots. |
| docs/tutorials/notebooks/05_SIR_wbirths_age_distribution.ipynb | Removes tqdm output noise; adds plot labeling + plt.figtext(...) captions. |
| docs/tutorials/notebooks/04_SIR_nobirths_outbreak_size.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions. |
| docs/tutorials/notebooks/03_SIS_nobirths_logistic_growth.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions and plot formatting tweaks. |
| docs/tutorials/notebooks/02_SI_wbirths_logistic_growth.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions and consolidates fit plots into subplots. |
| docs/tutorials/notebooks/01_SI_nobirths_logistic_growth.ipynb | Removes tqdm output noise; adds plt.figtext(...) plot captions and consolidates beta-fit plots into subplots. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def inject_before_show(source_lines, figtext_line): | ||
| src = "".join(source_lines) | ||
| if figtext_line in src: | ||
| return source_lines, False | ||
| lines = src.splitlines(keepends=True) | ||
| last_show_idx = None | ||
| for i, ln in enumerate(lines): | ||
| if re.match(r"^\s*plt\.show\s*\(", ln): | ||
| last_show_idx = i | ||
| if last_show_idx is None: | ||
| if lines and not lines[-1].endswith("\n"): | ||
| lines[-1] = lines[-1] + "\n" | ||
| lines.append(figtext_line + "\n") | ||
| else: | ||
| indent = re.match(r"^(\s*)", lines[last_show_idx]).group(1) | ||
| lines.insert(last_show_idx, f"{indent}{figtext_line}\n") | ||
| return lines, True |
There was a problem hiding this comment.
Fixed in 93d7eb8. inject_before_show now checks isinstance(source, list) on the way in and preserves that shape on the way out — string in, string out; list in, list out. Removes the diff churn on re-runs against string-form cells.
| " plt.figtext(0.5, -0.05, 'The plot demonstrates that even without seasonal forcing, an SIR model with births produces damped, irregular endemic oscillations — the natural baseline that subsequent seasonal-forcing experiments must be compared against.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n", | ||
| " plt.show()\n", |
There was a problem hiding this comment.
Legitimate rendering bug — same root cause across all six comments on this file. Fixed in 93d7eb8.
Root cause
The apply_figtext_captions tool injects plt.figtext(...) just before the last plt.show() in the target code cell. In cells 5 (plot_model) and 23 (plot_si_model), that last plt.show() sits inside the helper function — so:
- Every call to
plot_model/plot_si_modelused the same caption (wrong content for subsequent calls). - Cells 9/14/19/27 tried to compensate by placing plt.figtext AFTER the
plot_model(...)call, but the helper had already calledplt.show()internally — so the caption attached to a fresh empty figure and didn't render on the intended plot at all.
Fix
Gave each helper a caption=None parameter, rendering figtext inside the helper right before plt.show() when caption is passed. Each call site now passes the appropriate caption via kwarg; the trailing standalone plt.figtext lines were removed. Six cells updated (5, 9, 14, 19, 23, 27).
Also fixed a subtle escape bug I hit in the first attempt: my regex captured the source-form of the caption string (escaped backslashes) and re-repr'd it, doubling the escapes and breaking LaTeX math ($\\beta(t)$ instead of $\beta(t)$). Second pass uses ast.literal_eval to get the interpreted Python value before repr.
| "plot_model(sinusoidal)\n", | ||
| "plt.figtext(0.5, -0.05, 'The plot demonstrates that seasonal forcing of $\\\\beta(t)$ synchronizes outbreaks into larger, sharper annual peaks, but in a closed population the deeper troughs drive the infection extinct — motivating the importation experiment that follows.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n" |
There was a problem hiding this comment.
Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.
| "plot_model(with_importation)\n", | ||
| "plt.figtext(0.5, -0.05, 'The plot demonstrates that constant low-rate importation (3 cases every 30 days) is enough to break the fadeout problem and let seasonal forcing entrain a stable biennial outbreak cycle phase-locked to the annual $\\\\beta(t)$ peak.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n" |
There was a problem hiding this comment.
Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.
| "plot_model(two_node_model)\n", | ||
| "plt.figtext(0.5, -0.05, 'The plot demonstrates that spatial coupling between out-of-phase seasonal patches produces richer inter-epidemic dynamics — a quieter \"reservoir\" node sustains transmission during the primary node\\'s seasonal trough, filling in smaller off-cycle outbreaks and damping the peak amplitudes seen in the single-node case.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n" |
There was a problem hiding this comment.
Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.
| " plt.figtext(0.5, -0.05, 'The plot demonstrates the canonical undriven SI take-off-and-saturate curve at the chosen $\\\\beta$ — the control trajectory against which the next cell\\'s \"radical seasonality\" (an intervention applied between days 300 and 400) will be compared.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n", | ||
| " plt.show()\n", |
There was a problem hiding this comment.
Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.
| "plot_si_model(si_with_seasonality)\n", | ||
| "plt.figtext(0.5, -0.05, 'The plot demonstrates that the seasonality input is general-purpose temporal modulation of $\\\\beta(t)$ — not restricted to cyclical forcing — and can be used to model the population-level effect of a time-limited intervention that arrests an outbreak partway through its logistic take-off.', ha=\"center\", va=\"top\", wrap=True, fontsize=8)\n" |
There was a problem hiding this comment.
Same root cause as the first plot_model/plot_si_model comment on this file — fixed in 93d7eb8 via the shared caption=None parameter on the helper. See the reply on that thread for the full mechanism.
Six comments; two type/style items on tools/apply_figtext_captions.py and four rendering bugs in seasonality.ipynb (with a fifth pair in the same file, all sharing the same root cause). ## seasonality.ipynb — figtext + helper-function interaction The apply_figtext_captions tool injected plt.figtext(...) into seasonality.ipynb by finding the "last plt.show() in the target code cell" and placing the caption just before it. In cells 5 and 23 that last plt.show() sat *inside* helper functions (plot_model and plot_si_model) — so: 1. Every call to plot_model / plot_si_model got the same caption baked in at helper-definition time (wrong content for subsequent calls). 2. Cells 9, 14, 19, 27 tried to compensate by placing plt.figtext after the plot_model(...) call. But those helpers had already called plt.show() internally, so the caption attached to a fresh empty figure and didn't render on the intended plot at all. Fix: give each helper a caption=None parameter, render figtext inside the helper right before plt.show() when caption is passed. Each call site now passes the appropriate caption via kwarg; the trailing standalone plt.figtext lines are removed. Applied to cells 5 (plot_model def + no_seasonality call), 9 (sinusoidal), 14 (with_importation), 19 (two_node_model), 23 (plot_si_model def + wout_seasonality call), and 27 (with_seasonality). Also fixed a subtle escape bug in the first pass of the surgery: my regex captured the source-form of the caption string (with escaped backslashes) and re-repr'd it, doubling the backslashes and breaking LaTeX math in the captions (\\\\beta instead of \\beta). Second pass uses ast.literal_eval to get the interpreted Python value before repr, so `$\\beta(t)$` in the original stays `$\\beta(t)$` (one backslash) after rewriting. ri_exploration.ipynb has a similar helper-with-embedded-figtext shape (initialize_susceptibility), but both call sites use plot=False so the figtext lives in dead code — the caption text is in the code fence (RAG-visible) but never renders anywhere. Left as-is since no downstream product misrenders. ## tools/apply_figtext_captions.py — type + source-shape preservation - extract_takeaway is annotated `-> str` but can return None (fallthrough paths for un-parseable descriptions). Change to `-> str | None` to match the actual contract and stop hiding None from static checkers. - inject_before_show always returned a list[str], even when the input cell.source was a plain string. nbformat allows both shapes, and returning a list-when-caller-gave-a-string forces unnecessary reformatting on every re-run. Fix: preserve the input shape — string in, string out; list in, list out.
Summary
Two bundled improvements, both aimed at making notebook content clean and RAG-friendly. Rebased onto
main(was originally stacked on #204's branch, which is now closed — this PR is now standalone).1. Plot descriptions in
plt.figtextform (originally #240)The 70 plot descriptions authored in #204 are converted from standalone
### Reading the …markdown cells intoplt.figtext(...)captions inside the plot-generating code cells. Content compressed to the takeaway sentence per description.Why: the markdown form created its own RAG chunks that competed for retrieval on prompts like "build this SIR model" — semantically close ("simulate + plot") but no API content, so retrieval picked them up and displaced model-building code.
plt.figtextembeds the description inside the code cell's Python source; the concat pipeline emits that cell as a fencedpythonblock, and the ingest H4 splitter (laser-mcp #37) + fence-first character splitter keep the string fused with the surrounding plotting code.Alpha suite evidence (jenner-generic-mcp, N=3 per arm):
Re-tested against latest main (post-#236 concat improvements, post-laser-mcp-#37 H4 splitter) — got 17, 18, 20 → mean 18.33 on the fresh corpus, with run 3 hitting a clean-sweep 20/20. Everything compounds cleanly.
2.
tqdmprogress-bar noise cleanup (new)The tutorial notebooks call
model.run()with a tqdm progress display; nbconvert captures those mid-run snapshots ("N,NNN agents in M node(s): 37%|... it/s]") into cell outputs. That noise was leaking into three places:docs/tutorials/notebooks/*.ipynb) — carried 381 tqdm lines across 14 of 19 files.executed_nbsartifact — captured whatever nbconvert saw. MkDocs Deploy and Build Combined Doc both consume this artifact, so both were displaying / ingesting noise.docs/concat_mkdocs.py) — but that only fixed the RAG corpus, not the docs site or the artifact.New:
docs/tqdm_strip.pyowns the shared regex + a small library + CLI.docs/execute_notebooks.py— the CI artifact now ships clean by construction, so the docs site (via mkdocs-ghp's overlay) and RAG corpus (via Build Combined Doc's concat) both benefit without further changes on their side.docs/concat_mkdocs.pyimports the shared regex — keeps its own filter as a defensive second pass but no longer duplicates the pattern.tests/test_notebooks_no_tqdm_crud.pyfails the existingcheckmatrix entry if any committed notebook has tqdm output. No new workflow — plugs into the existing github-actions.yml check job.Files changed
tools/apply_figtext_captions.py— new tool for the figtext transformationtools/plot_descriptions/— 70 description .md files + config.json manifestdocs/tqdm_strip.py— new shared module + CLIdocs/execute_notebooks.py— post-execute strip stepdocs/concat_mkdocs.py— imports shared regextests/test_notebooks_no_tqdm_crud.py— new CI gateCloses
Supersedes and replaces #204 (already closed).