Skip to content

Commit ecce666

Browse files
committed
Fourth coverage push: 100% (109/109); 103 figures; lessons captured
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
1 parent 8241edf commit ecce666

4 files changed

Lines changed: 299 additions & 4 deletions

File tree

docs/lessons-learned.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,20 @@ git diff --check
8484
- Keep `README.md` focused on how to understand, verify, and deploy the project.
8585
- Keep this lessons document updated when a bug reveals a general rule.
8686
- Record user-visible changes in `CHANGELOG.md` before significant commits or releases.
87+
88+
## Visualisations and marginalia
89+
90+
- **A diagram set needs a grammar, not a collection of one-off layouts.** Hand-drawn SVGs drift in stroke weight, cell size, type-tag placement, arrow style. A locked `Canvas` grammar (palette, tokens, words, phrases, metrics) makes drift structurally impossible. Cards declare figures by composing primitives; the library guarantees consistency.
91+
- **Emit explicit `width`/`height` on every SVG; use `max-width: 100%` in CSS, never `width: 100%`.** Without `width`/`height` the browser stretches a small viewBox to fill its container, doubling text inside. This was the root cause of every "figure too big" report. The fix lives in `Canvas.to_svg()` and the CSS rules in `public/site.css`, `scripts/build_prototypes.py`, and `scripts/build_marginalia.py`.
92+
- **A figure's diagrammatic content must not duplicate its figcaption.** SVGs may carry functional labels (`stdout`, `iter()`, panel tags like `before` / `after`, type signatures like `x: int | str | None`). Full sentences describing the figure as a whole are prose and belong in the figcaption. Captions are the canonical voice. The exception is review-only pages (`marginalia-gestalt`) where cards have no figcaption; figures destined for promotion to production must drop their inline prose first.
93+
- **Emphasis is scarce.** With site `--accent` saturated for UI use, a coral arrow on every line reads as no emphasis at all. `closed_arrow` defaults to `emphasis=False`; figures opt in only for the single element the prose names. Same rule for accent dots, gates, and ring highlights.
94+
- **Soft fills should be neutral, not accent-tinted.** A 5% warm-brown tint reads as a quiet container. An accent-tinted soft fill makes every object box look highlighted, which breaks the scarcity rule a second way.
95+
- **Two rubrics, one craft section.** Journey-section figures depict a *conceptual shift* across multiple lessons; example-cell figures depict the *single move* the surrounding cell discusses. `docs/journey-visualisation-rubric.md` and `docs/example-figure-rubric.md` score each on 10 points: content fidelity, craft, context. Topic gates per kind of section / cell shape.
96+
- **Constraint-shaped sections resist mechanism figures.** Workers' "Preserve the lesson while respecting the runtime" is a principle, not a mechanism. Forcing figures on such sections scores below the 8.5 gate. Either reframe the section around a mechanism or accept the gap deliberately.
97+
- **Authoring stays on the contributor; figures stay on the curator.** Example markdown does not include figure references. `src/marginalia.py` holds `FIGURES` (paint functions) and `ATTACHMENTS` (slug → cell → figure → caption). Curating figures is a single-file edit that contributors never see.
98+
- **Inline between prose and code is the production layout; banners between cells is the prototyped richer grammar.** Cells with figures drop to single-column stacking (prose, figure, code) via `.lp-cell.has-figure { grid-template-columns: 1fr }`. Cells without figures keep today's `prose | code` 2-column grid bit-for-bit. The banner-between approach (`/prototyping/layout-banner-*`) supports multi-figure small-multiples between cells when one inline figure isn't enough.
99+
- **Centralised gestalt pages catch drift that page-by-page review misses.** `/prototyping/marginalia-gestalt`, `/prototyping/journey-figures-gestalt`, and `/prototyping/production-figures-gestalt` show every figure in three different framings. Seeing all section figures of a journey in one 3-up row exposes inconsistencies invisible across six tabs.
100+
- **Mapping reuses existing figures; promoting moves design to production.** Half of example coverage came from attaching existing FIGURES to new examples (no paint code). The other half from new paint code copied or designed from gestalt cards. Both paths must pass the rubric.
101+
- **Tests against the cell layout must allow the `has-figure` class.** When the renderer adds `has-figure` to cells with attached figures, assertions on the literal string `class="lesson-step lp-cell"` fail. Change those tests to check the substring `lesson-step lp-cell` so both variants match.
102+
- **Score what's shipping, not what was designed.** A scoring dict on the gestalt is design-time review. Production figures live in `src/marginalia.py` `FIGURES` and may have been redesigned during promotion. Scoring should track the production version with the gestalt as separate history.
103+
- **Some examples should never have figures.** Constraint-shaped, infrastructure-shaped, and aggregator-shaped slugs lack a single mechanism to depict. Force-fitting figures on them scores below the gate. Leave them figure-less and document why rather than ship weak figures.

