Skip to content

Commit b989bfa

Browse files
committed
Sizing + prose-duplication root-cause rules; ship 12 example figures
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
1 parent 43b5056 commit b989bfa

12 files changed

Lines changed: 727 additions & 5 deletions

docs/example-figure-rubric.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Example figure rubric
2+
3+
Parallel to `docs/journey-visualisation-rubric.md`, but for the figures
4+
that attach to **example pages** (literate-program lessons), not journey
5+
sections. The journey rubric scores the figure beside a section heading;
6+
this one scores the figure that sits between prose and code inside a
7+
single cell of an example walkthrough.
8+
9+
The two rubrics share craft criteria (palette, primitives, emphasis
10+
scarcity) and diverge on content criteria, because the audience and
11+
task differ. A journey-section figure depicts the *conceptual shift*
12+
unifying multiple lessons; an example figure depicts the *single move*
13+
the surrounding cell discusses.
14+
15+
Score each example figure on a 10-point scale.
16+
17+
## Content (5.5)
18+
19+
1. **Cell fidelity (0-1.5)** — the figure depicts the move the cell's
20+
prose discusses, not the example's title. If the example is
21+
"Mutability" but cell 1 is about immutable strings, a figure on
22+
cell 1 must depict immutability, not aliasing. Wrong cell, wrong
23+
figure.
24+
2. **Match the running variables (0-1.0)** — names, values, and shapes
25+
in the figure match the cell's source. If the cell uses `first` and
26+
`second` on a list, the figure says `first` and `second`. Generic
27+
placeholders (`a`, `b`, `xs`) are fine *only* when the cell itself
28+
is generic; specific names earn their place when the cell uses them.
29+
3. **One conceptual move (0-1.0)** — exactly one shift, before-state
30+
to after-state, or one mechanism. Squint test: a reader should
31+
identify the figure's single point in two seconds.
32+
4. **Mechanism over metaphor (0-1.0)** — the figure shows the actual
33+
machinery (the cell, the binding, the dispatch, the iterator),
34+
not a cartoon of it. Knuth's rule.
35+
5. **Caption asserts; figure depicts (0-1.0)**`figcaption` is a
36+
declarative sentence about what the figure shows. The SVG itself
37+
contains no prose duplicating the caption — only diagrammatic
38+
labels (`stdout`, `iter()`, panel tags, type signatures). See
39+
pipeline invariant 2 in the spec.
40+
41+
## Craft (3.0)
42+
43+
6. **Grammar conformance (0-1.0)** — composed exclusively from
44+
`Canvas` primitives in `src/marginalia_grammar.py`. No bespoke
45+
SVG, no new colours, no stroke weights outside the locked set.
46+
7. **Emphasis scarcity (0-1.0)** — at most one accent mark per
47+
figure. The accent goes on the single element the cell prose
48+
names (the live mutation, the captured cell, the dispatch arrow).
49+
Three accent marks competing for attention is no emphasis at all.
50+
8. **Restraint (0-1.0)** — no decoration that does not carry
51+
information. No drop shadows, gradients, ornamental rules,
52+
non-orthogonal tilts, or marks placed for "balance".
53+
54+
## Context (1.5)
55+
56+
9. **Cell-column fit (0-1.0)** — the figure's intrinsic width sits
57+
comfortably inside `.cell-figure`'s `max-width: 360px`. Wider
58+
intrinsic widths are clamped (good — figures shrink, never grow);
59+
much narrower widths leave whitespace on either side. Aim for an
60+
intrinsic viewBox between 200 and 360 px wide.
61+
10. **Pairs with the code, not the title (0-0.5)** — when the figure
62+
sits next to its source block (cell-figure layout), the eye reads
63+
prose → figure → source as one move. The figure should make the
64+
*source* easier to read, not stand alone as a generic
65+
illustration of the example title.
66+
67+
## Topic gates (cell-shape specific)
68+
69+
- **Binding cells** (assignments, `=`) — show the name-arrow with the
70+
type tag and the resulting value. The canonical Python picture.
71+
- **Mutation cells** — show before-state and after-state with the
72+
same object identity, OR rebinding with a new identity. The
73+
difference is the lesson.
74+
- **Iteration cells** — show the iterator advance: a caret moving,
75+
or `iter()`+`next()` producing values one at a time.
76+
- **Function-definition cells** — show the signature with parameter
77+
separators (`/`, `*`) explicit when relevant, or the
78+
caller→body→return shape.
79+
- **Class cells** — show state and methods bundled, or the
80+
instance→class→type triangle, or MRO chain. Pick one, not all.
81+
- **Exception cells** — show the lanes (try/except/else/finally)
82+
with a single traced path, or the exception-cause arrow (`__cause__`
83+
vs `__context__`).
84+
- **Async cells** — show two parallel lanes (loop · coroutine) with
85+
await handoffs.
86+
87+
## Release gates outside the score
88+
89+
- **One figure per cell, at most.** Two figures on one cell signal
90+
the cell is doing two things; split the cell instead.
91+
- **figcaption present and declarative.** Captions in the form
92+
"Two names share one mutable list — appending through one name
93+
changes the object visible through both." Not "this shows X" or
94+
"see how Y".
95+
- **figcaption agrees with the cell's prose.** The cell's prose
96+
paragraph in the markdown and the figure's figcaption assert the
97+
same thing in different words. If they disagree, one is wrong.
98+
- **Palette discipline.** Only `INK`, `INK_SOFT`, `EMPHASIS`,
99+
`SOFT_FILL`. No literal hex codes, no `rgba(0,0,0,…)` neutrals.
100+
- **Pipeline invariants** (see spec) hold: SVG renders at intrinsic
101+
size; SVG contains no prose duplicating the caption.
102+
103+
## Quality bands
104+
105+
- **9.0-10.0** — depicts the cell's move in two seconds; the figcaption
106+
could only describe this figure; reads pleasantly on return visits.
107+
- **8.0-8.9** — depicts the right move but uses generic placeholders
108+
where specific names would land harder, or the caption hedges, or
109+
one secondary mark steals attention from the primary one.
110+
- **7.0-7.9** — depicts the cell but loses something in scope: shows
111+
the example title rather than the specific cell's move; or topic
112+
gate not satisfied.
113+
- **below 7.0** — wrong cell, wrong shape, multiple primary ideas
114+
competing, or accent marks scattered rather than scarce. Redesign
115+
before promoting.
116+
117+
## Project gate
118+
119+
A cell figure may ship to production once it scores **≥ 8.5**. The
120+
example's figure average should exceed **8.7** so a multi-figure
121+
example reads as a coherent set rather than independently authored
122+
diagrams.
123+
124+
The score is a guide, not a substitute for reading the cell beside
125+
its surrounding prose.

