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.