Skip to main content

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 v1 is the contract name. The per-locale schema discriminator remains passage-rich-content/v1 in 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:

  • defaultLocale and locales MUST appear together.
  • Locale keys MUST be normalized BCP-47 tags (en, en-gb).
  • defaultLocale MUST match a key in locales.
  • 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:

  • paragraph
  • heading
  • list
  • table
  • image

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 lang MUST return the canonical localized envelope.
  • Read with lang MUST return the resolved single-locale blob and a locale field indicating which locale was served.
  • Fallback order is frozen as:
    1. exact locale match
    2. parent language (fr-ca -> fr)
    3. defaultLocale
    4. 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.

ActionCapability
Read question-authored contentquestions.read
Create/update question-authored contentquestions.write
Read evaluation-authored contentevaluations.read
Create/update/publish evaluation-authored contentevaluations.write
Create/update passage contentquestions.write
Upload/finalize managed media assetsmedia.manage
Locale/admin governancelocales.manage
Taxonomy assignment remains separatetaxonomy.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, or content.publish unless a later ADR supersedes this contract.

5. Runtime API contract (frozen)​

5.1 authoring read/write surface​

  • POST /api/v1/passages and PATCH /api/v1/passages/\{id\} accept ContentDocument v1 envelope or legacy plain JSON compatibility input.
  • GET /api/v1/passages{?lang} and GET /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 passageVersionId inside 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 passageVersionId for 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.