Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ make build

## Before opening a pull request

Install the committed dependency locks and start the local Worker with the repository-pinned Wrangler:
Use Node 22, install the committed dependency locks, and start the local Worker with the repository-pinned Wrangler:

```bash
node --version # v22.x
uv sync --locked --all-groups
npm ci --ignore-scripts
make dev
Expand Down
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
PY := uv run --python 3.13
NODE_DEPS_STAMP := node_modules/.package-lock.json

.PHONY: node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint
.PHONY: check-node-version node-deps test embed-examples embed-editorial-registry build-search-index build check-generated fingerprint prototypes browser-layout-test search-ranking-test social-cards check-social-cards seo-cache-lint verify-examples check-registry-integrity check-confusable-pairs check-broad-surface-tours check-footgun-coverage check-notes-supported check-program-covers-cells check-prose-duplication check-inline-links score-example-criteria check-quality-scores check-no-figure-rationales check-journey-outcomes audit-example-graph quality-checks rubric-audit format-examples verify-python-version verify smoke-deployment dev deploy lint

$(NODE_DEPS_STAMP): package.json package-lock.json
check-node-version:
@major="$$(node -p 'process.versions.node.split(".")[0]')"; \
if test "$$major" != "22"; then \
echo "Node 22 is required for Pywrangler/Pyodide (found $$(node --version))." >&2; \
exit 1; \
fi

$(NODE_DEPS_STAMP): package.json package-lock.json | check-node-version
npm ci --ignore-scripts

node-deps: $(NODE_DEPS_STAMP)
node-deps: check-node-version $(NODE_DEPS_STAMP)

test:
$(PY) python -m unittest discover -s tests -v
Expand Down Expand Up @@ -117,5 +124,5 @@ smoke-deployment:
$(PY) scripts/smoke_deployment.py $(URL)

deploy: node-deps check-generated
uv run --group workers pywrangler sync
uv run --group workers pywrangler sync --force
uv run --group workers pywrangler deploy
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Production: <https://www.pythonbyexample.dev> (`workers.dev` is disabled so the
- Learner-behavior reporting from Worker wide events (`docs/learner-analytics.md`)
- A quality-gate suite for registries, rubric scores, coverage, and teaching structure that fails builds when content regresses
- `/about` page describing the verification pipeline, runner sandbox, figure grammar, and live design tokens
- `/privacy` notice covering runner submissions, operational logs, Invisible Turnstile, cookies, and optional browser CDNs
- Copy buttons on read-only source cells
- Keyboard prev/next navigation (`←`/`→`) on example pages
- "Copy link" sharing of the runner's current code via `#code=` URLs
Expand Down Expand Up @@ -67,9 +68,10 @@ This project is developed with red-green-refactor TDD:
2. Implement the smallest change that makes it pass.
3. Refactor while keeping tests green.

