Content Document v1 Contract
Status
Accepted. This is the canonical contract for authored rich content.
1. Scope
- Applies today to passage content and preview/runtime passage projection.
- New authored rich-content fields MUST reuse this contract; Packages B/C MUST NOT invent a parallel schema.
ContentDocument v1is the contract name. The per-locale schema discriminator remainspassage-rich-content/v1in v1 for compatibility with the live backend.
2. Canonical source contract
2.1 Localized envelope (MUST)
Persisted authored content MUST use this envelope:
{
"defaultLocale": "en",
"locales": {
"en": {
"schemaVersion": "passage-rich-content/v1",
"type": "doc",
"blocks": []
}
}
}
Rules:
defaultLocaleandlocalesMUST appear together.- Locale keys MUST be normalized BCP-47 tags (
en,en-gb). defaultLocaleMUST match a key inlocales.- All locale payloads inside one envelope MUST resolve to the same schema version.
2.2 Compatibility input (MUST)
- Existing passage write APIs MAY accept plain single-locale JSON.
- Server behavior remains: wrap plain JSON into the localized envelope with
defaultLocale = "en". - This compatibility path MUST NOT be used for new surfaces or new generated content.
2.3 Per-locale payload (v1)
Canonical new writes SHOULD emit:
schemaVersion: "passage-rich-content/v1"type: "doc"blocks: []
Supported v1 block kinds are frozen as:
paragraphheadinglisttableimage
Rules:
- Text content lives in child text nodes, not raw HTML.
- Images MUST use managed asset IDs, not floating external URLs.
- Tables MUST carry authorable accessibility fields (
caption; header semantics at renderer/compiler level). - Unknown block kinds or marks MUST be rejected by compiler/validator code, not silently passed through runtime.
3. Read/projection contract
- Read without
langMUST return the canonical localized envelope. - Read with
langMUST return the resolved single-locale blob and alocalefield indicating which locale was served. - Fallback order is frozen as:
- exact locale match
- parent language (
fr-ca->fr) defaultLocale- lexicographically first available locale (legacy safety fallback)
This applies to:
GET /api/v1/passages/\{id\}GET /api/v1/passages- localized preview/runtime projections that surface passage or question payload content
4. Capability model (frozen for v1)
4.1 No new content.* capabilities
Packages B/C MUST use existing authoring/admin capabilities in v1.
| Action | Capability |
|---|---|
| Read question-authored content | questions.read |
| Create/update question-authored content | questions.write |
| Read evaluation-authored content | evaluations.read |
| Create/update/publish evaluation-authored content | evaluations.write |
| Create/update passage content | questions.write |
| Upload/finalize managed media assets | media.manage |
| Locale/admin governance | locales.manage |
| Taxonomy assignment remains separate | taxonomy.read / taxonomy.manage |
Rules:
- Publish stays under the parent resource write capability in v1.
- Runtime submission/read flows do not gain a separate content capability.
- Do NOT add
content.read,content.write, orcontent.publishunless a later ADR supersedes this contract.
5. Runtime API contract (frozen)
5.1 authoring read/write surface
POST /api/v1/passagesandPATCH /api/v1/passages/\{id\}accept ContentDocument v1 envelope or legacy plain JSON compatibility input.GET /api/v1/passages{?lang}andGET /api/v1/passages/\{id\}{?lang}follow the read/projection contract above.
5.2 Preview surface is non-durable
POST /api/v1/evaluations/\{id\}/preview{?lang}MAY project localized content.- Preview MUST NOT mint durable snapshot truth or rewrite published refs.
- Preview is for authoring inspection only.
5.3 Delivery and forensic read surface
POST /api/v1/evaluations/\{id\}/sessions/\{sessionId\}/submit{?lang}creates the durable execution boundary.GET /api/v1/submissions/\{id\}{?lang}and downstream forensic/reporting reads MUST reconstruct from frozen snapshot facts only.
6. Lifecycle rules
6.1 Compile at publish
At the publish boundary for a versioned parent resource, the system MUST freeze content facts needed for deterministic replay:
- canonical localized envelope
- canonical content hash
- plain text extracts (
defaultLocale, all locales) - canonicalizer/hash scheme versions
- pinned child content refs required by the parent version (for example
passageVersionIdinside evaluation version snapshots) - pinned asset version refs when media blocks are present
For passages specifically, the live service already canonicalizes, hashes, and versions content on passage version creation/update; that active passage version is the publishable unit other resources pin to.
6.2 Pin at submission
Submission MUST pin immutable content truth and MUST NOT follow current active authoring state afterward.
In v1 this means:
- question payloads/rubrics are copied into
submissions.version_snapshot.questions - section items carry pinned
passageVersionIdfor passage-backed items - runtime and reporting MUST use pinned passage version refs, not the passage's current active version
- compatibility fallback to current active passage version is allowed only for legacy submissions created before passage-version pinning existed
6.3 No live-authoring joins for historical truth
- Historical rendering/reporting MUST NOT resolve passage/question content from current draft/live authoring state.
- If a required pinned ref is absent on pre-contract legacy data, the fallback must be explicitly marked compatibility-only in code/comments/tests.
7. Package B/C guardrails
- Package B MUST compile/publish against this exact envelope, capability model, and freeze semantics.
- Package C MUST render localized runtime content only from envelope projections plus pinned publish/submission refs.
- Neither package may redefine locale fallback, introduce a second content schema name, or bypass pinned passage/version truth.