Skip to content

Diagram survey Phase 1-2: 16 diagrams across Ch1-10, plus pre-PR hardening - #24

Merged
mzargham merged 75 commits into
mainfrom
diagram-survey/phase-0-1-spec
Oct 2, 2026
Merged

mzargham merged 75 commits into
mainfrom
diagram-survey/phase-0-1-spec

Conversation

@mzargham

@mzargham mzargham commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements the diagram-survey mission's Phase 1 (per-chapter survey) and Phase 2 (implementation): 16 new diagrams across Chapters 1, 2, 3, 4, 5, 6, 7, 8, and 10 (Chapter 9 has no diagram candidates by design — it adds no new model element). Diagrams are generated from the model via model_to_dot()/containment_subgraph(), OpenSysML's own -render CLI (action-flow, state), and sysml-toolkit's viz CLI (port-level interconnection, where port identity is the pedagogical point).

  • Phase 1 (decisions/diagram-survey.md): 10 parallel per-chapter survey agents inventoried every notebook against Phase 0's own real-fixture capability matrix (merged separately as Phase 0: re-run the diagram trade study against real chapter fixtures #22), producing a concrete, reviewed table of where a diagram should be added.
  • Phase 2: implemented through this repo's own builder/reviewer harness, one contract per chapter, author and reviewer always on different models.

What's in this PR beyond the diagram implementation

Two things surfaced after Phase 2 landed and are included here, since they were found reviewing this same branch before opening the PR:

  1. A pre-PR user-testing pass (29 reports: 2-3 personas × 9 chapters, plus 3 new longitudinal Ch1→Ch10 runs) — zero blocking issues. Found and fixed one real cross-chapter inconsistency (stale prose in Ch5 naming a superseded renderer).

  2. Two real, cross-cutting bugs in model_to_dot(), found mid-implementation by independent review and fixed at the root rather than worked around per chapter:

    • A requirement's/case's own subject reference was drawn as false part composition.
    • Package-owned usages were drawn with a false composition diamond (a package composes nothing), and specialization (:>) edges were never drawn at all.

    Both required re-rendering and re-reviewing the five already-built chapters they affected.

  3. A harness-discipline incident and its remediation. Partway through the pre-PR pass, a stretch of edits were made directly instead of through this repo's own builder/reviewer contracts — a real process violation, confirmed by an independent audit to have produced four real regressions (the worst: a notebook-title fix that broke plain Jupyter rendering for all 32 chapter notebooks). All four were fixed and independently re-verified across four review rounds before merging; the real fix mechanism is now documented in toaster-recipe/SKILL.md so it isn't lost. Recorded in full in decisions/log.md DL-090/DL-091, for the record.

Known, tracked gaps (not fixed here, by design)

Recorded in DEFERRED.md and decisions/log.md's own "not determined" sections — all confirmed to affect no real fixture in this tutorial today:

  • model_to_dot(): PartUsage labels show qualified names, not short names; the composition diamond is drawn at the opposite end from UML/SysML convention.
  • render_action_flow()/render_state_flow(): OpenSysML's own -render CLI drops typed flow pins and a state's own performed-action name (DEFERRED.md D-037).
  • build_interconnection_intent(): three narrow allocation-scoping edge cases (package-root allocations, inherited allocations, non-part-owned allocations).

Test plan

  • uv run pytest tests/ glossary/tests/ -q — 444 passed, 7 deselected
  • uv run python scripts/check_construction.py --check — clean, all 10 chapters consistent
  • All 32 chapter notebooks re-executed end to end, zero cell errors
  • uv run python -m glossary lint (chapters/) — 16 pre-existing hits, zero new
  • 29-report simulated-learner battery (9 chapters × 2-3 personas + 3 longitudinal runs) — zero blocking
  • Every diagram/notation fix independently reviewed by a different model before merge (full trail in decisions/log.md DL-086 through DL-091)

…illustrate

Z's addition to the spec, folded in as decision 7: the diagram sequence
across the tutorial doubles as a visual-syntax curriculum, bridging
diagram-first systems engineers and this tutorial's code-first approach.
Simple structural diagrams establish the basic visual vocabulary before
later chapters layer in notation that assumes it (structure -> interconnection
-> action-flow -> state, the same order decision 2 already uses). Diagrams
stay strictly derived views of the model (decision 1 unchanged); reviewing
one is real work, the same construct-and-analyze loop the tutorial already
teaches, exercised visually.

