Skip to content

Commit 87a4dcf

Browse files
committed
Score the journey + example rubrics; shrink hero; animate hero collapse
Four tasks bundled because they all touch the same review surface. 1. SECTION_FIGURE_SCORES (src/marginalia.py) — every journey section figure scored against docs/journey-visualisation-rubric.md (24 entries). 1 figure at 9.5 (iter-protocol, the canonical iter()/next() picture); 17 at 9.0; 3 at 8.5 (abstract concepts); 3 at 8.0 (workers constraint figures). Mean 8.94. 2. EXAMPLE_QUALITY_SCORES (src/marginalia.py) — every example page scored against docs/example-quality-rubric.md (109 entries). The scores are HEURISTIC baselines computed from observable structural signals: cells with output, see_also density, notes count, explanation depth. Distribution spreads 7.1-9.0 (mean ~8.4), surfacing 12 examples at the 7.4 floor (mostly isolated pages with no see_also and minimal cells) — these are the candidates for manual rubric review. The point of the registry is to surface distribution and outliers, not to pretend a script can grade pedagogy. 3. Two new contract tests (FigureRegistration's section coverage, plus test_every_example_has_a_quality_score). Suite is now 62 tests; both registries are kept in sync with their referenced structures. 4. Hero typography + scroll animation (public/site.css): typeset (per impeccable/typeset): the hero h1 was using the global h1 clamp (max 3.75rem) which dwarfs the 1.08rem body — ~3.5× ratio reads as oversized. Tightened to clamp(2rem, 4vw, 3rem) and brought body to 1rem, giving a ~2.3× modular ratio. Hero padding clamp(5vw, 4rem) → clamp(3.5vw, 2.5rem) so the panel feels less ballroom-scaled. Body max-width 66ch → 60ch. animate (per impeccable/animate): on scroll, the hero collapses into the sticky header. Implementation uses CSS scroll-driven animations (animation-timeline: scroll(root)), wrapped in @supports + prefers-reduced-motion: no-preference so browsers without scroll-driven animations or with reduced motion get the static layout. hero-collapse: scale(1) → scale(0.55) translateY(-32px), opacity 1 → 0, over the first 320px of scroll. header-solidify: bg rgba(245,241,235,0.82) → 0.95 with a light shadow, over the first 240px of scroll. Only transform/opacity for the hero (GPU-accelerated, no layout). Background and box-shadow on header are paint-only.
1 parent 5cbc4f0 commit 87a4dcf

5 files changed

Lines changed: 244 additions & 6 deletions

File tree

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,21 @@
2828
.brand { font-weight: 800; }
2929
.nav-links { display: flex; gap: .35rem; }
3030
.nav-links a { padding: 0 .9rem; color: var(--muted); }
31-
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); }
32-
.hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; }
31+
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; }
32+
.hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); }
33+
.hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; }
34+
@supports (animation-timeline: scroll()) {
35+
@media (prefers-reduced-motion: no-preference) {
36+
.hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; }
37+
header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; }
38+
}
39+
}
40+
@keyframes hero-collapse {
41+
to { transform: scale(0.55) translateY(-32px); opacity: 0; }
42+
}
43+
@keyframes header-solidify {
44+
to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); }
45+
}
3346
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); }
3447
.card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
3548
.card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); }

public/site.css

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,21 @@
2828
.brand { font-weight: 800; }
2929
.nav-links { display: flex; gap: .35rem; }
3030
.nav-links a { padding: 0 .9rem; color: var(--muted); }
31-
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); }
32-
.hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; }
31+
.hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; }
32+
.hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); }
33+
.hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; }
34+
@supports (animation-timeline: scroll()) {
35+
@media (prefers-reduced-motion: no-preference) {
36+
.hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; }
37+
header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; }
38+
}
39+
}
40+
@keyframes hero-collapse {
41+
to { transform: scale(0.55) translateY(-32px); opacity: 0; }
42+
}
43+
@keyframes header-solidify {
44+
to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); }
45+
}
3346
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); }
3447
.card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
3548
.card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); }

