dig-rpc-protocol 0.10.1

Canonical DIG-node JSON-RPC protocol: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server logic. (Formerly dig-rpc-types.)
Documentation
//! Band invariants for [`ErrorCode`]: a new code must not collide with a numeric
//! range this ecosystem has already published.
//!
//! An error code is a permanent, published branch key — a consumer switches on it
//! to decide whether to retry a holder or abandon it. A new variant landing inside
//! a band another subsystem already published is therefore not a cosmetic defect:
//! it makes two different failures indistinguishable at exactly the moment the
//! client must tell them apart.
//!
//! Plain numeric uniqueness across the whole enum is already asserted by
//! `error::tests::all_codes_unique_and_complete` in the crate itself; this file
//! covers what that test cannot see — the published BAND a number falls in.

use dig_rpc_protocol::error::ErrorCode;

/// The onion / private-retrieval band, published normative in `SYSTEM.md` →
/// "Canonical DIG-node RPC interface". `-32021` in particular is
/// `PRIVACY_REQUIRES_LOCAL_NODE` and MUST NOT be reused.
const ONION_BAND: std::ops::RangeInclusive<i32> = -32022..=-32020;

/// **Proves:** `RANGE_METADATA_UNREPRESENTABLE` exists, sits outside the onion
/// band, and is not `-32021`.
///
/// It is load-bearing rather than decorative: dig-nat's encoder directs a holder
/// to answer this code when a resource's metadata alone cannot fit a frame — a
/// permanent property of that holder's view of the resource. A client that cannot
/// tell it from an ordinary transport failure will retry a holder that can never
/// succeed, which is why it needs its own number rather than a shared one.
#[test]
fn range_metadata_unrepresentable_is_outside_the_onion_band() {
    let code = ErrorCode::RangeMetadataUnrepresentable.code();

    assert_ne!(code, -32021, "-32021 is PRIVACY_REQUIRES_LOCAL_NODE");
    assert!(
        !ONION_BAND.contains(&code),
        "code {code} falls inside the published onion band {ONION_BAND:?}"
    );
    assert_eq!(
        ErrorCode::RangeMetadataUnrepresentable.machine_code(),
        "RANGE_METADATA_UNREPRESENTABLE"
    );
    assert!(
        ErrorCode::RangeMetadataUnrepresentable.is_jsonrpc_reserved(),
        "the code must live in the JSON-RPC implementation-defined server band"
    );
}

/// **Proves:** the module-level rustdoc taxonomy table lists EVERY code in
/// [`ErrorCode::ALL`], with the same number and variant name.
///
/// That table is the taxonomy published on docs.rs, and it is where a reimplementer
/// builds its branch table from — so a code missing there is a code that
/// reimplementation treats as a generic server error, which for
/// `RANGE_METADATA_UNREPRESENTABLE` means retrying a holder that can never serve the
/// range. The table was hand-maintained beside the enum and had already drifted once
/// by exactly one row, so the agreement is asserted mechanically rather than trusted.
#[test]
fn the_rustdoc_taxonomy_table_lists_every_code() {
    // The source of the module whose doc comment IS the published table. Read at
    // compile time so this test cannot go stale against a moved file.
    const ERROR_MODULE_SOURCE: &str = include_str!("../src/error.rs");

    let table: String = ERROR_MODULE_SOURCE
        .lines()
        .take_while(|line| line.starts_with("//!"))
        .collect::<Vec<_>>()
        .join("\n");

    for code in ErrorCode::ALL {
        let row = format!("| `{}` | [`{:?}`]", code.code(), code);
        assert!(
            table.contains(&row),
            "the rustdoc taxonomy table is missing a row for {} ({}); expected to find {row:?}",
            code.machine_code(),
            code.code()
        );
    }
}

/// **Proves:** the new code is reachable through the OpenRPC error catalogue, so a
/// peer that discovers the surface learns it can be answered with this code.
///
/// dig-node's serve leg answers it and dig-download's client branches on it; both
/// discover the taxonomy from this document rather than from Rust.
#[test]
fn range_metadata_unrepresentable_is_published_in_openrpc() {
    let doc = dig_rpc_protocol::openrpc::openrpc_document("0.0.0-test");
    let catalogue = doc["components"]["x-dig-errors"]
        .as_array()
        .expect("error catalogue is an array");

    let entry = catalogue
        .iter()
        .find(|e| e["machineCode"] == "RANGE_METADATA_UNREPRESENTABLE")
        .expect("RANGE_METADATA_UNREPRESENTABLE is absent from the OpenRPC catalogue");

    assert_eq!(
        entry["code"],
        ErrorCode::RangeMetadataUnrepresentable.code(),
        "the published number must match the enum"
    );
}