Phase 1's method gains a concrete placement trigger from this: scan for
cells with large, dense text/string output -- the exact signal Z named --
as the prime diagram-replacement candidates, not left to judgment alone.
Phase 1's own output table gains a 'visual notation introduced' column
(new vs. reused from an earlier chapter), and Verification gains a check
that no chapter's proposed diagrams assume notation nothing earlier
established.
…ssing)

Decision 1 and Phase 0's Method discussed only 4 of the original trade
study's 5 renderer paths -- DEMA SysML2Tools was never mentioned, adopted
or rejected. Z confirmed (popup): rerun all 5 for Phase 0's real-fixture
pass, matching decision 6's own stated rigor; OpenSysML's separate
PlantUML render path stays out of scope since only its DOT/Graphviz path
is actually in force (DL-001/DL-002).
… was just stale

The model fix, the always-on guard (allocate-connector-end-accessibility,
DEFERRED.md D-032), and four rounds of independent hardening (DL-059 and
its three addenda) all landed 2026-09-29, the same day DL-058 ruled this
gap -- this register entry's own 'Status: not yet drafted here' text was
simply never updated afterward. No new work was needed; this corrects the
record to point at D-032, the complete, current account, rather than
restating or re-deriving a fix that already exists. Reconfirmed:
uv run pytest tests/ -k allocat -- 37 passed.
…dd status banner

All 4 tasks were already fully implemented and tested (containment_subgraph,
model_to_dot's elements/layout params, build_interconnection_intent's depth
param, decisions/diagram-tool-gaps.md seeded) but the plan's own checkboxes
were never ticked when the work landed. Verified directly rather than
assumed: all three test files pass (test_containment_subgraph.py,
test_diagram_probe.py, test_interconnection.py), including two extra
input-validation tests beyond what the plan originally specified.

Also verified (no changes needed): DL-057's two action items -- the
render_sysmld -> render_interconnection rename and the sysml-diagrams
skill's renderer-choice table correction -- are both fully and correctly
applied already.
…rvey.md)

Ten read-only research agents, one per chapter, each given the fixed
visual-syntax progression, Phase 0's capability matrix, and the already-
built containment_subgraph()/model_to_dot()/render_interconnection()
tooling. 16 diagram placements proposed across 9 of 10 chapters (Ch9 has
a legitimate zero-candidate finding, not a gap). Progression ordering and
tool choice both verified consistent against the fixed table and Phase 0's
matrix; no chapter recommends the OMG pilot or SysMLD/sysml2d anywhere.

Two things flagged for Z, not resolved here: Ch5's agent recommends
upgrading the chapter's own existing, shipped interconnection figure from
render_interconnection() to sysml-toolkit (real port boxes, not an edge
label) since the chapter's whole point is a conjugated port; and a
cross-chapter question on whether that upgrade should be a one-off or a
standing rule, since Ch6 deliberately keeps the in-house renderer for its
own allocation-edge diagrams. No implementation happens in this pass, per
the spec's own Non-goals -- recommendations only.
…ix a second stale paper-trail gap (recipes.md)

Z's decision (item 2 of the two open items from the diagram survey): make
'use sysml-toolkit when port identity itself is the pedagogical point,
otherwise default to the in-house renderer' an explicit rule in the
renderer-choice table, not an implicit per-chapter judgment call.

While locating where to add it, found that references/recipes.md was never
updated when DL-057 corrected SKILL.md's own table: it still told readers to
use the OMG pilot for decomposition and state views, and SysMLD for
interconnection -- all three confirmed to fail entirely on real content by
Phase 0 (decisions/diagram-study-real-fixtures.md). Rewrote all three
recipes to the current, confirmed-working pipelines (model_to_dot(),
render_interconnection()/sysml-toolkit, OpenSysML CLI), added the explicit
sysml-toolkit criterion with a worked command, and fixed the action-flow
recipe's render form (plantuml -> dot, matching what's actually confirmed
on real content). No PilotFigure.java/scripts/ dead references existed to
clean up -- confirmed by search, not assumed.
…eal viz CLI

For when port identity itself is the pedagogical point (e.g. a conjugated port):
sysml-toolkit draws real port names as their own boxes, where the in-house
render_interconnection() only draws a single edge label. render_interconnection()
stays the default everywhere else.
…fixture

