Show error/cancel teardowns in the `Context` diagram

The `context_handshake` sequence diagram only showed the graceful
`Return` ending - underselling the whole SC pitch (errors + cancels
propagate *both* ways across the wire). Add a "... or, instead of a
graceful Return" group with the non-graceful endings,

- parent-initiated: `ctx.cancel()` (or a parent-side error) ->
  child relays `Error: ContextCancelled`,
- child-initiated: child ships `Error: <raised>` (or
  `ContextCancelled`).

Labelled `Error: ...` since `ContextCancelled` is an exception that
rides inside an `Error` wire msg, not its own msg type - keeping
the diagram's wire-level labels honest. Re-render the committed
`_diagrams/context_handshake.svg` fallback (CI has no `d2` bin) to
match.

Also cache-bust d2 images in `d2diagrams.py`: sphinx tags css/js
with `?v=<hash>` but not images, so an edited diagram kept serving
the browser's stale copy at the stable `_images/<stem>.svg` url
(cost us a manual hard-refresh). Hash each rendered svg + rewrite
only the d2 `<img src>` to `...svg?v=<hash8>` via the
`html-page-context` hook - content-addressed, so unchanged diagrams
keep their query.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
docs_vibed_for_serious
Gud Boi 2026-06-28 21:55:39 -04:00
parent 23edf03563
commit 43216c8d4a
3 changed files with 160 additions and 93 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -46,8 +46,10 @@ Usage,
from __future__ import annotations from __future__ import annotations
import enum import enum
import hashlib
import os import os
from pathlib import Path from pathlib import Path
import re
import shlex import shlex
import shutil import shutil
import subprocess as sp import subprocess as sp
@ -273,6 +275,17 @@ class D2Diagram(SphinxDirective):
) )
if width := self.options.get('width'): if width := self.options.get('width'):
img['width'] = width img['width'] = width
# content-hash the rendered svg so the served
# `<img src>` carries a `?v=<hash>` cache-buster
# (sphinx tags css/js this way but not images);
# without it an edited diagram keeps showing the
# browser's stale copy at the stable
# `_images/<stem>.svg` url on autobuild reload.
if out.exists():
img['d2_stem'] = out.stem
img['d2_cachebust'] = hashlib.sha256(
out.read_bytes()
).hexdigest()[:8]
fig = nodes.figure() fig = nodes.figure()
fig += img fig += img
classes: list[str] = ( classes: list[str] = (
@ -318,11 +331,50 @@ def _reset_seen(
_seen_outputs.clear() _seen_outputs.clear()
def _cachebust_d2_images(
app: Sphinx,
pagename: str,
templatename: str,
context: dict,
doctree,
) -> None:
'''
Append a ``?v=<content-hash>`` query to every d2
diagram ``<img src>`` in the rendered page ``body``
so a browser re-fetches an *edited* SVG instead of
its stale cached copy at the stable
``_images/<stem>.svg`` url (sphinx cache-busts
css/js this way, but not images).
Scoped strictly to d2 images (those carrying the
`d2_cachebust` attr set in `D2Diagram.run()`); all
other markup is left untouched.
'''
if doctree is None or 'body' not in context:
return
busts: dict[str, str] = {}
for img in doctree.findall(nodes.image):
if ver := img.get('d2_cachebust'):
busts[img['d2_stem']] = ver
if not busts:
return
body: str = context['body']
for stem, ver in busts.items():
body = re.sub(
r'(src="[^"]*?' + re.escape(stem) + r'\.svg)(")',
rf'\1?v={ver}\2',
body,
)
context['body'] = body
def setup(app: Sphinx) -> dict: def setup(app: Sphinx) -> dict:
app.add_config_value('d2_bin', None, 'env') app.add_config_value('d2_bin', None, 'env')
app.add_config_value('d2_args', [], 'env') app.add_config_value('d2_args', [], 'env')
app.add_directive('d2', D2Diagram) app.add_directive('d2', D2Diagram)
app.connect('builder-inited', _reset_seen) app.connect('builder-inited', _reset_seen)
app.connect('html-page-context', _cachebust_d2_images)
return { return {
'version': '0.1.0', 'version': '0.1.0',
# NB: run() writes the rendered SVG during the # NB: run() writes the rendered SVG during the

View File

@ -19,3 +19,15 @@ parent -> child: "Yield: stream.send()"
child -> parent: "Yield: stream.send()" child -> parent: "Yield: stream.send()"
parent -> child: "Stop: stream closed" parent -> child: "Stop: stream closed"
child -> parent: "Return: fn return value" child -> parent: "Return: fn return value"
# the non-graceful endings: errors/cancels always
# propagate, both ways (`ContextCancelled` rides in
# an `Error` msg, hence the `Error: ...` labels).
"... or, instead of a graceful Return": {
parent-initiated: {
parent -> child: "ctx.cancel() | parent-side error"
child -> parent: "Error: ContextCancelled"
}
child-initiated: {
child -> parent: "Error: child raised | ContextCancelled"
}
}