/// **Proves:** every published code lies inside the JSON-RPC server-error range
/// this crate advertises, and every variant in `ALL` contributes a distinct number.
///
/// # What actually guards against a duplicate, measured
///
/// `rustc` rejects two variants sharing a discriminant outright — setting
/// `ContentMissInconclusive` to `-32009` (the value it was first written as, already
/// owned by `RangeMetadataUnrepresentable`) does not fail this test, it fails to
/// COMPILE with `E0081: discriminant value -32009 assigned more than once`. So the
/// uniqueness half below is a restatement of a compiler guarantee, kept because it
/// also covers what `E0081` cannot see: a variant silently missing from
/// [`ErrorCode::ALL`], which is the hand-maintained list that drives the OpenRPC
/// catalogue and this crate's published taxonomy.
///
/// The RANGE half is the load-bearing assertion and is independently falsifiable:
/// `-31999` compiles perfectly, serializes perfectly, and is outside the band, so a
/// consumer's JSON-RPC layer may legitimately refuse it as a non-server error.
///
/// The collision this taxonomy is actually exposed to is a consumer HAND-WRITING an
/// integer it believes is free — which is what dig-node did with `-32009`, and
/// which no test in this crate can reach. That risk is addressed by publishing the
/// number in the rustdoc table and the OpenRPC catalogue (asserted by the sibling
/// tests here) so a consumer never has to guess.
#[test]
fn every_published_code_is_unique_and_in_the_jsonrpc_server_range() {
    use std::collections::HashMap;

    let mut seen: HashMap<i32, ErrorCode> = HashMap::new();
    for &code in ErrorCode::ALL {
        let n = code.code();

        if let Some(&prior) = seen.get(&n) {
            panic!(
                "error code {n} is claimed by BOTH {} and {} — a client cannot \
                 branch on it, so one of them must be renumbered to a free value",
                prior.machine_code(),
                code.machine_code()
            );
        }
        seen.insert(n, code);

        assert!(
            code.is_jsonrpc_reserved(),
            "{} = {n} lies outside the JSON-RPC implementation-defined server range \
             (-32768..=-32000) this crate publishes",
            code.machine_code()
        );
    }

    assert_eq!(
        seen.len(),
        ErrorCode::ALL.len(),
        "every variant in ALL must contribute a distinct number"
    );
}

/// **Proves:** `CONTENT_MISS_INCONCLUSIVE` sits on `-32017` and on neither of the
/// two numbers it was actually written as first.
///
/// A targeted companion to the general uniqueness test above: it names both wrong
/// numbers, so a revert to either fails with the reason rather than with an
/// arithmetic mismatch. `-32009` is holder-FATAL
/// (`RANGE_METADATA_UNREPRESENTABLE`); `-32015` is dig-node's released
/// `METADATA_TOO_LARGE` and is now declared here as such.
#[test]
fn content_miss_inconclusive_does_not_squat_range_metadata_unrepresentable() {
    let inconclusive = ErrorCode::ContentMissInconclusive;

    assert_eq!(inconclusive.code(), -32017);
    assert_ne!(
        inconclusive.code(),
        ErrorCode::MetadataTooLarge.code(),
        "-32015 is METADATA_TOO_LARGE, a released dig-node code catalogued on          docs.dig.net; it was picked here as \"the next free code\" against this          crate's own list, which is precisely the check that does not work"
    );
    assert_ne!(
        inconclusive.code(),
        ErrorCode::RangeMetadataUnrepresentable.code(),
        "-32009 is RANGE_METADATA_UNREPRESENTABLE, which is holder-FATAL; this code \
         means the opposite (keep looking, this holder is still eligible)"
    );
    assert!(
        !ONION_BAND.contains(&inconclusive.code()),
        "must not fall inside the published onion band"
    );
    assert_eq!(inconclusive.machine_code(), "CONTENT_MISS_INCONCLUSIVE");
}

