Skip to content

Commit 7ab3f27

Browse files
committed
Enforce emphasis scarcity: at most one orange accent per figure
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.
1 parent 1b9657f commit 7ab3f27

9 files changed

Lines changed: 64 additions & 17 deletions

public/prototyping/journey-control-flow.html

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

public/prototyping/journey-figures-gestalt.html

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

public/prototyping/journey-iteration.html

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

public/prototyping/journey-types.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<h1>Types</h1>
3838
<p class="meta">This journey maps Python&#x27;s runtime object model to optional static annotations so learners know what types can and cannot promise.</p>
3939
</section>
40-
<section class="journey-section"><div><h2>Keep runtime and static analysis separate.</h2><p class="meta">The first lesson is that annotations describe expectations for tools while ordinary Python objects still run the program.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/type-hints">Type Hints</a><p class="meta">document expected types and feed type checkers</p></li><li><a class="text-link journey-item-title" href="/examples/protocols">Protocols</a><p class="meta">describe required behavior by structural shape</p></li><li><a class="text-link journey-item-title" href="/examples/enums">Enums</a><p class="meta">name a fixed set of symbolic values</p></li><li><a class="text-link journey-item-title" href="/examples/runtime-type-checks">Runtime Type Checks</a><p class="meta">show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 236 80" width="236" height="80" xmlns="http://www.w3.org/2000/svg"><text x="0" y="36" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="start">def f(x: int, y: str) -> bool: …</text><line x1="54" y1="28" x2="76" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/><line x1="102" y1="28" x2="124" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/><line x1="150" y1="28" x2="192" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/></svg><figcaption>Annotations describe expected types for tools; the runtime accepts any object regardless.</figcaption></figure></section><section class="journey-section"><div><h2>Describe realistic data shapes.</h2><p class="meta">Typed Python becomes useful when annotations explain optional values, unions, callables, and JSON-like records.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/union-and-optional-types">Union and Optional Types</a><p class="meta">show `X | Y` and `None`-aware APIs</p></li><li><a class="text-link journey-item-title" href="/examples/type-aliases">Type Aliases</a><p class="meta">name complex types with `type` statements or aliases</p></li><li><a class="text-link journey-item-title" href="/examples/typed-dicts">TypedDict</a><p class="meta">type dictionary records that come from JSON</p></li><li><a class="text-link journey-item-title" href="/examples/literal-and-final">Literal and Final</a><p class="meta">express constrained values and names that should not be rebound</p></li><li><a class="text-link journey-item-title" href="/examples/callable-types">Callable Types</a><p class="meta">type functions that are passed as arguments</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 172 108" width="172" height="108" xmlns="http://www.w3.org/2000/svg"><text x="0" y="14" font-family="-apple-system, 'Source Sans Pro', sans-serif" font-size="8" fill="rgba(82, 16, 0, 0.7)" text-anchor="start" letter-spacing="0.5">X: INT|STR|NONE</text><rect x="0" y="22" width="44" height="28" fill="none" stroke="#521000" stroke-width="1.0"/><text x="22.0" y="40.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">x</text><line x1="44" y1="36" x2="74.02702716443629" y2="17.650150066177822" stroke="#521000" stroke-width="1.0"/><polygon points="80,14 75.48708719090742,20.039339200403308 72.56696713796516,15.260960931952336" fill="#521000"/><line x1="44" y1="36" x2="73.0" y2="36.0" stroke="#521000" stroke-width="1.0"/><polygon points="80,36 73.0,38.8 73.0,33.2" fill="#521000"/><line x1="44" y1="36" x2="74.02702716443629" y2="54.34984993382218" stroke="#521000" stroke-width="1.0"/><polygon points="80,58 72.56696713796516,56.73903906804766 75.48708719090742,51.960660799596695" fill="#521000"/><rect x="82" y="4" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="19.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">int</text><rect x="82" y="26" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="41.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">str</text><rect x="82" y="48" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="63.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">None</text></svg><figcaption>A typed slot can accept one of several shapes — `int | str | None` covers expected absence and alternatives.</figcaption></figure></section><section class="journey-section"><div><h2>Scale annotations for reusable libraries.</h2><p class="meta">Advanced typing exists to preserve information across reusable functions, containers, and decorators.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/generics-and-typevar">Generics and TypeVar</a><p class="meta">write reusable typed containers and functions</p></li><li><a class="text-link journey-item-title" href="/examples/paramspec">ParamSpec</a><p class="meta">preserve callable signatures through decorators</p></li><li><a class="text-link journey-item-title" href="/examples/overloads">Overloads</a><p class="meta">describe APIs whose return type depends on the input shape</p></li><li><a class="text-link journey-item-title" href="/examples/casts-and-any">Casts and Any</a><p class="meta">show escape hatches and their tradeoffs</p></li><li><a class="text-link journey-item-title" href="/examples/newtype">NewType</a><p class="meta">create distinct static identities for runtime-compatible values</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 266 98" width="266" height="98" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="30" width="36" height="28" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="18.0" y="48.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">T</text><line x1="36" y1="44" x2="65.0" y2="44.0" stroke="#FF4801" stroke-width="1.4"/><polygon points="72,44 65.0,46.8 65.0,41.2" fill="#FF4801"/><rect x="74" y="26" width="100" height="36" fill="none" stroke="#521000" stroke-width="1.0"/><text x="80" y="23" font-family="-apple-system, 'Source Sans Pro', sans-serif" font-size="8" fill="rgba(82, 16, 0, 0.7)" text-anchor="start" letter-spacing="0.5">FN[T]</text><line x1="174" y1="44" x2="203.0" y2="44.0" stroke="#FF4801" stroke-width="1.4"/><polygon points="210,44 203.0,46.8 203.0,41.2" fill="#FF4801"/><rect x="212" y="30" width="36" height="28" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="230.0" y="48.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">T</text></svg><figcaption>A generic type variable preserves shape across a call: the same T flows in and out.</figcaption></figure></section>
40+
<section class="journey-section"><div><h2>Keep runtime and static analysis separate.</h2><p class="meta">The first lesson is that annotations describe expectations for tools while ordinary Python objects still run the program.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/type-hints">Type Hints</a><p class="meta">document expected types and feed type checkers</p></li><li><a class="text-link journey-item-title" href="/examples/protocols">Protocols</a><p class="meta">describe required behavior by structural shape</p></li><li><a class="text-link journey-item-title" href="/examples/enums">Enums</a><p class="meta">name a fixed set of symbolic values</p></li><li><a class="text-link journey-item-title" href="/examples/runtime-type-checks">Runtime Type Checks</a><p class="meta">show `type()`, `isinstance()`, and `issubclass()` without turning Python into Java</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 236 80" width="236" height="80" xmlns="http://www.w3.org/2000/svg"><text x="0" y="36" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="start">def f(x: int, y: str) -> bool: …</text><line x1="54" y1="28" x2="76" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/><line x1="102" y1="28" x2="124" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/><line x1="150" y1="28" x2="192" y2="28" stroke="#521000" stroke-width="0.6" stroke-dasharray="2 2"/></svg><figcaption>Annotations describe expected types for tools; the runtime accepts any object regardless.</figcaption></figure></section><section class="journey-section"><div><h2>Describe realistic data shapes.</h2><p class="meta">Typed Python becomes useful when annotations explain optional values, unions, callables, and JSON-like records.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/union-and-optional-types">Union and Optional Types</a><p class="meta">show `X | Y` and `None`-aware APIs</p></li><li><a class="text-link journey-item-title" href="/examples/type-aliases">Type Aliases</a><p class="meta">name complex types with `type` statements or aliases</p></li><li><a class="text-link journey-item-title" href="/examples/typed-dicts">TypedDict</a><p class="meta">type dictionary records that come from JSON</p></li><li><a class="text-link journey-item-title" href="/examples/literal-and-final">Literal and Final</a><p class="meta">express constrained values and names that should not be rebound</p></li><li><a class="text-link journey-item-title" href="/examples/callable-types">Callable Types</a><p class="meta">type functions that are passed as arguments</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 172 108" width="172" height="108" xmlns="http://www.w3.org/2000/svg"><text x="0" y="14" font-family="-apple-system, 'Source Sans Pro', sans-serif" font-size="8" fill="rgba(82, 16, 0, 0.7)" text-anchor="start" letter-spacing="0.5">X: INT|STR|NONE</text><rect x="0" y="22" width="44" height="28" fill="none" stroke="#521000" stroke-width="1.0"/><text x="22.0" y="40.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">x</text><line x1="44" y1="36" x2="74.02702716443629" y2="17.650150066177822" stroke="#521000" stroke-width="1.0"/><polygon points="80,14 75.48708719090742,20.039339200403308 72.56696713796516,15.260960931952336" fill="#521000"/><line x1="44" y1="36" x2="73.0" y2="36.0" stroke="#521000" stroke-width="1.0"/><polygon points="80,36 73.0,38.8 73.0,33.2" fill="#521000"/><line x1="44" y1="36" x2="74.02702716443629" y2="54.34984993382218" stroke="#521000" stroke-width="1.0"/><polygon points="80,58 72.56696713796516,56.73903906804766 75.48708719090742,51.960660799596695" fill="#521000"/><rect x="82" y="4" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="19.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">int</text><rect x="82" y="26" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="41.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">str</text><rect x="82" y="48" width="70" height="22" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="117.0" y="63.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">None</text></svg><figcaption>A typed slot can accept one of several shapes — `int | str | None` covers expected absence and alternatives.</figcaption></figure></section><section class="journey-section"><div><h2>Scale annotations for reusable libraries.</h2><p class="meta">Advanced typing exists to preserve information across reusable functions, containers, and decorators.</p><ul class="journey-list"><li><a class="text-link journey-item-title" href="/examples/generics-and-typevar">Generics and TypeVar</a><p class="meta">write reusable typed containers and functions</p></li><li><a class="text-link journey-item-title" href="/examples/paramspec">ParamSpec</a><p class="meta">preserve callable signatures through decorators</p></li><li><a class="text-link journey-item-title" href="/examples/overloads">Overloads</a><p class="meta">describe APIs whose return type depends on the input shape</p></li><li><a class="text-link journey-item-title" href="/examples/casts-and-any">Casts and Any</a><p class="meta">show escape hatches and their tradeoffs</p></li><li><a class="text-link journey-item-title" href="/examples/newtype">NewType</a><p class="meta">create distinct static identities for runtime-compatible values</p></li></ul></div><figure class="journey-figure"><svg viewBox="-8 -14 266 98" width="266" height="98" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="30" width="36" height="28" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="18.0" y="48.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">T</text><line x1="36" y1="44" x2="65.0" y2="44.0" stroke="#521000" stroke-width="1.0"/><polygon points="72,44 65.0,46.8 65.0,41.2" fill="#521000"/><rect x="74" y="26" width="100" height="36" fill="none" stroke="#521000" stroke-width="1.0"/><text x="80" y="23" font-family="-apple-system, 'Source Sans Pro', sans-serif" font-size="8" fill="rgba(82, 16, 0, 0.7)" text-anchor="start" letter-spacing="0.5">FN[T]</text><line x1="174" y1="44" x2="203.0" y2="44.0" stroke="#FF4801" stroke-width="1.4"/><polygon points="210,44 203.0,46.8 203.0,41.2" fill="#FF4801"/><rect x="212" y="30" width="36" height="28" fill="rgba(82, 16, 0, 0.05)" stroke="#521000" stroke-width="1.0"/><text x="230.0" y="48.0" font-family="'JetBrains Mono', 'IBM Plex Mono', Menlo, monospace" font-size="10" fill="#521000" text-anchor="middle">T</text></svg><figcaption>A generic type variable preserves shape across a call: the same T flows in and out.</figcaption></figure></section>
4141
</article>
4242

