Skip to content

Commit 8eb6500

Browse files
committed
Merge origin/main (#19 figure tokens) into claude/verification-audit-fixes
#19 recoloured marginalia figures with --fig-* theme tokens and removed the dark-mode light "paper chip". Conflict and follow-up: - tests/test_app.py: keep this branch's removal of the unchanged test_css_defines_a_dark_palette source-text test (the browser check measures the palette switch and contrast), and keep #19's new test_dark_mode_recolours_marginalia_figures_through_tokens. - scripts/check_browser_layout.mjs: the rendered contract "dark mode keeps marginalia figures on light paper" contradicted #19. It now measures that figure ink is recoloured between light and dark, keeps at least 4.5:1 contrast against the page in both schemes, and that no painted chip sits behind the SVG. Generated files were rebuilt with make build and check-social-cards; nothing drifted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012eisRQmQvg4TphwztxhQGJ
2 parents 59ec97f + 2ab5b23 commit 8eb6500

140 files changed

Lines changed: 1656 additions & 552 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0
1313
- A Run whose Turnstile token the server rejects now ends with the server's message instead of re-challenging. The runner used to solve again every time the response carried the challenge marker, so a persistently failing Siteverify (wrong secret, hostname, or action) turned one click into an unbounded loop of solves, Worker POSTs, and Siteverify subrequests. Each Run now earns at most one challenge, and the failure message says to press Run again.
1414
- Siteverify calls time out after 10 seconds, tokens longer than Cloudflare's documented 2048 characters are rejected without a subrequest, and a `success: true` response with a missing or non-string `hostname` fails closed instead of raising a 500. The gaps surfaced by comparing `_verify_turnstile` with the canonical handler in Cloudflare's Turnstile Spin skill.
1515
- Production Python packages are locked. Pywrangler 1.17.4 vendors a committed, hash-pinned `pylock.toml` instead of resolving an unpinned `fastapi` at deploy time, and `uv.lock` pins the same versions (FastAPI 0.141.1, Starlette 1.7.0, Pydantic 2.10.6) so the test suite runs against exactly what ships. `tests/test_dependency_locks.py` fails on drift, and CI and `make deploy` fail if a sync would change `pylock.toml`. Starlette 1.7.0 also clears the five advisories against the previously tested 1.0.0.
16+
- The bytes-and-bytearray figure rendered doubled backslashes (`b'\\x63\\x61\\x66'`) and drew bytes the example never uses (#18). Redrawn from the example's real values, and three new figure contracts close the class of bug: no rendered text may carry a doubled backslash and escape sequences must appear in the example source (13), every arrow is at least 20 units (14), and `site.css` must define every figure token in both schemes (15).
1617
- Example-page runner wiring (Run interception, Reset, the share button, and keyboard navigation) no longer waits for the CDN-backed highlighter and editor modules: `runner.js` loads `async`, so a slow or unreachable esm.sh cannot stall it — ordered module scripts otherwise execute strictly after every preceding module settles, including their top-level awaits.
1718

19+
### Changed
20+
21+
- Figures now paint with `--fig-ink`, `--fig-ink-soft`, `--fig-accent` and `--fig-soft` tokens instead of literal colours, and `site.css` defines the tokens for both colour schemes. Dark mode recolours every figure with the page; the light "paper chip" behind figures is gone. Figure mono and sans faces follow the page's own font stacks, and mono runs are pinned to the grammar's character advance with `textLength` so divider positions hold on every platform. Paired figures in a banner share a baseline (a two-row subgrid). Five figures lost an accent that marked an arbitrary row (`naming-decisions`, `iteration-loop-selector`, `type-shape-catalog`, `reliability-signal-map`, `container-methods`); nine arrows that were mostly arrowhead were lengthened to the new 20-unit minimum.
22+
- Ten figures redrawn as mechanisms carrying the lesson's own values: closures is an environment diagram with the returned frame ghosted and one live `__closure__` reference; generators shows the cell's two `next()` calls on `countdown(3)` with the surviving local beside each; recursion shows the example's tree at the deepest frame of `total(tree)` with the values each frame returns; dicts shows the cell's keys hashed to slots with the insert of `"year"`; sentinel iteration shows `read_chunk` yielding `"py"` and `"thon"` and stopping on the empty string; string formatting's format spec is a railroad tracing the cell's `05.1f`, skipped stations ghosted under a bypass rail; bytes-and-bytearray draws the example's bytes as integer slots with the in-place write and moves to the cell that performs it; regular expressions shows the cell's two capture groups landing on `Ada: 10`; values shows `text` bound to a `str` object and the `type()` hop to where `upper()` lives; logging shows the cell's three records crossing the handler's INFO gate. The binding figure is a parametrised factory, so `variables` shows `message → "hi"` and `constants` shows `MAX_RETRIES → 3`.
23+
- The rubric states that table-shaped figures are redrawn rather than removed, that a figure whose caption names no single element carries no accent, and how cell-0 anchors are scored. The review, principles and queue are in `docs/diagram-upgrade-plan.md`.
24+
1825
### Added
1926

2027
- `/about` page describing how the site is made — the verified-output pipeline, the runner sandbox, the figure grammar, and the quality gates — with a design-language section that renders the live CSS tokens (color swatches, spacing scale, type specimens, and a real walkthrough cell as a specimen). Linked from the nav on every page and listed in the sitemap.

‎docs/diagram-upgrade-plan.md‎

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Diagram upgrade plan
2+
3+
Written 2026-09-27 from a review of every attached figure on the live
4+
site, the gestalt review pages, the grammar, both rubrics and the
5+
registry. The rubrics remain the scoring instruments. This document
6+
records what the review found, the principles that follow from it,
7+
what the first pull request changed, and the queue after that.
8+
9+
## What the review found
10+
11+
The library's strengths are enforced rather than hoped for: the palette,
12+
stroke weights and typography are locked by contracts, captions assert
13+
what is true instead of narrating the picture, and the gestalt review
14+
page paints from the production paint functions. The figures that teach
15+
best show a mechanism changing state, such as aliasing before and after
16+
an append, the iterator caret advancing one row at a time, and the MRO
17+
diamond flattening into a chain.
18+
19+
The weaknesses are measurable.
20+
21+
| finding | measure |
22+
|---|---:|
23+
| paint functions using only cell, arrow, dashed line and text | 66 of 124 |
24+
| `cell` calls / `closed_arrow` calls / `lanes` calls | 236 / 129 / 1 |
25+
| attachments anchored after cell 0 | 106 of 115 |
26+
| example figures scoring exactly 9.0 (the rest 9.5, none lower) | 106 of 109 |
27+
| journey figures scoring exactly 9.0 | 21 of 21 |
28+
| arrows shorter than 20 units | 9 |
29+
30+
Behind the numbers: figures hard-coded the light palette, so in dark
31+
mode each one sat on a cream chip; a paired banner had no shared
32+
baseline; the SVG asked for JetBrains Mono and Source Sans while the
33+
page loaded no web fonts, so the visitor's operating system chose the
34+
faces; several selector figures put the accent on whichever row the
35+
loop reached last; and because no contract read figure text, the
36+
bytes-and-bytearray figure rendered doubled backslashes for months and
37+
the regular-expressions figure drew a pattern the example never uses.
38+
39+
## Principles
40+
41+
1. **Show state over time.** One frame is a label. The same frame after
42+
each step is a mechanism. Repeating one drawing is cheap in this
43+
grammar; `iterator-unroll` and `generator-resume` do it.
44+
2. **Draw the heap.** Python's picture is frames on the left and objects
45+
on the right with arrows crossing between them. The grammar now has
46+
an `env` phrase for the frame, and closures uses it.
47+
3. **Redraw a weak figure; never replace it with a note.** A drawing
48+
that would be no worse as a two-column table needs a mechanism
49+
drawn in its place. Removing it and recording a rationale leaves the
50+
page with less than it had, which the first draft of this plan got
51+
wrong for four pages.
52+
4. **The figure carries the lesson's own names.** Reuse the mechanism
53+
by parametrising the paint function, so `variables` shows `message`
54+
and `constants` shows `MAX_RETRIES`.
55+
5. **Accent only what the caption names.** When the caption names no
56+
single element, the figure has no accent.
57+
6. **Check figure content against the example.** Geometry contracts
58+
cannot see a wrong label. A content contract can.
59+
7. **Paint with tokens.** The grammar emits `var(--fig-*)`; the page's
60+
stylesheet decides the ink in each colour scheme.
61+
8. **Typography follows the page.** Mono and sans use the page's own
62+
stacks, and mono runs carry `textLength` so divider maths holds on
63+
every platform. The italic serif stays for identifiers.
64+
9. **Rank before scoring.** With 106 of 109 figures at 9.0, the score
65+
is a ship gate. Choosing what to redesign next needs a ranking.
66+
67+
## What the first pull request changed
68+
69+
Grammar (`src/marginalia_grammar.py`):
70+
71+
- `INK`, `INK_SOFT`, `EMPHASIS` and `SOFT_FILL` are `var(--fig-*)`
72+
references. `FIGURE_TOKENS_LIGHT` and `FIGURE_TOKENS_DARK` hold the
73+
values; `figure_token_css()` emits them for the gestalt pages and the
74+
social-card shell.
75+
- `FONT_MONO` and `FONT_SANS` match `public/site.css`; every mono run is
76+
pinned to `MONO_ADVANCE` with `textLength`.
77+
- `ARROW_MIN = 20`; `Canvas` records every arrow's length.
78+
- `name_box` and `bind` take a width so long names fit.
79+
- The `env` phrase draws a frame with bindings, optionally ghosted, and
80+
returns anchors for heap arrows.
81+
82+
Figures (`src/marginalia.py`):
83+
84+
- `closure-cell` is an environment diagram: the returned frame ghosted,
85+
`double` naming a heap function object, one accented `__closure__`
86+
reference back into the frame.
87+
- `generator-resume` replaces `generator-ribbon`: three ribbons for
88+
three `next()` calls on the cell's own `countdown(3)`, the surviving
89+
local beside each.
90+
- `format-spec` is a railroad tracing the cell's own `05.1f`: visited
91+
stations shaded, skipped stations ghosted under a bypass rail.
92+
- `bytes-vs-bytearray` draws the example's bytes as integer slots and
93+
the in-place write `packet[0] = ord("P")`, anchored to the cell that
94+
performs it.
95+
- `regex-groups` replaces `regex-anchors`: the cell's two capture
96+
groups dropped onto the record `Ada: 10`.
97+
- `value-type-lookup` replaces `value-types`: `text` bound to a `str`
98+
object, then the `type()` hop to the class where `upper()` lives.
99+
- `logging-threshold` replaces `logging-levels`: the cell's three
100+
records crossing the handler's INFO gate, `debug` dropped.
101+
- `variables-bind` and `constants-bind` come from one parametrised
102+
factory.
103+
- Nine arrows lengthened to the minimum; five figures lost an accent
104+
that marked an arbitrary row.
105+
- `literal-forms` and `collections-containers` stay as they were, scored
106+
8.5, until a mechanism replaces them.
107+
108+
Page (`public/site.css`, `src/templates/about.html`):
109+
110+
- `--fig-ink`, `--fig-ink-soft`, `--fig-accent` and `--fig-soft` defined
111+
for light and dark; the dark-mode paper chip removed.
112+
- `.cell-banner figure` is a two-row subgrid, so paired figures
113+
bottom-align their drawings and start both captions on one line.
114+
115+
Contracts (`tests/test_marginalia_geometry.py`):
116+
117+
- 13, content: no doubled backslash anywhere; an escape sequence in an
118+
attached figure must appear in that example's source.
119+
- 14, arrow length: every arrow at least `ARROW_MIN`.
120+
- 15, theme tokens: `site.css` defines all four tokens in both schemes
121+
with the grammar's values, and every grammar colour is a token.
122+
123+
Rubrics: the redraw rule, the zero-accent rule, the placement note and
124+
the three contracts are in `docs/example-figure-rubric.md` and
125+
`docs/journey-visualisation-rubric.md`; the token and typography
126+
invariants are in `docs/visual-explainer-spec.md`.
127+
128+
## Queue
129+
130+
Ordered by payoff. Each item is one pull request.
131+
132+
1. **State over time** for `context-managers` (enter, body, exit, with
133+
the exception path as a second strip), `decorators` (two calls, two
134+
wrappers) and `object-lifecycle`.
135+
2. **Heap drawings** with `env` for `scope-global-nonlocal`, `recursion`
136+
(frames with `n` bindings instead of labelled cells) and `unpacking`.
137+
3. **Mechanisms for the two table figures.** `literals`: the same value
138+
spelled three ways converging on one `int` object. `collections-module`:
139+
one container's operation, or split the page.
140+
4. **Parametrise the remaining reused figures**: `iterator-unroll`,
141+
`iter-protocol`, `class-triangle`, `operator-dispatch`,
142+
`comprehension-equivalence`.
143+
5. **Move anchors to the cell where the move happens.** For each cell-0
144+
attachment, ask whether a later cell holds the mutation, dispatch or
145+
resume the figure depicts. Rewrite criterion 10 once the distribution
146+
is honest.
147+
6. **Rank the gestalt.** A pairwise ranking table in the registry, and
148+
redesign from the bottom ten. The 8.5 score stays as the gate.
149+
7. **Widen the content contract** from escape sequences to every mono
150+
literal that looks like Python source: it must appear in the
151+
example's code or output.
152+
8. **Buffers and hashes.** `dict-buckets` and `set-buckets` with hash
153+
values from the example; `list-append` and `slice-ruler` as header
154+
plus buffer.
155+
156+
## What to study
157+
158+
- **Composing Programs** (composingprograms.com) and **Python Tutor**
159+
(pythontutor.com) for the environment diagram: frames left, objects
160+
right, and how a closure and a recursive call are drawn.
161+
- **Ned Batchelder, "Facts and myths about Python names and values"**
162+
for name-to-value drawings done the same way every time.
163+
- **Crafting Interpreters** (craftinginterpreters.com) for warm ink on
164+
cream, one idea per figure, held consistent across a whole book.
165+
- **Sam Rose, samwho.dev** ("Hashing", "Memory Allocation") for
166+
programmer-facing figures with a locked grammar; the model for the
167+
bucket figures.
168+
- **Lydia Hallie, "JavaScript Visualized"** for the frame designs of
169+
scope chain and closures.
170+
- **Bartosz Ciechanowski** (ciechanow.ski) for figures where exactly
171+
one variable changes between frames.
172+
- **SQLite syntax diagrams** for what a railroad is; `format-spec`
173+
follows them.
174+
- **The Rust Book, chapter 4** and **"Go Slices: usage and internals"**
175+
for header-plus-buffer drawings of list, bytearray and slice.
176+
- **Tufte, Visual Explanations, "the smallest effective difference"**
177+
for how little contrast emphasis needs.
178+
- **Maggie Appleton** and **Julia Evans** for mechanism drawings that stay playful without adding marks.
179+
- **Distill.pub** for captions that carry the claim.

‎docs/example-figure-rubric.md‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ per-figure scoring.
6060
figure. The accent goes on the single element the cell prose
6161
names (the live mutation, the captured cell, the dispatch arrow).
6262
Three accent marks competing for attention is no emphasis at all.
63+
When the caption names no single element, as in a selector where
64+
every branch is equal, the figure has zero accents, because an
65+
accent on the row a loop happened to reach last marks nothing.
6366
8. **Restraint (0-1.0)** — no decoration that does not carry
6467
information. No drop shadows, gradients, ornamental rules,
6568
non-orthogonal tilts, or marks placed for "balance".
@@ -77,6 +80,14 @@ per-figure scoring.
7780
cell just made, not stand alone as a generic illustration of the
7881
example title.
7982

83+
As of September 2026, 106 of 115 attachments anchor after cell 0,
84+
so this criterion is usually scored against the first cell. An
85+
opening figure is a legitimate design, and it earns the 0.5 by
86+
summarising the first cell's move rather than the title. When a
87+
later cell holds the mutation, dispatch or resume the figure
88+
depicts, move the anchor there; `bytes-and-bytearray` moved to
89+
cell 3 for that reason.
90+
8091
## Topic gates (cell-shape specific)
8192

8293
- **Binding cells** (assignments, `=`) — show the name-arrow with the
@@ -107,6 +118,34 @@ the figure can merge.
107118

108119
- **One figure per cell, at most.** Two figures on one cell signal
109120
the cell is doing two things; split the cell instead.
121+
- **Table figures are redrawn, never removed.** Rows of `name | role`,
122+
`type | spellings` or `level | number` are reference material; they
123+
score 8.5 at most under criteria 2 and 4 because a table in the prose
124+
would do the same work. The remedy is a mechanism in the same slot:
125+
`values` became the `text → str` binding with the `type()` hop to
126+
where `upper()` lives, and `logging` became three records crossing the
127+
handler's level gate. Removing a shipped figure and recording a
128+
rationale leaves the page with less than it had; the
129+
`no_figure_rationales` registry is for pages whose lesson has no
130+
drawable mechanism, and a page that once carried a figure does not
131+
qualify.
132+
- **The figure carries the lesson's own names.** Where one mechanism
133+
serves several lessons, parametrise the paint function
134+
(`_bind_figure("MAX_RETRIES", "int", "3")`) instead of choosing
135+
between placeholder names and a bespoke copy.
136+
- **Content fidelity, enforced.** No rendered text may contain a
137+
doubled backslash, and a mono label containing an escape sequence
138+
must appear verbatim in the example's Markdown source. The
139+
`bytes-vs-bytearray` figure shipped `b'\\x63\\x61\\x66'` for
140+
months because every contract measured geometry and none read the
141+
label. *Contract 13 — FigureContentContract.*
142+
- **Minimum arrow length, enforced.** Every closed arrow is at least
143+
`ARROW_MIN` (20) units long; below that it is mostly head. Widen
144+
the gap between the boxes, never shrink the head. *Contract 14.*
145+
- **Both colour schemes, enforced.** Figures paint with the
146+
`var(--fig-*)` tokens; `public/site.css` defines all four in the
147+
light root and the dark media block with the grammar's values, so
148+
no figure needs a light chip in dark mode. *Contract 15.*
110149
- **figcaption present and declarative.** Captions in the form
111150
"Two names share one mutable list — appending through one name
112151
changes the object visible through both." Not "this shows X" or
@@ -231,4 +270,8 @@ example reads as a coherent set rather than independently authored
231270
diagrams.
232271

233272
The score is a guide, not a substitute for reading the cell beside
234-
its surrounding prose.
273+
its surrounding prose. Because 106 of 109 attached figures score 9.0
274+
and the rest 9.5, the score no longer says which figure to redesign
275+
next. Use the gestalt page to rank the library pairwise and redesign
276+
from the bottom, and keep the score as the ship gate
277+
(`docs/diagram-upgrade-plan.md`).

‎docs/journey-visualisation-rubric.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ Score each section figure on a 10-point scale.
4343
7. **Emphasis scarcity (0-1.0)** — at most one accent mark per
4444
figure. The accent goes on the single element the section names
4545
(the live yield, the dispatch arrow, the captured cell). If three
46-
things are orange the figure has no emphasis at all.
46+
things are orange the figure has no emphasis at all. When the
47+
caption names no single element, as in a selector where every branch
48+
is equal, the figure has zero accents, because an accent on the row a
49+
loop happened to reach last marks nothing.
4750
8. **Restraint (0-1.0)** — no decoration that does not carry
4851
information. No drop shadows, gradients, ornamental rules,
4952
non-orthogonal tilts, or marks placed for "balance".
@@ -95,7 +98,9 @@ Score each section figure on a 10-point scale.
9598
width the layout gives it (`clamp(280px, 70vw, 640px)`); design at a
9699
viewBox whose rendered width fits the shared 640px ceiling.
97100
- **Uses only the four palette constants.** `INK`, `INK_SOFT`,
98-
`EMPHASIS`, `SOFT_FILL`. Anything else is grounds for redesign.
101+
`EMPHASIS`, `SOFT_FILL`, which resolve to the page's `--fig-*`
102+
tokens so a section figure renders in both colour schemes without a
103+
chip. Anything else is grounds for redesign.
99104

100105
## Quality bands
101106

0 commit comments

Comments
 (0)