Skip to content

Commit 3aff70b

Browse files
authored
Merge pull request #5 from adewale/claude/project-state-improvements-pcftyu
Project-state housekeeping and diagram-grammar hardening
2 parents e04e8d2 + d6feef3 commit 3aff70b

10 files changed

Lines changed: 259 additions & 95 deletions
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
name: Preview viz
1+
name: Preview
22

33
on:
4-
push:
5-
branches:
6-
- claude/tuftean-marginalia-viz-TB0fw
74
workflow_dispatch:
5+
inputs:
6+
name:
7+
description: Preview Worker name prefix
8+
required: false
9+
default: preview
10+
type: string
811

912
permissions:
1013
contents: read
1114

1215
concurrency:
13-
group: preview-viz
16+
group: preview-${{ inputs.name || 'preview' }}
1417
cancel-in-progress: true
1518

1619
jobs:
@@ -45,11 +48,11 @@ jobs:
4548
run: |
4649
set -x
4750
uv run pywrangler preview \
48-
--name viz \
51+
--name "${{ inputs.name || 'preview' }}" \
4952
--message "${{ github.sha }}" \
5053
--json
5154
- name: Smoke test deployed Preview
52-
run: scripts/smoke_deployment.py https://viz-pythonbyexample.adewale-883.workers.dev
55+
run: scripts/smoke_deployment.py "https://${{ inputs.name || 'preview' }}-pythonbyexample.adewale-883.workers.dev"
5356
- name: Dump wrangler logs on failure
5457
if: failure()
5558
run: |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Regenerate generated files
2+
3+
# Merges made through the GitHub UI bypass the local git hooks that keep
4+
# src/asset_manifest.py and friends in sync, so a merge to main can land with
5+
# stale generated files (and a red Verify run). This workflow regenerates them
6+
# and pushes a fix-up commit. Pushes made with GITHUB_TOKEN do not trigger
7+
# other workflows, so the fix-up commit is not re-verified by CI; it only ever
8+
# contains deterministic `make build` output.
9+
10+
on:
11+
push:
12+
branches:
13+
- main
14+
15+
permissions:
16+
contents: write
17+
18+
concurrency:
19+
group: regenerate-generated-files
20+
cancel-in-progress: false
21+
22+
jobs:
23+
regenerate:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.13'
30+
- name: Regenerate embedded examples and fingerprinted assets
31+
run: make build
32+
- name: Commit and push regenerated files if they drifted
33+
run: |
34+
git add -A src/example_sources_data.py src/asset_manifest.py public
35+
if git diff --cached --quiet; then
36+
echo "Generated files are current."
37+
exit 0
38+
fi
39+
git config user.name "github-actions[bot]"
40+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
41+
git commit -m "Regenerate fingerprinted assets"
42+
git pull --rebase origin main
43+
git push origin main

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,19 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0
66

77
## Unreleased
88

9+
## 2026-05-16
10+
911
### Added
1012

13+
- Production custom domain `www.pythonbyexample.dev` (with `workers.dev` as fallback).
14+
- Learning journeys: curated multi-example arcs with per-section learner outcomes and a journeys index.
15+
- Marginalia figure system: a locked SVG diagram grammar (`src/marginalia_grammar.py`), curator-owned example-cell figures and journey section figures (`src/marginalia.py`), and gestalt review pages under `/prototyping/*`.
16+
- Figure geometry contract tests covering clipping, collision, palette, caption uniqueness, and anchor resolution for every registered figure.
17+
- Quality registries (`docs/quality-registries.toml`) with criterion-level scoring, confusable-pair checks, footgun coverage, no-figure rationales, journey outcome contracts, and enforced quality-score gates.
18+
- Optional Turnstile protection for edited-code runs: secret-gated, session-scoped clearance cookie, Invisible-mode widget loaded only when the server requires a challenge, and a smoke-test bypass header.
19+
- Structured wide-event observability for the Worker, with smoke checks asserting the custom event payload.
20+
- Deployment smoke script (`scripts/smoke_deployment.py`) checking rendered pages and representative Dynamic Worker POST runs.
21+
- Footer link to the GitHub repository.
1122
- MIT license.
1223
- SEO metadata for the home page and all example pages.
1324
- SEO/cache linter for future page additions.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: test embed-examples build check-generated fingerprint browser-layout-test seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint
22

