Content Lifecycle & Storage Convention¶
How app content is stored and tracked, repo-first. Canonical terms:
glossary.md. Design rationale:
superpowers/specs/2026-06-04-repo-spine-and-content-map-convention-design.md.
Storage model (hybrid)¶
The CSVs are the planning backlog. When an item enters production it gets a
per-item production file that owns its content + lifecycle state. The two are
linked by slug/ID.
| Content class | Per-item file path (EN canonical) |
|---|---|
| Session | sessions/<template_type>/<slug>/session.md |
| Coach conversation | coach-conversations/items/<conversation-id>.md |
| Coach module | coach-modules/<module-id>.md |
| Sound | sounds/<slug>/album.md (spec; audio is a referenced asset) |
<template_type> ∈ audioGuided, breathing, chipExercise, valuesSort,
experiment, insight, reframe, sentenceCompletion.
Frontmatter schema¶
See templates/item-frontmatter.md. Fields:
type, template_type (sessions only), slug, id (UUID once assigned),
locale (EN canonical for now), status, program_week (1–12), tier
(free|premium), category / segment_affinity / pain_points (advisory
placement from the research dossier's field-7, editor-confirmed), _editor_confirm
(the placement fields the writer filled but the editor must confirm), gate_scores
(filled at review), source_refs (building-block IDs / dossiers / citations),
directus_id (stamped on publish). The Markdown body holds the content; its
per-type shape is owned by the authoring standards (Phase 0 sub-piece B).
Hero illustrations¶
Sessions and sound albums each carry a square hero illustration, produced by the
naluma-app-editor-copilot:illustrate skill (gpt-image-1.5, 1024×1024 WebP). The chosen
image lives as <item-folder>/hero.webp — a sibling of the item's main file
(sessions/<template_type>/<slug>/hero.webp or sounds/<slug>/hero.webp) — and is
recorded in the item's frontmatter via image: hero.webp (sibling-relative),
image_alt, image_scene, and image_prompt_meta. The binary is committed in the item folder (full tracing) and uploaded to R2 by
publish, which stamps image_asset_id (left blank until then, like directus_id).
The per-item research dossier is the sibling research.md
(sessions/<template_type>/<slug>/research.md), committed and referenced in frontmatter
as source_refs: [research.md]. The review sidecar is <item-folder>/review.md —
committed, and its scores are also promoted to frontmatter gate_scores.
Per item, git tracks session.md, research.md, review.md, and hero.webp; only the
rendered audio (audio/…/*.m4a) stays out of git (canonical in R2; ElevenLabs retains
only the temporary raw per-segment generations, not the mastered file).
Lifecycle states (ordered)¶
backlog: CSV row only, no file.researching … final: file exists;statusadvances as copilot skills run;gate_scoresset atreview.published:directus_idstamped; row is live in Directus.
A read-only router skill (
naluma-app-editor-copilot:pipeline) reports each item's stage, the next action, and any blockers — item-by-item or as a board across the content folders.
Backlog status mirrors the per-item file status¶
sessions/sessions-backlog.csv's status column uses this same vocabulary and
mirrors each item's session.md status, so the CSV shows at a glance where every
item stands — no second vocabulary, no private CSV states:
- A row whose item has a
session.md→statusequals that file'sstatus. - A file-less row (planning only, no
session.md) →backlog.
(Earlier ad-hoc CSV values were retired 2026-06-15: planned/idea → backlog,
producing → the item's real file state.)
Update procedure — keep it in sync. Whenever an item's session.md status
changes — a copilot skill advances it (research/write-session/review/humanize),
the editor marks it final, or publish stamps it published — set the matching
backlog row's status to the same value in the same change. Add a new
file-less row as backlog. The status is owned by the EN session.md
(localized siblings carry their own per-locale status; the backlog tracks one row
per slug). Verify no drift before committing:
python3 - <<'PY'
import csv, os
rows = list(csv.reader(open('sessions/sessions-backlog.csv'))); si = rows[0].index('status')
def fs(slug, tt):
f = f'sessions/{tt}/{slug}/session.md'
if not os.path.exists(f): return 'backlog'
for line in open(f):
if line.startswith('status:'): return line.split(':', 1)[1].strip()
return 'backlog'
drift = [(r[0], r[si], fs(r[0], r[1])) for r in rows[1:] if r and r[si] != fs(r[0], r[1])]
print('OK — backlog mirrors files' if not drift else f'DRIFT ({len(drift)}): {drift[:10]}')
PY
CSV ↔ file linkage¶
- The consolidated planned-session backlog is
sessions/sessions-backlog.csv(all 8template_types; the formersessions/insight/insight-blocks.csvwas migrated into it). The coaching-programme source tables arecoach-conversations/coach-conversations.csv(57) andcoach-conversations/building-blocks.csv(61, carriesMaps To). coach-conversations/building-blocks.csv→Maps Toroutes each block:session:<template_type>|coach_module|inline(aninlineblock has no file of its own; it is authored into a conversation'sitems[]).- A backlog row links to its file by matching
slug/ID.
Programme map¶
The 12-week graph lives in programme-map.yaml
(repo root) — the single source of truth for structure (becomes Directus
coach_conversations + coach_schedule + coach_modules + referenced
session/sound rows on publish). Top-level keys: identifiers (namespace
registry), schedule (day/slot → conversation), conversations (per-conversation opening_module/closing_module + composed_of + session_cards/sound_cards).
Invariant rules (enforced later by a validation skill)¶
- Forward referential integrity. Every
session_cards[].session_slug,sound_cards[].ref, andopening/closing_modulemust resolve to anidentifiers:entry — so a Directus stub row can exist before a conversation publishes (thecoach-content-validatehook blocks a save with a missing reference). - Identifier format. Sessions + sound albums: slug
^[a-z0-9]+(?:-[a-z0-9]+)*$. Sound tracks: UUID. Conversations/modules: stable internal IDs (conv-…,mod-…) — these resolve to the Directusslug(conversations and modules both have aslugcolumn) at publish. Asound_cardreference is the two-field shape{ ref_kind: album|track, ref: <slug|uuid> }(album→slug, track→uuid); asession_cardreference carriessession_slug+session_id(value = aSessionKind.name, one of the 8 template_type values). - Backlog linkage. Every
composed_ofID exists inbuilding-blocks.csv; every block routed to its own file has a matching entry inidentifiers:. - Status ordering.
statusadvances only along the sequence above. - Slug stability. A published
slugis immutable (renames follow the Directus slug-rename guard).