Review markup (RFM)
Annotations, verdicts, and suggestions live inside the markdown as CriticMarkup/RFM spans — so anchors survive every edit and sync for free.
Docs adopts RFM (RoughDraft Flavored Markdown): the markdown file is the database for review state. Annotations, comments, and suggestions are stored inline as CriticMarkup spans, with metadata in a YAML “endmatter” block at the end of the document.
Why markup-as-anchor
The hard problem in document review is anchoring annotations to text that keeps changing. RFM solves it by making the marker travel with the text:
- Anchors survive every edit. No stored offsets, no fuzzy re-anchoring, no orphaned annotations — through human edits, agent edits, and external tools alike.
- Sync is free. The markers live inside the shared CRDT text, so every collaborator sees annotations appear live with zero extra sync machinery.
- Ecosystem compatibility. Documents annotated in Docs open in RoughDraft and any CriticMarkup-aware tool with full fidelity.
The editor never shows raw markers — it renders them as highlights, gutter badges, and review-rail cards. Raw markup is only visible if you fetch the document over the API without cleaning it.
The syntax
Annotations (anchored comments with verdicts)
An annotation wraps the anchor text and attaches an explanation:
The app {==must support offline mode==}{>>Valid — confirmed in §3.2<<}{id="a1" by="agent_demo" at="2026-06-12T09:15:32.670Z"}
{==…==}— the anchor: the exact document text the annotation is attached to.{>>…<<}— the body: the reviewer’s explanation.{id=… by=… at=…}— attribution metadata.
The verdict lives in the endmatter entry for that id:
| Verdict | Badge | Meaning |
|---|---|---|
valid | green check | requirement is clear & consistent |
question | blue question mark | needs clarification / open question |
conflict | orange warning | conflicts with another requirement — use refs to point at the counterpart annotation ids |
conflict annotations carry refs: [a7]-style references; the editor renders “conflicts with →” links that jump between the two anchors.
Comments (verdict-less discussion)
Comments use the same anchored shape but carry no verdict — they are Layer 2 discussion. Replies are threaded under a parent comment, and threads can be resolved. A comment posted without anchorText is a document-level comment.
Suggestions (tracked changes)
Suggestions are CriticMarkup change syntax stored directly in the document text:
{++added text++} addition
{--deleted text--} deletion
{~~old~>new~~} substitution
The editor renders these as tinted inline spans with Accept / Reject controls. Accepting strips the marker and keeps the new text; rejecting mirrors that. Both are available over the API too — see Review API.
Endmatter
A YAML block at the end of the document stores per-item metadata (verdicts, resolution status, threading). You normally never touch it directly — the API maintains it — but it is part of the document text, which is why anchor texts must match outside existing markup when you create new items.
The export boundary
CriticMarkup’s one weakness is cosmetic: third-party renderers show the markers as literal text. Docs closes that gap at every exit point:
GET /api/docs/:id/export.md?clean=1
Clean export unwraps annotations and comments, rejects pending suggestions, and removes the endmatter — producing publish-ready markdown. Think of review markup like Word track-changes: review-time state, resolved before publishing.