docs(protocol): publish the paged range-frame metadata split and -32009 - #48
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Range-frame metadata divides by whether it scales with the resource. The fixed-size identity set (root, total_length, chunk_count, plus chunk_index on a chunk-aligned window) rides EVERY frame, because root + total_length are what detect a wrong-generation holder as the first bytes arrive. The resource-scaling layout set (chunk_lens, inclusion_proof) rides the first frame or a paged prologue once per range stream, each page located by chunk_lens_offset, and is omitted entirely when a client sets skip_layout. chunk_lens is a decrypt input, not a verify input: per-chunk AES-GCM-SIV needs the whole array and a reader rejects one whose entries do not sum to total_length. That is why a large layout is paged rather than sliced, and it is stated on the page because an implementer who slices it ships a stream no client can decrypt. Also publishes the five frame bounds, records that a serve path splits on the payload cap rather than the per-request window, marks range_proof and first_chunk_index reserved, and adds -32009 RANGE_METADATA_UNREPRESENTABLE to the error taxonomy on both the prose page and the generated catalog (with its holder-fatal, not-a-transport-failure client rule). Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS (correctness gate, fresh context, read-only)
All seven checks green; zero prior review threads. Reviewed at ce764d7.
§4.3 — swept, not spot-checked
I ran a repo-wide sweep at the head SHA (not a spot-check of the touched hunks) for v0.N.N, pre-0., backward(s) compat, forward-compatib, no longer, previously, used to, deprecated, superseded, as of, and separately for first frame / first range frame / FIRST FRAME ONLY / range_proof / first_chunk_index across docs/, scripts/, and the four static/*.json. No version narration, drift commentary, or "this changed" phrasing survives on any range-frame surface, and the generator's schema descriptions were swept with the prose (OPTIONAL (v0.4.0+) → RESERVED, FIRST FRAME ONLY → identity/layout-set wording). No internal-file references and no internal issue numbers were introduced. A reader arriving cold cannot tell the rule was ever different. The remaining version-string hits in the sweep are pre-existing and unrelated to this contract (digstore-version CI pins, the chia:// scheme note, a troubleshooting release floor) — out of scope.
Technical accuracy
Checked against the superproject SYSTEM.md RPC-contract row rather than reconstructed from the crates. Identity set (root, total_length, chunk_count, chunk_index when chunk-aligned) on every frame, layout set (chunk_lens, inclusion_proof) once per stream on a first frame or paged prologue located by chunk_lens_offset, suppressed by skip_layout — all stated correctly on the protocol page, in the §10 summary row, and in the generator. The why-a-split-not-a-slice reasoning is present and correct: chunk_lens as a decrypt input needing the whole array summing to total_length, and root + total_length as the fixed-size arrival-time wrong-generation check with the per-resource-leaf reason a per-chunk proof cannot substitute. All five bounds match the canonical values exactly (65536 / 32768 / 4096 / 2048 / 2486), and the split-on-payload-cap-never-the-3-MiB-window rule is stated. -32009 carries the do-not-count-as-transport clause in all three places it appears (protocol prose, #range-errors table, error catalog) — this is the clause whose absence makes a client retry a peer that can never succeed, so I checked each occurrence individually.
The four downstream first-frame claims are each correct now — range-integrity step 1 ("the stream's chunk_lens and the frame's chunk_index"), multi-source pseudocode step 3 ("from the range stream's layout"), the §10 Streaming + range summary row (rewritten to the split), and the RangeFrame schema description. I swept for a fifth and found one candidate, noted non-gating below.
Audience separation — held
error-codes.md received the code, its meaning and the client action only. The paging mechanics, the decrypt-input reasoning and the bounds table stayed on the protocol page. The one bound value that crosses (4096) is the cause of the error and an integrator cannot act on the code without it — that is meaning, not protocol internals.
The two judgement calls
reserved over optional for range_proof / first_chunk_index — right call, and the fail-closed one. dig-nat's RangeFrame has no such fields at all, and it is the type every real producer and consumer uses. Publishing them as "optional" would invite a server to emit and a client to depend on a field the shipped wire cannot carry; MUST NOT emit / MUST NOT require is the only statement that keeps both sides conforming, and it matches the SYSTEM.md row's RESERVED.
Documenting -32009 rather than deferring — the reasoning was sound at the time it was made, independent of dig-rpc-protocol 0.6.0 having since published. dig-nat 0.13.0 answers the code on the live peer wire and enforces MAX_INCLUSION_PROOF_B64 at the encoder, so the code is reachable by a client written today; the mirror reflects an already-live wire fact rather than creating it. The specific harm of withholding is concrete: without the holder-fatal-vs-transport distinction published, a client's default is to classify it as transport and retry a peer that can never succeed. Deferring would have left a reachable error undocumented with a known wrong default.
Generated artifacts
The static/*.json diffs are confined to the RangeFrame schema, the dig.fetchRange method entry, the two -32009 catalog rows, and the version field. I enumerated every changed JSON key — no unrelated content shifted, which is the place a regeneration hides an accident. The prose table and the generator catalog agree, and the drift gate was proven load-bearing by deletion-then-restore rather than assumed.
Version
0.6.0 → 0.7.0 minor is right. error-codes.json gains a code and openrpc*.json gain chunk_count / chunk_lens_offset / skip_layout — a machine-consumable contract gained surface additively (§6.2), which is more than a patch, and nothing was removed or renumbered, so not a major.
Two non-gating notes posted inline and resolved by me so they cannot block the merge. No gating findings; nothing bars the squash-merge.
…length to any frame Three same-class fixes on the numbered surfaces this unit publishes: - the -32007 remediation pointed at the FIRST range frame for total_length, the last first-frame-scoped framing of a field that now rides every frame; - -32009 sat above -32004 in the range-error table, leaving that one table out of numeric order — the same out-of-order declaration that hid the missing row this unit exists to close; - section 7's peer-RPC table enumerates the node-profile codes a peer method adds and omitted -32009, which dig.fetchRange adds. Regenerating also picks up the OpenRPC info.version, which tracks package.json and still read 0.6.0 after the bump — a machine artifact understating its own version. Co-Authored-By: Claude <noreply@anthropic.com>
What changed
Two user-facing surfaces published a range-frame contract and an error taxonomy that no longer match the protocol.
docs/protocol/peer-network.md§9 (#range) published range-frame metadata as first-frame-only. It now publishes the split:root,total_length,chunk_count, pluschunk_indexon a chunk-aligned window — rides every frame, with the reason a protocol implementer needs:root+total_lengthare what detect a wrong-generation holder as the first bytes arrive, and each is one scalar, so it costs the same on a 3-chunk resource as a million-chunk one.chunk_lens,inclusion_proof— rides the first frame or a paged prologue, once per range stream, each page located bychunk_lens_offset, and is omitted entirely when the client setsskip_layout.chunk_lensis a decrypt input, not a verify input — per-chunk AES-256-GCM-SIV needs the whole array, and a reader rejects any array whose entries do not sum tototal_length. A per-chunk proof could not do the arrival-time check either, because merkle leaves commit per resource.MAX_FRAMED_BODY65536,MAX_RANGE_FRAME_PAYLOAD32768,MAX_INCLUSION_PROOF_B644096,MAX_CHUNK_LENS_PER_FRAME2048,MAX_FIRST_FRAME_CHUNK_LENS2486), and that a serve path splits on the payload cap, never the per-request 3 MiB window.range_proof/first_chunk_indexare now published as reserved (a server MUST NOT emit them). The page previously invited servers to emit them.skip_layoutadded to the request params.docs/support/error-codes.mdadds-32009RANGE_METADATA_UNREPRESENTABLEto the JSON-RPC prose list and table, with the client rule: holder-fatal — skip the holder for that range, do not re-request it, and do not count it as a transport failure (counted as transport, a client retries a peer that can never succeed). Also added to the §9 range-error table at protocol depth.scripts/dig-spec.mjsis the generator behindstatic/openrpc*.json+static/error-codes.json, and the repo runs a build-time drift gate between that catalog and the prose table — so the machine artifacts had to move in the same commit. TheRangeFrameschema, thedig.fetchRangemethod entry, and the error catalog all carry the new contract.Why documenting
-32009nowdig-nat0.13.0 is live on crates.io (verified against the crates.io API) and it is the crate every real producer and consumer ofRangeFrameuses — itsmux.rsalready answersRANGE_METADATA_UNREPRESENTABLE, and itsMAX_INCLUSION_PROOF_B64is enforced at the encoder as of that release. So a client written against the shipped peer wire can receive this code today, and the holder-fatal-vs-transport distinction is exactly the thing a client gets wrong without documentation.dig-rpc-protocol0.6.0 mirrors the constant, but it is a mirror of an already-published wire fact, not the thing that makes the code real. Documenting it is the coherent call; withholding it would leave a shipped, reachable error undocumented.How verified
npm ciclean;npm run buildgreen —onBrokenLinks/onBrokenAnchorsarethrow, so the new#range-errorsanchor and every touched cross-reference are build-validated. Build ran throughpostbuild(14 sitemaps, 1,680 URLs).npm run typecheck(tsc) exit 0;npm run lint(eslint .) exit 0;npm run test:unit25 pass / 0 fail / 1 skipped.npm run gendrift gate: 58 codes across 4 surfaces, gate passed.-32009row fromerror-codes.mdmakesnpm run genthrow; restoring it returns the tree clean and the gate green. The gate genuinely fails on the defect it claims to catch.Blast radius checked
gitnexus is disabled in the loop (§2.0 temp override), so radius was established by ripgrep + direct read at
origin/main, and it is a docs repo with no importable symbols:docs/,scripts/,static/*.json,i18n/forfirst frame,range_proof,first_chunk_index,chunk_lens. English hits: the#rangesection, the §9 range-error table, the multi-source pseudocode step 3, the range-integrity split step, the §10 summary row, and the generator schema — all updated. No other English page asserts the metadata rule.static/openrpc.json,static/openrpc-node.json,static/error-codes.json) are regenerated in-commit —static/knowledge-graph.jsonis regenerated too but is not in the diff, because regeneration produced byte-identical output, so the machine surface cannot lag the prose.Version
0.6.0→0.7.0— minor. New published capability on a machine-consumable contract:error-codes.jsongains a code andopenrpc*.jsongainschunk_count/chunk_lens_offset/skip_layout. An agent branching on the catalog sees an additive surface change, which is a minor, not a patch. Nothing was removed or renumbered, so it is not a major.Deferred, deliberately
Per repo convention, English only. The 13 translated locales now describe the superseded first-frame-only rule (listed in the lane report); changing that policy is not this unit's call.
Follow-up commit on this PR
Three same-class fixes plus one artifact correction, after the gate PASS on
ce764d7:error-codes.md— the-32007remediation said to readtotal_length"from the first range frame", the last first-frame-scoped framing of a field that now rides every frame. Now "any range frame".peer-network.md#range-errors—-32009was seated above-32004, leaving that one table out of numeric order. Reseated after-32008. Out-of-order declaration is what hid the missing row this unit exists to close, so it is not worth reproducing on the page that documents it.peer-network.md§7 "Peer RPC error codes" — this table enumerates the node-profile codes the peer methods add, and omitted-32009, whichdig.fetchRangeadds. Row added, ordered last. Found while reseating the other table: the same missing-row class as the defect this unit closes.static/openrpc.json+static/openrpc-node.json—info.versionis stamped frompackage.json, so both still read0.6.0after the bump. Regenerated to0.7.0; a machine artifact understating its own version is exactly the drift these generated specs exist to prevent.Drift gate re-run: 58 codes across 4 surfaces, passed.