Exercises the real sysml-toolkit binary, sysml.library, PlantUML jar and java;
skips with a clear reason if any of those four real paths is missing on the
machine running it.
…t's viz CLI

Cell 16 now calls render_toolkit_interconnection() instead of
build_interconnection_intent()/render_interconnection(), per diagram-survey.md's
Ch5 recommendation: this chapter's whole point is a conjugated port, and the new
pipeline draws the real port names (durationIn, durationOut) as their own boxes
instead of folding them into one edge label. Cells 17-18 now describe the real
port names the figure shows, not only the interface-level connection. Regenerates
the shipped figure and keeps its intermediate .puml alongside it for inspection,
per the sysml-diagrams skill's convention for generated build products.
…tion

Review found two real defects in render_toolkit_interconnection():

1. The connector PlantUML draws by default between the two ports is a
   diagonal line that cuts straight through the nearer part's own
   <<part>> stereotype and title text (confirmed by rendering and
   rasterizing the real Ch5 output). skinparam linetype ortho routes it
   in axis-aligned segments along box edges instead, via a new
   _apply_interconnection_layout_fixes() post-processing step applied to
   the generated .puml before PlantUML renders it.

2. The same post-processing wraps each port's own auto-generated label
   at its ' : ' (e.g. durationIn / : ~DurationPort, same text, just
   line-broken) so it no longer overlaps the neighboring box's border --
   also confirmed by rendering before and after. Both are presentation
   only: neither changes which elements or edges the .puml describes.

Also tightens the lib/binary/plantuml_jar/java argument checks from a
bare Path.exists() to what each path actually needs to be (a directory,
an executable file, a file), so an empty string or a directory passed
as binary now raises this function's own ToolkitRenderError instead of
a raw PermissionError or NotADirectoryError surfacing from the
subprocess call.
…ion fixes

The old assertion 'durationIn' in puml is trivially true even when only
the durationInterface edge label survives, since 'durationIn' is a
literal substring of 'durationInterface' -- it could never actually
detect losing the conjugated port, which is the one thing this upgrade
exists to catch. Now asserts the full declaration text instead (with
the layout fix's line-wrap undone first, so the check depends on what
the label says, not how it is wrapped on screen). Verified against a
copy of the real .puml with its port lines stripped: the old assertion
passed on that regression, the new one correctly fails.

Adds a test for the new ortho-routing skinparam, and a test that a
directory passed as binary raises this module's own ToolkitRenderError
rather than a raw PermissionError; updates the existing missing-binary
test's match string for the new, more specific error wording.
Review found the comment cited 'the sysml-diagrams skill' by name -- an
internal, builder-facing reference a learner has no context for (same
class of violation AGENTS.md 1.10 already covers for Tall's three
worlds). Reworded to state the actual reason in plain terms instead,
without changing what the comment explains.
…ut fix

Byproduct of render_toolkit_interconnection()'s layout fix: the shipped
SVG and its intermediate .puml now reflect ortho connector routing and
wrapped port labels, clear of both parts' own borders and title text.
… pages

Every chapter's index.md and conclusion.md had an H1 identical (or near-
identical) to its myst.yml TOC group title ('Chapter N: ...'), so the
sidebar showed the bold group header immediately followed by a sub-item
repeating the same text. Each file now carries frontmatter title: Overview
or title: Conclusion, which becomes the page's own banner heading and
sidebar label; the body's own 'Chapter N: ...' heading is unchanged and
still renders as the page's first section, appearing correctly in the
in-page Contents outline.

A toc-level title: override on the myst.yml entry itself does not affect
the rendered sidebar label in this mystmd version (v1.11.0) -- confirmed
by inspecting the built config.json before and after; frontmatter title:
on the file itself is what actually controls it.
…s the CLI-provisioning infrastructure they depend on
…g A)