public/prototyping/production-figures-gestalt.html

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

src/asset_manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Generated by scripts/fingerprint_assets.py. Do not edit by hand.
22
ASSET_PATHS = {'SITE_CSS': '/site.489bc3f7eb6d.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '9974d2516af6'
3+
HTML_CACHE_VERSION = '943aba25c847'

src/marginalia.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,187 @@ def delete_name_erased(c: Canvas) -> None:
979979
c.cell(102, 60, "[1, 2, 3]", w=90, h=22, soft=True)
980980

981981

982+
# ─── Fourth coverage push: constraint-shaped examples ─────────────────
983+
984+
985+
def package_tree(c: Canvas) -> None:
986+
"""Packages · a directory with __init__.py becomes an importable package; submodules nest."""
987+
c.frame(70, 0, 100, 22, label="mypackage")
988+
c.mono(120, 14, "__init__.py")
989+
c.stroke(120, 22, 40, 50)
990+
c.stroke(120, 22, 120, 50)
991+
c.stroke(120, 22, 200, 50)
992+
c.cell(10, 50, "a.py", w=60, h=22)
993+
c.cell(90, 50, "b.py", w=60, h=22)
994+
c.cell(170, 50, "sub/", w=60, h=22, soft=True)
995+
996+
997+
def venv_boundary(c: Canvas) -> None:
998+
"""Virtual environments · a venv isolates a project's interpreter and packages from the system."""
999+
c.frame(0, 0, 110, 70, label="project")
1000+
c.cell(12, 18, "code", w=84, h=20)
1001+
c.cell(12, 42, "requirements", w=84, h=20)
1002+
c.closed_arrow(110, 35, 142, 35, emphasis=True)
1003+
c.frame(144, 0, 130, 70, label="venv")
1004+
c.mono(209, 22, "python")
1005+
c.mono(209, 42, "site-packages")
1006+
1007+
1008+
def subprocess_spawn(c: Canvas) -> None:
1009+
"""Subprocesses · spawn a child process; capture stdout, stderr, and exit code as portable evidence."""
1010+
c.cell(0, 22, "parent", w=70, h=24)
1011+
c.closed_arrow(70, 34, 110, 34, emphasis=True)
1012+
c.label(90, 26, "spawn", anchor="middle")
1013+
c.cell(112, 22, "child process", w=110, h=24, soft=True)
1014+
c.closed_arrow(222, 34, 252, 34, emphasis=False)
1015+
c.cell(254, 22, "output", w=70, h=24)
1016+
1017+
1018+
def logging_levels(c: Canvas) -> None:
1019+
"""Logging · five levels; messages below the configured threshold are dropped."""
1020+
levels = [("CRITICAL", "50"), ("ERROR", "40"), ("WARNING", "30"), ("INFO", "20"), ("DEBUG", "10")]
1021+
for i, (name, num) in enumerate(levels):
1022+
c.cell(0, i * 22, name, w=120, h=20)
1023+
c.cell(122, i * 22, num, w=40, h=20, soft=True)
1024+
1025+
1026+
def aaa_pattern(c: Canvas) -> None:
1027+
"""Testing · arrange-act-assert: set up, run the behavior, compare the result."""
1028+
rows = [("arrange", "set up state"), ("act", "perform behavior"), ("assert", "compare result")]
1029+
for i, (label_text, body) in enumerate(rows):
1030+
c.cell(0, i * 24, label_text, w=80, h=22, soft=(i == 2))
1031+
c.closed_arrow(80, i * 24 + 11, 108, i * 24 + 11, emphasis=False)
1032+
c.cell(110, i * 24, body, w=140, h=22)
1033+
1034+
1035+
def protocol_layers(c: Canvas) -> None:
1036+
"""Networking · each layer in the stack hides the next; HTTP rests on TCP on IP on the link."""
1037+
layers = ["application · HTTP", "transport · TCP", "network · IP", "link"]
1038+
for i, name in enumerate(layers):
1039+
c.cell(0, i * 22, name, w=200, h=20)
1040+
1041+
1042+
def gil_lanes(c: Canvas) -> None:
1043+
"""Threads and processes · the GIL serialises Python bytecode across threads; processes run in parallel."""
1044+
c.lane(20, x0=0, x1=240, label="GIL")
1045+
c.lane(50, x0=0, x1=240, label="thread A")
1046+
c.lane(80, x0=0, x1=240, label="thread B")
1047+
c.cell(10, 44, "", w=30, h=12)
1048+
c.cell(70, 74, "", w=30, h=12)
1049+
c.cell(130, 44, "", w=30, h=12)
1050+
c.cell(190, 74, "", w=30, h=12)
1051+
1052+
1053+
def cast_escape(c: Canvas) -> None:
1054+
"""Casts and any · cast(T, x) tells the type checker to treat x as T; runtime is unaffected."""
1055+
c.cell(0, 22, "Any", w=70, h=24, ghost=True)
1056+
c.closed_arrow(70, 34, 110, 34, emphasis=True)
1057+
c.label(90, 26, "cast(T, x)", anchor="middle")
1058+
c.cell(112, 22, "T", w=70, h=24, soft=True)
1059+
1060+
1061+
def newtype_phantom(c: Canvas) -> None:
1062+
"""NewType · two static identities backed by the same runtime type."""
1063+
c.tag(0, 0, "runtime: int")
1064+
c.cell(0, 12, "42", w=60, h=24)
1065+
c.tag(0, 50, "static: UserId")
1066+
c.cell(0, 62, "UserId(42)", w=90, h=24, soft=True)
1067+
1068+
1069+
def overload_signatures(c: Canvas) -> None:
1070+
"""Overloads · @overload declares multiple signatures; one implementation routes to the right return type."""
1071+
c.tag(0, 0, "@overload")
1072+
c.cell(0, 12, "def f(x: int) -> str", w=180, h=20)
1073+
c.cell(0, 36, "def f(x: str) -> int", w=180, h=20)
1074+
c.closed_arrow(180, 32, 220, 32, emphasis=True)
1075+
c.cell(222, 22, "one impl", w=80, h=22, soft=True)
1076+
1077+
1078+
def paramspec_preserve(c: Canvas) -> None:
1079+
"""ParamSpec · the decorator preserves the wrapped function's full signature, parameter for parameter."""
1080+
c.cell(0, 22, "f(P)", w=50, h=24)
1081+
c.closed_arrow(50, 34, 80, 34, emphasis=True)
1082+
c.frame(82, 12, 100, 44, label="@dec")
1083+
c.mono(132, 36, "P preserved")
1084+
c.closed_arrow(182, 34, 212, 34, emphasis=True)
1085+
c.cell(214, 22, "wrapper(P)", w=80, h=24, soft=True)
1086+
1087+
1088+
def literal_constrained(c: Canvas) -> None:
1089+
"""Literal · the type narrows the slot to a fixed set of constant values."""
1090+
c.tag(0, 0, "Literal['red', 'green', 'blue']")
1091+
c.cell(0, 12, "x", w=40, h=24)
1092+
c.closed_arrow(40, 24, 70, 4, emphasis=False)
1093+
c.closed_arrow(40, 24, 70, 24, emphasis=False)
1094+
c.closed_arrow(40, 24, 70, 44, emphasis=False)
1095+
c.cell(72, 0, "'red'", w=70, h=20, soft=True)
1096+
c.cell(72, 22, "'green'", w=70, h=20, soft=True)
1097+
c.cell(72, 44, "'blue'", w=70, h=20, soft=True)
1098+
1099+
1100+
def callable_type(c: Canvas) -> None:
1101+
"""Callable types · the annotation captures the call shape: argument types and return type."""
1102+
c.tag(0, 0, "Callable[[int, str], bool]")
1103+
c.cell(0, 12, "(int, str)", w=100, h=22)
1104+
c.closed_arrow(100, 23, 130, 23, emphasis=False)
1105+
c.cell(132, 12, "bool", w=60, h=22)
1106+
1107+
1108+
def isinstance_check(c: Canvas) -> None:
1109+
"""Runtime type checks · isinstance asks the runtime; the answer is a bool, not a refinement."""
1110+
c.cell(0, 22, "isinstance(x, T)", w=140, h=24)
1111+
c.closed_arrow(140, 22, 170, 4, emphasis=True)
1112+
c.cell(172, 0, "True", w=60, h=20, soft=True)
1113+
c.closed_arrow(140, 46, 170, 56, emphasis=False)
1114+
c.cell(172, 50, "False", w=60, h=20)
1115+
1116+
1117+
def collections_containers(c: Canvas) -> None:
1118+
"""Collections module · four specialised containers for shapes the built-in types don't cover well."""
1119+
rows = [("deque", "fast appends both ends"), ("Counter", "key → count"), ("defaultdict", "missing key default"), ("namedtuple", "tuple with names")]
1120+
for i, (name, role) in enumerate(rows):
1121+
c.cell(0, i * 22, name, w=110, h=20)
1122+
c.cell(112, i * 22, role, w=170, h=20, soft=True)
1123+
1124+
1125+
def typed_dict_shape(c: Canvas) -> None:
1126+
"""Structured data shapes · TypedDict names each key's value type; the dict obeys the declared shape."""
1127+
c.frame(0, 0, 200, 86, label="User TypedDict")
1128+
rows = [("id", "int"), ("name", "str"), ("active", "bool")]
1129+
for i, (k, v) in enumerate(rows):
1130+
c.cell(14, 18 + i * 20, f"{k}: {v}", w=172, h=18)
1131+
1132+
1133+
def csv_records(c: Canvas) -> None:
1134+
"""CSV data · rows of records; each line has the same columns in the same order."""
1135+
c.tag(0, 0, "rows · records")
1136+
headers = ["id", "name", "score"]
1137+
rows = [["1", "Ada", "97"], ["2", "Bo", "88"], ["3", "Cy", "76"]]
1138+
for j, h in enumerate(headers):
1139+
c.cell(j * 70, 12, h, w=70, h=20, soft=True)
1140+
for i, r in enumerate(rows):
1141+
for j, v in enumerate(r):
1142+
c.cell(j * 70, 32 + i * 20, v, w=70, h=18)
1143+
1144+
1145+
def warning_signal(c: Canvas) -> None:
1146+
"""Warnings · a soft signal: the warning is reported, execution continues."""
1147+
c.cell(0, 22, "code path", w=90, h=24)
1148+
c.closed_arrow(90, 22, 120, 4, emphasis=False)
1149+
c.cell(122, 0, "DeprecationWarning", w=170, h=22, soft=True)
1150+
c.closed_arrow(90, 46, 120, 56, emphasis=True)
1151+
c.cell(122, 50, "execution continues", w=170, h=22)
1152+
1153+
1154+
def object_lifecycle(c: Canvas) -> None:
1155+
"""Object lifecycle · __init__ creates; the object lives while refcount > 0; __del__ finalises."""
1156+
c.cell(0, 22, "__init__", w=80, h=24)
1157+
c.closed_arrow(80, 34, 110, 34, emphasis=True)
1158+
c.cell(112, 22, "live · refcount > 0", w=140, h=24, soft=True)
1159+
c.closed_arrow(252, 34, 282, 34, emphasis=False)
1160+
c.cell(284, 22, "__del__", w=80, h=24)
1161+
1162+
9821163
def lazy_stream(c: Canvas) -> None:
9831164
"""Iteration · Compose lazy value streams: filter and map flow values without materialising."""
9841165
c.object_box(0, 26, "source", "[a,b,c]", w=78, h=24)
@@ -1086,6 +1267,26 @@ def lazy_stream(c: Canvas) -> None:
10861267
"custom-exception-chain": (custom_exception_chain, 220, 90),
10871268
"exception-group-peel": (exception_group_peel, 240, 50),
10881269
"delete-name-erased": (delete_name_erased, 200, 84),
1270+
# Fourth coverage push: 19 figures for constraint-shaped examples
1271+
"package-tree": (package_tree, 240, 76),
1272+
"venv-boundary": (venv_boundary, 274, 76),
1273+
"subprocess-spawn": (subprocess_spawn, 324, 60),
1274+
"logging-levels": (logging_levels, 164, 124),
1275+
"aaa-pattern": (aaa_pattern, 250, 80),
1276+
"protocol-layers": (protocol_layers, 200, 100),
1277+
"gil-lanes": (gil_lanes, 244, 100),
1278+
"cast-escape": (cast_escape, 184, 56),
1279+
"newtype-phantom": (newtype_phantom, 96, 92),
1280+
"overload-signatures": (overload_signatures, 304, 64),
1281+
"paramspec-preserve": (paramspec_preserve, 294, 60),
1282+
"literal-constrained": (literal_constrained, 144, 76),
1283+
"callable-type": (callable_type, 196, 40),
1284+
"isinstance-check": (isinstance_check, 232, 76),
1285+
"collections-containers": (collections_containers, 284, 92),
1286+
"typed-dict-shape": (typed_dict_shape, 200, 92),
1287+
"csv-records": (csv_records, 212, 96),
1288+
"warning-signal": (warning_signal, 292, 80),
1289+
"object-lifecycle": (object_lifecycle, 366, 60),
10891290
}
10901291