/// The numbers this ecosystem is MEASURED to occupy outside this crate, and the
/// canonical name each one carries.
///
/// # Why a hand-checked list, and what it can and cannot prove
///
/// The collision this crate is actually exposed to is invisible from inside it.
/// `rustc` already rejects two variants sharing a discriminant (`E0081`), so a
/// uniqueness test over [`ErrorCode`] can never fail — every real collision here
/// has been with a number a CONSUMER emits as a bare integer, or that
/// docs.dig.net publishes, neither of which Rust can see. `-32009` was the first
/// (`RANGE_METADATA_UNREPRESENTABLE`, hand-written in dig-node); `-32015` was the
/// second (`METADATA_TOO_LARGE`, released and catalogued) — both chosen as "the
/// next free code" against this crate's own list, where both genuinely were free.
///
/// So occupancy is brought IN as data. It is a snapshot, produced by scanning the
/// superproject:
///
/// ```text
/// for n in $(seq 32000 32059); do
///   rg -c -- "-$n" modules -g '!crates/00-foundation/dig-rpc-protocol/**'
/// done
/// ```
///
/// **It can go stale**, and a stale snapshot cannot see a code a consumer adds
/// tomorrow. Refreshing it means re-running that scan across `modules/` and
/// reconciling every hit — which is exactly the step that was skipped twice, and
/// having it written down is what makes skipping it visible. It is not a
/// substitute for the consumer-side guard (dig-node's
/// `no_local_wire_code_collides_with_a_different_canonical_code`); the two look at
/// the same hazard from opposite ends, and the owning crate is the one that must
/// not hand out a duplicate.
///
/// `None` means the number is emitted as a bare integer with no published machine
/// name. Such a number is RESERVED: this crate must not declare it at all, because
/// declaring it under any name would silently give one wire number two meanings.
const CONSUMER_OCCUPANCY: &[(i32, Option<&str>)] = &[
    // dig-node, absorbed by dig-rpc-protocol 0.10.0 (numbers unchanged).
    (-32015, Some("METADATA_TOO_LARGE")),
    (-32016, Some("PUSH_PENDING_LIMITED")),
    (-32050, Some("NO_IDENTITY")),
    (-32051, Some("NO_PEER_NETWORK")),
    (-32052, Some("SEND_FAILED")),
    // dig-node, held and NOT yet absorbed — reserved against reassignment.
    // `cache.pushCapsule` refuses an unsigned push over the peer surface with a
    // bare -32001 (dig-node-core `seams/capsule/push_capsule.rs`); it has no
    // published machine name, so it cannot be declared here without inventing one.
    (-32001, None),
    (-32033, Some("CONTROL_INGRESS_LIMITED")),
    (-32040, Some("WALLET_NO_CHAIN_SOURCE")),
    (-32041, Some("WALLET_NOT_SYNCED")),
    (-32042, Some("WALLET_READ_FAILED")),
    (-32043, Some("WALLET_RATE_LIMITED")),
];

/// **Proves:** no declared code contradicts the measured consumer occupancy —
/// neither by claiming an occupied number under a DIFFERENT name, nor by claiming
/// a number whose consumer meaning has no published name at all.
///
/// This is the guard that would have caught both collisions. Reverting
/// `ContentMissInconclusive` to `-32015` fails it by name — `-32015` is
/// `METADATA_TOO_LARGE` — rather than by an arithmetic mismatch, and reverting it
/// to `-32009` still fails to COMPILE, which is the other half of the same defence.
#[test]
fn no_declared_code_contradicts_the_measured_consumer_occupancy() {
    for &(number, consumer_name) in CONSUMER_OCCUPANCY {
        let declared = ErrorCode::ALL.iter().find(|c| c.code() == number);

        match (declared, consumer_name) {
            (Some(code), Some(name)) => assert_eq!(
                code.machine_code(),
                name,
                "{number} is declared here as {} but a consumer emits it as {name};                  one wire number cannot carry two meanings — renumber the new code,                  never the released one",
                code.machine_code()
            ),
            (Some(code), None) => panic!(
                "{number} is declared here as {} but a consumer already emits it as a                  bare integer with no published name, so its meaning cannot be                  reconciled — pick a number this ecosystem does not use",
                code.machine_code()
            ),
            // Not declared here: a consumer-held number, correctly left alone.
            (None, _) => {}
        }
    }
}

/// **Proves:** the five codes 0.10.0 absorbed are all present, each under the
/// consumer's existing name and number.
///
/// Separate from the test above because that one passes vacuously for any number
/// this crate simply does not declare — including these five, if a later refactor
/// dropped one from `ALL`. A code missing from `ALL` is invisible to the OpenRPC
/// catalogue, which is how `-32015` stayed unknown to this taxonomy long enough to
/// be handed out twice.
#[test]
fn the_absorbed_consumer_codes_are_declared_with_their_released_numbers() {
    for (number, name) in [
        (-32015, "METADATA_TOO_LARGE"),
        (-32016, "PUSH_PENDING_LIMITED"),
        (-32050, "NO_IDENTITY"),
        (-32051, "NO_PEER_NETWORK"),
        (-32052, "SEND_FAILED"),
    ] {
        let code = ErrorCode::ALL
            .iter()
            .find(|c| c.machine_code() == name)
            .unwrap_or_else(|| panic!("{name} is absent from ErrorCode::ALL"));

        assert_eq!(
            code.code(),
            number,
            "{name} is a released, catalogued wire number and MUST stay {number}"
        );
    }
}