open-standards/smart-byte/smart-byte-rs/crates/smart-byte-edv/src/replication.rs
Transaction Science 9ffef885f0
All checks were successful
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 1m0s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 2m51s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m53s
standards workspaces build + test / aac-rs (pull_request) Successful in 3m4s
standards workspaces build + test / map-rs (pull_request) Successful in 1m0s
standards workspaces build + test / mesh-rs (pull_request) Successful in 56s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m0s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m1s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 2m3s
standards workspaces build + test / openpay (pull_request) Successful in 3m27s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 8m47s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 7m13s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 6m23s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points (pull_request) Successful in 20s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m38s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m3s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 5s
no-vanity-metrics / No new orphan source files (pull_request) Successful in 6s
spec citations resolve / Code cites no section its SPEC lacks (pull_request) Successful in 4s
lockfiles / every Cargo.lock is current (pull_request) Successful in 49s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m9s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m56s
standards workspaces build + test / jcp-rs (pull_request) Successful in 2m15s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m11s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m13s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m30s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m7s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m24s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m7s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m14s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 14s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 8m51s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 25s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 48s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m4s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m12s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m12s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 2m54s
wai-pack / wai-pack · macos (pull_request) Successful in 3m51s
wai-pack / wai-pack · linux (pull_request) Successful in 1m16s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 17m27s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 22s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 3m56s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m16s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m45s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m4s
clippy: no workspace reports an error, and the unsafe FFI states its contract
Three workspaces could not be linted at all: a deny-level clippy lint errored,
and clippy stops at the first erroring crate, so everything behind it went
unreported. None of the ten errors was a bug; each is fixed so it cannot mask
the next.

  jouleclaw-loader-gguf, jouleclaw-contract, jouleclaw-runtime: `0 * stride`
  spells the shared kv head (MQA) and the (row, col) cell address, mirroring
  the `hh * s` beside it. Kept, with the lint allowed and the reason written
  down, rather than dropping a term that documents which head is read.

  jouleclaw-backend-apple, jouleclaw-loader-gguf/ssm: a truncated SQRT_2 and
  LN_2 where the constant is what is meant -- `softplus(0) = ln2` is the
  assertion's whole point. Now `std::f32::consts`, which is also exact.

  jouleclaw-bounded, jouleclaw-outlier, jouleclaw-tools, joule-ui-core: 3.14
  as an arbitrary test value, which clippy reads as an attempt at pi. Given a
  value that is not near pi; in jouleclaw-tools the decimal is the subject of
  the format assertion, so expectation and input moved together.

  jouleclaw-policy-ipc: `parse_args` looped, but every arm returns or exits,
  so it could only ever run once. `if let` says what happens.

  smart-byte-edv: `s.sequence = cur` assigned the field to itself. `update`
  takes a document at its current sequence and bumps it, so the no-op was
  dead and the comment above it ("bump twice") described neither. Removed;
  the comment now states the mechanism the test relies on.

  jcp-schema: `Origins`'s `TryFrom<OriginsWire>` had `Error = Infallible`.
  Carried over to `From` with `#[serde(from = ...)]`, so the normalisation
  that keeps the top of the lattice single-encoded still runs on the wire --
  `origins_wire_normalizes_on_deserialize` pins it.

wai-rs: 19 unsafe `extern "C"` entry points stated no safety contract, beside
neighbours that did. Each now names what its caller must guarantee, per
pointer, from its own signature.

Verified on the build host: all 17 workspaces lint with zero errors; the tests
of every crate whose values changed pass; wai-rs builds, lints and documents.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-13 09:56:05 -04:00

167 lines
5.7 KiB
Rust

