โ† All docs  ยท  Home

ADR 0024 โ€” Replies & threaded discussion via oa:replying

Context

Text feedback (comments, and now issues) should be able to start a discussion: a reader replies to a comment, someone replies to that reply, and a thread forms. The question is how to model a reply without inventing vocabulary, and how threads relate to the rest of the annotation graph.

Freedback annotations already have identities โ€” a server-assigned id and, more importantly, a content-address dedup_id. So a comment is itself a resource with a URI, and "feedback on a comment" is just an annotation whose target is that comment.

Does W3C standardize this? Yes, exactly. The Web Annotation Vocabulary (W3C Recommendation, 23 Feb 2017) defines the replying motivation (https://www.w3.org/TR/annotation-vocab/#replying, verified against https://www.w3.org/ns/oa.ttl, 2026-07-07):

oa:replying (IRI http://www.w3.org/ns/oa#replying, an oa:Motivation) โ€” "The motivation for when the user intends to reply to a previous statement, either an Annotation or another resource."

The vocabulary's own Example 58 is precisely this shape โ€” a reply annotation whose oa:hasTarget is another annotation's IRI:

<http://example.org/anno57> a oa:Annotation ;
    oa:hasBody [ a oa:TextualBody ; rdf:value "A reply to a question" ] ;
    oa:hasTarget <http://example.com/anno1> ;
    oa:motivatedBy oa:replying .

There is no first-class oa:Thread / oa:Discussion class in the vocabulary (the full motivation set is assessing, bookmarking, classifying, commenting, describing, editing, highlighting, identifying, linking, moderating, questioning, replying, tagging). A thread is therefore not a declared object but an emergent structure: the transitive closure of the target links, which a reader reconstructs into a tree.

Decision

A reply is an ordinary W3C Web Annotation:

In the Rust model this is Motivation::Replying plus a distinct Body::Reply { value } variant, so the wire purpose mirrors the motivation just as commenting/tagging/editing do. Target::annotation(dedup_id) builds the urn:freedback:annotation:<dedup_id> reference, and a helper reads the referenced dedup_id back out.

Distinguishing kinds needs no new type: the motivation is the discriminator. A top-level comment is oa:commenting with the subject as target; a reply is oa:replying with an annotation as target. Tags, ratings, and issues keep oa:tagging / oa:assessing / oa:editing. "Discussion" is not a kind โ€” it is the shape of the target graph.

Why the content-address, not the server id. Targeting the dedup_id URN (rather than https://<server>/annotations/<id>) makes a thread:

Opt-in is a widget concern, not vocabulary. The reply button is rendered only when a widget sets data-replies; the wire format carries no opt-in flag. (A future per-author opt-in โ€” "I allow replies to my comment" โ€” would be a small net-new boolean gated by SHACL; deferred until wanted.)

Consequences

Alternatives considered