The test corrupted only ("darwin", "arm64")'s own sha256 pin, so on any
other platform ensure_cli_binary() would look up and correctly verify
against its own real, uncorrupted hash -- silently not testing anything.
Now corrupts whichever pin opensysml.binary.detect_platform() (the same
lookup the real code performs) resolves to on the machine actually
running the test.
PartUsage owned by a RequirementDefinition/RequirementUsage (a requirement's
declared subject, e.g. TimelyToast's subject toaster : Toaster) is a
reference binding, not real composition. model_to_dot() now resolves each
PartUsage's owner @type via a once-per-call index and skips both the
composition and typing edges when the owner is a requirement, leaving real
part composition untouched.
…mily

subject means the same reference-binding thing (not composition) on every
kind that declares it, not just RequirementDefinition/RequirementUsage:
ConcernDefinition/ConcernUsage, CaseDefinition/CaseUsage and its own
specializations VerificationCaseDefinition/VerificationCaseUsage,
UseCaseDefinition/UseCaseUsage, AnalysisCaseDefinition/AnalysisCaseUsage.
Confirmed on the real Ch3 fixture: TimelyToastTest (a
VerificationCaseDefinition) was still leaking its subject usage as false
composition under the narrower skip-set.

Also tighten the Ch2 test's vacuous 'Toaster' in dot substring check to the
real node-declaration lines, and record the known narrow limitation (a
subject usage's own further-nested parts are not suppressed) in the
docstring.
…ocation

An AllocationUsage is now only included in a diagram's allocs when its own
owner is the diagram's fqn or something in the already-computed expanded
containment set, resolved via to_api_json()'s own owner reference followed
to that owner's record in the same payload (covers anonymous allocations too,
which model.query() does not surface). Previously every AllocationUsage in
the whole model was drawn regardless of which element it belonged to, so a
HeatingAssembly-scoped diagram drew Toaster's own heatAllocation plus an
orphan 'heating' box, just because one of its endpoints collided with a part
name. flows extraction is untouched. Adds tests against the real Ch6/Ch5
fixtures proving the fix and that in-scope allocations are unaffected.
The second review (opus-5-5) confirmed REQUIREMENT_OWNER_TYPES's docstring
claim of covering "the whole Requirement/Case family" is not spec-complete:
two synthetic toolkit constructs it built (viewpoint def's own subject,
satisfy requirement's own subject) still leak as false composition, and a
third toolkit quirk (objective as a bare PartUsage) can't be caught by any
type-list fix. None of these appear in any real fixture in this tutorial
(ch01-ch10), where the only subject-bearing owner types actually observed
are RequirementDefinition and VerificationCaseDefinition -- both already
handled. Record the gap next to the existing nested-subject-parts note,
same reasoning: tracked, not fixed.

Also strengthens the Ch3 real-fixture test to assert "TimelyToast" (Ch2's
requirement, carried into Ch3's cumulative fixture) is absent from the DOT
output, not just "TimelyToastTest".

No production behavior change: docstring text and one added test assertion.
Stops model_to_dot() from drawing a requirement's, concern's, or case's
own subject reference as if it were real part composition. Confirmed
systemic across Ch2, Ch3, Ch5, Ch8 and Ch10's diagrams; fixed at the
shared rendering function rather than worked around per-chapter.
TimelyToast no longer leaks into the diagram as false composition.
Bridge or reorder each setup-cell-to-diagram-cell adjacency so no two code
cells sit back to back without narration, per toaster-recipe's pacing rule.
…y fixes

build_interconnection_intent() now scopes allocations to HeatingAssembly's
own subtree, so the diagram shows exactly one allocation (heatGenAllocation),
matching the narration. Rewrote the bridge and post-diagram cells, which
previously described a removed print's output instead of the diagram's
actual content; dropped a dead find_allocations() assignment.
cell-20 said heatGenAllocation and Chapter 5's heatAllocation are 'the same
connection' -- they are different allocations with different ends; restored
'the same way... by query'. Also replaced two literal em-dash characters
introduced by this task (nb03 cells 3 and 9) with a colon/comma per
tutorial-style-guide's no-em-dash rule; the ASCII '--' used elsewhere in this
diff is the established convention, not the banned character, and is left
unchanged.
Infrastructure task plus all 9 chapter tasks merged. Records the two
cross-cutting render.py bugs found and fixed at the root mid-batch
(model_to_dot's requirement/case-subject leak, build_interconnection_intent's
allocation scoping) and the known gaps carried forward.
… renderer

Both still described build_interconnection_intent()/render_interconnection()
(notebook 03 has used render_toolkit_interconnection() since the sysml-toolkit
upgrade). Found independently by the Novice and SE Practitioner personas and
the longitudinal Returning Learner run during the pre-PR user-testing pass.
- Ch3 nb02: join exercise-pointer cell's two sentences into one
  (semicolon, matching this tutorial's own established convention).
- Ch4 index.md: name both new diagrams in the Ingredients table.
- Ch5: rename 01-concept-selection.ipynb to 01-model-navigation.ipynb --
  its content teaches model.find()/model.get() navigation, not selection
  among alternatives; already flagged in decisions/pass4-backlog.md and
  decisions/audits/ch05-layer-audit.md but never actioned. Updated
  myst.yml's toc and index.md's link accordingly.
…h1-7's plain style

Two issues found while reviewing the local build:

1. Every chapter notebook (all 32, including Ch1) rendered its own first
   heading twice -- once as the implicit page banner/sidebar label (MyST's
   fallback when no frontmatter title is set), once again as the first
   in-body heading. Fixed by adding a short YAML frontmatter title
   (ChN-NN) to each notebook's first cell, the same mechanism already used
   for index.md/conclusion.md. Sidebar entries are now uniformly terse
   (Ch1-01, Ch1-02, ...) across all ten chapters, matching what Ch8-10
   already showed before this fix.

2. Starting at Chapter 8, every notebook's own heading drifted from Ch1-7's
   plain, construct-named style ("abstract part def", "requirement def",
   "level-2 function and logical carrier") into a "ChN-NN -- <essay-style
   clause>" format ("A hand-restated lemma of the same shape", "Proof,
   point evaluation, a genuine violation, and a genuine 'not sure'").
   Rewrote all 9 Ch8-10 notebook headings to match the established plain
   style; the ChN-NN prefix moved into the new frontmatter title instead
   of living in the heading text.
…tUsage, draw specialization edges

Defect 1: a PartUsage owned directly by a Package (e.g. nominal, slow) is
not real part composition, so the composition diamond is no longer drawn
for that owner kind; the usage's own typing edge is unaffected.

Defect 2: model_to_dot() never drew specialization (:>) edges at all. Adds
one edge per direct specializes target (resolved from the raw API-JSON
payload's own specializes reference field, handling both single-dict and
list shapes), styled as a solid line with an open/hollow arrowhead to stay
visually distinct from both composition (filled diamond) and typing
(dashed, open arrow). When elements is a scoped subset, an edge is only
drawn when both ends are in that scope, the same discipline already
applied to flows and allocs elsewhere in this file.
…dge fixes

test_model_to_dot_draws_specialization_edge_on_real_ch08_fixture: proves
Defect 2's fix against the real Ch8 fixture (ResistanceCoil :> HeatGenerator),
asserting the literal DOT line and that it differs from the typing edges
also present in the same output.

test_model_to_dot_excludes_package_composition_on_real_ch02_fixture: proves
Defect 1's fix against the real Ch2 fixture (nominal/slow owned directly by
the ToasterDemo package), asserting the false composition line is absent
while the usages' own typing edges and the fixture's real composition
elsewhere are still present.

test_model_to_dot_real_composition_is_unaffected (existing, unmodified)
already covers the negative-control case for Defect 1 (a PartUsage owned by
a real PartDefinition) since its own fixture's owner is a PartDefinition,
not a Package -- no new test needed for that case.
Review findings on the package-composition/specialization fix: the
docstring's own Nodes/Edges summary didn't mention either new behavior,
and two real-but-unexercised edge cases (untyped package-owned usages now
disappear entirely; a non-PartDefinition specialization target gets an
unstyled node) were undocumented. Recorded both, not fixed, matching this
function's own established gap-tracking pattern.

Also fixed: source is now iterated twice (once for scoped_qnames, once in
the main loop), so a one-shot iterable passed as elements would silently
exhaust after the first pass. Materialized to a list once; no behavior
change for any real caller, all of which already pass a list.
…zation edges

A package is not a part and does not compose anything, but model_to_dot()
drew every package-owned PartUsage (nominal, slow, rated, weak,
heatGenCheck) with a false composition diamond from the package. Fixed by
skipping that edge when the owner's @type is Package. Also adds
specialization (:>) edges, previously never drawn at all -- Ch8's own
caption had to carry the entire 'typed by' vs 'specializes' distinction
in prose with no visual reinforcement.
…o_dot notation fix

Package-owned usages (nominal, slow, rated, weak, heatGenCheck) no longer
show a false composition diamond; specialization edges (Toaster's own
:> ToastingSystem, and in Ch8/Ch10 also HeatingAssembly :> HeatingSystem,
ResistanceCoil :> HeatGenerator) now appear as their own solid,
hollow-triangle edges, distinct from composition and typing.

Caption fixes: Ch8's own caption previously said the ResistanceCoil :>
HeatGenerator link 'is not drawn' -- now false, since the diagram draws it
directly; rewrote to describe what's actually shown. Ch2 and Ch5's
'containment skeleton' captions didn't account for the new specialization
edge appearing alongside composition; added one clause naming it. Ch3's
caption makes no composition-only claim and needed no change; Ch10's
rooting/reachability claim is unaffected by either change.
…er edge

Review finding: 'drawn as a package sibling of rated and weak' still implied
a shared owner edge, which the package-composition fix just removed
entirely. All three are now free-standing nodes with no owner edge at all.
…activity action names

Formalizes the two render_action_flow()/render_state_flow() gaps found
during Phase 2 and the pre-PR user-testing pass (decisions/log.md DL-087
known gap (b), DL-088 finding 5a). Z's decision: track only for now.
MyST's YAML frontmatter block in cell 0 fixed MyST's own double-heading
display, but plain CommonMark (jupyter nbconvert, JupyterLab, GitHub
notebook preview) misparses the --- delimiters as a horizontal rule plus
a setext heading literally reading title: "ChN-NN", visible to the
primary learner workflow (docs/setup.md line 29).

Investigated and empirically tested on a correctly-scoped local MyST
dev server (myst 1.11.0, reading /opt/homebrew/lib/node_modules/mystmd/dist/myst.cjs):
- notebook metadata.title (top-level .ipynb metadata key): confirmed to
  have zero effect on the rendered banner/tab title. getPageFrontmatter()
  auto-fills frontmatter.title from the body's first heading before the
  notebook-level metadata (nbFrontmatter) is ever consulted, so the filled
  value always wins over metadata.title.
- notebook metadata.short_title: confirmed this DOES change the sidebar/
  TOC label (fileInfo3.short_title read at the TOC-render call site), but
  it does not touch the banner/tab title or the in-body heading, so it
  only satisfies half of the constraint.
- a raw cell containing YAML frontmatter: confirmed no effect, matching
  the prior investigation; raw cells never reach the markdown-only
  frontmatter parser (processNotebookFull only calls parseMyst, which
  does the frontmatter detection, on cell_type markdown).
- the literal YAML block in cell 0: the only mechanism that sets a page
  title distinct from the body heading, and it is exactly what breaks
  plain CommonMark rendering.

No mechanism found inside the 32 notebooks themselves satisfies both
constraints at once. Per the contract's own fallback instruction,
reverting restores the known, already-tracked MyST double-heading
regression rather than compounding it with the worse plain-CommonMark
regression.
The notebook declares assert constraint deliveredEnergyBoundedBySupply
(models/ch08-cumulative.sysml line 263); 'invariant' never appears
anywhere else in the notebook's own prose. Matches Ch1-7's own
name-the-actual-construct heading style (## action def, ## requirement
def). Already flagged once in decisions/audits/ch08-layer-audit.md line
140.
The 9 table entries still quoted the old essay-style headings. Updated
each to match its notebook's own current heading text exactly (post-F3
for Ch8-01).
Ch4 (01-action-def-ffbd.ipynb) renders ToastBread, not ApplyHeat as the
bullet claimed; only Ch6 (01-subsystem-requirements.ipynb) renders
ApplyHeat. Confirmed against grep of both evidence SVGs' <text>
elements: neither figures/ch04-toastbread-flow.svg (ToastBread's own
flows and its applyHeat:ApplyHeat sub-action's flows) nor
figures/ch06-applyheat-flow.svg (ApplyHeat's own flows and its
generateHeat:GenerateHeat sub-action's flows) draws any flow-pin text
at all.
Ch2-03 cell 3 and Ch5-01 cell 3 were each one semicolon-joined sentence
(31 and 32 words); split each into two sentences (16/15 and 18/15
words) stating what the figure shows and what conclusion it supports,
re-verified against the single existing Toaster->ToastingSystem
specialization edge in figures/ch02-structure.svg and
figures/ch05-structure.svg.

Ch8-01 cell 4 had two sentences, both over the ceiling (33 and 26
words); split into five short sentences (13/15/7/12/16 words), also
dropping a stray double-hyphen em-dash, re-verified against
figures/ch08-structure.svg's own heatGenCheck/rated/weak/ResistanceCoil
edges for the sibling/no-owner-edge distinction.
Traced mystmd v1.11.0's own getFrontmatter() (node_modules/mystmd, not the
stale homebrew v1.9.1 install): when a markdown cell's first block is a
level-1 heading, mystmd auto-derives frontmatter.title from it and deletes
the heading from the body -- this only fires for H1 (depth 1), never H2,
which is exactly why the prior "## heading" text rendered twice no matter
what frontmatter was added around it.

Changed all 32 chapter notebooks' cell 0 first line from "## heading" to
"# heading" (same text, H1 instead of H2) and added notebook.metadata.
short_title = "ChN-NN" (confirmed via fillProjectFrontmatter/
manifestPagesFromProject in myst.cjs to drive the sidebar/TOC label
through a separate code path from the page's own banner/tab title, which
now shows the real heading text, promoted by the H1 lift).

Verified empirically: `jupyter nbconvert --to html` on 3 notebooks across
3 chapters shows zero <hr>, zero stray frontmatter text, exactly one
heading matching the real text. A MyST dev server confirmed scoped to
this worktree (lsof -a -p <pid> -d cwd) shows the sidebar as "ChN-NN",
the page banner/tab title as the full heading text, and exactly one
heading rendered on the page.
Ch2-03 and Ch5-01 cell 3's trim from the prior pass opened with "This
figure shows...", the only self-referential caption phrasing in the
32 notebooks, banned by tutorial-style-guide's no-metanarration rule
(a caption states the fact directly, it does not point at itself).
Restored the original direct phrasing ("The containment skeleton this
model carries so far...") as sentence one, kept sentence two unchanged;
both sentences stay under the 20-word ceiling, re-verified against the
single Toaster->ToastingSystem specialization edge in figures/
ch02-structure.svg and figures/ch05-structure.svg.

Ch8-01 cell 4's trim from the prior pass crammed five sentences into the
word ceiling, violating the style guide's exactly-two-sentences caption
rule, and lost the causal link between "no owner edge" and "declared at
package scope." Split the content instead, matching Chapter 6's own
bridge-then-caption pattern: cell 4 (before the diagram) is now a short
bridge narrating the typed-directly/specializes distinction and the
package-scope reasoning across four sentences, each under the ceiling;
a new cell after the diagram is the terse, exactly-two-sentence caption
(13 and 15 words). Re-verified against figures/ch08-structure.svg's own
edge styles: ResistanceCoil->HeatGenerator is solid with a hollow
(unfilled) triangle, the three others are dashed with filled triangles.
…n edges

Ch8-01's caption (cell-03d) and bridge (cell-03a) claimed the
ResistanceCoil-to-HeatGenerator edge was the only solid, hollow-triangle
specialization edge in ch08-structure.svg. Third-pass review found and
I confirmed directly against the SVG that three such edges exist
(Toaster->ToastingSystem, HeatingAssembly->HeatingSystem,
ResistanceCoil->HeatGenerator), because Ch8's diagram is unscoped
unlike Ch2/Ch5's genuinely single-edge figures. Reworded both
sentences to describe that edge's own styling without claiming
uniqueness.
…irect edits

Four rounds of independent review. F1: the notebook title-duplication fix
previously broke plain Jupyter/nbconvert/GitHub rendering (a stray
'title: "ChN-NN"' heading); replaced with mystmd's own H1-heading
title-lifting plus a short_title metadata field, which fixes MyST's
sidebar/banner duplication with zero visible artifacts in any other
renderer. F2: synced Ch8-10's index.md tables to corrected headings.
F3: Ch8-01's heading named a construct ('invariant') the notebook
doesn't use; corrected to 'assert constraint'. F4: DEFERRED.md D-037
cited the wrong action-flow call site; corrected. F5: trimmed captions
to the style guide's own word-count ceiling without introducing
self-reference, breaking the two-sentence caption rule, or losing the
causal content the originals carried.
…convention

Per skill-editor's own protocol (DL-091): DL-090 found and fixed a real
duplicate-heading bug whose actual mechanism -- cell 0 must use a level-1
heading, and the notebook's metadata must carry short_title -- was never
written down anywhere a future builder or reviewer would see it. Added
one paragraph to the skeleton template and one clause to the A6
checklist.
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@mzargham
mzargham merged commit 497d734 into main Oct 2, 2026
2 checks passed
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.

1 participant