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.0 → 1.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 n →
release_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:
- 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. - 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 onrelease_day > days_elapsed;release_dayremains the ordering key and fallback. This shipped with noschema_versionbump 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 onid(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:14in wavelength-demo).
Grounded in aptitude-course@064c6ca, 2026-07-31.