Skip to content

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.mdcommitted, 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 → researching → drafting → review → humanized → final → published
  • backlog: CSV row only, no file.
  • researching … final: file exists; status advances as copilot skills run; gate_scores set at review.
  • published: directus_id stamped; 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.mdstatus equals that file's status.
  • A file-less row (planning only, no session.md) → backlog.

(Earlier ad-hoc CSV values were retired 2026-06-15: planned/ideabacklog, 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 8 template_types; the former sessions/insight/insight-blocks.csv was migrated into it). The coaching-programme source tables are coach-conversations/coach-conversations.csv (57) and coach-conversations/building-blocks.csv (61, carries Maps To).
  • coach-conversations/building-blocks.csvMaps To routes each block: session:<template_type> | coach_module | inline (an inline block has no file of its own; it is authored into a conversation's items[]).
  • 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)

  1. Forward referential integrity. Every session_cards[].session_slug, sound_cards[].ref, and opening/closing_module must resolve to an identifiers: entry — so a Directus stub row can exist before a conversation publishes (the coach-content-validate hook blocks a save with a missing reference).
  2. 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 Directus slug (conversations and modules both have a slug column) at publish. A sound_card reference is the two-field shape { ref_kind: album|track, ref: <slug|uuid> } (album→slug, track→uuid); a session_card reference carries session_slug + session_id (value = a SessionKind.name, one of the 8 template_type values).
  3. Backlog linkage. Every composed_of ID exists in building-blocks.csv; every block routed to its own file has a matching entry in identifiers:.
  4. Status ordering. status advances only along the sequence above.
  5. Slug stability. A published slug is immutable (renames follow the Directus slug-rename guard).