docs/visual-explainer-spec.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,36 @@ Edit `ATTACHMENTS` in `src/marginalia.py`. Add a paint function (composed
207207
from grammar primitives) and register it in `FIGURES`. Append a tuple of
208208
`(anchor, figure_name, caption)` to `ATTACHMENTS[slug]`. Done.
209209
210+
## Pipeline invariants (root-cause rules)
211+
212+
These rules exist because we hit, and fixed, both failure modes
213+
explicitly. Re-introducing either is a defect.
214+
215+
1. **The SVG element renders at intrinsic CSS-pixel size.**
216+
`Canvas.to_svg()` emits `width="W"` and `height="H"` matching the
217+
`viewBox`. CSS that displays a figure must use `max-width: 100%`,
218+
never `width: 100%`. With `width: 100%` a small viewBox is stretched
219+
to fill the container, which doubles or triples the apparent text
220+
size inside; with `max-width: 100%` the figure renders at its
221+
designed size and only shrinks when the container is narrower.
222+
223+
2. **A figure's diagrammatic content does not duplicate its figcaption.**
224+
Where a figure is rendered with a `<figcaption>` (production cell
225+
pages, prototype journey pages, the journey-figures gestalt) the
226+
SVG must not contain an inline `<text>` that repeats the caption's
227+
sentence. Captions are the canonical prose; the SVG is diagrammatic.
228+
Functional labels inside the SVG (`stdout`, `iter()`, `next()`,
229+
`await`, panel tags like `before` / `after`, type-signature
230+
annotations like `x: int | str | None`) are diagrammatic — they
231+
name a part of the figure, not the figure as a whole. A full
232+
sentence describing the figure is prose and belongs in the
233+
figcaption.
234+
235+
The `marginalia-gestalt` review page is an exception: cards there
236+
have no figcaption, so inline prose can stand in as the only
237+
explanation. Figures destined for promotion to the production
238+
registry must drop their inline prose first.
239+
210240
## Files
211241
212242
- `src/marginalia_grammar.py` — palette, tokens, words, phrases, metrics.