4343
</body>

public/prototyping/production-figures-gestalt.html

Lines changed: 1 addition & 1 deletion
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.be98c8af1bb8.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '8a4057af1896'
3+
HTML_CACHE_VERSION = 'd3142faf61e4'

src/marginalia.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def iterator_unroll(c: Canvas) -> None:
7373
for i in range(4):
7474
y = 8 + i * 30
7575
c.cells(20, y, items)
76-
c.caret(20 + i * 24 + 12, y)
76+
c.caret(20 + i * 24 + 12, y, emphasis=(i == 3))
7777
suffix = " — last" if i == 3 else ""
7878
c.label(124, y + 16, f"next(){suffix}")
7979

@@ -137,7 +137,7 @@ def branch_fork(c: Canvas) -> None:
137137
def loop_repetition(c: Canvas) -> None:
138138
"""The shape of a loop: walk the sequence, run the body, return."""
139139
c.cells(0, 28, ["a", "b", "c", "d"], w=28)
140-
c.caret(0 + 14, 28)
140+
c.caret(0 + 14, 28, emphasis=False)
141141
c.closed_arrow(116, 40, 142, 40, emphasis=False)
142142
c.cell(144, 28, "body", w=56, h=24)
143143
c.dashed(172, 54, 172, 76)
@@ -282,7 +282,7 @@ def union_types(c: Canvas) -> None:
282282
def generic_preservation(c: Canvas) -> None:
283283
"""Types · Scale annotations: a generic preserves the input type through the call."""
284284
c.cell(0, 30, "T", w=36, h=28, soft=True)
285-
c.closed_arrow(36, 44, 72, 44, emphasis=True)
285+
c.closed_arrow(36, 44, 72, 44, emphasis=False)
286286
c.frame(74, 26, 100, 36, label="fn[T]")
287287
c.closed_arrow(174, 44, 210, 44, emphasis=True)
288288
c.cell(212, 30, "T", w=36, h=28, soft=True)
@@ -339,7 +339,7 @@ def naming_decisions(c: Canvas) -> None:
339339
def early_exit(c: Canvas) -> None:
340340
"""Control flow · Stop as soon as the answer is known: the loop exits on first match."""
341341
c.cells(0, 28, ["a", "b", "c", "d", "e"], w=28)
342-
c.dot(70, 40, emphasis=True)
342+
c.dot(70, 40)
343343
c.closed_arrow(70, 56, 70, 78, emphasis=True)
344344
c.cell(40, 80, "found · break", w=80, h=24, soft=True)
345345
c.label(70, 14, "first match", anchor="middle")
@@ -969,9 +969,9 @@ def exception_group_peel(c: Canvas) -> None:
969969
for x in (20, 36, 52, 68):
970970
c.ghost(40, 18, x, 40)
971971
c.dot(20, 44)
972-
c.dot(36, 44, emphasis=True)
972+
c.dot(36, 44)
973973
c.dot(52, 44)
974-
c.dot(68, 44, emphasis=True)
974+
c.dot(68, 44)
975975
c.closed_arrow(90, 30, 140, 30, emphasis=True)
976976
c.label(115, 22, "except*", anchor="middle")
977977
c.tag(160, 0, "after")
@@ -1094,7 +1094,7 @@ def overload_signatures(c: Canvas) -> None:
10941094
def paramspec_preserve(c: Canvas) -> None:
10951095
"""ParamSpec · the decorator preserves the wrapped function's full signature, parameter for parameter."""
10961096
c.cell(0, 22, "f(P)", w=50, h=24)
1097-
c.closed_arrow(50, 34, 80, 34, emphasis=True)
1097+
c.closed_arrow(50, 34, 80, 34, emphasis=False)
10981098
c.frame(82, 12, 100, 44, label="@dec")
10991099
c.mono(132, 36, "P preserved")
11001100
c.closed_arrow(182, 34, 212, 34, emphasis=True)
@@ -1194,7 +1194,7 @@ def match_dispatch_ladder(c: Canvas) -> None:
11941194
for i, txt in enumerate(cases):
11951195
c.cell(0, 30 + i * 22, txt, w=170, h=20)
11961196
c.dashed(186, 32, 186, 122)
1197-
c.dot(186, 74, emphasis=True)
1197+
c.dot(186, 74)
11981198
c.closed_arrow(186, 110, 186, 124, emphasis=True)
11991199
c.label(196, 76, "first match", anchor="start")
12001200

