The Content Sync Protocol
Everything a documentation platform needs to be syncable correctly, written down and runnable against any platform — including ours, and including one that is not.
npx @forumulate/cli conformance https://forumulate.com/api/orgs/{org}/spaces/{space}/itemsThe twelve checks
Check | What it asks | Why it matters |
|---|---|---|
| The description is findable from any response | Without it an integrator probes five paths, finds nothing, and writes a defensive parser instead of generating a client. |
| The collection is keyed after its path | A collection named after its controller forces every reader to learn your internals before they can read a row. |
| The page carries limit, has_more, next_cursor and total | A caller cannot tell a short page from the last page, so a sync silently stops early. |
| A cursor walk visits every row exactly once | A cursor that repeats or skips rows corrupts a corpus without ever erroring. |
| The walk agrees with the reported total | A total nobody can reconcile against is a number that hides a truncated sync. |
| A server-defined checkpoint comes back with the page | Without one, every client invents its own watermark out of its own clock and loses rows to skew. |
| The checkpoint narrows the answer | An incremental sync that returns everything is a full sync wearing a parameter. |
| An unparseable window is refused, not ignored | Silently ignoring it hands back a full list that looks like a successful incremental sync. |
| Removals are explicit, not inferred from absence | Absence means "archived" on a full sync and "not on this page" on an incremental one. Conflating them deletes a corpus. |
| Each item carries a content hash | Without it a client re-embeds everything to discover that nothing changed. |
| A conditional request answers 304 | Polling costs a full transfer to learn nothing moved. |
| Rate limits are stated, with their window | A limit with no period is a number per something, and a client that guesses wrong either crawls or is refused. |
Tombstones, specifically
This is the part everyone gets wrong, so it is written down rather than implied:
On a full list, a piece that is absent has been removed.
On an incremental list (
updated_since=…), a piece that is absent has simply not changed.
Conflating the two deletes a customer's corpus on the first incremental sync. Ask for include_deleted=true and a removal arrives as a row with deleted: true — for 30 days, after which reconcile with a full list.
Why the suite uses no SDK
It is plain fetch against whatever URL you give it. A conformance suite that only runs against the platform that wrote it proves nothing and convinces nobody.