33
test:
4-
python3 -m unittest discover -s tests -v
4+
uv run --python 3.13 python -m unittest discover -s tests -v
55

66
embed-examples:
77
scripts/embed_example_sources.py

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Production: <https://www.pythonbyexample.dev> (`workers.dev` remains enabled as
66

77
## Features
88

9-
- 104 curated Python 3.13 examples in learning order
9+
- 109 curated Python 3.13 examples in learning order
1010
- Literate source/output cells for each example walkthrough
1111
- Editable complete examples powered by CodeMirror
1212
- Read-only syntax highlighting powered by Shiki
@@ -60,9 +60,11 @@ This project is developed with red-green-refactor TDD:
6060
Install dependencies with `uv`, then run:
6161

6262
```bash
63-
python3 -m unittest discover -s tests -v
63+
make test
6464
```
6565

66+
The test suite (and the example loader it imports) requires Python 3.13; `make test` runs it through `uv run --python 3.13` so a system `python3` on another version still works.
67+
6668
After cloning, install the local git hooks once so merges and rebases regenerate `src/asset_manifest.py` instead of producing conflicts:
6769

6870
```bash

docs/example-figure-rubric.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,15 @@ the figure can merge.
138138
- **Stroke-weight discipline.** Only `W_HAIRLINE`, `W_STROKE`,
139139
`W_EMPHASIS`, `W_GHOST`. *Contract 5c.*
140140
- **Emphasis scarcity, enforced.** At most ONE accent mark
141-
(`EMPHASIS`-coloured arrowhead, caret, dot, or rect stroke) per
142-
figure. Was a soft v1 criterion; now hard. *Contract 9.*
141+
(`EMPHASIS`-coloured arrowhead, caret, dot, traced path, or rect
142+
stroke) per figure. Was a soft v1 criterion; now hard. The census
143+
is semantic, counted by the grammar itself (`Canvas.accent_count`),
144+
because an arrow's shaft and a standalone gate line are
145+
indistinguishable in raw SVG. Two codified carve-outs: all gates in
146+
a figure collectively count as ONE accent (repeated structural
147+
punctuation — every pause point on a ribbon — reads as one system),
148+
and a `lanes` traced path plus its terminal dot count as one mark.
149+
A gate set plus any focal accent still fails. *Contract 9.*
143150
- **Banner-fit, enforced.** Every figure's intrinsic width
144151
(Canvas.w + 2 · PAD_X) must fit `.cell-banner--1`'s 440px max
145152
ceiling. *Contract 8.*

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.e87d4baf77e6.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'}
3-
HTML_CACHE_VERSION = '35cf77d48ca5'
3+
HTML_CACHE_VERSION = 'c09a7489d1e1'

src/marginalia.py

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,17 @@
3434

3535
def aliasing_mutation(c: Canvas) -> None:
3636
"""Two names binding to one mutable list, before and after a mutation."""
37-
c.tag(0, 12, "before")
38-
c.name_box(0, 18, "first")
39-
c.name_box(0, 48, "second")
40-
c.closed_arrow(60, 30, 86, 46, emphasis=False)
41-
c.closed_arrow(60, 60, 86, 46, emphasis=False)
42-
c.object_box(88, 32, "", '["python"]', w=88, h=28)
43-
44-
c.tag(0, 100, "after append")
45-
c.name_box(0, 108, "first")
46-
c.name_box(0, 138, "second")
47-
c.closed_arrow(60, 120, 86, 136, emphasis=False)
48-
c.closed_arrow(60, 150, 86, 136, emphasis=False)
49-
c.object_box(88, 122, "", '["python","workers"]', w=130, h=28)
37+
c.two_names_one_object(0, 18, "before", "first", "second", '["python"]', object_w=88)
38+
c.two_names_one_object(0, 108, "after append", "first", "second",
39+
'["python","workers"]', object_w=130)
5040

5141

5242
def tuple_no_mutation(c: Canvas) -> None:
5343
"""The contrast: two names binding to one immutable tuple — no mutation possible."""
54-
c.tag(0, 12, "tuple — frozen")
55-
c.name_box(0, 18, "first")
56-
c.name_box(0, 48, "second")
57-
c.closed_arrow(60, 30, 86, 46, emphasis=False)
58-
c.closed_arrow(60, 60, 86, 46, emphasis=False)
59-
c.object_box(88, 32, "", '("python",)', w=110, h=28)
60-
61-
c.tag(0, 100, "no .append")
62-
c.name_box(0, 108, "first")
63-
c.name_box(0, 138, "second")
64-
c.closed_arrow(60, 120, 86, 136, emphasis=False)
65-
c.closed_arrow(60, 150, 86, 136, emphasis=False)
66-
c.object_box(88, 122, "", '("python",)', w=110, h=28)
44+
c.two_names_one_object(0, 18, "tuple — frozen", "first", "second",
45+
'("python",)', object_w=110)
46+
c.two_names_one_object(0, 108, "no .append", "first", "second",
47+
'("python",)', object_w=110)
6748
c.label(150, 170, "tuples raise AttributeError", anchor="middle")
6849

6950

@@ -597,14 +578,7 @@ def dataclass_fields(c: Canvas) -> None:
597578

598579
def class_triangle(c: Canvas) -> None:
599580
"""Classes · instance → class → type — every Python value sits on this triangle."""
600-
c.dot(20, 28)
601-
c.label(20, 54, "instance", anchor="middle")
602-
c.closed_arrow(26, 28, 86, 28, emphasis=False)
603-
c.frame(88, 10, 60, 36, label="class")
604-
c.mono(118, 32, "Class")
605-
c.closed_arrow(148, 28, 208, 28, emphasis=False)
606-
c.frame(210, 10, 60, 36, label="type")
607-
c.mono(240, 32, "type")
581+
c.type_triangle("type", "type")
608582

609583

610584
def exception_cause_context(c: Canvas) -> None:
@@ -729,17 +703,15 @@ def sort_stability(c: Canvas) -> None:
729703
def kw_only_separator(c: Canvas) -> None:
730704
"""Keyword-only arguments · `*` divides positional from keyword-only."""
731705
c.mono(0, 18, "def f(a, b, *, c, d): …", anchor="start")
732-
# JetBrains Mono advances ~6px per char at fs=10; '*' sits at index 12.
733-
c.dashed(75, 22, 75, 38)
706+
c.mono_divider(0, 12, 22, 38) # the '*' sits at index 12
734707
c.label(33, 50, "positional or kw", anchor="middle")
735708
c.label(120, 50, "keyword only", anchor="middle")
736709

737710

738711
def positional_only_separator(c: Canvas) -> None:
739712
"""Positional-only parameters · `/` divides positional-only from positional-or-kw."""
740713
c.mono(0, 18, "def f(a, b, /, c, d): …", anchor="start")
741-
# JetBrains Mono advances ~6px per char at fs=10; '/' sits at index 12.
742-
c.dashed(75, 22, 75, 38)
714+
c.mono_divider(0, 12, 22, 38) # the '/' sits at index 12
743715
c.label(33, 50, "positional only", anchor="middle")
744716
c.label(120, 50, "positional or kw", anchor="middle")
745717

@@ -899,14 +871,7 @@ def property_fork(c: Canvas) -> None:
899871

900872
def metaclass_triangle(c: Canvas) -> None:
901873
"""Metaclasses · instance → class → metaclass; the metaclass is the type of the class."""
902-
c.dot(20, 30)
903-
c.label(20, 56, "instance", anchor="middle")
904-
c.closed_arrow(26, 30, 86, 30, emphasis=False)
905-
c.frame(88, 12, 60, 36, label="class")
906-
c.mono(118, 34, "Class")
907-
c.closed_arrow(148, 30, 218, 30, emphasis=False)
908-
c.frame(220, 12, 80, 36, label="metaclass")
909-
c.mono(260, 34, "type")
874+
c.type_triangle("metaclass", "type", third_w=80)
910875

911876

912877
def sys_path_resolution(c: Canvas) -> None:

0 commit comments

Comments
 (0)