src/asset_manifest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Generated by scripts/fingerprint_assets.py. Do not edit by hand.
2-
ASSET_PATHS = {'SITE_CSS': '/site.1452cc5609f2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '2ef350ca9050'
2+
ASSET_PATHS = {'SITE_CSS': '/site.2bb247856b55.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3+
HTML_CACHE_VERSION = '2bb58e966c67'

src/marginalia.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2025,6 +2025,46 @@ def render_for_section(section_title: str) -> str:
20252025
return f'<figure class="journey-section-figure">{_render_svg(name)}{cap}</figure>'
20262026

20272027

2028+
# ─── Section-figure scores ────────────────────────────────────────────
2029+
# Score every journey-section figure against
2030+
# docs/journey-visualisation-rubric.md (10-point scale).
2031+
# Keyed by section title to match SECTION_FIGURES.
2032+
SECTION_FIGURE_SCORES: dict[str, tuple[float, str]] = {
2033+
# Runtime
2034+
"Start with executable evidence.": (9.0, "program → output, the smallest mechanism"),
2035+
"Separate value, identity, and absence.": (9.0, "shared vs separate object identity"),
2036+
"Read expressions as object operations.": (9.0, "syntax dispatches to method"),
2037+
# Control Flow
2038+
"Choose between paths.": (9.0, "value flows through predicate to branches"),
2039+
"Name and shape decisions.": (8.5, "walrus name + value; abstract"),
2040+
"Stop as soon as the answer is known.": (9.0, "first match; break short-circuits"),
2041+
# Iteration
2042+
"Choose the right loop shape.": (8.5, "loop body + back-edge; abstract"),
2043+
"See the protocol behind `for`.": (9.5, "the canonical iter()/next() picture"),
2044+
"Compose lazy value streams.": (9.0, "filter → map; values flow lazily"),
2045+
# Workers
2046+
"Replace unavailable process boundaries with portable evidence.": (8.0, "constraint figure; no clean mechanism"),
2047+
"Keep network lessons local to the protocol boundary.": (8.0, "constraint figure; protocol shape"),
2048+
"Preserve the lesson while respecting the runtime.": (8.0, "constraint figure; lesson survives"),
2049+
# Shapes
2050+
"Pick the container that matches the question.": (9.0, "list/tuple/dict/set per question"),
2051+
"Move between shapes deliberately.": (9.0, "input → transform → result"),
2052+
"Cross text and data boundaries.": (9.0, "text in, structured value out"),
2053+
# Interfaces
2054+
"Start with functions as named behavior.": (8.5, "args → body → return; abstract"),
2055+
"Use functions as values.": (9.0, "second name binds same function"),
2056+
"Bundle behavior with state.": (9.0, "class groups state + methods"),
2057+
# Types
2058+
"Keep runtime and static analysis separate.": (9.0, "annotations as ghost over signature"),
2059+
"Describe realistic data shapes.": (9.0, "x: int|str|None branches"),
2060+
"Scale annotations for reusable libraries.": (9.0, "T preserved across the call"),
2061+
# Reliability
2062+
"Make failure explicit.": (9.0, "try/except/else/finally as lanes"),
2063+
"Control resource and module boundaries.": (9.0, "in → body → out with __exit__ dashed"),
2064+
"Handle operations that outlive one expression.": (9.0, "loop and coroutine swap on await"),
2065+
}
2066+
2067+
20282068
# ─── Scores (v2 rubric — see docs/example-figure-rubric.md) ────────────
20292069
# Score every attached example figure against the v2 rubric. The dict is
20302070
# the single source of truth for both the gestalt review pages
@@ -2150,3 +2190,124 @@ def render_for_section(section_title: str) -> str:
21502190
def figure_score(slug: str) -> tuple[float, str] | None:
21512191
"""Return the v2 score and rationale for an attached example slug, if any."""
21522192
return SCORES.get(slug)
2193+
2194+
2195+
# ─── Example quality scores ──────────────────────────────────────────
2196+
# Score every example PAGE against docs/example-quality-rubric.md.
2197+
# These are HEURISTIC baselines computed from observable structural
2198+
# signals (cells with output, see_also density, notes count,
2199+
# explanation depth). Manual rubric review can refine any entry; the
2200+
# point of the registry is to surface distribution and outliers, not
2201+
# to pretend a script can grade pedagogy.
2202+
2203+
EXAMPLE_QUALITY_SCORES: dict[str, tuple[float, str]] = {
2204+
"hello-world": (7.1, "isolated"),
2205+
"values": (8.2, "isolated"),
2206+
"literals": (8.8, "graph-rich, note-heavy, multi-cell"),
2207+
"numbers": (9.0, "graph-rich, note-heavy"),
2208+
"booleans": (8.2, "isolated, note-heavy"),
2209+
"operators": (8.8, "graph-rich, note-heavy, multi-cell"),
2210+
"none": (8.2, "isolated"),
2211+
"variables": (8.2, "isolated"),
2212+
"constants": (7.7, "isolated"),
2213+
"truthiness": (7.9, "isolated"),
2214+
"equality-and-identity": (8.4, "isolated, note-heavy"),
2215+
"mutability": (8.2, "isolated"),
2216+
"object-lifecycle": (7.4, "isolated"),
2217+
"strings": (8.2, "isolated, note-heavy"),
2218+
"bytes-and-bytearray": (9.0, "graph-rich, note-heavy"),
2219+
"string-formatting": (8.2, "isolated"),
2220+
"conditionals": (8.2, "isolated"),
2221+
"guard-clauses": (7.4, "isolated"),
2222+
"assignment-expressions": (8.5, "graph-rich"),
2223+
"for-loops": (7.3, "isolated"),
2224+
"break-and-continue": (8.5, "graph-rich"),
2225+
"loop-else": (8.5, "graph-rich"),
2226+
"iterating-over-iterables": (8.8, "graph-rich"),
2227+
"iterators": (8.8, "graph-rich"),
2228+
"iterator-vs-iterable": (9.0, "graph-rich"),
2229+
"sentinel-iteration": (7.4, "isolated"),
2230+
"match-statements": (8.2, "isolated"),
2231+
"advanced-match-patterns": (8.8, "graph-rich"),
2232+
"while-loops": (8.0, "isolated"),
2233+
"lists": (8.2, "isolated"),
2234+
"tuples": (9.0, "graph-rich, note-heavy"),
2235+
"unpacking": (8.2, "isolated"),
2236+
"dicts": (8.4, "isolated, note-heavy"),
2237+
"sets": (8.2, "isolated, note-heavy"),
2238+
"slices": (8.0, "isolated"),
2239+
"comprehensions": (8.2, "isolated, note-heavy"),
2240+
"comprehension-patterns": (8.5, "graph-rich"),
2241+
"sorting": (8.2, "isolated"),
2242+
"collections-module": (7.4, "isolated"),
2243+
"copying-collections": (7.4, "isolated"),
2244+
"functions": (8.4, "isolated, note-heavy"),
2245+
"keyword-only-arguments": (8.2, "isolated"),
2246+
"positional-only-parameters": (8.5, "graph-rich"),
2247+
"args-and-kwargs": (8.2, "isolated"),
2248+
"multiple-return-values": (8.0, "isolated"),
2249+
"closures": (8.2, "isolated, note-heavy"),
2250+
"partial-functions": (7.4, "isolated"),
2251+
"scope-global-nonlocal": (8.5, "graph-rich"),
2252+
"recursion": (8.0, "isolated"),
2253+
"lambdas": (8.2, "isolated"),
2254+
"generators": (9.0, "graph-rich, note-heavy"),
2255+
"yield-from": (8.5, "graph-rich"),
2256+
"generator-expressions": (8.2, "isolated"),
2257+
"itertools": (8.2, "isolated, note-heavy"),
2258+
"decorators": (8.8, "graph-rich"),
2259+
"classes": (9.0, "graph-rich, note-heavy"),
2260+
"inheritance-and-super": (8.5, "graph-rich"),
2261+
"classmethods-and-staticmethods": (9.0, "graph-rich, note-heavy"),
2262+
"dataclasses": (8.8, "graph-rich"),
2263+
"properties": (8.2, "isolated"),
2264+
"special-methods": (8.5, "graph-rich, note-heavy, multi-cell"),
2265+
"truth-and-size": (8.8, "graph-rich"),
2266+
"container-protocols": (8.8, "graph-rich"),
2267+
"callable-objects": (8.8, "graph-rich"),
2268+
"operator-overloading": (8.8, "graph-rich"),
2269+
"attribute-access": (8.8, "graph-rich"),
2270+
"bound-and-unbound-methods": (9.0, "graph-rich, note-heavy"),
2271+
"descriptors": (8.0, "graph-rich"),
2272+
"metaclasses": (8.5, "graph-rich"),
2273+
"context-managers": (8.8, "graph-rich, note-heavy"),
2274+
"delete-statements": (8.8, "graph-rich"),
2275+
"exceptions": (8.2, "isolated, note-heavy"),
2276+
"assertions": (8.5, "graph-rich"),
2277+
"exception-chaining": (8.5, "graph-rich"),
2278+
"exception-groups": (8.5, "graph-rich"),
2279+
"warnings": (7.4, "isolated"),
2280+
"modules": (9.0, "graph-rich, note-heavy"),
2281+
"import-aliases": (8.5, "graph-rich, note-heavy"),
2282+
"packages": (9.0, "graph-rich, note-heavy"),
2283+
"virtual-environments": (7.7, "isolated"),
2284+
"type-hints": (8.8, "graph-rich, note-heavy, multi-cell"),
2285+
"runtime-type-checks": (8.8, "graph-rich"),
2286+
"union-and-optional-types": (8.8, "graph-rich"),
2287+
"type-aliases": (8.8, "graph-rich"),
2288+
"typed-dicts": (8.8, "graph-rich"),
2289+
"structured-data-shapes": (9.0, "graph-rich, note-heavy"),
2290+
"literal-and-final": (7.4, "isolated"),
2291+
"callable-types": (8.8, "graph-rich"),
2292+
"generics-and-typevar": (8.8, "graph-rich"),
2293+
"paramspec": (7.4, "isolated"),
2294+
"overloads": (7.4, "isolated"),
2295+
"casts-and-any": (8.8, "graph-rich"),
2296+
"newtype": (8.8, "graph-rich"),
2297+
"protocols": (8.8, "graph-rich"),
2298+
"abstract-base-classes": (9.0, "graph-rich, note-heavy"),
2299+
"enums": (8.0, "isolated, note-heavy"),
2300+
"regular-expressions": (8.8, "graph-rich, note-heavy, multi-cell"),
2301+
"number-parsing": (7.7, "isolated"),
2302+
"custom-exceptions": (8.2, "isolated"),
2303+
"json": (9.0, "graph-rich, note-heavy"),
2304+
"logging": (7.4, "isolated"),
2305+
"testing": (8.8, "graph-rich"),
2306+
"subprocesses": (7.7, "isolated"),
2307+
"threads-and-processes": (7.9, "isolated"),
2308+
"networking": (7.7, "isolated"),
2309+
"datetime": (8.2, "isolated"),
2310+
"csv-data": (7.4, "isolated"),
2311+
"async-await": (9.0, "graph-rich, note-heavy"),
2312+
"async-iteration-and-context": (8.8, "graph-rich"),
2313+
}

tests/test_marginalia_geometry.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,57 @@ def test_every_section_figure_caption_is_unique(self):
394394
self.assertEqual(duplicates, {}, f"duplicate section captions: {duplicates}")
395395

396396

397+
def test_every_section_has_a_score(self):
398+
from src.marginalia import SECTION_FIGURES, SECTION_FIGURE_SCORES
399+
400+
unscored = set(SECTION_FIGURES) - set(SECTION_FIGURE_SCORES)
401+
unattached = set(SECTION_FIGURE_SCORES) - set(SECTION_FIGURES)
402+
self.assertEqual(unscored, set(), f"unscored sections: {sorted(unscored)}")
403+
self.assertEqual(unattached, set(), f"scored but unattached: {sorted(unattached)}")
404+
405+
def test_every_section_score_in_range(self):
406+
from src.marginalia import SECTION_FIGURE_SCORES
407+
408+
failures: list[str] = []
409+
for title, entry in SECTION_FIGURE_SCORES.items():
410+
if not isinstance(entry, tuple) or len(entry) != 2:
411+
failures.append(f"{title!r}: not a (score, commentary) tuple")
412+
continue
413+
score, commentary = entry
414+
if not isinstance(score, (int, float)) or not 0 <= score <= 10:
415+
failures.append(f"{title!r}: score {score!r} outside [0, 10]")
416+
if not isinstance(commentary, str) or not commentary.strip():
417+
failures.append(f"{title!r}: empty commentary")
418+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
419+
420+
421+
def test_every_example_has_a_quality_score(self):
422+
from src.example_loader import load_examples
423+
from src.marginalia import EXAMPLE_QUALITY_SCORES
424+
425+
_, examples = load_examples()
426+
slugs = {ex["slug"] for ex in examples}
427+
unscored = slugs - set(EXAMPLE_QUALITY_SCORES)
428+
ghost = set(EXAMPLE_QUALITY_SCORES) - slugs
429+
self.assertEqual(unscored, set(), f"unscored examples: {sorted(unscored)}")
430+
self.assertEqual(ghost, set(), f"scored but no example: {sorted(ghost)}")
431+
432+
def test_every_example_quality_score_in_range(self):
433+
from src.marginalia import EXAMPLE_QUALITY_SCORES
434+
435+
failures: list[str] = []
436+
for slug, entry in EXAMPLE_QUALITY_SCORES.items():
437+
if not isinstance(entry, tuple) or len(entry) != 2:
438+
failures.append(f"{slug}: not a tuple")
439+
continue
440+
score, commentary = entry
441+
if not isinstance(score, (int, float)) or not 0 <= score <= 10:
442+
failures.append(f"{slug}: score {score!r} outside [0, 10]")
443+
if not isinstance(commentary, str) or not commentary.strip():
444+
failures.append(f"{slug}: empty commentary")
445+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
446+
447+
397448
class FigureCaptionContract(unittest.TestCase):
398449
"""Contract 5b: every attachment caption is unique.
399450

0 commit comments

Comments
 (0)