src/marginalia_grammar.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,17 @@ def cells(self, x, y, items, *, w=CELL, h=CELL):
205205
self.cell(x + i * w, y, c, w=w, h=h)
206206
return (x, y, x + len(items) * w, y + h)
207207

208-
def caret(self, x, y_top):
209-
"""Triangular caret pointing down into the cell whose top is at y_top."""
210-
self._add(f'<polygon points="{x},{y_top - 1} {x - 4},{y_top - 7} {x + 4},{y_top - 7}" fill="{EMPHASIS}"/>')
208+
def caret(self, x, y_top, *, emphasis=True):
209+
"""Triangular caret pointing down into the cell whose top is at y_top.
210+
211+
Defaults to the orange emphasis colour because a caret typically
212+
marks the live position. Set emphasis=False when multiple carets
213+
appear in the same figure (small multiples) and the surrounding
214+
prose only names one of them — the others paint in ink so the
215+
scarce-emphasis rule still holds.
216+
"""
217+
fill = EMPHASIS if emphasis else INK
218+
self._add(f'<polygon points="{x},{y_top - 1} {x - 4},{y_top - 7} {x + 4},{y_top - 7}" fill="{fill}"/>')
211219

212220
def register(self, x, y, w, *, divisions=None, between=False):
213221
"""Hairline with regular ticks."""

0 commit comments

Comments
 (0)