public/prototyping/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<h1>Visual explainer prototypes</h1>
3535
<p class="meta">Real example pages with their attached figures, plus the design-review pages. The example pages all use the production layout: a cell with an attached figure stacks prose, figure, and code vertically; cells without figures keep today's prose|code grid.</p>
3636
</section>
37-
<ul class="prototype-list"><li><a class="text-link" href="/prototyping/marginalia-gestalt.html"><strong>Marginalia gestalt</strong></a><p class="meta">Every journey and example as a card, drawn from the shared grammar. Pure design review.</p></li><li><a class="text-link" href="/prototyping/journey-figures-gestalt.html"><strong>Journey-figures gestalt</strong></a><p class="meta">All 18 journey section figures on one page, grouped by journey, for uniform rubric review.</p></li><li><a class="text-link" href="/prototyping/operators-polish-comparison.html"><strong>Operators alignment polish</strong></a><p class="meta">Side-by-side before/after for the tree-edge alignment fix; demonstrates Canvas.connect().</p></li><li><a class="text-link" href="/prototyping/layout-banner-single.html"><strong>Layout · banner between cells</strong></a><p class="meta">The grammar: cells stay 2-column always; figures live in banner rows BETWEEN cells. Holds one figure here. The intended union of Tufte/Knuth/algebrica.</p></li><li><a class="text-link" href="/prototyping/layout-banner-pair.html"><strong>Layout · banner with small-multiples pair</strong></a><p class="meta">Same grammar with two figures in the banner — a Tufte small-multiple. The mutable list and the immutable tuple side by side, captioned, between the same pair of cells.</p></li><li><a class="text-link" href="/prototyping/layout-banner-trio.html"><strong>Layout · multiple banners across the walkthrough</strong></a><p class="meta">The grammar at scale: a single-figure banner before the walkthrough, a pair-banner between two cells, a single-figure summary after the last cell. Multiple diagrams; cells never displaced.</p></li><li><a class="text-link" href="/prototyping/journey-runtime.html"><strong>Journey · Runtime</strong></a><p class="meta">Programs run statements, names refer to objects, expressions become method calls.</p></li><li><a class="text-link" href="/prototyping/journey-control-flow.html"><strong>Journey · Control Flow</strong></a><p class="meta">Branches choose paths; the figure depicts a value flowing through a predicate to one of several branches.</p></li><li><a class="text-link" href="/prototyping/journey-iteration.html"><strong>Journey · Iteration</strong></a><p class="meta">Loops repeat; the protocol behind for is iter() then next() until exhausted.</p></li><li><a class="text-link" href="/prototyping/journey-shapes.html"><strong>Journey · Shapes</strong></a><p class="meta">Containers answer different questions; reshaping is the everyday move; text becomes structured data.</p></li><li><a class="text-link" href="/prototyping/journey-interfaces.html"><strong>Journey · Interfaces</strong></a><p class="meta">Functions are named behavior; functions are values; classes bundle state with behavior.</p></li><li><a class="text-link" href="/prototyping/journey-types.html"><strong>Journey · Types</strong></a><p class="meta">Annotations describe but don&#x27;t enforce; unions cover alternatives; generics preserve shape across calls.</p></li><li><a class="text-link" href="/prototyping/journey-reliability.html"><strong>Journey · Reliability</strong></a><p class="meta">Failure is explicit; resources have boundaries; concurrency outlives single expressions.</p></li><li><a class="text-link" href="/prototyping/journey-workers.html"><strong>Journey · Workers</strong></a><p class="meta">Workers-specific journey added on main; section figures pending design.</p></li></ul>
37+
<ul class="prototype-list"><li><a class="text-link" href="/prototyping/marginalia-gestalt.html"><strong>Marginalia gestalt</strong></a><p class="meta">Every journey and example as a card, drawn from the shared grammar. Pure design review.</p></li><li><a class="text-link" href="/prototyping/journey-figures-gestalt.html"><strong>Journey-figures gestalt</strong></a><p class="meta">All journey section figures on one page, grouped by journey, for uniform rubric review.</p></li><li><a class="text-link" href="/prototyping/production-figures-gestalt.html"><strong>Production figures gestalt</strong></a><p class="meta">Every figure currently registered in src/marginalia.py FIGURES, with a tag showing where it renders (example attachment, journey section, or unattached).</p></li><li><a class="text-link" href="/prototyping/operators-polish-comparison.html"><strong>Operators alignment polish</strong></a><p class="meta">Side-by-side before/after for the tree-edge alignment fix; demonstrates Canvas.connect().</p></li><li><a class="text-link" href="/prototyping/layout-banner-single.html"><strong>Layout · banner between cells</strong></a><p class="meta">The grammar: cells stay 2-column always; figures live in banner rows BETWEEN cells. Holds one figure here. The intended union of Tufte/Knuth/algebrica.</p></li><li><a class="text-link" href="/prototyping/layout-banner-pair.html"><strong>Layout · banner with small-multiples pair</strong></a><p class="meta">Same grammar with two figures in the banner — a Tufte small-multiple. The mutable list and the immutable tuple side by side, captioned, between the same pair of cells.</p></li><li><a class="text-link" href="/prototyping/layout-banner-trio.html"><strong>Layout · multiple banners across the walkthrough</strong></a><p class="meta">The grammar at scale: a single-figure banner before the walkthrough, a pair-banner between two cells, a single-figure summary after the last cell. Multiple diagrams; cells never displaced.</p></li><li><a class="text-link" href="/prototyping/journey-runtime.html"><strong>Journey · Runtime</strong></a><p class="meta">Programs run statements, names refer to objects, expressions become method calls.</p></li><li><a class="text-link" href="/prototyping/journey-control-flow.html"><strong>Journey · Control Flow</strong></a><p class="meta">Branches choose paths; the figure depicts a value flowing through a predicate to one of several branches.</p></li><li><a class="text-link" href="/prototyping/journey-iteration.html"><strong>Journey · Iteration</strong></a><p class="meta">Loops repeat; the protocol behind for is iter() then next() until exhausted.</p></li><li><a class="text-link" href="/prototyping/journey-shapes.html"><strong>Journey · Shapes</strong></a><p class="meta">Containers answer different questions; reshaping is the everyday move; text becomes structured data.</p></li><li><a class="text-link" href="/prototyping/journey-interfaces.html"><strong>Journey · Interfaces</strong></a><p class="meta">Functions are named behavior; functions are values; classes bundle state with behavior.</p></li><li><a class="text-link" href="/prototyping/journey-types.html"><strong>Journey · Types</strong></a><p class="meta">Annotations describe but don&#x27;t enforce; unions cover alternatives; generics preserve shape across calls.</p></li><li><a class="text-link" href="/prototyping/journey-reliability.html"><strong>Journey · Reliability</strong></a><p class="meta">Failure is explicit; resources have boundaries; concurrency outlives single expressions.</p></li><li><a class="text-link" href="/prototyping/journey-workers.html"><strong>Journey · Workers</strong></a><p class="meta">Workers-specific journey added on main; section figures pending design.</p></li></ul>
3838
</article>
3939

4040
</body>

public/prototyping/journey-figures-gestalt.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)