Install the exact committed Python and Node dependency sets, then run:
Use Node 22 (Pywrangler's Pyodide runtime is not compatible with Node 26), then install the exact committed Python and Node dependency sets:

```bash
node --version # v22.x
uv sync --locked --all-groups
npm ci --ignore-scripts
make test
Expand Down Expand Up @@ -231,5 +233,6 @@ Use the active Cloudflare-supported Python version.
- Dynamic Worker IDs include Python version, example slug, and submitted code hash.
- Dynamic Workers run with `globalOutbound: null` and tight CPU/subrequest limits.
- POST example runs carry `Cache-Control: no-store` and are never cached.
- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` install/use that local version through Pywrangler.
- Wrangler is an exact dev dependency in `package-lock.json`; `make dev` and `make deploy` require Node 22 and install/use that local version through Pywrangler.
- `make deploy` forces a fresh Pywrangler vendor sync so an empty or stale `python_modules/` directory cannot produce a dependency-free upload.
- Production still requires the account-level WAF/rate-limit rule documented in `docs/turnstile-runner-protection-spec.md`; repository settings alone do not prove that external control is active.
3 changes: 2 additions & 1 deletion docs/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ git diff --check
- **When every score is green, resist score inflation and look for unsupported claims.** The current steady state is intentionally boring: one documented `hello-world` waiver, no below-target example diagrams, no below-target journey figures, no orphaned examples, and no stale weak-section backlog. Future audits should preserve that baseline by finding semantic drift, note claims without cells, weak `See also` edges, or journey sections that no longer match their support examples — not by raising 9.0 scores to 9.5 without new evidence.
- **Journey audit is graph audit plus outcome audit.** A journey section is healthy only when its examples form a prerequisite-respecting mental map and its declared outcomes are backed by cells on those examples. A section can have a beautiful figure and still fail if the support list is a catalog slice, if the `See also` graph isolates one example, or if the section caption describes a conceptual shift the examples do not actually make.
- **A green total can still hide a weak criterion.** The journey-figure audit found every section figure above the project gate while 15 sections still reused a lesson paint function, which weakened the independence-from-lesson-figures dimension. Bespoke runtime, control-flow, iteration, types, and reliability section figures cleared that watchlist; keep tracking criterion-level weakness even when the aggregate score remains shippable.
- **Deployment tooling must fail before an empty Python vendor upload.** Pywrangler 1.9.3's Pyodide interpreter requires Node 22; under Node 26 its Node flag fails and a stale sync token can leave `python_modules/` containing only metadata while the sync command still reports success. Pin Node 22, force `pywrangler sync --force` before deploy, and confirm the Wrangler module table includes FastAPI's vendored modules rather than `Vendored Modules 0.00 KiB`.
- **Deployment smoke belongs beside CI, and POST smoke must assert rendered output.** `scripts/smoke_deployment.py` checks rendered Worker pages, runtime-boundary pages, journey pages, prototype review pages, and representative Dynamic Worker POST runs for HTTP failures, exception markers, and stale edited-code output. Build success is not enough; the deployed Worker must render and execute edited examples. With Turnstile enabled, submitted code can appear in the editor textarea even when it did not run, so POST smoke must inspect the output panel rather than searching the whole HTML document.
- **Observability smoke should assert the custom event, not the whole tail envelope.** Use unique `x-request-id` values, exercise cache miss/hit/bypass and client-error paths, and assert on the structured payload inside `logs[].message[]`. If Turnstile is enabled, Dynamic Worker error-path probes need the smoke bypass secret; otherwise they only verify the Turnstile-fail path.
- **Turnstile should be secret-gated, session-scoped, and invisible until needed.** Protect edited-code POST runs only when `TURNSTILE_SECRET_KEY` and an explicit challenge mode are configured, lazy-load/render the Invisible-mode widget only after the server returns a challenge-required marker, and issue a signed clearance cookie so normal session runs skip Siteverify. The Cloudflare widget mode is `Invisible`; the client-side render option is `execution: "execute"`, not `size: "invisible"`. If production smoke must POST through a protected endpoint, use a separate `PBE_SMOKE_BYPASS_SECRET` header so smoke remains a deployment check rather than a CAPTCHA solver. See `docs/turnstile-runner-protection-spec.md` for the full runner-protection design.
Expand All @@ -135,7 +136,7 @@ git diff --check
- **Dark mode for locked-palette SVGs: change the mat, not the art.** The marginalia grammar hardcodes the light palette in every figure, and recolouring 109 figures would have meant touching the locked grammar and re-auditing geometry. Instead, dark mode renders each figure on a light "paper" chip (`--figure-paper` background, small padding, rounded corners). The figures stay byte-identical, read as intentional artifacts, and the grammar's palette constraint survives.
- **Theme changes need live browser state, not just first-paint CSS.** Read-only Shiki blocks are rendered with the current light/dark theme and rerender when the OS preference changes. CodeMirror rebuilds its theme compartment from the same `matchMedia` change signal. Both also retain readable plain-text fallbacks when the optional CDN graph is unavailable.
- **Rasterized artifacts need structural gates, not byte-parity gates.** Social-card bytes vary across Chrome versions and platforms, so CI validates deterministic input provenance, the exact expected JPEG filename set, JPEG decodability, and 1200×630 dimensions rather than comparing raster bytes. The SEO linter separately verifies that every `og:image` URL resolves to a committed card.
- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes the current 119-card set via CDP navigation and `Page.captureScreenshot` clips.
- **Social cards should reuse the curated figure set.** Each example's card composes its marginalia figure beside the title and summary, so a shared link carries the same diagram the page teaches with. `render_first_figure(slug)` is the only new marginalia surface the card builder needed; one Chrome session rasterizes the current 120-card set via CDP navigation and `Page.captureScreenshot` clips.
- **Copy that describes data state goes stale silently.** Journey meta descriptions still claimed "explicit placeholders for missing examples" long after the last gap placeholder was filled, and the gap-rendering UI suggested holes that no longer existed. Prose that asserts a property of the data ("all examples run," "placeholders mark gaps") should either be derived from the data or covered by a check; otherwise fixing the data falsifies the copy.
- **Make targets that import the example loader must go through uv.** The loader executes example code that uses 3.12+ syntax (`type UserId = int`), so any script importing `src.app` breaks under an older system `python3` even though its shebang says `python3`. CI masks this by installing 3.13 as the default interpreter. Build steps that import the catalog (`build_search_index`, `build_social_cards`) run as `uv run --python 3.13 scripts/...` in the Makefile so local machines with an older `python3` behave like CI.
- **Index notes text, not just titles, and normalize at build time.** The search index concatenates the slug words and every note line, lowercased at build time, so concept queries ("walrus", "GIL"-style vocabulary that titles avoid) hit the right example and the client never re-normalizes entry content. Exporting the ranking function from `search.js` lets a plain Node script (`make search-ranking-test`) assert ranking behaviour against the real generated index without a JS test framework.
Expand Down
2 changes: 1 addition & 1 deletion docs/pr-evidence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ These artifacts provide reviewable evidence for the runner, journey-page, and so
| [Runtime journey page](pr-6-journey-runtime.png) | `/journeys/runtime` renders its overview, first learning outcome, figure, and linked examples at a 1200×1260 desktop viewport. | `a85bbf7` | `eb1db35d5ff7fc7974ad3a3891b694aaa5f8f277ba291b94a24c62767340e04d` |
| [`/journeys` social card](../../public/og/journeys.jpg) | The exact 1200×630 JPEG referenced by the `/journeys` `og:image` tag. | `a85bbf7` | `3042997bb2b3acd9e2ab74106f5b4d3d4c58dafcfe23f1ddc548ed39e4548a3c` |

`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies its provenance, the exact 119-card JPEG set, and each card's JPEG decodability and 1200×630 dimensions.
`public/og/journeys.jpg` is a tracked PR artifact and its deterministic HTML input is recorded in `public/og/manifest.json`. `make check-social-cards` verifies its provenance, the exact 120-card JPEG set, and each card's JPEG decodability and 1200×630 dimensions.

Regenerate each page with its commit checked out, serve the rendered `/examples/values` HTML plus `public/` on a local HTTP server, then run:

Expand Down
4 changes: 2 additions & 2 deletions docs/turnstile-runner-protection-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ The following non-secret Worker vars are checked into `wrangler.jsonc`:

Do **not** put secret keys in `wrangler.jsonc`.

Manual browser verification and production smoke with `PBE_SMOKE_BYPASS_SECRET` both passed after deploying session-scoped Invisible-mode Turnstile.
Manual browser verification and production smoke with `PBE_SMOKE_BYPASS_SECRET` both passed after deploying session-scoped Invisible-mode Turnstile. A privacy notice is published at `/privacy` and linked from every page footer.

Docs:

Expand All @@ -393,7 +393,7 @@ Docs:

Invisible mode note: Cloudflare documents that invisible widgets have no visual footprint, and that widget `size` values are `normal`, `flexible`, or `compact`; invisibility is selected by widget mode, not by `size="invisible"`.

Privacy note: Cloudflare documents that using Invisible mode requires referencing the Cloudflare Turnstile Privacy Addendum in your own privacy policy.
Privacy note: Cloudflare documents that using Invisible mode requires referencing the Cloudflare Turnstile Privacy Addendum in your own privacy policy. The globally linked `/privacy` notice now includes that addendum, Cloudflare links, cookie details, runner processing, operational-log fields, and optional CDN disclosures.

## Remaining TODO

Expand Down
51 changes: 3 additions & 48 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"scripts": {
"wrangler": "wrangler"
},
"engines": {
"node": "22.x"
},
"devDependencies": {
"wrangler": "4.110.0"
}
Expand Down
1 change: 1 addition & 0 deletions public/og/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"paramspec.jpg": "bd96b51b333f30496c90b4bed08edf867e583258c208786dab8690acbd7ac727",
"partial-functions.jpg": "fd8ae2052a7d6b1cb17418111fb5083887b0903c19ef37394777b5ab304ad0f0",
"positional-only-parameters.jpg": "93d22d48edb838e725b7a1f7bf285e72dad09bb56d151f21eb91ecc3f36ddff8",
"privacy.jpg": "3eb2a28f8ace3441f6b95ad43facefaa78dabd11c0061bbfc70ef7a5b15c4fa7",
"properties.jpg": "f42d17549e38b8b4ee8b18797deee917495ed75807da28bdcfca05db27c66a9b",
"protocols.jpg": "fee2838223a57c3352a848f4988ada030e48bd9b4bb36e496682f68e6556fa2c",
"recursion.jpg": "d3287842bdefa7ad12e8634f98e394e47d4b1990f7efcd8aa60a9bc418997830",
Expand Down
Binary file added public/og/privacy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions scripts/build_social_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,21 @@ def render_about_social_card_html() -> str:
)


def render_privacy_social_card_html() -> str:
return _card_shell(
_card_body(
eyebrow="Python By Example · Privacy",
title="Privacy and data use",
summary="How the runner, operational logs, Turnstile, cookies, and optional browser dependencies handle data.",
)
)


def card_html() -> dict[str, str]:
cards = {
"about": render_about_social_card_html(),
"home": render_home_card_html(),
"privacy": render_privacy_social_card_html(),
"journeys": render_journeys_index_social_card_html(),
}
cards.update({example["slug"]: render_social_card_html(example) for example in list_examples()})
Expand Down
Loading
Loading