10911292

@@ -1497,6 +1698,83 @@ def lazy_stream(c: Canvas) -> None:
14971698
"cell-0", "value-types",
14981699
"Each literal form constructs an object of a specific type; the source spelling and the value type stay in sync.",
14991700
)],
1701+
# Fourth coverage push: constraint-shaped examples
1702+
"packages": [(
1703+
"cell-0", "package-tree",
1704+
"A directory with __init__.py becomes an importable package; submodules and subpackages nest beneath it.",
1705+
)],
1706+
"virtual-environments": [(
1707+
"cell-0", "venv-boundary",
1708+
"A venv carries its own interpreter and site-packages, isolating a project's dependencies from the system.",
1709+
)],
1710+
"subprocesses": [(
1711+
"cell-0", "subprocess-spawn",
1712+
"subprocess.run spawns a child process and captures its stdout, stderr, and exit code as portable evidence.",
1713+
)],
1714+
"logging": [(
1715+
"cell-0", "logging-levels",
1716+
"Five severity levels; the logger's configured threshold drops everything below it.",
1717+
)],
1718+
"testing": [(
1719+
"cell-0", "aaa-pattern",
1720+
"arrange-act-assert: set up the state, perform the behavior under test, compare the result to expectations.",
1721+
)],
1722+
"networking": [(
1723+
"cell-0", "protocol-layers",
1724+
"Network protocols stack: HTTP rests on TCP, which rests on IP, which rests on the link layer.",
1725+
)],
1726+
"threads-and-processes": [(
1727+
"cell-0", "gil-lanes",
1728+
"Threads share memory but the GIL serialises Python bytecode; processes run in parallel with isolated memory.",
1729+
)],
1730+
"casts-and-any": [(
1731+
"cell-0", "cast-escape",
1732+
"cast(T, x) tells the type checker to treat x as T; the runtime is unaffected.",
1733+
)],
1734+
"newtype": [(
1735+
"cell-0", "newtype-phantom",
1736+
"NewType creates a distinct static identity backed by the same runtime type — UserId is int with a name.",
1737+
)],
1738+
"overloads": [(
1739+
"cell-0", "overload-signatures",
1740+
"@overload declares multiple call signatures; one underlying implementation routes input shape to return type.",
1741+
)],
1742+
"paramspec": [(
1743+
"cell-0", "paramspec-preserve",
1744+
"ParamSpec preserves the wrapped function's signature through a decorator, parameter for parameter.",
1745+
)],
1746+
"literal-and-final": [(
1747+
"cell-0", "literal-constrained",
1748+
"Literal narrows a slot to a fixed set of constant values; Final says the binding will not change.",
1749+
)],
1750+
"callable-types": [(
1751+
"cell-0", "callable-type",
1752+
"Callable[[A, B], R] captures the call shape: a tuple of argument types and one return type.",
1753+
)],
1754+
"runtime-type-checks": [(
1755+
"cell-0", "isinstance-check",
1756+
"isinstance and issubclass ask the runtime; the answer is a bool, not a static type refinement.",
1757+
)],
1758+
"collections-module": [(
1759+
"cell-0", "collections-containers",
1760+
"Four specialised containers for shapes the built-in types don't cover well: deque, Counter, defaultdict, namedtuple.",
1761+
)],
1762+
"structured-data-shapes": [(
1763+
"cell-0", "typed-dict-shape",
1764+
"TypedDict names each key's value type; the dict obeys the declared shape at static-check time.",
1765+
)],
1766+
"csv-data": [(
1767+
"cell-0", "csv-records",
1768+
"CSV files are rows of records; each line has the same columns in the same order.",
1769+
)],
1770+
"warnings": [(
1771+
"cell-0", "warning-signal",
1772+
"A warning is a soft signal: the message is reported, but execution continues unless filters elevate it.",
1773+
)],
1774+
"object-lifecycle": [(
1775+
"cell-0", "object-lifecycle",
1776+
"__init__ constructs the object; it lives while at least one reference holds it; __del__ runs when refcount hits zero.",
1777+
)],
15001778
}
15011779

15021780

0 commit comments

Comments
 (0)