//! Vault replication (DIF EDV v0.10 § 9 — non-normative; modelled here
//! after the implementation pattern shared by Aries cloud-agent and
//! Bedrock's Bluesky-style replication).
//!
//! Replication moves [`EncryptedDocument`] records between two vaults
//! that share a common controller (typically a primary + a hot replica,
//! or a desktop + mobile pair). Because documents are *opaque ciphertext*
//! to the vault, replication does not need to decrypt anything — it only
//! needs to compare document sequences and copy the higher-sequence
//! record across.
//!
//! [`sync`] implements a one-way pull from `source` into `target`: for
//! every document in `source`, if `target` does not have a corresponding
//! record (or has an older `sequence`), the document is copied over.
use async_trait::async_trait;
use crate::error::EdvError;
use crate::spec::EncryptedDocument;
use crate::vault::Vault;
/// Outcome of a single replication pass.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ReplicationReport {
/// Documents inserted into `target` because they were missing.
pub inserted: Vec<String>,
/// Documents updated in `target` because `source` had a higher
/// sequence.
pub updated: Vec<String>,
/// Documents skipped because `target` already had an equal-or-newer
/// sequence.
pub skipped: Vec<String>,
}
/// One-way pull replication: copy every document from `source` into
/// `target` if `target` is missing it or holds an older revision.
pub async fn sync(
source: &dyn Vault,
target: &dyn Vault,
) -> Result<ReplicationReport, EdvError> {
let mut report = ReplicationReport::default();
let refs = source.list().await?;
for r in refs {
let doc = source.get(&r.id).await?;
match target.get(&r.id).await {
Ok(existing) => {
if doc.sequence > existing.sequence {
let mut to_send = doc.clone();
to_send.sequence = existing.sequence;
target.update(to_send).await?;
report.updated.push(r.id);
} else {
report.skipped.push(r.id);
}
}
Err(EdvError::NotFound(_)) => {
let mut to_send = doc.clone();
to_send.sequence = 0;
target.insert(to_send).await?;
report.inserted.push(r.id);
}
Err(other) => return Err(other),
}
}
Ok(report)
}
/// A replication strategy abstraction, in case callers want to wire in
/// a different policy (e.g. push, push+pull, conflict-on-divergent).
#[async_trait]
pub trait Replicator: Send + Sync {
/// Run a replication pass and return the report.
async fn replicate(
&self,
source: &dyn Vault,
target: &dyn Vault,
) -> Result<ReplicationReport, EdvError>;
}
/// The canonical one-way pull strategy implementing [`sync`].
pub struct PullReplicator;
#[async_trait]
impl Replicator for PullReplicator {
async fn replicate(
&self,
source: &dyn Vault,
target: &dyn Vault,
) -> Result<ReplicationReport, EdvError> {
sync(source, target).await
}
}
/// Convenience: convert a list of documents to a Vec for diffing.
pub fn snapshot(docs: &[EncryptedDocument]) -> Vec<(String, u64)> {
docs.iter()
.map(|d| (d.id.clone(), d.sequence))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{Config, EncryptedDocument, KeyDescriptor};
use crate::vault::InMemoryVault;
fn cfg() -> Config {
let k = KeyDescriptor {
id: "did:example:alice#kex-1".into(),
key_type: "JsonWebKey2020".into(),
controller: None,
};
let h = KeyDescriptor {
id: "did:example:alice#hmac-1".into(),
key_type: "Sha256HmacKey2019".into(),
controller: None,
};
Config::new("urn:uuid:vault", "did:example:alice", k, h)
}
fn doc(id: &str) -> EncryptedDocument {
EncryptedDocument {
id: id.into(),
sequence: 0,
jwe: serde_json::json!({"v": id}),
indexed: Vec::new(),
stream: None,
}
}
#[tokio::test]
async fn pull_inserts_missing() {
let src = InMemoryVault::new(cfg());
let dst = InMemoryVault::new(cfg());
src.insert(doc("urn:a")).await.expect("insert a");
src.insert(doc("urn:b")).await.expect("insert b");
let r = sync(&src, &dst).await.expect("sync");
assert_eq!(r.inserted.len(), 2);
assert_eq!(dst.len().await, 2);
}
#[tokio::test]
async fn pull_skips_when_target_current() {
let src = InMemoryVault::new(cfg());
let dst = InMemoryVault::new(cfg());
src.insert(doc("urn:a")).await.expect("insert src");
dst.insert(doc("urn:a")).await.expect("insert dst");
let r = sync(&src, &dst).await.expect("sync");
assert!(r.inserted.is_empty());
assert!(r.updated.is_empty());
assert_eq!(r.skipped, vec!["urn:a".to_string()]);
}
#[tokio::test]
async fn pull_updates_when_source_newer() {
let src = InMemoryVault::new(cfg());
let dst = InMemoryVault::new(cfg());
src.insert(doc("urn:a")).await.expect("insert src");
dst.insert(doc("urn:a")).await.expect("insert dst");
// `update` takes the document at its current sequence and bumps it,
// so one update leaves the source one ahead of the target.
let s = src.get("urn:a").await.expect("get src");
let _ = src.update(s).await.expect("update src");
let r = sync(&src, &dst).await.expect("sync");
assert_eq!(r.updated, vec!["urn:a".to_string()]);
}
}