Skip to content

aptitude-course — consumption contract

How the corpus travels from a merged PR to the adepthood app, and what the app is allowed to depend on. Canonical sources: CONSUMPTION.md (issue #22) and CONTENT_FORMAT.md (issue #17); this page condenses them with the code that enforces each promise.

The published surface — exactly three things

The app "vendors a pinned commit of this repository (the SHA lives in the app's CONTENT_VERSION)" and may rely on (CONSUMPTION.md:17-30):

Surface Contract
manifest.json Generated by scripts/build_manifest.py, validated against schema/manifest.schema.json, carries schema_version
Markdown bodies Only files the manifest references via path — chapters, site resources, and (since 1.1.0) stage intros
Assets Images/media referenced from bodies or media[], repo-relative and stable

Everything else — google_docs/, scripts/, markdown/backup/, markdown/meta/, per-stage READMEs/TOCs, CI config — is internal and "may be renamed, moved, or deleted without notice. If the app reads it, that's a bug on the app side" (CONSUMPTION.md:31-36). Two corollaries the spec calls out: reads go through the manifest (never glob markdown/**), and identity is id — the durable key for progress tracking, never reused once shipped (CONSUMPTION.md:41-48).

schema_version semver (contract, not content)

schema_version (currently 1.1.0) versions the manifest contract, not the content (CONSUMPTION.md §2):

Bump When
Patch Content-only changes — in practice not even bumped; "the version describes the contract, and most releases are content-only"
Minor Additive, backwards-compatible: new optional field, new ignorable content_type value, new optional top-level array
Major Breaking: remove/rename a field, change a type, tighten a rule, change identity semantics

The one precedent: 1.0.01.1.0 added the optional stage_intros[] array and touched nothing else, so a 1.0.0 consumer keeps validating and "simply never sees stage_intros" (CONSUMPTION.md §2; schema/manifest.schema.json:5 documents the same). On a major mismatch "the app must refuse the content update rather than render undefined behavior."

Release tagging

The app pins a tag (resolved to its SHA), never a moving branch. Tags are date-based — content-vYYYY.MM.DD (with .N suffixes for same-day cuts) — because "content releases are a stream of editorial snapshots, not an API" (CONSUMPTION.md §3). Tags are immutable once pushed; a mistake means a new tag, never a moved one.

The update handshake

Verbatim flow from CONSUMPTION.md §4:

content PR merged into main
        │
        ▼  Content CI green (frontmatter/manifest/lint/links — required checks)
tag content-vYYYY.MM.DD pushed
        │
        ▼  adepthood#391: bump CONTENT_VERSION to the new tag's SHA
app build vendors manifest.json + markdown/** + assets at that SHA
        │
        ▼  app deploy
users see the new content

Properties: content ships independently of app deploys (a release is a pin bump); every deployed corpus is auditable by SHA; rollback is a pin revert; and majors invert the order — the app-side schema/renderer change merges first behind the old pin, then the content major is tagged, then both ship together (CONSUMPTION.md §4).

Drip-feed semantics (the subtle part)

release_day is "the number of days after a stage begins that an item becomes available"; the default authoring pattern is daily — chapter nrelease_day = n - 1 — and it is "authored by a human … not derived — it encodes curricular intent" (CONTENT_FORMAT.md §5.2).

Two ownership boundaries keep the repos decoupled:

  1. Stage duration is app territory. The content repo numbers stages 1-10 by archetype and "makes no assumption about how many calendar weeks a stage spans or where it falls in the 36-week schedule" (CONTENT_FORMAT.md §4) — the 10-archetype ↔ 36-week mapping is owned by adepthood.
  2. Unlock computation is app behavior, not contract. Since 2026-07 the app derives a proportional drip — unlocked = ceil(chapter_count / stage_duration_days × day_in_stage) — instead of gating purely on release_day > days_elapsed; release_day remains the ordering key and fallback. This shipped with no schema_version bump because the authored value and meaning were unchanged (CONTENT_FORMAT.md §5.2 app-side note; CONSUMPTION.md §2 "Not a version bump").

Media contract

Small media are committed in-repo under markdown/<NN-stage>/assets/ and referenced with file-relative inline images; video/large media live at external https:// URLs referenced only via frontmatter media[], never embedded inline (CONTENT_FORMAT.md §6.1). media[] entry shape is enforced by the generator: type in {video, image, audio}, exactly one of url/path, url must be absolute https://, optional poster/caption strings (scripts/build_manifest.py:219-241). As of this SHA every chapter's media is [] — the mechanism is contracted but unused.

Ecosystem integration summary

  • adepthood (the app): vendors the manifest + bodies at CONTENT_VERSION; renders CommonMark natively; keys progress on id (CONSUMPTION.md §1). The paired app-side specs are adepthood#389 (manifest schema), #391 (content sync/pin), #394 (native rendering), and #717 (stage intros).
  • WavelengthWatch / wavelength-demo: no code dependency on this repo; they share its ontology through the curriculum spreadsheet CSVs preserved under google_docs/database_of_course_curriculum/ (see the repo map).
  • The public site (aptitude.guru) publishes this corpus; wavelength-demo links straight into it (src/pages/HomePage.tsx:14 in wavelength-demo).

Grounded in aptitude-course@064c6ca, 2026-07-31.