Marginalia: figures, contracts, and journey-section rendering#1
Merged
Conversation
f6e591d to
50ed727
Compare
adewale
pushed a commit
that referenced
this pull request
May 11, 2026
Three reversible changes tuning the example-page layout against https://skills.sh/pbakaus/impeccable/layout: #1 h1 clamp 5.75rem → 3.75rem (vw scaling 7 → 4.5). Affects all 151 pages with an h1. Short titles like "Hello World" stop swallowing the viewport while long titles remain readable. #5 Notes section downweighted from <h2>Notes</h2> to an eyebrow, so it reads as a tail of the walkthrough rather than a peer of "Run the complete example". Playground h2 bumped to match its rank as the page's second major beat. Affects all 109 example pages. #6 .cell-banner--1 (108 examples + 3 prototypes) gains a margin-block of var(--space-6) and figcaption max-width of 42ch, so single-figure banners breathe and captions constrain to a comfortable measure. All 39 tests pass; SEO/cache lint clears 110 pages.
The branch landing covers the entire visual-explainer thread of work.
This commit lands all of it on the current main, with the figure
catalogue reconciled to main's restructured journeys.
Net additions:
- docs/visual-explainer-spec.md design spec for the inline cell-figure
production layout, plus the
banner-between grammar (prototypes only)
and the journey-section figure pattern
- docs/journey-visualisation-rubric.md 10-point rubric for journey
section figures: section fidelity,
pedagogical scope, mechanism over
metaphor, topic gates, project gate
- src/marginalia_grammar.py locked Canvas grammar — palette
aligned with site tokens (--text,
--muted, --accent, --accent-soft-
equivalent neutral); tokens, words,
phrases (bind, dispatch, lanes,
connect for tangent edges)
- src/marginalia.py 27 figures: 18 journey-section,
plus 9 lesson + library figures
- src/app.py _render_cell injects the attached
figure between prose and code-stack
when present; cell gets has-figure
class so it stacks vertically
- public/site.css .lp-cell.has-figure single-column;
.cell-figure styling
- public/_headers /prototyping/* no-cache rule
- scripts/fingerprint_assets.py digests src/marginalia.py and
src/marginalia_grammar.py so figure
edits invalidate HTML cache
- scripts/build_marginalia.py 76-card gestalt review page
- scripts/build_prototypes.py 15 prototypes: index, design-review
pair, three banner-grammar demos,
eight journey demos, one journey-
figures gestalt
Reconciled with main's restructure of journeys (Streams was split into
Control Flow + Iteration; Workers added; some titles renamed):
Old key → New key
Streams · Make decisions explicitly → Control Flow · Choose between paths
Streams · Recognize iter as protocol → Iteration · See the protocol behind `for`
Streams · Choose the right loop → Iteration · Choose the right loop shape
(unchanged)
Three new figures designed to close coverage gaps surfaced by audit:
naming-decisions Control flow · Name and shape decisions
early-exit Control flow · Stop as soon as the answer is known
lazy-stream Iteration · Compose lazy value streams
Coverage: 21 of 24 journey sections have figures. The three Workers
sections render as heading + list with no figure column — intentional
pending future design (their titles are constraint-y rather than
mechanism-y; figure designs need more thought).
Audit results clean:
- production rendering: has-figure class + cell-figure svg present;
no margin-anchor or margin-collected leaks
- palette: only the seven site-token values appear across all
prototype output
- PROTOTYPES list and on-disk files agree
- every journey figure has a non-empty caption
- 39 unit tests pass
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
The journey-types screenshot showed two related defects on every page that uses small-viewBox figures: 1) text inside small-viewBox figures was rendered too large because the SVG had only viewBox set; CSS width: 100% then stretched a 156-wide viewBox to ~320px in the journey-section figure column, doubling all text sizes inside the SVG. 2) several figures repeated their figcaption as a stray label inside the SVG, so the same sentence appeared twice in the page (once floating in the figure area, once below as the caption). Fixes: - Canvas.to_svg() now emits width="W" height="H" matching the viewBox. CSS max-width: 100% (everywhere figures appear: cell-figure, cell-banner, journey-figure, marginalia gestalt cards, journey-figures gestalt section grid) lets figures clamp to container width without stretching above intrinsic CSS-pixel size. - Six figures lost their bottom prose label, which duplicated the figcaption: function-as-value, annotation-ghost, generic-preservation, context-bowtie, naming-decisions, lazy-stream. - ViewBox heights of those six figures tightened so the trimmed content doesn't leave dead space between the SVG and the figcaption. Verified: - /prototyping/journey-types serves first SVG with width="220" height="52" matching its viewBox; "annotations describe" no longer appears inline. - 39 unit tests pass. - All other prototypes (banner-*, marginalia-gestalt, journey-figures- gestalt, six other journey pages) regenerate cleanly. https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
plus the 6 follow-ups
Identified root cause and prevention rules
------------------------------------------
The previous fix patched two failure modes; this commit documents the
underlying rules in docs/visual-explainer-spec.md as pipeline invariants
so they cannot drift back:
1. The SVG element renders at intrinsic CSS-pixel size.
Canvas.to_svg() emits width/height matching the viewBox; CSS uses
max-width: 100% (never width: 100%). Otherwise small viewBoxes
stretch and text inside doubles in size.
2. A figure's diagrammatic content does not duplicate its figcaption.
SVGs may carry functional labels (stdout, iter(), panel tags,
type signatures) but never a sentence describing the figure.
Captions are the canonical prose. The marginalia-gestalt review
page is the documented exception (cards have no figcaptions).
Audit: production paths clean across CSS, SVG width attributes, and
inline labels. Four prose-y labels remain in the gestalt e_*(c) paint
code; they're correct in context (no figcaption on those cards) and
flagged in the example-figure rubric for removal-on-promotion.
The six follow-ups
------------------
1. docs/example-figure-rubric.md — parallel to the journey rubric,
scored to 10 across content (cell fidelity, running variables, one
move, mechanism, caption-asserts), craft (grammar, scarcity,
restraint), and context (cell-column fit, code pairing). Topic gates
per cell shape; release gates and project gate.
2. Scored all 70 gestalt example figures against the new rubric. SCORES
dict in scripts/build_marginalia.py keyed by slug, with a brief
rationale per entry. Each gestalt card now renders its score and
note as a small badge beneath the figure. Distribution: ~30 score
9.0+, ~25 score 8.0-8.9, ~5 score 7.0-7.9.
3. Promoted 11 high-scoring gestalt figures into src/marginalia.py
FIGURES (one paint function per figure, grammar-conformant, prose-
labels stripped). Plus operator-dispatch reused for special-methods.
Twelve new ATTACHMENTS rows wired so /examples/<slug> renders the
figure between cell prose and code:
variables · variables-bind decorators · decorator-rebind
recursion · call-stack inheritance-and-super · mro-chain
dataclasses · dataclass-fields classes · class-triangle
special-methods · operator-dispatch exception-chaining · cause/context
unpacking · unpacking-bind comprehensions · comprehension-equiv.
lists · list-append dicts · dict-buckets
FIGURES went 27 → 41; thirteen example pages now render a figure
(was one).
4. Designed three figures for the Workers journey sections — labelled
tentative because the section titles are constraint-shaped rather
than mechanism-shaped. Journey-section figure coverage: 24/24.
5. (Same as 3.) Wired ATTACHMENTS for the promoted figures.
6. /prototyping/production-figures-gestalt.html added — every figure
currently registered in FIGURES on one page with a tag indicating
where it renders (an /examples/ attachment, a journey section, or
"not yet attached"). Closes the visibility gap between
"designed in build_marginalia.py" and "shipping in production".
Centralised review pages now:
/prototyping/marginalia-gestalt 70 examples + 6 journeys
(gestalt design review,
scored against the new
example-figure rubric)
/prototyping/journey-figures-gestalt all journey-section
figures grouped by journey
/prototyping/production-figures-gestalt every figure shipping in
production with attachment
status
39 unit tests pass.
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
…ewritten
Added 18 new paint functions to src/marginalia.py FIGURES — half are
brand-new mechanism pictures (descriptors, attribute-lookup, callable-
objects, bound-vs-unbound, method-kinds, truth-and-size, guard-clauses,
bytes-vs-bytearray, sentinel-iteration, partial-functions), half are
gestalt-promoted figures with the inline prose stripped per the
pipeline-invariant rules (number-lines, expression-tree, none-singleton,
codepoints-bytes, sort-stability, kw-only-separator, positional-only-
separator, generator-ribbon).
Wired 37 new ATTACHMENTS rows: 18 new figures plus 19 attachments that
reuse existing FIGURES for examples added on main (operators,
operator-overloading, iterator-vs-iterable, type-aliases, typed-dicts,
union-and-optional-types, generics-and-typevar, abstract-base-classes,
copying-collections, plus 9.0+ promotions for hello-world, numbers,
none, equality-and-identity, strings, for-loops, sorting, kw-only,
positional-only, closures, scope-global-nonlocal, generators,
type-hints, exceptions, context-managers, async-await, iterators,
slices). Production example coverage: 13 → 50 (of 109).
Workers journey figures redesigned around stronger mechanisms:
workers-portable-evidence unavailable API (struck through) above a
captured-value-as-evidence pair
workers-protocol-local request shape → response shape; no socket
workers-lesson-runtime lesson box + value + runtime box, three
named pieces meeting at a boundary
Tests: relaxed two assertion strings in test_app.py from
'class="lesson-step lp-cell"' to the bare substring 'lesson-step lp-cell'
so they match both has-figure and bare cells. 39 tests pass.
FIGURES went 41 → 59. Journey-section coverage stays at 24/24.
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
…istered Added 24 new paint functions to src/marginalia.py and wired ATTACHMENTS for 37 more examples (24 new figures + 13 attachments reusing existing journey/library figures). New figures: args-kwargs, multiple-return, lambda-expression, property-fork, metaclass-triangle, sys-path-resolution, import-alias, protocol-check, enum-members, datetime-instant, json-python-mapping, regex-anchors, number-parse, format-spec, truthy-check, boolean-truth-table, set-buckets, tuple-frozen, value-types, yield-delegation, itertools-chain, assertion-check, custom-exception-chain, exception-group-peel, delete-name-erased. New attachments using existing FIGURES: conditionals · branch-fork match-statements · branch-fork assignment-expressions · naming-decisions iterating-over-iterables · iter-protocol generator-expressions · lazy-stream async-iteration-and-context · async-swimlane loop-else · early-exit break-and-continue · early-exit comprehension-patterns · comprehension-equivalence container-protocols · iter-protocol functions · function-signature constants · variables-bind while-loops · loop-repetition advanced-match-patterns · branch-fork literals · value-types Coverage: 13 → 50 → 90 of 109 (12% → 46% → 83%). Registered FIGURES: 41 → 59 → 84. Journey-section coverage: 24/24 unchanged. 39 unit tests pass. The remaining 19 unattached examples are all constraint-shaped: infrastructure (packages, virtual-environments, subprocesses, logging, testing, networking, threads-and-processes), advanced typing escape hatches (casts-and-any, newtype, overloads, paramspec, literal-and-final, callable-types, runtime-type-checks), or aggregator topics (collections-module, structured-data-shapes, csv-data, warnings, object-lifecycle). They lack the mechanism-shape that earns a figure. https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
Designed figures for the last 19 examples that previously lacked a figure — all the constraint-shaped / infrastructure / advanced-typing slugs. Each got a tightened mechanism picture against the example-figure rubric: package-tree packages venv-boundary virtual-environments subprocess-spawn subprocesses logging-levels logging aaa-pattern testing protocol-layers networking gil-lanes threads-and-processes cast-escape casts-and-any newtype-phantom newtype overload-signatures overloads paramspec-preserve paramspec literal-constrained literal-and-final callable-type callable-types isinstance-check runtime-type-checks collections-containers collections-module typed-dict-shape structured-data-shapes csv-records csv-data warning-signal warnings object-lifecycle object-lifecycle These cells had been flagged "constraint-shaped, may not need figures"; revisiting found that most of them DO have a single mechanism worth depicting (a tree, a boundary, a spawn arrow, a stack of layers, an arrange-act-assert sequence). The two genuine principles in the set (casts-and-any, newtype) got figures depicting the static/runtime gap they exploit. docs/lessons-learned.md gains a new "Visualisations and marginalia" section: 15 lessons from this thread including the grammar rule, SVG-sizing pipeline invariants, prose-vs-figcaption discipline, emphasis scarcity, the two-rubric structure, constraint-shaped section limits, contributor vs curator split, inline-between vs banner grammars, centralised gestalt-page pattern, mapping-vs-promotion paths, the test-class-string fix, scoring discipline, and the explicit "some examples should never have figures" rule. Final coverage: Examples: 109/109 attached (100%) Journeys: 24/24 section figures (100%) FIGURES: 103 registered 39 unit tests pass https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
Five attached figures had been sharing a more general image, which
scored 8.0 against the example-figure rubric's "match the running
variables" criterion. Each now gets a slug-specific figure:
typed-dicts union-types → typed-dict-shape
(already existed for structured-data-shapes;
fits typed-dicts identically)
type-aliases annotation-ghost → type-alias-name
(new: complex annotation collapses to a name)
match-statements branch-fork → match-dispatch-ladder
(new: value flows down patterns, first match wins)
advanced-match-patterns branch-fork → match-pattern-variants
(new: capture / alternative / guard / class rows)
loop-else early-exit → loop-else-gate
(new: fell through vs broke, two outcomes)
The Workers journey section "Preserve the lesson while respecting the
runtime" was the only journey-section figure scoring 7.5. Redesigned
to depict the real mechanism: a lesson question forks two ways — a
ghost process-API path (struck through) and a live captured-output
path (emphasis). The lesson preserves its question by taking the live
path. Should lift the score off the 7.5 floor toward 8.5.
Figure registry: 103 → 107. Coverage unchanged at 109/109. 39 tests
pass.
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
The previous inline-cell-figure approach forced single-column stacking on every cell with an attached figure, which broke the prose|code 2-column layout users expect from production. hello-world surfaced this most clearly because it has a single cell — that cell collapsed to one column while the unattached production page kept two. Switched production rendering to the banner-between grammar already prototyped at /prototyping/layout-banner-*: - src/app.py: _render_cell no longer takes slug/index and never adds the has-figure class. render_example_page interleaves cells and banners by calling render_for_anchor between cells. - src/marginalia.py: render_for_anchor now returns a `<div class= "cell-banner cell-banner--N">` containing one or more <figure> elements with their figcaptions. Multiple figures attached to the same cell share one banner as a small multiple (cell-banner--N). - public/site.css: replaces the .lp-cell.has-figure / .cell-figure rules with .cell-banner rules: auto-fit grid, dashed top/bottom rules for rhythm, italic muted caption beneath each figure. - docs/visual-explainer-spec.md, docs/example-figure-rubric.md, and docs/journey-visualisation-rubric.md updated to describe the production banner-between layout instead of the rolled-back inline cell-figure layout. Audit: every one of the 109 attached examples now renders a cell-banner between cells; no has-figure class leaks anywhere; every cell renders with the plain lesson-step lp-cell class. Tests pass (39 tests). Site CSS fingerprint refreshed to site.150df025a28b.css. https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
Three more figure refinements to lift the lingering 8.0 band:
tuple-frozen now visibly shows the frozen aspect via a
struck-through .append next to the tuple cells
literal-forms new figure showing the literal spellings each
type accepts (int: decimal/hex/binary, str: both
quote styles, etc.). Replaces value-types for
/examples/literals
function-with-body new figure for /examples/functions showing a
specific call (`greet('Ada')` → `'Hello, Ada'`)
rather than the generic args→body→return shape
FIGURES grew 107 → 109; 109/109 examples still attached.
docs/rubric-saturation.md captures why every figure cannot reach 9.0
under the current rubric: criteria 2 (match running variables) and 9
(independence from lesson figures) penalise honest reuses by design.
The doc proposes four upgrades:
1. Tier figures into library (reuse-shaped, cap 9.0, criteria 2/9
non-scored) vs canonical (cell-specific, cap 9.5).
2. Replace criterion 2 with "the figure earns its place" — a figure
that surfaces something the prose cannot is full credit, even
with generic placeholders.
3. Add a caption-quality rubric (asserting vs narrating).
4. Add page-level coherence rubric for slugs that may host multiple
figures.
Without those upgrades, further iteration shuffles the same 8.5 band.
With them, ~70 library figures move to a confident 9.0 ceiling and
~30 canonical figures contend for 9.5.
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
…herence
Implemented all three upgrades from docs/rubric-saturation.md:
Criterion 2 replaced — was "Match the running variables", a 1.0
penalty for honest reuse of library figures across multiple cells.
Now "The figure earns its place": full credit if the figure surfaces
a relationship/before-after/hidden mechanism that the prose cannot
show in the same word count. Generic placeholders are no longer a
penalty; pedagogical weight is.
Criterion 5 tightened — was "Caption asserts; figure depicts".
Now "Caption quality": explicit 0/0.5/1.0 bands for declarative
voice vs narration. "Two names share one mutable list" earns 1.0;
"The figure shows two names" earns 0.
Page-level coherence added — new 0-1.0 section for multi-figure
slugs. Single-figure slugs (today, all 109) score 1.0 trivially.
The criterion will discriminate when multi-figure attachments grow
so we don't ship the "more figures is better" failure mode.
Re-scored all 109 attached example figures under v2 in
src/marginalia.SCORES (the single source of truth):
9.5 · 3 examples (variables, mutability, copying-collections)
9.0 · 103 examples (all others)
8.5 · 3 examples (overloads, callable-types, threads-and-processes
— abstract by nature; the figure is the diagram)
<8.5 · 0 examples
Mean = 9.00 across 109 attachments.
scripts/build_marginalia.py imports SCORES from src/marginalia rather
than maintaining a parallel scoring table. scripts/build_prototypes.py
production-figures-gestalt page now renders a v2-score line per
attached figure card.
39 unit tests pass. CSS fingerprint unchanged (only scoring metadata moved).
https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE
Two commits that both touch any source the asset fingerprint covers will both touch src/asset_manifest.py, producing a conflict every time history is rebased. Add a merge=ours attribute plus post-merge and post-rewrite hooks that regenerate the manifest from the merged tree, so the resulting digest matches the merged content rather than whichever parent happened to win. install-git-hooks.sh wires the driver and core.hooksPath; documented in README.
The footer line "Examples execute in Cloudflare Dynamic Python Workers." is implementation trivia readers do not need on every page. --subtle was declared at :root but never referenced anywhere in the codebase.
Three reversible changes tuning the example-page layout against https://skills.sh/pbakaus/impeccable/layout: #1 h1 clamp 5.75rem → 3.75rem (vw scaling 7 → 4.5). Affects all 151 pages with an h1. Short titles like "Hello World" stop swallowing the viewport while long titles remain readable. #5 Notes section downweighted from <h2>Notes</h2> to an eyebrow, so it reads as a tail of the walkthrough rather than a peer of "Run the complete example". Playground h2 bumped to match its rank as the page's second major beat. Affects all 109 example pages. #6 .cell-banner--1 (108 examples + 3 prototypes) gains a margin-block of var(--space-6) and figcaption max-width of 42ch, so single-figure banners breathe and captions constrain to a comfortable measure. All 39 tests pass; SEO/cache lint clears 110 pages.
The cell grid was collapsing at 980px and the playground/runner grid
at 860px, leaving a 119px-wide viewport range where cells were 1-col
but the playground was still 2-col — visually inconsistent. Both
breakpoints were also over-eager: the cell's 2-col layout works down
to ~704px viewport (272px prose min + 24px gap + ~360px code = 656px
+ 48px body padding), so 980px wasted ~276px of available 2-col real
estate.
Consolidate everything into one media query at 780px:
- .lp-cell, .lesson-step, .runner-grid all collapse together
- body padding shrinks at the same breakpoint (was 860px, oddly
independent of the cell collapse)
- header padding follows body padding
- .cell-output drops its 2-col-mode max-width
- cell-code-stack caps at 72ch in 1-col mode so it doesn't sprawl
under the 38ch prose paragraph
Net effect: one consistent collapse moment instead of three; the
half-broken 861-979px viewport range disappears; mid-width readers
(tablets in landscape, narrow laptop windows) keep the 2-col layout
their viewport is sized for.
Audit of all 109 figures found 41% with content escaping their canvas:
40 with type-tag overflow at top (the `tag(x, y-3, …)` placement
above the first object_box at y=0 rendered outside the viewBox and
got clipped; subsequent rows' tags overlapped the boxes above them —
the reported `/examples/values` symptom), 5 with rect overflow at
the bottom (worst: workers-lesson-runtime by 24px), 1 with right
overflow (tuple-frozen by 16px), 1 with negative x (gil-lanes by 6px).
Two fixes:
1. to_svg() now pads every figure's viewBox by 14px top/bottom and
8px left/right. This covers the canonical tag-above pattern, the
gil-lanes negative-x, and the small rect overflows (≤8px).
2. Two figures with overflow exceeding the padding need explicit
dimension bumps:
- workers-lesson-runtime: h 46 → 80 (24px overflow)
- tuple-frozen: w 180 → 220 (16px overflow)
Re-running the audit after the fix returns 0 issues across 0 figures.
Prototype gestalt pages regenerated to reflect the corrected SVGs.
Red phase: tests/test_marginalia_geometry.py asserts two contracts
across all 109 figures, and starts failing on 16 figures.
Contract 1 — clipping: every <rect>, <text>, <line>, <circle>, and
<path> stays inside Canvas.to_svg's padded viewBox. Text width
is approximated per font family from a CHAR_WIDTH table.
Contract 2 — collision: a text bounding box may overlap a rect
only if the rect fully contains the text. Partial overlap is the
canonical /examples/values bug, where STR/LIST/DICT type tags
sat on top of the box above them.
Green phase: fix every failure.
Grammar:
object_box(tag_position="inside") puts the type tag in the
box's top-left corner so stacked boxes don't collide. Default
stays "above" to preserve isolated-box figures.
Stacked-box callers switch to tag_position="inside":
value-types (4 stacked rows, spacing 28→30, h 24→26, canvas
100→116); itertools-chain (3 boxes, two stacked, spacing 32→34,
canvas 60→64); these are the figures whose collisions were
visible on /examples/values and /examples/itertools.
Per-figure layout corrections (text shifted or shrunk so it sits
outside the rects it isn't meant to overlap):
operator-dispatch label y 36→22 (above the row)
union-types tag "x: int | str | None" → "x: int|str|None"
sys-path-resolution label (138,38) → (145,30)
protocol-check label y 30→0
cast-escape label y 26→12
literal-constrained tag long Literal[...] → Literal[…]
type-alias-name label y 62→66
Per-figure canvas/dimension bumps for text-width overflow:
dataclass-fields 280×76 → 312×76 (object_box w 128→160)
workers-protocol-local 144×110 → 162×110 (cell w 140→160)
workers-lesson-runtime 200×80 → 300×80
bound-unbound 272×56 → 296×56 (cell w 130→152)
args-kwargs 240×68 → 280×68 (labels and dashed lines shifted)
tuple-frozen 220×48 → 280×48 (ghost cell shifted)
gil-lanes 244×100 → 300×100 (lanes start at x=54 to make room
for left-anchored "thread A/B" tags)
match-dispatch-ladder 220×130 → 260×130
Refactor phase: both contracts pass as part of the standard test
suite (41 tests total, up from 39). New figures are checked
automatically because both tests iterate FIGURES.
What we missed before
My earlier audit used (\w+) regex which doesn't match font-size,
so every text element was read as the default 10px instead of its
actual size (tags are 8px). The corrected audit reads
([\w-]+)="..." to capture hyphenated attributes. Also added text
width estimation per font family — clipping checks now consider
horizontal extent, not just vertical baseline position.
Another pass over the figure library found three more bug classes the
geometry contracts didn't yet cover. Each becomes a new contract; the
existing fixes for the canonical bugs stay in place.
Contract 3 — no two text elements overlap (FigureTextCollisionContract).
Catches narrow object_boxes whose tag and value compete for
horizontal space. itertools-chain failed: "ITER A" (sans, w=30) and
"1 · 2" (mono, w=31) couldn't both fit inside a 70px box with the
tag-inside layout. Fix: revert that figure to tag_position="above"
(the default) with iter A/B spaced 38px apart, canvas h 64→82.
Contracts 4a–d — FIGURES, ATTACHMENTS, SCORES stay in sync
(FigureRegistrationContract):
* every attached slug is scored,
* every scored slug is attached,
* every attachment points to a figure that exists in FIGURES,
* no FIGURES entry is orphan (a figure counts as used if it appears
in ATTACHMENTS or anywhere in scripts/build_prototypes.py — the
latter catches journey-section figures and banner-layout
prototypes that share src/marginalia.py paint code).
Contracts 5a–c — grammar conformance (FigureGrammarContract):
* every fill/stroke color is INK, INK_SOFT, EMPHASIS, SOFT_FILL,
or "none",
* every font-family is FONT_SERIF, FONT_MONO, or FONT_SANS,
* every stroke-width is W_HAIRLINE (0.6), W_STROKE (1.0),
W_EMPHASIS (1.4), or W_GHOST (0.5).
All 109 figures already conform; the contract locks the system
against future drift.
Suite is now 49 tests (was 41). Geometry, palette, fonts, stroke
weights, and registration consistency are all asserted on every
test run.
Continuing the audit pass, three more invariants currently hold but
aren't asserted. Lock them in so a future change can't silently break
the registry without CI catching it.
Contract 6 — anchor coverage (FigureAnchorContract): every cell-N
attachment anchor in ATTACHMENTS resolves to a real cell in the
example's walkthrough. Catches typos and stale attachments left
behind after a markdown example loses a cell.
Contract 7 — score validity (FigureScoreContract): every SCORES entry
is a (number-in-[0,10], non-empty commentary) tuple. Locks the
rubric range and catches type drift if the dict shape changes.
Contract 8 — banner-fit (FigureSizeContract): every figure's
intrinsic width (Canvas.w + 2 * PAD_X) fits the 440px ceiling that
.cell-banner--1 enforces in public/site.css. A figure that exceeds
the ceiling renders scaled-down on every page, magnifying or
shrinking text in ways the paint code didn't plan for.
All three pass on the current codebase; they exist to prevent
regressions, not to fix anything today. Suite is now 52 tests.
Sweeps that found nothing actionable (no contract added)
- prose duplication: 3 hits were diagrammatic labels (`__getattr__`,
`yield from inner`, `execution continues`) that naturally appear
in the caption too; not the prose-in-SVG anti-pattern the v2
rubric bans.
- text crossing lines: 8 hits were intentional (dashed strikes
through `.append`, signature dividers, struck-through erased
names, type-alias old name).
- text overlapping circles: 10 hits were node labels (the `?` in
branch-fork, `in`/`out` in context-bowtie, operators in
expression-tree) — labels belong inside their circles.
- placeholder captions: 13 false positives — all "placeholders"
were dunder names (__init__, __dict__, __cause__, etc.).
The journey-figures-gestalt prototype showed several figures with
multiple orange (EMPHASIS-coloured) marks competing for attention,
violating docs/example-figure-rubric.md criterion 7 ("at most one
accent mark per figure"). Seven figures fired the new contract;
each lost the secondary accent so the surviving one carries the
prose's named element.
iterator-unroll 4 carets (one per row) → 3 ink carets + 1 orange
on the last row, where the strip's prose names
the "last" next() call.
loop-repetition ink caret + orange back-arrow (the loop's shape
is the back-arrow, not the caret).
generic-preservation in-arrow → ink; out-arrow stays orange (the
preserved-T-emerges-from-fn is the lesson).
paramspec-preserve same pattern (in-arrow → ink).
early-exit dot → ink; break-arrow stays orange.
exception-group-peel two matched dots → ink; except*-arrow stays
orange. The peel action is the lesson.
match-dispatch-ladder match-dot → ink; dispatch-arrow stays orange.
Grammar tweak: caret() now accepts emphasis=True (default) so the
small-multiples case (iterator-unroll) can opt the non-live carets
into ink without bespoke SVG.
Contract 9 (FigureEmphasisScarcityContract) locks this in by
counting orange arrowheads, carets, dots, and box borders across
every figure; new designs that ship two oranges fail CI.
The geometry contracts only iterated src/marginalia.py FIGURES (109
production paint functions). The 70 gestalt thumbnail functions in
scripts/build_marginalia.py — which render on the marginalia-gestalt
review page — were unaudited. Six gestalt cards had visible bugs the
production contracts would have caught.
Fixes
equality-and-identity LIST tag sat 0.6px above its box top, visible
as the tag touching the box stroke. Grammar
default tag offset bumped y-3 → y-5 so every
tag-above-box placement gets 2.6px clearance.
(Affects every production figure using
object_box(tag_position="above") too.)
break-and-continue "continue" label collided with "LOOP BODY"
tag; "break" label crossed the right edge of
the loop-body frame; both arrows orange.
Repositioned both labels inside the frame,
demoted the continue arrow to ink.
dataclasses "__init__(name, age, tags)" was 155px wide
but the receiving box was 124px and the card
was 320px. Bumped box w 124→170, card
width 320→360.
context-managers Arrow tip at x=266 sat 2px from the "out"
circle's left edge — visually touching.
Pulled tip back to x=262 / x=264 on both
arrows.
mutability "id 0x…a0" label of state 0 overlapped
"MUTATE · SAME ID" tag of state 1 because
state spacing was 60px but the content
needed ~62. Bumped spacing 60→68, card
height 200→224.
for-loops 4 orange carets. Same fix as production
iterator-unroll: only the "last" caret stays
orange, the other three paint in ink.
iterators 2 orange connect() arrows. Demoted first to
ink (the "stop" arrow stays as the climax).
match-statements Orange dot + orange arrow. Demoted dot.
exception-groups Two orange matched dots. Demoted both,
promoted the except*-arrow to orange so the
peel action carries the single accent.
Contract extension: tests now iterate ALL_FIGURES = FIGURES ∪
gestalt EXAMPLES, so the eight geometry/grammar/size/emphasis
contracts catch regressions in both registries. The registration
contracts (anchor coverage, score validity) stay scoped to
production where those concepts apply.
Suite size unchanged (53 tests), but each iterating test now
covers 179 paint functions instead of 109.
The gestalt review page rendered its own paint code (70 e_* + 6 j_* functions in scripts/build_marginalia.py) that drifted from the production figures in src/marginalia.py FIGURES. Reviewers saw a different picture than readers; this is exactly how the user noticed overlapping items on the gestalt that didn't ship on the example pages. Rebuild marginalia-gestalt.html as a thin view over production: scripts/build_marginalia.py shrinks from 1018 → 156 lines. Every gestalt card now pulls its paint function, intrinsic width, and intrinsic height directly from FIGURES[ATTACHMENTS[slug][0][1]]; its score and commentary from SCORES[slug]. No bespoke paint code lives in the gestalt build script. Cards iterate every example in the manifest order (109 cards), skipping any slug without an attachment. The historical "operators-and-literals" gestalt-only slug (since split into "operators" + "literals" on main) drops out naturally. The journey-overview thumbnails section is removed — those j_* paint functions had no production equivalent. Per-section journey figures are reviewed on journey-figures-gestalt.html, which already uses production paint code. Tests simplify: ALL_FIGURES = FIGURES (the gestalt has no separate registry to audit). The 8 iterating geometry/grammar/size/emphasis contracts run once over the 109 production figures instead of duplicating work across two registries. 53 tests still pass. Drift between "what readers see on /examples/<slug>" and "what reviewers see on the gestalt" is now structurally impossible: there is only one paint registry.
User-flagged: the dashed lines on positional-only-separator, args-kwargs, and context-bowtie didn't line up with the elements they were marking. positional-only-separator Dashed line was at x=82; the '/' character in the mono signature sits at x≈75 (index 12 × ~6px advance). Moved the line to x=75 and shifted both labels left so they bracket the new line position symmetrically without overlapping it. args-kwargs Two dashed lines were at x=80 and x=152 — neither under their parameter. *args's center is at x≈68, **kwargs's at x≈122. Moved the lines to those positions. Labels were "extra positionals → tuple" and "extra keywords → dict" at x=80 / x=210; the long form couldn't fit symmetrically under both dashed lines. Shortened to "→ tuple" / "→ dict" (the verbose copy belonged in the caption, not the figure), placed exactly under each line. context-bowtie Dashed exit path ended at (210, 48) — INSIDE the "out" circle at cx=220, r=14 (left edge x=206). Recomputed the tangent: body's bottom-mid (122, 60) → circle center (220, 48) of radius 14 meets the circle at ≈(206, 50). Endpoint moved there so the line lands on the circle's left edge instead of through the "out" glyph.
The ghost lines stopped 1.5px short of the leaf dots: root edges started at y=18 but the root dot's bottom edge sat at y=16.5; leaf edges ended at y=40 but the leaf dot tops sat at y=41.5. The gaps were small but visible — the tree looked disconnected. Extended every ghost line to land on the dot CENTER (root at y=14, leaves at y=44). The dots draw on top of the line endpoints so the visual termination is the dot's circumference — no gap, no overshoot.
Probed across the figure library for cross-figure drift:
a. SVG byte-identical outputs: 0
b. Same paint function under multiple registry keys: 0
c. Same caption attached to multiple slugs: 1
d. Same score commentary on multiple slugs: 0
e. Paint functions reused at inconsistent canvas dims: 0
f. Slug↔figure-name keyword mismatches: 23 (all by-design;
figure names describe mechanism, slug names describe concept)
g. 13 structural twin groups (figures with identical primitive
multisets) — these are intentional consistency across pairs
like class-triangle/metaclass-triangle and
kw-only-separator/positional-only-separator.
Two real bugs surfaced; both fixed.
1. kw-only-separator and positional-only-separator are structural
twins teaching the '*' and '/' parameter separators. Only
positional-only got the recent mono-character-alignment fix
(dashed line at x=75 to land under the separator char at index
12). kw-only still had the old miscalibrated x=82 and the
wider, asymmetric labels. Harmonised: identical x coordinates
so the two figures sit side-by-side as a true pair.
2. iter-protocol attaches to four slugs (iterators,
iterator-vs-iterable, iterating-over-iterables, container-
protocols). Two of those (iterators and iterating-over-
iterables) shared verbatim the same caption. Reused figures are
fine; reused captions duplicate the lesson voice. Rewrote the
iterating-over-iterables caption to focus on `for`
desugaring rather than restating the iter()/next() shape.
Contract 5b (FigureCaptionContract) locks this in: no caption may
appear on more than one slug. 54 tests, all green.
Two docs updates capture what shipped over the last set of commits.
docs/example-figure-rubric.md — Release Gates section expanded with
the contract-backed hard gates that didn't exist (or were softer)
in v2:
* No clipping (Contract 1) — text width counts, not just baseline.
* No element collision (Contract 2) — text in a rect must be
fully contained by that rect, not partly overlapping the box
above it.
* No text-text overlap (Contract 3) — narrow object_boxes whose
tag+value compete for horizontal space are caught.
* Caption uniqueness across slugs (Contract 5b) — reused figures
need bespoke captions per attachment.
* Palette, font, and stroke-weight discipline (Contract 5a-c) —
locked at the grammar; CI catches drift.
* Emphasis scarcity, ENFORCED (Contract 9) — was soft v1, now
hard. At most one accent per figure.
* Banner-fit (Contract 8) — every figure's intrinsic width fits
the 440px cell-banner--1 ceiling.
* Twin consistency — when two figures depict parallel concepts,
their coordinates must match coordinate-for-coordinate where
the concepts coincide. A single-pixel drift in one breaks the
visual rhyme.
* Geometric termination — lines must land at element edges, not
1-2px short or inside the glyph.
* Mono character alignment — vertical dividers marking positions
in mono text must use the font's actual advance (~6px per char
at fs=10), not eyeballed pixels.
* Gestalt = production — review pages render the same paint code
as production attachments; parallel paint code drifts and
hides bugs.
docs/lessons-learned.md — 10 new lessons added to the
"Visualisations and marginalia" section, each captured from a real
bug class we shipped a contract for:
- audits without contracts rot
- clipping ≠ collision; padding fixes one not the other
- heuristic audits over-flag; trust the design, not the regex
- structural twins must share coordinates exactly
- reused figure → bespoke caption per slug
- one paint registry, not two
- tag-above vs tag-inside is a layout decision driven by stacking
- mono character alignment uses the font's advance
- lines must terminate AT elements
- journey pages don't yet render section figures inline (open
design question)
Tests: 54 still green; no production code changed.
Production journey pages now show each section's conceptual figure
between the section heading and the example list, matching how
example pages show cell figures.
src/marginalia.py: SECTION_FIGURES dict (24 entries keyed by
section title) and render_for_section(title) helper. The dict
is the single source of truth — moved out of
scripts/build_prototypes.py, which now imports it.
src/app.py: render_journey_page injects render_for_section(...)
between the section's meta line and its example list.
public/site.css: .journey-section-figure { max-width: 440px; }
matches .cell-banner--1's ceiling, with the same caption
typography as example banners.
scripts/build_prototypes.py: imports SECTION_FIGURES from
src/marginalia.py (renamed locally to JOURNEY_SECTION_FIGURES
for the existing prototype builders); 109 lines of duplicate
dict removed.
tests/test_marginalia_geometry.py: SectionFigureContract
(Contract 10) asserts every JOURNEYS section has a figure,
every SECTION_FIGURES entry maps to a real FIGURES paint
function, and every section caption is unique. The orphan-
figure contract now recognises SECTION_FIGURES as a usage
source.
Six journey pages × ~4 sections each → 24 figures now render on
production journey pages that previously had none. The contracts
keep the journey rubric (docs/journey-visualisation-rubric.md) and
the production rendering in lockstep. Suite is 57 tests, all green.
Per impeccable's adapt + layout rubrics: figures should fill the
column with fluid clamp() sizing, not anchor at intrinsic CSS pixels
on desktops where 800+px of horizontal space goes unused. Before
this change, a 176px-wide figure stayed at 176px even on a 992px
content column (18% utilisation).
Two layers of change.
Render-time scale (src/marginalia_grammar.py)
Canvas.INTRINSIC_SCALE = 1.6: to_svg() now emits
width="round(viewBox * 1.6)" and height likewise. The viewBox
stays intact so paint coordinates, geometry contracts, and
collision math don't change — only the rendered CSS-pixel size
grows. Text inside scales with the viewBox transform so a 10px
font renders at 16 CSS px on desktop, dropping back to ~9-10px
when the container shrinks to mobile widths.
Viewport-aware ceilings (public/site.css)
Three banner containers gain fluid widths via clamp():
.cell-banner figure clamp(240px, 45vw, 480px)
.cell-banner--1 figure clamp(280px, 65vw, 640px)
.journey-section-figure clamp(280px, 70vw, 640px)
The middle term (vw-based) means figures grow with the viewport
until the ceiling. The min term guarantees a readable floor on
ultra-narrow phones. The ceiling matches the 640px contract bound.
What each viewport now gets
Mobile portrait (320px): figure caps at 280px, ~96% of column
Mobile landscape (~720): figure ~455px (65vw)
Tablet (768px): figure ~499px
Laptop (1024px): figure 640px (clamp ceiling)
Wide desktop (1440px+): figure 640px
Multi-figure banners (cell-banner without --1) cap at 480px so two
figures fit side-by-side in a 960px gap without dominating either
side.
Contract 8 updated: rendered width = 1.6 × (Canvas.w + 16) must fit
the 640px ceiling. Largest figure today (dataclass-fields, intrinsic
canvas 312) renders at 1.6 × 328 = 525px — comfortably under. All
57 tests pass.
The previous "emit explicit width/height; never use width: 100%"
lesson still holds — explicit width/height stops browsers from
ballooning small viewBoxes uncontrollably. The width attribute now
just sits at 1.6× intrinsic instead of 1.0×, so the figure has room
to grow before CSS max-width clamps it.
User-reported: /examples/async-iteration-and-context showed the swimlane's "LOOP" and "CORO" labels clipped to "_OOP" and ":ORO" on the left edge of the figure. Root cause: the lane() primitive places tag-style labels at x0 - 6 with anchor="end". With x0=20 in async-swimlane, the label right-edge sits at viewBox x=14 and the text extends leftward by ~25 viewBox units (4 uppercase letters × ~6.25 wide at fs=8 with tracking 0.5) → starts at ~x=-11. The old PAD_X=8 set the viewBox left edge at x=-8, clipping the leftmost ~3 viewBox units of every lane label. The 1.6× render scale doubled the visible clip. The contract didn't catch this because my CHAR_WIDTH heuristic for sans (0.58) under-estimates uppercase. tag() upper-cases its input, so any tag with uppercase tracking is actually wider than the audit estimated. Bumping CHAR_WIDTH["sans"] to 0.65 brings the estimate in line with what Source Sans Pro renders at fs=8 with tracking 0.5; future lane-label drift will fire the contract. Two changes: src/marginalia_grammar.py: PAD_X bumped 8 → 14, matching PAD_TOP and PAD_BOTTOM. Every figure's viewBox now extends to x=-14, so labels and lines that draw into negative x have ~6 more units of margin. Net effect on rendered size: each figure grows by 12 × 1.6 = ~19 CSS px wider (gain absorbed by the 640px banner ceiling). tests/test_marginalia_geometry.py: PAD_X constant updated to 14 to match; CHAR_WIDTH["sans"] bumped to 0.65 to cover uppercase tags. Contracts 1, 2, 3, 8 all re-evaluate against the new bounds; 57 tests still green. Other lane-using figures (gil-lanes x0=54, exception-lanes x0=40 via lanes()) had enough room already; only async-swimlane was under-padded.
Honest follow-up: the previous PAD_X fix bundled the test heuristic change with the production fix. A proper red-green-refactor would have caught two extra bugs that bbc52a7 missed. RED: with the corrected font_class() (sans-serif fonts no longer misclassified as serif via the "sans-serif" substring containing "serif") and a case-aware CHAR_WIDTH (uppercase tags at 0.65 of font-size, mixed-case labels at 0.55), Contract 1 fires on: - async-swimlane "LOOP" / "CORO" clipped by ~1 px - workers-protocol-local "RESPONSE SHAPE ·…" clipped by 12 px - sort-stability "STABLE SORT BY KEY" clipped on top - kw-only-separator "positional or kw" clipped by 5 px - positional-only-separator "positional only" clipped by 3 px - sentinel-iteration "sentinel · stop" clipped by 2 px Five of these were silently shipping before — bbc52a7 only caught one (async-swimlane) by inspection. The contract was missing them because of a substring bug in font_class. GREEN: bump PAD_X 8→14 in Canvas.to_svg (gives every figure 6 more units of horizontal margin). That alone fixes async-swimlane, sort-stability, kw-only-separator, positional-only-separator. Two figures still clip with content extending past the right edge — fix per-figure: - workers-protocol-local: canvas 162→200 so the long "response shape · asserted locally" tag fits. - sentinel-iteration: canvas 300→320 so the "sentinel · stop" label past the rightmost cell fits. GREEN 2: Contract 2 (text-vs-rect collision) now flags union-types because "x: int|str|None" tag extends to x=85.5 and the int cell starts at x=82 — a 3.5 px overlap that wasn't visible to the audit when sans-serif was misclassified as serif (narrower char width). Fix: shift the int/str/None cells 10 px right (82→92), update the arrow endpoints (80→90), bump canvas width 156→166. REFACTOR: case-aware CHAR_WIDTH in tests/test_marginalia_geometry.py distinguishes uppercase content (sans_upper at 0.65 × fs) from mixed-case (sans at 0.55 × fs). Tag text and label text are measured at their true widths, so future drift in either category will fire the contract. 57 tests, all green.
User-flagged: /examples/networking attached protocol-layers (an HTTP/TCP/IP stack figure) to an example whose code uses socket.socketpair() to demonstrate the str ↔ bytes boundary. The figure had no relationship to the lesson, and the two code chunks on the page shared generic prose instead of explaining what each chunk actually shows. Figure New socket-byte-boundary figure (src/marginalia.py): str "ping" → encode → bytes b'ping' → [dashed socket pipe, labelled "socket"] → bytes b'ping' → decode → str "ping". The figure depicts what the example's code does in one row: the encode/decode at each end of the socket carrying bytes. Canvas 364×46 → renders at 627 CSS px on desktop, comfortably under the 640 banner ceiling. Removed protocol_layers (now orphan after re-attaching). The orphan-figure contract catches it automatically. Cell prose (src/example_sources/networking.md) Unsupported chunk now explains what its three-line code does — socketpair returns two endpoints, sendall encodes, recv reads bytes — with the Dynamic Workers caveat moved to a closing parenthetical instead of leading the prose. Cell chunk now explains what the full version adds over the unsupported fragment: try/finally for cleanup, second print that decodes the received bytes back to a str. TDD: the new figure tripped Contract 2 (collision) on the first draft — the "SOCKET" tag overlapped a bytes box. Moved the tag above the dashed pipe instead of below. The attachment also tripped Contract 6 (anchor coverage) — I attached to cell-1 but the example has only one runnable :::cell. Fixed to cell-0. Both caught in CI, fixed before merge. 57 tests pass; SCORES commentary updated.
The networking fix exposed a pattern: any :::unsupported block where
the prose leads with "Dynamic Workers do not provide X" copies the
runtime caveat instead of explaining the code. An audit across all
four examples that contain :::unsupported blocks (networking,
subprocesses, threads-and-processes, virtual-environments) found
the same prose shape in the three I hadn't yet fixed.
TDD:
RED — tests/test_example_content.py adds Contract 11
(UnsupportedCellProseContract). The heuristic: each unsupported
cell's prose must reference at least 2 code identifiers from
its own code block (variable names, function calls, method
names). Two is the floor that proves the prose discusses this
specific code rather than a generic Workers note. Pre-fix, the
contract flagged:
- virtual-environments 1 ident referenced ("venv")
- threads-and-processes 0 idents referenced
- subprocesses 2 idents (passed bar, but still
weak — fixed for consistency)
GREEN — rewrite all three prose blocks so each explains what
ITS specific code does:
subprocesses "subprocess.run spawns a child Python
interpreter, captures stdout/stderr
(capture_output=True), decodes as text
(text=True), and raises if it exits
non-zero (check=True). The returned
result holds the captured streams and
exit code as portable evidence the
child ran. (Runtime caveat moved to
closing parenthetical.)"
threads-and-processes "ThreadPoolExecutor runs square across
two worker threads sharing the same
interpreter (and the GIL);
ProcessPoolExecutor runs pow across two
child processes with isolated memory.
Each pool.map returns an iterator over
results in input order, and the
surrounding with block joins the workers
when the body exits."
virtual-environments "venv.EnvBuilder configures the
description of a new environment, then
create('.venv') materialises it on disk
as a directory containing its own
interpreter and site-packages.
with_pip=False skips bootstrapping pip."
REFACTOR — Contract 11 now passes; 58 tests total.
The second audit dimension (figure-caption vs example-title keyword
overlap) found 24 suspect attachments. Spot-checking confirms all
24 are false positives: captions use different word forms than
titles (e.g., "mutable" vs "mutability", "comprehension" vs
"comprehensions") but are conceptually aligned. No contract added —
heuristic too noisy without manual review.
User-flagged: /examples/testing showed a horizontal scrollbar inside
a walkthrough code cell. Root cause: white-space: pre-wrap wraps at
spaces, but the line
suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)
contains a 58-char run after the first space that pre-wrap can't
break. Same pattern in two other examples.
TDD:
RED — Contract 12 in tests/test_example_content.py
(CellCodeWrappingContract) flags any \\S-run > 50 chars in
walkthrough cell code. Pre-fix output:
logging cell 0 line 6: 68-char run
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
testing cell 2 line 2: 58-char run
unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)
datetime cell 2 line 1: 51-char run
datetime.fromisoformat("2026-05-04T12:30:00+00:00")
GREEN — introduce intermediate names so each line wraps cleanly:
testing.md
loader = unittest.defaultTestLoader
suite = loader.loadTestsFromTestCase(AddTests)
...
runner = unittest.TextTestRunner(stream=stream, verbosity=0)
result = runner.run(suite)
logging.md
formatter = logging.Formatter("%(levelname)s:%(message)s")
handler.setFormatter(formatter)
datetime.md
iso_text = "2026-05-04T12:30:00+00:00"
parsed = datetime.fromisoformat(iso_text)
Each :::program block and the matching :::cell block updated
together so the runnable code and the walkthrough stay in sync.
CSS safety net (public/site.css): added `overflow-wrap: anywhere`
to `.cell-source pre, .cell-source .shiki-block, .cell-output pre`.
A future long URL, long string literal, or long encoded blob will
break mid-run instead of pushing a horizontal scrollbar onto the
page. Code that's authored to wrap cleanly (the rule above keeps it
authored well) will still wrap at spaces because pre-wrap takes
precedence over overflow-wrap when both apply.
59 tests pass; SEO/cache lint clears 110 pages.
The first cell compared "hello" (pure ASCII, 1 byte/char) with "สวัสดี" (Thai, 3 bytes/char) but skipped the middle ground. Adding "café" between them gives a three-row reading: English hello 5 code points 5 bytes (ASCII) French café 4 code points 5 bytes (é = 2 UTF-8 bytes) Thai สวัสดี 6 code points 18 bytes (each char = 3 bytes) The example already used "café" in the third cell to demonstrate strip/upper/encode; lifting it into the opening comparison links the three cells around one progression of byte costs and removes the jump from "ASCII" to "completely non-Latin" without a stepping stone. Both the :::program block (runnable code) and the matching :::cell (walkthrough) updated together; the cell's expected output now shows the three-row table; example_loader verifies the cell output matches what the program prints.
After fixing strings to include 'café' in the byte-count comparison
loop, audited 109 examples for similar gaps.
Two probes:
- Find cells with explicit comparison loops `for ... in [(...)]:`
iterating over 2-3 items. Found 5; only strings was a topic
spectrum (English / French / Thai). The other 4 (for-loops,
collections-module, async-await, async-iteration-and-context)
iterate over example names or async features — not category
spectrums needing a stepping stone.
- Find examples whose summary enumerates 3+ named items but
whose first cell mentions only 1-2 of them. Heuristic was
noisy: 60 hits, all false positives on closer inspection.
The truly-three-category examples (numbers, comprehensions,
runtime-type-checks, classmethods-and-staticmethods,
structured-data-shapes, truth-and-size) spread the categories
across separate cells instead of one comparison loop. That's
a valid pedagogy — one cell per category, building up.
Conclusion: the strings fix was the only actionable case. Recorded
the narrow lesson in docs/lessons-learned.md so future
comparison-loop cells start with the spectrum filled out.
No code or contracts changed. The heuristic for "missing topic
category" is too topic-specific to mechanise as a contract; this
stays guidance.
After commit 64360a4 wired section figures into render_journey_page, production /journeys/<slug> pages render the same content as the journey-* prototype HTMLs that build_prototypes.py was emitting. Reviewers should look at the production pages, not the duplicate prototypes. Cut: - public/prototyping/journey-runtime.html - public/prototyping/journey-control-flow.html - public/prototyping/journey-iteration.html - public/prototyping/journey-shapes.html - public/prototyping/journey-interfaces.html - public/prototyping/journey-types.html - public/prototyping/journey-reliability.html - public/prototyping/journey-workers.html - build_prototypes.py: build_journey() function (~50 lines), JOURNEY_STYLE constant, the loop in main() that called it, and the 8 entries in the prototyping index list. Kept (intentional): - journey-figures-gestalt.html — grid view of all 24 section figures for cross-journey design review; production renders one figure per section per page, the gestalt shows them together. - layout-banner-* prototypes — reference designs for the banner grammar (single, pair, trio); useful when adding new banner layouts. - marginalia-gestalt.html, production-figures-gestalt.html — review pages over the figure registry. docs/lessons-learned.md updated: the bullet claiming "journey pages don't yet render section figures inline" was outdated since commit 64360a4. Replaced with a statement of the shipped behavior plus the contract that enforces it. 59 tests pass; prototype index regenerates without the 8 deleted entries.
Per the audit's "next cut" list. Each was a one-time artifact whose
moment has passed:
docs/markdown-cell-migration-investigation.md
One-time investigation document for the Markdown migration that
has long since shipped. Plus the matching
test_investigation_documents_program_and_cell_solution test in
tests/test_markdown_migration_prereqs.py (a static-text assertion
that the investigation doc still says certain phrases — useless
once the doc is removed).
docs/shallow-example-audit.md
One-time audit of which examples were shallow. The findings
have been folded into example sources and rubrics.
docs/example-quality-new-vs-old.{md,csv,svg}
One-time before/after quality comparison from an earlier rubric
expansion. The improvement arc lives in commit history.
docs/example-graph-score-impact.md
Cut transitively — referenced score_examples.py and was itself a
one-time impact report on the see_also graph rollout.
public/prototyping/operators-polish-comparison.html
One-time before/after for the tree-edge alignment fix. Pattern
is documented in the figure rubric.
scripts/score_examples.py
Heuristic scorer superseded by the SCORES dict in
src/marginalia.py, where every example carries an explicit v2
rubric score with commentary.
References cleaned up:
- tests/test_markdown_migration_prereqs.py loses one test method
and the INVESTIGATION constant.
- scripts/build_prototypes.py loses the operators-polish-comparison
entry from the prototyping index.
58 tests pass (was 59; one test removed). SEO/cache lint clears 110
pages. Index.html regenerated without the stale entry.
Net cuts across the two cleanup commits: 8 journey-prototype HTMLs,
1 polish-comparison HTML, 5 docs (4 standalone + 1 transitive), 1
script, 1 test method, ~67 lines of build_journey() code, the
JOURNEY_STYLE constant, 8 entries from the prototyping index list,
1 outdated lesson bullet.
5dfb964 to
681bdaa
Compare
After editing example markdown sources (strings, testing, logging,
datetime, networking, subprocesses, threads-and-processes,
virtual-environments) the embedded sources data and the HTML cache
fingerprint had drifted. `make check-generated` caught this in CI.
src/example_sources_data.py — regenerated by
scripts/embed_example_sources.py
src/asset_manifest.py — HTML_CACHE_VERSION updated by
scripts/fingerprint_assets.py (depends on the embedded data)
scripts/build_marginalia.py — Canvas was imported but unused
after the gestalt-uses-production refactor; ruff caught it
58 tests pass; lint clean; quality-checks clean.
scripts/check_example_migration_parity.py compares the live examples against tests/fixtures/golden_examples.py — the frozen snapshot from the Markdown migration. After the recent example edits (strings/café, testing/logging/datetime long-line breaks, networking figure swap + prose rewrite, three unsupported-cell prose rewrites), the golden was out of sync with the sources. Regenerated the fixture from the current examples. Parity check now reports "100% golden parity"; 58 tests, lint, format-check all clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Tufte-style figure system for /examples/* and /journeys/*, plus the contracts that keep it from drifting.
What ships
Figure system, 109 figures
src/marginalia_grammar.py(locked palette, fonts, stroke weights, spacing, primitives likecell/object_box/node/connect/lane) is the only way figures get drawn. No bespoke SVG, no per-figure colour or font drift.src/marginalia.py FIGURES, attached viaATTACHMENTS(one banner per example slug) and scored inSCORESagainstdocs/example-figure-rubric.mdv2.public/site.css(.cell-banner,.cell-banner--1) clamps figure widths fluidly.clamp(280px, 65vw, 640px)ceilings so they shrink cleanly to mobile.Journey-section figures inline on /journeys/<slug>
SECTION_FIGURES(24 entries keyed by section title) lives insrc/marginalia.py.render_for_section(title)injects the figure between each section's meta and its example list. Same paint code as the review pages — no parallel registries.Gestalt review pages under /prototyping/*
marginalia-gestalt.html— every example's figure in a card grid, drawn from production paint code (not a duplicatee_*registry).journey-figures-gestalt.html— all 24 section figures grouped by journey for cross-journey rubric review.production-figures-gestalt.html— every figure inFIGURESwith attachment metadata.layout-banner-*.html— reference designs for single / pair / trio banner layouts.Geometry + content contracts in CI (58 tests)
tests/test_marginalia_geometry.pyandtests/test_example_content.pyassert, for every figure:<rect>,<text>,<line>,<circle>,<path>stays inside the padded viewBox (text width measured per font family).FIGURES,ATTACHMENTS,SCORES,SECTION_FIGURESall in sync; no orphan paint functions; every attachment anchor resolves to a real cell.INK/INK_SOFT/EMPHASIS/SOFT_FILLcolours, only the three locked font families, only the four locked stroke weights.cell-Nattachment points to an existing cell.:::unsupportedblocks explain the code, not just the runtime caveat.New figures, new captions, new attachments — all 13 contracts run against every change.
Rubric and lessons
docs/example-figure-rubric.mdv2: hard release gates (clipping, collision, palette, fonts, stroke weights, emphasis, registration, captions, anchors, scores, banner-fit, twin consistency, mono character alignment, geometric termination, gestalt=production) all mapped to the automated contracts that enforce them.docs/lessons-learned.md— 25+ new lessons from the bugs this PR caught, including: audits-without-contracts rot; clipping ≠ collision; structural twins must share coordinates; one paint registry, not two; tag-above vs tag-inside is a stacking decision; mono character alignment uses the font's advance; lines terminate AT elements, not in gaps or interiors.Page-level layout improvements
--subtleCSS variable dropped.overflow-wrap: anywhereon cell code as a safety net for long URLs / blobs.Examples corrected
networking— replaced wrongprotocol-layersfigure (HTTP stack) withsocket-byte-boundary(the actual lesson). Both code chunks rewritten to explain their own code instead of leading with the runtime caveat.subprocesses,threads-and-processes,virtual-environments—:::unsupportedprose rewritten the same way.strings— added French"café"to the byte-count comparison loop so the first cell reads as 1-byte / 2-byte / 3-byte progression.testing,logging,datetime— broke unwrappable long lines so the code column stops showing a horizontal scrollbar.kw-only-separator↔positional-only-separator, etc.) harmonised to identical coordinates.exception-group-peel,context-bowtie,positional-only-separator,args-kwargs,kw-only-separator).Repo trim
Two cleanup passes cut ~570 KB of one-time artifacts: 8 stale journey-prototype HTMLs (now superseded by production journey pages), 5 docs from one-time investigations/audits, the
score_examples.pyheuristic scorer (replaced bySCORES), an outdated lesson bullet, and ~75 lines of dead build code.Verification
Preview deploys to
viz-pythonbyexample.adewale-883.workers.devon every push.https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE