โ† All docs  ยท  Home

ADR 0007 โ€” JSON-LD is primary on ingest, not interop

Context

INVARIANT 1 says the native wire format is a W3C Web Annotation (JSON-LD). But the first implementation only accepted annotations that matched our exact serde byte-shape (body as an array, compact term names, @context as our two-element array, โ€ฆ). That is not "accepting JSON-LD" โ€” it is accepting one serialization of it. A conformant annotation from Hypothesis, Annotorious, or RecogitoJS โ€” semantically identical but serialized differently (a single body object, a bare-IRI target, prefixed property names) โ€” would be rejected.

There was also a subtler problem. The dedup id and the detached signature are computed over JCS(annotation), which is serialization-dependent: the same feedback serialized two ways would produce two different content addresses and two non-comparable signatures. That undermines content-addressing and dedup โ€” the whole point of the protocol.

Decision

Normalize on ingest. protocol-lib::jsonld::from_jsonld parses any conformant serialization that uses the pinned Freedback/anno vocabulary into the canonical [Annotation] model, tolerating JSON-LD's serialization freedom:

The feedback server runs this on every POST (it is the first step of the ingest pipeline), before dedup, validation, and storage. Because normalization happens before the dedup id and signature are computed, two different serializations of the same feedback now collapse to the same content address, and a signature is verified against the normalized form.

from_jsonld(serialize(ann)) == ann for our own output (a round-trip test), so normalizing our own clients' POSTs is a no-op โ€” existing signatures keep verifying.

Why not the full json-ld crate (yet)

The Haudebourg json-ld crate is a complete processor (arbitrary @context expansion). We will need it to ingest annotations that use third-party contexts outside our vocabulary. But it is async, requires a non-trivial local-context loader to stay offline, and is partial-wasm32. The normalizer above covers the form every real W3C annotation tool actually emits (the compact anno-context form) plus the prefixed variants, is pure Rust (native + wasm, deterministic, offline), and ships green today. The full-processor path remains the documented extension (issue #12, retargeted from "interop" to "arbitrary external contexts"), behind a jsonld feature when added.

Consequences