A code cell inside any directive — {exercise}, {solution}, {note} — renders with run controls but is never registered with the kernel. Clicking gives no output and no error, and any later cell depending on it raises NameError.
Found while debugging QuantEcon/lecture-wasm#64, where it makes the short_path exercise impossible to complete in the browser.
Root cause
@myst-theme/jupyter/dist/execute/utils.js (this repo depends on @myst-theme/jupyter: ^1.3.0):
export function executableNodesFromBlock(block) {
if (!block || block.type !== 'block') return;
let target = block;
if (block.children?.length === 1 && block.children[0].type === 'container') target = block.children[0];
if (target.children?.length >= 2 && target.children[0].type === 'code') {
return { codeCell: target.children[0], output: target.children[1] };
}
}
notebook.cells = mdast.children.map((block) => { ... executableNodesFromBlock(block) ... });
Two things combine:
notebook.cells = mdast.children.map(...) walks only the root's direct children — no recursion.
executableNodesFromBlock requires the code node to be children[0] of a top-level block, with a single special case for a lone container (the figure path).
A cell inside an exercise sits at root > block > exercise > block > code. The outer block's children[0] is the exercise node, so the check fails and the cell never enters notebook.cells.
Rendering and execution-registration are separate paths, which is why the cell still draws a ▶ button. That is the part that makes this expensive to diagnose.
This is not a directive bug
myst-ext-exercise is behaving correctly: it nests the body under the exercise node, which is semantically right, and exercise-start is literally alias: ["exercise"] in the directive spec. The gated -start/-end form is a parsing convenience for content that cannot be indented — it was never a hoist. Changing that would break cross-referencing and numbering, so the fix belongs here in the execution layer.
Reproduction
Six cells in one page, each appending a marker to a list, with a top-level REPORT cell printing what actually ran. Measured on a live preview:
EXECUTED : ['A_toplevel_setup', 'C_toplevel_after_magic']
MISSING : ['D_in_gated_exercise', 'E_in_gated_solution', 'F_in_note']
%%file worked at top level: True
| Position |
Ran? |
| top level |
✅ |
top level, %%file magic |
✅ (proves magics and the FS are fine) |
gated {exercise-start} |
❌ |
gated {solution-start} |
❌ |
plain {note} |
❌ |
Full probe page and method: QuantEcon/lecture-wasm#65.
The marker-and-report pattern is the important part of that reproduction. A stranded cell produces no output and no error — and for an assignment cell, no output is also the correct result. The two are indistinguishable by eye, which is why this was misdiagnosed twice as a %%file problem before the pattern was used.
Suggested fix
Make cell collection recurse into container children so any block holding a code node is registered regardless of depth. Two constraints:
- Preserve document order —
notebook.cells order determines execution sequence, so a recursive walk must be depth-first in document order, not appended.
- Keep the existing figure special-case working (
block > container > code).
Why this repo can ship it before upstream
This repo already uses patch-package — "postinstall": "patch-package --patch-dir patches", with existing patches for @jupyter-widgets/controls and jupyterlab-plotly. A third patch against @myst-theme/jupyter would land in the theme build immediately, and the same diff is the upstream PR to jupyter-book/myst-theme.
I searched jupyter-book/myst-theme, jupyter-book/mystmd and jupyter-book/thebe and found no existing report, though those searches were not exhaustive.
Impact today
Small, but not zero. An audit of all 42 published pages of lecture-wasm — walking every ancestor type that wraps an executable cell, not a guessed list — found exactly two stranded cells on one page (short_path, both inside {exercise-start}). Every page is kind: Notebook, so no kernelspec problems.
The reason it matters beyond that count is that the failure is silent and invisible to CI: lecture-wasm's ci.yml runs myst build --html with no execution, so a stranded cell builds green and only fails for a reader. Any lecture author who puts a code cell inside an admonition or exercise gets a page that looks correct and cannot be run.
Worth noting one trap for whoever fixes short_path in the meantime: its solution cells currently do run, but only because a malformed ```{solution} closer leaves that directive unbalanced and spills its content to root level. Correcting that directive without also moving cells out would take the page from one stranded cell to six — confirmed by the probe, where a correctly gated {solution-start} cell does not run.
A code cell inside any directive —
{exercise},{solution},{note}— renders with run controls but is never registered with the kernel. Clicking gives no output and no error, and any later cell depending on it raisesNameError.Found while debugging QuantEcon/lecture-wasm#64, where it makes the
short_pathexercise impossible to complete in the browser.Root cause
@myst-theme/jupyter/dist/execute/utils.js(this repo depends on@myst-theme/jupyter: ^1.3.0):Two things combine:
notebook.cells = mdast.children.map(...)walks only the root's direct children — no recursion.executableNodesFromBlockrequires thecodenode to bechildren[0]of a top-levelblock, with a single special case for a lonecontainer(the figure path).A cell inside an exercise sits at
root > block > exercise > block > code. The outer block'schildren[0]is theexercisenode, so the check fails and the cell never entersnotebook.cells.Rendering and execution-registration are separate paths, which is why the cell still draws a ▶ button. That is the part that makes this expensive to diagnose.
This is not a directive bug
myst-ext-exerciseis behaving correctly: it nests the body under theexercisenode, which is semantically right, andexercise-startis literallyalias: ["exercise"]in the directive spec. The gated-start/-endform is a parsing convenience for content that cannot be indented — it was never a hoist. Changing that would break cross-referencing and numbering, so the fix belongs here in the execution layer.Reproduction
Six cells in one page, each appending a marker to a list, with a top-level REPORT cell printing what actually ran. Measured on a live preview:
%%filemagic{exercise-start}{solution-start}{note}Full probe page and method: QuantEcon/lecture-wasm#65.
The marker-and-report pattern is the important part of that reproduction. A stranded cell produces no output and no error — and for an assignment cell, no output is also the correct result. The two are indistinguishable by eye, which is why this was misdiagnosed twice as a
%%fileproblem before the pattern was used.Suggested fix
Make cell collection recurse into container children so any
blockholding acodenode is registered regardless of depth. Two constraints:notebook.cellsorder determines execution sequence, so a recursive walk must be depth-first in document order, not appended.block > container > code).Why this repo can ship it before upstream
This repo already uses
patch-package—"postinstall": "patch-package --patch-dir patches", with existing patches for@jupyter-widgets/controlsandjupyterlab-plotly. A third patch against@myst-theme/jupyterwould land in the theme build immediately, and the same diff is the upstream PR tojupyter-book/myst-theme.I searched
jupyter-book/myst-theme,jupyter-book/mystmdandjupyter-book/thebeand found no existing report, though those searches were not exhaustive.Impact today
Small, but not zero. An audit of all 42 published pages of
lecture-wasm— walking every ancestor type that wraps an executable cell, not a guessed list — found exactly two stranded cells on one page (short_path, both inside{exercise-start}). Every page iskind: Notebook, so no kernelspec problems.The reason it matters beyond that count is that the failure is silent and invisible to CI:
lecture-wasm'sci.ymlrunsmyst build --htmlwith no execution, so a stranded cell builds green and only fails for a reader. Any lecture author who puts a code cell inside an admonition or exercise gets a page that looks correct and cannot be run.Worth noting one trap for whoever fixes
short_pathin the meantime: its solution cells currently do run, but only because a malformed```{solution}closer leaves that directive unbalanced and spills its content to root level. Correcting that directive without also moving cells out would take the page from one stranded cell to six — confirmed by the probe, where a correctly gated{solution-start}cell does not run.