Skip to content

Commit dceb834

Browse files
committed
Audit unsupported-cell prose; fix three more examples with the same bug
The networking fix exposed a pattern: any :::unsupported block where the prose leads with "Dynamic Workers do not provide X" copies the runtime caveat instead of explaining the code. An audit across all four examples that contain :::unsupported blocks (networking, subprocesses, threads-and-processes, virtual-environments) found the same prose shape in the three I hadn't yet fixed. TDD: RED — tests/test_example_content.py adds Contract 11 (UnsupportedCellProseContract). The heuristic: each unsupported cell's prose must reference at least 2 code identifiers from its own code block (variable names, function calls, method names). Two is the floor that proves the prose discusses this specific code rather than a generic Workers note. Pre-fix, the contract flagged: - virtual-environments 1 ident referenced ("venv") - threads-and-processes 0 idents referenced - subprocesses 2 idents (passed bar, but still weak — fixed for consistency) GREEN — rewrite all three prose blocks so each explains what ITS specific code does: subprocesses "subprocess.run spawns a child Python interpreter, captures stdout/stderr (capture_output=True), decodes as text (text=True), and raises if it exits non-zero (check=True). The returned result holds the captured streams and exit code as portable evidence the child ran. (Runtime caveat moved to closing parenthetical.)" threads-and-processes "ThreadPoolExecutor runs square across two worker threads sharing the same interpreter (and the GIL); ProcessPoolExecutor runs pow across two child processes with isolated memory. Each pool.map returns an iterator over results in input order, and the surrounding with block joins the workers when the body exits." virtual-environments "venv.EnvBuilder configures the description of a new environment, then create('.venv') materialises it on disk as a directory containing its own interpreter and site-packages. with_pip=False skips bootstrapping pip." REFACTOR — Contract 11 now passes; 58 tests total. The second audit dimension (figure-caption vs example-title keyword overlap) found 24 suspect attachments. Spot-checking confirms all 24 are false positives: captions use different word forms than titles (e.g., "mutable" vs "mutability", "comprehension" vs "comprehensions") but are conceptually aligned. No contract added — heuristic too noisy without manual review.
1 parent d7e84c2 commit dceb834

5 files changed

Lines changed: 84 additions & 4 deletions

File tree

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.f9a6740c684b.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '4a27d9ac9a3e'
3+
HTML_CACHE_VERSION = 'c48d15294056'

src/example_sources/subprocesses.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ print(result.returncode)
3131
:::
3232

3333
:::unsupported
34-
Dynamic Workers do not provide child processes.
34+
`subprocess.run` spawns a child Python interpreter, captures its stdout and stderr (`capture_output=True`), decodes them as text (`text=True`), and raises `CalledProcessError` if the child exits non-zero (`check=True`). The returned `result` holds the captured streams and exit code as portable evidence the child ran. (This fragment runs in standard Python only — Dynamic Workers don't provide child processes.)
3535

3636
```python
3737
result = subprocess.run(

src/example_sources/threads-and-processes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ print(ProcessPoolExecutor.__name__)
2929
:::
3030

3131
:::unsupported
32-
Dynamic Workers do not provide native threads or child processes.
32+
`ThreadPoolExecutor` runs `square` across two worker threads sharing the same interpreter (and the GIL); `ProcessPoolExecutor` runs `pow` across two child processes with isolated memory. Each `pool.map` returns an iterator over results in input order, and the surrounding `with` block joins the workers when the body exits. (This fragment runs in standard Python only — Dynamic Workers don't provide native threads or child processes.)
3333

3434
```python
3535
with ThreadPoolExecutor(max_workers=2) as pool:

src/example_sources/virtual-environments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ with tempfile.TemporaryDirectory() as directory:
3030
:::
3131

3232
:::unsupported
33-
Dynamic Workers do not provide the `venv` module or a project environment workflow.
33+
`venv.EnvBuilder` configures the description of a new environment, then `create(".venv")` materialises it on disk as a directory containing its own interpreter and `site-packages`. `with_pip=False` skips bootstrapping pip — useful when the venv is for an isolated tool that doesn't need to install third-party packages. (This fragment runs in standard Python only — Dynamic Workers don't provide the `venv` module or a project environment workflow.)
3434

3535
```python
3636
builder = venv.EnvBuilder(with_pip=False)

tests/test_example_content.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Content contracts for example markdown sources.
2+
3+
These contracts check pedagogical correctness rather than geometry:
4+
does each cell's prose explain the code in that cell? Does the
5+
unsupported-cell prose lead with the lesson or with a runtime
6+
caveat?
7+
8+
The contracts complement tests/test_marginalia_geometry.py
9+
(geometry, palette, registration) by catching content-shaped bugs
10+
that geometry contracts can't see.
11+
"""
12+
from __future__ import annotations
13+
14+
import re
15+
import unittest
16+
17+
from src.example_loader import load_examples
18+
19+
20+
_, EXAMPLES = load_examples()
21+
22+
23+
def _code_identifiers(code: str) -> set[str]:
24+
"""Return identifier-like tokens from a code block.
25+
26+
Strips Python keywords and the common stdlib names that appear
27+
in nearly every example (`print`, `import`) so the audit
28+
measures the lesson-specific vocabulary, not the language.
29+
"""
30+
keywords = {
31+
"def", "return", "import", "from", "as", "if", "else",
32+
"elif", "try", "except", "finally", "with", "for", "in",
33+
"and", "or", "not", "is", "true", "false", "none",
34+
"print", "pass", "raise", "while", "break", "continue",
35+
"class", "lambda", "yield", "global", "nonlocal",
36+
}
37+
return {w for w in re.findall(r"\b[a-z_][a-z_0-9]+\b", code.lower())
38+
if w not in keywords and len(w) > 1}
39+
40+
41+
class UnsupportedCellProseContract(unittest.TestCase):
42+
"""Contract 11: every :::unsupported cell's prose explains the
43+
code, not just the runtime constraint.
44+
45+
The :::unsupported block is rendered on production pages as a
46+
walkthrough cell with prose + code. When the prose only says
47+
'Dynamic Workers do not provide X', the reader sees the
48+
constraint but no pedagogical content. The fix is to lead with
49+
what the code does and move the runtime caveat to a closing
50+
parenthetical (or to the Notes section).
51+
52+
Heuristic: each unsupported cell's prose must mention at least
53+
two code identifiers — variable names, function calls, or
54+
method names from the code block. Two is the minimum that
55+
proves the prose discusses *this specific code* rather than
56+
a generic note about Workers.
57+
"""
58+
59+
MIN_IDENT_OVERLAP = 2
60+
61+
def test_unsupported_prose_mentions_code(self):
62+
failures: list[str] = []
63+
for ex in EXAMPLES:
64+
for cell in ex.get("cells", []):
65+
if cell.get("kind") != "unsupported":
66+
continue
67+
prose = " ".join(cell.get("prose", [])).lower()
68+
idents = _code_identifiers(cell.get("code", ""))
69+
hits = sum(1 for ident in idents if ident in prose)
70+
if hits < self.MIN_IDENT_OVERLAP:
71+
failures.append(
72+
f"{ex['slug']}: unsupported cell prose references "
73+
f"{hits} code identifier(s) (need ≥ {self.MIN_IDENT_OVERLAP}); "
74+
f"prose looks generic. idents: {sorted(idents)[:5]}…"
75+
)
76+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
77+
78+
79+
if __name__ == "__main__":
80+
unittest.main()

0 commit comments

Comments
 (0)