Commits
Three tests, using the oracle the previous coverage lacked: a tree freshly
built from the surviving keyset. Asserting a delete against another code
path (lazy vs eager) only proves the two agree, which a shared bug
satisfies — comparing against a fresh build is what catches a wrong root.
- right-side collapse: removing a key whose subtree it alone occupies must
prune that subtree. Fails without the prune from the previous commit.
- height-gap intermediate: removing a height-1 key must KEEP its node as an
empty pass-through when a height-0 child survives, or the leaf gets
promoted a level and the root is non-canonical. Catches over-pruning.
- lazy stubs: an unloaded stub is indistinguishable from an empty node by
inspection, so emptiness may only be judged after loading. Currently
unreachable in our flow (pruneIfEmpty only sees children already through
ensureChildNode) but documents the invariant.
Shapes and fixture keys ported from atmos's TestRemoveCanonicality_HeightGap
and TestLazyLoadRemoveTrimsThroughUnloadedStub, which had this covered.
deleteFromNode recursed into a child subtree and marked the parent dirty
but never dropped the child when the delete emptied it. MST nodes are
content-addressed, so the emptied node serialized as a real block and
changed every ancestor CID — the tree no longer equalled the tree that
never contained the key, which is the equality commit-proof inversion
depends on. The TS reference prunes here (deleteRecurse: remove the entry
when the returned subtree has no entries); we only trimmed at the root.
Effect: verifyCommitDiff returned PrevDataMismatch for valid second
commits from a real PDS. Any repo with two records whose keys differ in
height hits it — the lower key lives alone in a subtree, and inverting
its creation must prune that subtree.
Found downstream in zlay through the atmoq relay-conformance corpus,
where the sync11/commit2-valid control was being dropped.
Existing coverage missed it three ways: the lazy-vs-eager delete test
asserts the two paths agree rather than that either is right; the
interop commit-proof fixtures only delete high keys at or above the root
layer; and the verifyCommitDiff test builds its own fixture, inverts an
update rather than a create, and ships the whole tree instead of a
partial proof. Both new tests fail without the prune: one pure-MST, and
one golden fixture generated by @atproto/repo carrying a real partial
proof.
See REPORT-mst-inversion-prevdata.md.
Dependency bump only. websocket.zig 11e0f5c -> 73429df.
The client set socket timeouts through std.posix.setsockopt, which maps
BADF/NOTSOCK/INVAL to `unreachable` ("always a race condition"). Those are
reachable on a connection socket: an upstream resetting around the handshake
aborted the process from inside FirehoseClient.connectAndRead, and
`unreachable` cannot be caught by the reconnect loop.
Found downstream in stream, whose full suite aborted roughly one run in seven
and is clean across 27 runs after. websocket.zig had already applied the same
fix server-side; this is its client-side counterpart.
zig fmt --check and zig build test pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
subscribe blocks forever by design, which is right for steady-state
consumers but leaves bounded ones (bootstrap capture, tests, graceful
shutdown) no clean exit: future.cancel lands inside a websocket read,
surfaces as a connection error, and the reconnect loop absorbs it
forever. a handler may now declare shouldStop() bool — checked before
every frame and around every reconnect — and subscribe returns cleanly
when it goes true. comptime-gated like onError/onRawFrame: handlers
without the decl are byte-for-byte unchanged.
verifyBlockHash now checks the whole CID, not just the sha digest:
CIDv0 must be exactly the 34-byte sha2-256 multihash, CIDv1 must
declare version 1 with the digest ending the CID exactly, and —
the real gap — identity-hash digests are compared against content
(previously accepted unchecked). codec stays unrestricted so any
well-formed block from a PDS still parses. valid CARs are unaffected;
only forged or malformed CIDs are newly rejected.
when the handler declares onRawFrame([]const u8), FirehoseClient
delivers raw websocket frames instead of decoded events — for
consumers that decode on their own worker threads (e.g. parallel
signature verification). the reconnect cursor still advances via
peekSeq, a cheap header+payload peek with no CAR hydration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FirehoseClient host entries follow ecosystem norms (atmos
streaming.Options.URL, jetstream --relay-url): scheme carries tls,
port rides in the URL, path is ignored (subscribeRepos is fixed by
protocol). bare hostnames remain implicit wss — existing callers
unchanged. enables pointing at non-TLS local upstreams like the
bluesky jetstream simulator (ws://localhost:7777).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the production firehose (indigo/cbor-gen) emits RFC 7049 length-first
key order, but current DAG-CBOR spec encoders (e.g. bluesky's jetstream
simulator) emit RFC 8949 bytewise order. accept either on decode —
canonical-form enforcement belongs to encode/verify, which hash raw
bytes. still rejects duplicates and orders satisfying neither.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
it had no users: no downstream consumer calls it, and even atproto-bench
assembles the resolve/fetch/verify pipeline from the primitives. its only
call sites were a network-dependent unit test that no-ops in CI and a
commented-out stress test. real consumers verify with verifyCommitCar
(zds importRepo) and verifyCommitDiff (firehose diffs), which stay.
also removes the VerifyResult type, the fetchRepo helper, the
verifyRepo-only VerifyError members (InvalidIdentifier, SigningKeyNotFound,
PdsEndpointNotFound, FetchFailed), and the now-dead imports.
breaking change: ships with the next minor bump (0.4.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
no downstream consumer calls verifyRepo — the verification API people
actually use is verifyCommitCar (zds guards importRepo with it) and
verifyCommitDiff for firehose diffs. the what's-here section now shows
that, grounded in the real zds call site, with signCommit as the produce
side. also fixes recipe 04's reference to a 'commit verification recipe'
that doesn't exist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
codex and gemini CLIs read .agents/skills natively (the agent-skills
cross-tool location); claude code only reads .claude/skills, so that path
becomes a symlink. verified skill discovery and content loading through
the symlink with a headless claude session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'trust chain' isn't atproto vocabulary — the spec language is
self-authenticating data. the verifyRepo section now leads with why anyone
runs it (relay ingest from an untrusted PDS, backup integrity, migration
checks) instead of just listing the mechanism.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
roadmap said v0.3.1; the repo is at v0.3.10. restructured to a coherent arc
(where it is -> what's next -> boundaries -> how it got here), with the
release-by-release history condensed into eras and the changelog holding
the detail. the current-status section now names both sides of the repo
lifecycle (verify + produce via signCommit) and the OAuth toolkit.
benchmarks were stale and no longer true in spirit: decode is now
226k/62k/53k/20k (zat/atmos/rsky/indigo), sig-verify is led by rust (~27k)
with zat at 21.6k, and the MST bench (5.7M records/sec, root-byte parity)
was missing entirely. numbers from atproto-bench@20636e1.
license section now links the LICENSE file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active state is now a tinted pill with link-colored text; nested groups
(recipes, heading toc) indent behind a static 1px guide instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
the overview's 'used by' h3s sit inside a closed <details>, so the sidebar
toc's scrollIntoView was a silent no-op on hidden targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setting location.hash on first load fired a synthetic hashchange, which ran
the isNav focus path and yanked focus into #content — so first Tab skipped
the skip link. replaceState seeds the route without an event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- skip link (visually hidden until focused) + tabindex=-1 on #content
- SPA route changes announce the page title via a polite live region and
move focus into the content region
- aria-current="page" on the active sidebar link; aria-label on nav landmarks
- theme toggle aria-label reflects the action it performs
- light-theme comment token #7a7a90 -> #6a6a80 (was 4.19:1, below AA)
- :focus-visible ring, prefers-reduced-motion (CSS + JS smooth-scroll gate),
forced-colors outline, scroll-padding-top for the sticky header,
header-link target size bump
verified headless: skip-link reveal/focus, live-region announcement,
focus-to-content on nav, aria-current, zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code (grounded in the zig-0.16 notes):
- adopt pub fn main(init: std.process.Init) in the networked recipes (02-05):
init.io is a real threaded backend (debug_io single-threads and was
throttling the firehose), init.gpa leak-checks on exit
- .? -> orelse return error.X on values read from fetched DID documents
(pdsEndpoint/signingKey) — a malformed doc shouldn't panic
- 01: {t} format specifier instead of @tagName
Prose (grounded in the atproto notes — identity, data, sync-verification, firehose):
- 01: at:// authority is a DID or a handle; rkeys are user-controlled
- 02: DID is the identity, handle/PDS are mutable aliases; alsoKnownAs makes the
binding bidirectional; the signing key is why you resolve at all
- 03: name the trust seam — an unverified GET; 05 is the verify side
- 04: the view is a cache, never canonical (replay to rebuild); jetstream carries
no signatures, so you trust the relay
- 05: the walk stops one step short of a proof — verify the commit signature and
that the tree hashes to commit.data_cid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-column desktop layout (>=1024px): a sticky left page tree fills what was
dead margin, with the examples expanded to their recipes and the active prose
page's headings nested beneath it. Heading anchors scroll in-page without
touching the routing hash. The header's top nav is hidden on desktop (the tree
replaces it) and restored on mobile, where the sidebar collapses away.
build-site nests the recipe list under the Examples manifest entry so the tree
can render them. Verified with headless screenshots + click tests at 1400/1600
(nav + heading scroll) and 700px (mobile fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capped-prose approach stranded a ~690px text column beside a ~480px empty
gutter in a 1168px card. A single 940px width fills the card with prose and
still fits every example's code without horizontal scroll. Verified with
headless screenshots at 1600px.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cap prose at ~74ch for readability, but let .main grow to 1040px (>=1024px)
and 1200px (>=1440px) so code blocks use the horizontal space instead of
scrolling in a 720px column.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vendor Prism (core + zig grammar), highlight rendered code blocks, and add a
theme-aware token palette for light and dark. Reflow every example so no logic
line needs horizontal scrolling: a validity() helper in recipe 01, multi-line
allocPrint and named locals in 05, extracted locals in 02/03.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recipe 05 walks a repo's CAR via com.atproto.sync.getRepo -> loadCommitFromCAR
-> Mst.loadFromBlocks -> mst.walk, tallying records per collection (atproto.com,
25 collections). Frames the walk as the substrate for backup, migration,
mirroring, verification, and indexing; cites ken, pensieve, Bailey Townsend's
backup guide, and microcosm Hubble.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real DID + post rkey (jerry.bsky.social) instead of the service DID; column-
align the output with {s:<14}; reword the borrow note away from 'view'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DebugAllocator init/deinit ceremony with std.heap.smp_allocator —
a one-line general-purpose allocator, no leak-detector scaffolding to distract
from the atproto work. The matched defer deinit/free calls still model cleanup.
Drop comments that narrated zig-language mechanics (e.g. the io-is-explicit
note) or restated the code; keep the ones that explain the atproto domain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grounds the zero-allocation behavior in the view/borrow model and cites
std.Uri.parse as the same contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recipe 04 consumes the Jetstream firehose and folds events into a live
materialized view (a per-language post tally) — the event-log / build-your-own-
view model from atproto-for-distsys-engineers. Verified against the live stream.
The examples index now supports optional editable overview prose via
examples/_overview.md, rendered above the generated recipe list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more runnable recipes: 01 parse atproto identifiers (pure, shows
validation-at-the-boundary) and 03 read a record (resolve PDS → getRecord).
Number the example filenames (01_/02_/03_) so the on-ramp order lives in the
filesystem; build-site.mjs sorts by filename instead of a hardcoded list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three tests, using the oracle the previous coverage lacked: a tree freshly
built from the surviving keyset. Asserting a delete against another code
path (lazy vs eager) only proves the two agree, which a shared bug
satisfies — comparing against a fresh build is what catches a wrong root.
- right-side collapse: removing a key whose subtree it alone occupies must
prune that subtree. Fails without the prune from the previous commit.
- height-gap intermediate: removing a height-1 key must KEEP its node as an
empty pass-through when a height-0 child survives, or the leaf gets
promoted a level and the root is non-canonical. Catches over-pruning.
- lazy stubs: an unloaded stub is indistinguishable from an empty node by
inspection, so emptiness may only be judged after loading. Currently
unreachable in our flow (pruneIfEmpty only sees children already through
ensureChildNode) but documents the invariant.
Shapes and fixture keys ported from atmos's TestRemoveCanonicality_HeightGap
and TestLazyLoadRemoveTrimsThroughUnloadedStub, which had this covered.
deleteFromNode recursed into a child subtree and marked the parent dirty
but never dropped the child when the delete emptied it. MST nodes are
content-addressed, so the emptied node serialized as a real block and
changed every ancestor CID — the tree no longer equalled the tree that
never contained the key, which is the equality commit-proof inversion
depends on. The TS reference prunes here (deleteRecurse: remove the entry
when the returned subtree has no entries); we only trimmed at the root.
Effect: verifyCommitDiff returned PrevDataMismatch for valid second
commits from a real PDS. Any repo with two records whose keys differ in
height hits it — the lower key lives alone in a subtree, and inverting
its creation must prune that subtree.
Found downstream in zlay through the atmoq relay-conformance corpus,
where the sync11/commit2-valid control was being dropped.
Existing coverage missed it three ways: the lazy-vs-eager delete test
asserts the two paths agree rather than that either is right; the
interop commit-proof fixtures only delete high keys at or above the root
layer; and the verifyCommitDiff test builds its own fixture, inverts an
update rather than a create, and ships the whole tree instead of a
partial proof. Both new tests fail without the prune: one pure-MST, and
one golden fixture generated by @atproto/repo carrying a real partial
proof.
See REPORT-mst-inversion-prevdata.md.
Dependency bump only. websocket.zig 11e0f5c -> 73429df.
The client set socket timeouts through std.posix.setsockopt, which maps
BADF/NOTSOCK/INVAL to `unreachable` ("always a race condition"). Those are
reachable on a connection socket: an upstream resetting around the handshake
aborted the process from inside FirehoseClient.connectAndRead, and
`unreachable` cannot be caught by the reconnect loop.
Found downstream in stream, whose full suite aborted roughly one run in seven
and is clean across 27 runs after. websocket.zig had already applied the same
fix server-side; this is its client-side counterpart.
zig fmt --check and zig build test pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
subscribe blocks forever by design, which is right for steady-state
consumers but leaves bounded ones (bootstrap capture, tests, graceful
shutdown) no clean exit: future.cancel lands inside a websocket read,
surfaces as a connection error, and the reconnect loop absorbs it
forever. a handler may now declare shouldStop() bool — checked before
every frame and around every reconnect — and subscribe returns cleanly
when it goes true. comptime-gated like onError/onRawFrame: handlers
without the decl are byte-for-byte unchanged.
verifyBlockHash now checks the whole CID, not just the sha digest:
CIDv0 must be exactly the 34-byte sha2-256 multihash, CIDv1 must
declare version 1 with the digest ending the CID exactly, and —
the real gap — identity-hash digests are compared against content
(previously accepted unchecked). codec stays unrestricted so any
well-formed block from a PDS still parses. valid CARs are unaffected;
only forged or malformed CIDs are newly rejected.
when the handler declares onRawFrame([]const u8), FirehoseClient
delivers raw websocket frames instead of decoded events — for
consumers that decode on their own worker threads (e.g. parallel
signature verification). the reconnect cursor still advances via
peekSeq, a cheap header+payload peek with no CAR hydration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FirehoseClient host entries follow ecosystem norms (atmos
streaming.Options.URL, jetstream --relay-url): scheme carries tls,
port rides in the URL, path is ignored (subscribeRepos is fixed by
protocol). bare hostnames remain implicit wss — existing callers
unchanged. enables pointing at non-TLS local upstreams like the
bluesky jetstream simulator (ws://localhost:7777).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the production firehose (indigo/cbor-gen) emits RFC 7049 length-first
key order, but current DAG-CBOR spec encoders (e.g. bluesky's jetstream
simulator) emit RFC 8949 bytewise order. accept either on decode —
canonical-form enforcement belongs to encode/verify, which hash raw
bytes. still rejects duplicates and orders satisfying neither.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
it had no users: no downstream consumer calls it, and even atproto-bench
assembles the resolve/fetch/verify pipeline from the primitives. its only
call sites were a network-dependent unit test that no-ops in CI and a
commented-out stress test. real consumers verify with verifyCommitCar
(zds importRepo) and verifyCommitDiff (firehose diffs), which stay.
also removes the VerifyResult type, the fetchRepo helper, the
verifyRepo-only VerifyError members (InvalidIdentifier, SigningKeyNotFound,
PdsEndpointNotFound, FetchFailed), and the now-dead imports.
breaking change: ships with the next minor bump (0.4.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
no downstream consumer calls verifyRepo — the verification API people
actually use is verifyCommitCar (zds guards importRepo with it) and
verifyCommitDiff for firehose diffs. the what's-here section now shows
that, grounded in the real zds call site, with signCommit as the produce
side. also fixes recipe 04's reference to a 'commit verification recipe'
that doesn't exist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
codex and gemini CLIs read .agents/skills natively (the agent-skills
cross-tool location); claude code only reads .claude/skills, so that path
becomes a symlink. verified skill discovery and content loading through
the symlink with a headless claude session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'trust chain' isn't atproto vocabulary — the spec language is
self-authenticating data. the verifyRepo section now leads with why anyone
runs it (relay ingest from an untrusted PDS, backup integrity, migration
checks) instead of just listing the mechanism.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
roadmap said v0.3.1; the repo is at v0.3.10. restructured to a coherent arc
(where it is -> what's next -> boundaries -> how it got here), with the
release-by-release history condensed into eras and the changelog holding
the detail. the current-status section now names both sides of the repo
lifecycle (verify + produce via signCommit) and the OAuth toolkit.
benchmarks were stale and no longer true in spirit: decode is now
226k/62k/53k/20k (zat/atmos/rsky/indigo), sig-verify is led by rust (~27k)
with zat at 21.6k, and the MST bench (5.7M records/sec, root-byte parity)
was missing entirely. numbers from atproto-bench@20636e1.
license section now links the LICENSE file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- skip link (visually hidden until focused) + tabindex=-1 on #content
- SPA route changes announce the page title via a polite live region and
move focus into the content region
- aria-current="page" on the active sidebar link; aria-label on nav landmarks
- theme toggle aria-label reflects the action it performs
- light-theme comment token #7a7a90 -> #6a6a80 (was 4.19:1, below AA)
- :focus-visible ring, prefers-reduced-motion (CSS + JS smooth-scroll gate),
forced-colors outline, scroll-padding-top for the sticky header,
header-link target size bump
verified headless: skip-link reveal/focus, live-region announcement,
focus-to-content on nav, aria-current, zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code (grounded in the zig-0.16 notes):
- adopt pub fn main(init: std.process.Init) in the networked recipes (02-05):
init.io is a real threaded backend (debug_io single-threads and was
throttling the firehose), init.gpa leak-checks on exit
- .? -> orelse return error.X on values read from fetched DID documents
(pdsEndpoint/signingKey) — a malformed doc shouldn't panic
- 01: {t} format specifier instead of @tagName
Prose (grounded in the atproto notes — identity, data, sync-verification, firehose):
- 01: at:// authority is a DID or a handle; rkeys are user-controlled
- 02: DID is the identity, handle/PDS are mutable aliases; alsoKnownAs makes the
binding bidirectional; the signing key is why you resolve at all
- 03: name the trust seam — an unverified GET; 05 is the verify side
- 04: the view is a cache, never canonical (replay to rebuild); jetstream carries
no signatures, so you trust the relay
- 05: the walk stops one step short of a proof — verify the commit signature and
that the tree hashes to commit.data_cid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-column desktop layout (>=1024px): a sticky left page tree fills what was
dead margin, with the examples expanded to their recipes and the active prose
page's headings nested beneath it. Heading anchors scroll in-page without
touching the routing hash. The header's top nav is hidden on desktop (the tree
replaces it) and restored on mobile, where the sidebar collapses away.
build-site nests the recipe list under the Examples manifest entry so the tree
can render them. Verified with headless screenshots + click tests at 1400/1600
(nav + heading scroll) and 700px (mobile fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capped-prose approach stranded a ~690px text column beside a ~480px empty
gutter in a 1168px card. A single 940px width fills the card with prose and
still fits every example's code without horizontal scroll. Verified with
headless screenshots at 1600px.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vendor Prism (core + zig grammar), highlight rendered code blocks, and add a
theme-aware token palette for light and dark. Reflow every example so no logic
line needs horizontal scrolling: a validity() helper in recipe 01, multi-line
allocPrint and named locals in 05, extracted locals in 02/03.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recipe 05 walks a repo's CAR via com.atproto.sync.getRepo -> loadCommitFromCAR
-> Mst.loadFromBlocks -> mst.walk, tallying records per collection (atproto.com,
25 collections). Frames the walk as the substrate for backup, migration,
mirroring, verification, and indexing; cites ken, pensieve, Bailey Townsend's
backup guide, and microcosm Hubble.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DebugAllocator init/deinit ceremony with std.heap.smp_allocator —
a one-line general-purpose allocator, no leak-detector scaffolding to distract
from the atproto work. The matched defer deinit/free calls still model cleanup.
Drop comments that narrated zig-language mechanics (e.g. the io-is-explicit
note) or restated the code; keep the ones that explain the atproto domain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recipe 04 consumes the Jetstream firehose and folds events into a live
materialized view (a per-language post tally) — the event-log / build-your-own-
view model from atproto-for-distsys-engineers. Verified against the live stream.
The examples index now supports optional editable overview prose via
examples/_overview.md, rendered above the generated recipe list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more runnable recipes: 01 parse atproto identifiers (pure, shows
validation-at-the-boundary) and 03 read a record (resolve PDS → getRecord).
Number the example filenames (01_/02_/03_) so the on-ramp order lives in the
filesystem; build-site.mjs sorts by filename instead of a hardcoded list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>