Skills · Workflow

templater

/templater

The full extract → classify → blueprint → de-dup → publish pipeline for a finished `.ralphy/workspaces/<ws>/projects/<id>/`.

Workflowopsuser
SKILL.md

templater

You decompose a finished project into the content-entity model (#063) and classify its pieces into reusable blocks. The contract is: a future agent should be able to reproduce the work — same units, same blocks (Template / Style / Recipe / Asset) — without re-deriving any of it from the raw artifacts/ dump.

Where this sits in the Unit lifecycle. templater runs AFTER Unit formation (phase 17) in the canonical Unit lifecycle — it reads each units/<slug>/unit.json. Only extract/publish a project whose Unit is polished: the render must have passed the native-video final gate (ralphy project status <id> --contractpolished === true, #411). A keyframe-only eval does not qualify.

DO NOT FIRE

  • Scaffolding a new project — that is ralphy template use <existing-slug>, not this skill.
  • One-off renders — that is the producer playbook.
  • Quality evaluation — that is /evaluator.
  • Pushing to the live library — that is the #056 publish path (landing/scripts/publish-entity.ts); templater stops at printing the ordered commands. See HARD INVARIANTS below.

This skill is the full maximal-detail pipeline: one invocation, followed end-to-end, reproduces what a careful maintainer does by hand. The six stages are EXTRACT (read the finished deliverables) → CLASSIFY (factor into the five entities, incl. the recipe-vs-tag split) → BLUEPRINT (capture a per-unit reproduction payload) → DE-DUP (match every candidate against the live library first) → EMIT the entity bundle → PRINT the publish runbook (the exact ordered publish-entity.ts commands).

templater does the extraction, classification, blueprint capture, and de-dup itself. It does not invoke the live push (--push) — that is the user's deliberate step. It prints the full, ordered command set (dry-run first, then --push) and hands control back. Publishing to the live library (Supabase DB + Storage + the committed published.ts) is the #056 primitive — see Publish runbook.

The five entities (read this first)

The library model (landing/lib/library-v2/types.ts) has five entities. Every project decomposes into them:

  • Unit — a finished deliverable in a Format (video, carousel, sticker-pack, podcast-cuts, fb-creative, motion-design, poster, image), holding 1..N ordered media items. A Unit = exactly 1 Template + 1 Style + N Recipes + M Assets. That ingredient list is the Unit's provenance.
  • Template (block, single-per-unit) — the STRUCTURE / skeleton only, style-agnostic. The beat structure, the slide count + slot layout, the composition skeleton.
  • Style (block, single-per-unit) — the visual look / register. The aesthetic + its anchor reference images.
  • Recipe (block, multi-per-unit) — a composable effect / treatment. A VFX layer, an encode recipe, an overlay pass, a caption style.
  • Asset (block, multi-per-unit, has a sub: character / location / prop / music) — concrete reusable media: a locked character master, a location plate, a prop, a music bed.

The Unit source of truth is .ralphy/workspaces/<ws>/projects/<id>/units/*/unit.json (formed by ralphy unit create, #069). Each unit.json already carries format, ordered media, and a provenance block (template / style / recipes[] / assets[] slugs) — so much of the decomposition is reading, not inventing. The postmortem/06-units.md record (if the project has one) is the second-highest-signal input: it already marks each provenance block NEW vs. REUSED.

What this skill is NOT

  • Not a project scaffolder. That is ralphy template use <slug> --project <new-id>.
  • Not a postmortem generator. That is /postmortem. Run it first if the source has none — postmortem/02-lessons.md (lessons) and postmortem/06-units.md (units + provenance) are the highest-signal inputs here.
  • Not the library publisher. It does NOT push to Supabase, does NOT edit landing/lib/library-v2/published.ts, does NOT run seed-supabase.ts. The publish step is the #056 primitive landing/scripts/publish-entity.ts (--unit / --block modes). templater produces the classified bundle; publish pushes it.
  • Not a single-template extractor. The old create-from-project "one template per project" framing is retired. A project produces a SET of entities (often several units + several blocks), not one template.

Source-of-truth files in the source project

Every extraction is keyed to a known file. If a file is missing, the skill degrades gracefully (it derives what it can, never invents). scenario.json is PREFERRED, not required — scenario-less still / HyperFrames projects (sticker packs, FB packs, poster sets) are valid; derive structure from asset-manifest.json + units/ + index.html and skip the scene table (the #062 fix).

Source file in .ralphy/workspaces/<ws>/projects/<id>/Required?What gets pulledMaps to entity
units/*/unit.jsonREQUIREDFinished deliverables + ordered media + provenance block slugs (#069)Unit (+ seeds all four block axes via provenance)
asset-manifest.jsonREQUIREDLocked refs, music beds, character masters, location plates; final slot listAsset blocks; backstop for Units when units/ is thin
scenario.jsonPREFERREDScene skeleton: beats + durations + speaker map + VO/SFX flagsTemplate block (structure only)
prompts.jsonPREFERREDPer-stage prompts (image / video / VO / music / captions)Style block prompt cookbook (with {{slots}}) + Recipe hints
index.html (HyperFrames)PREFERREDComposition skeleton + per-template defaults; encode / overlay recipesTemplate block (composition); Recipe blocks (VFX / encode / overlay)
logs/generations.jsonlPREFERREDPer-stage model picks + params + cost rollupModel-stack notes on the Style / Template blocks
BRIEF.mdPREFERREDOriginal user brief — sentence-level intentDrives Format + category classification + descriptions
postmortem/06-units.mdPREFERREDUnits shipped + provenance, NEW vs REUSED per blockThe classification spine — read it before re-deriving anything
postmortem/02-lessons.mdPREFERREDTop rules + anti-patterns"Key rules" on the relevant blocks
postmortem/04-models-and-cost.mdPREFERREDSpend rollup per stageCost ballpark on the blocks

If units/ is genuinely empty AND asset-manifest.json has finished deliverables, surface that gap: the project has shippable media but no curated Units — suggest ralphy unit create <id> --slug <s> --format <f> --from '<glob>' first, then re-run. You can still classify candidate blocks from the manifest, but a clean per-unit publish needs the units formed.

See references/extraction-rules.md for the per-file extraction details and edge cases.

The workflow

  1. Resolve the project + read its Units. ralphy unit list <project-id> and read each units/<slug>/unit.json. These are the Units. Read postmortem/06-units.md if present — it pre-classifies the provenance blocks (NEW vs REUSED). NEVER fabricate units from the raw artifacts/ dump.

  2. Read BRIEF.md + (if present) scenario.json headers for the elevator pitch. This grounds the classification passes.

  3. Decompose into the five entities. For each Unit, resolve its provenance into concrete block candidates:

    • Template — from scenario.json (beats) or, for scenario-less projects, from index.html (composition skeleton) / the slide-slot layout. Structure only, style-agnostic.
    • Style — from prompts.json + the locked anchor refs. The look + its reference images.
    • Recipe candidates — VFX layers, encode recipes (-tune grain, CRF), overlay passes, caption styles, baked transitions, prompt techniques. From index.html + prompts.json + bake scripts + postmortem lessons. Each candidate is then split recipe-vs-tag in step 4.
    • Assets — locked character masters, location plates, props, music beds from asset-manifest.json. Each carries a sub (character/location/prop/music). Multi-value.
  4. Recipe-vs-tag split (the #082/#083 discipline — read references/recipe-vs-tag.md). For EACH recipe candidate from step 3, decide:

    • Recipe (stays a block, earns a detail page) ONLY if you can author a real, copyable artifact — an ffmpeg filtergraph, an encode/bake command, a HyperFrames snippet, or a concrete prompt-style technique — sourced from the project's own files, never invented: cli/lib/ffmpeg-recipes.ts (the canonical builders) for ffmpeg/encode/overlay, the project's scripts/*.sh bake scripts + the captured blueprint.json recipes[].command for bakes, index.html for HyperFrames overlays, the gen-log / prompts/** for prompt recipes. When kept, author the FULL enriched payload: recipeKind (ffmpeg/encode/overlay/bake/hyperframes/prompt) + body (markdown how-to) + artifact (the real code) + params (named knobs) + demo (a self-contained runnable demo.html for HyperFrames recipes; before/after media for ffmpeg recipes when real frames exist — never fabricated).
    • Tag (NOT a block, no detail page) if it is a pure textual descriptor with NO extractable artifact (e.g. "rain overlay" / "soft bloom" applied only as a vibe). It becomes a tags[] entry on the Unit(s) that used it, carried in unit.json.
    • The failure mode to kill: never publish an EMPTY recipe block (refs:0, no body, no artifact). That empty chip is the "tag cloud" anti-pattern. If you cannot author a real artifact, it is a tag.
  5. Match existing blocks FIRST (de-dup before you author). Before proposing ANY new block — and before authoring a kept recipe's body/artifact — check whether an existing library block already covers it. Read PUBLISHED_BLOCKS in landing/lib/library-v2/published.ts directly, and use ralphy template list / ralphy template suggest for template/style candidates and ralphy assets list --kind <kind> for asset candidates. If postmortem/06-units.md already marked a block REUSED, trust that. Only propose a NEW block for a genuine gap. Worked cautionary example: choose-path-xfade-master was published as a NEW recipe when the canonical ffmpeg-xfade-master already carried that exact artifact — a duplicate that #081/#083 had to delete and repoint. Reuse + cite the existing slug; never publish a second copy. Over-creating duplicate blocks is the failure mode this step prevents.

  6. Classify slots + tags + descriptions (LLM, via callLLM()). Through cli/lib/providers/llm.ts → callLLM():

    • Slots — extract {{slots}} (brand / product / character names / location keys / target language) from prompts.json per references/slot-detection.md, so the Style block's prompt cookbook is reusable across subjects.
    • Format — confirm each Unit's format (it is in unit.json; validate it against the eight library formats).
    • Unit tags — the descriptors demoted from step 4 land in each Unit's unit.json tags[] (filter-only labels for the feed's TAGS facet).
    • Category + description — per references/category-classifier.md, for any local templates/<category>/<slug>/ artifact.
    • English-only on disk. ANY captured prose — storyboards, prompts, VO lines, block bodies, tags, descriptions — must land in English. Translate folklore / foreign terms before they touch a file (the swamp lesson: the Russian word nechist → "the unclean"). The user can chat in any language; the on-disk artifact is English. This is docs/developing-ralphy.md's hard rule — the publish gate rg '\p{Cyrillic}' must come back empty.
  7. Capture each Unit's Blueprint (#080 — local + free). For EACH Unit, run ralphy blueprint create <project-id> --unit <slug>. This is the per-unit reproduction recipe: it reads the (gitignored) project files and writes a self-contained units/<slug>/blueprint/ payload (blueprint.json + copied index.html / prompt files / hard-asset files), validated against BlueprintSchema. It is extraction only — reads local files, makes no network call, costs nothing, and is append-only (a re-run on a slug that already has a blueprint/ writes blueprint.v2/, never overwrites). Record the result in the bundle as units[].blueprint (status NEW if this run created the first blueprint/, REUSED if a prior capture already existed, with the payload path units/<slug>/blueprint/). See references/extraction-rules.md → "Blueprint capture (per Unit)".

  8. Emit the entity bundle. The primary output is a classified bundle: the Units (with resolved provenance, demoted tags[], AND each Unit's captured Blueprint), plus the NEW blocks worth keeping — each recipe carrying its full recipeKind/body/artifact/params/demo. Mark every block NEW vs REUSED. This bundle is what the publish primitive consumes. Print it as JSON (see Output).

  9. (Optional) Write a local templates/<category>/<slug>/ artifact. When the user wants a downloadable repo template (the vibe-reference / vibe-style form), write it under templates/<category>/<slug>/ so ralphy template list / show / suggest / use pick it up. This is ONE optional output, not the skill's reason for being — the entity publish is the #056 primitive. Schema is the one cli/commands/template.ts consumes. Never modify the source project.

  10. Print the publish runbook (#056), do NOT push. Print the exact ordered publish-entity.ts commands the user runs — dry-run first, then --push — for the NEW blocks (--block-file, recipes carrying the enriched fields), the Units (--unit, carrying unit.json.tags), and each Unit's Blueprint (--blueprint). See Publish runbook for the full ordered set + the DB-migration prerequisite. Then stop. The user (or dev-publish-template) drives the actual push.

Output

JSON, pipe-friendly:

{
  "project": "free-air-vpn-stickerpack",
  "units": [
    {
      "slug": "stickers-outline",
      "format": "sticker-pack",
      "media_count": 32,
      "provenance": {
        "template": { "slug": "sticker-set", "status": "REUSED" },
        "style": { "slug": "free-air-jelly-pure", "status": "NEW" },
        "recipes": [{ "slug": "floodfill-diecut-cutout", "status": "NEW" }],
        "assets": [{ "slug": "free-air-mascot", "sub": "character", "status": "NEW" }]
      },
      "tags": ["white die-cut outline", "jelly mascot"],
      "blueprint": {
        "status": "NEW",
        "path": ".ralphy/workspaces/<ws>/projects/free-air-vpn-stickerpack/units/stickers-outline/blueprint",
        "publish_cmd": "cd landing && bun run scripts/publish-entity.ts --blueprint .ralphy/workspaces/<ws>/projects/free-air-vpn-stickerpack/units/stickers-outline/blueprint"
      },
      "publish_cmd": "cd landing && bun run scripts/publish-entity.ts --unit .ralphy/workspaces/<ws>/projects/free-air-vpn-stickerpack/units/stickers-outline"
    }
  ],
  "new_blocks": [
    { "kind": "style", "slug": "free-air-jelly-pure", "blurb": "...", "publish_cmd": "cd landing && bun run scripts/publish-entity.ts --block-file <spec.json>" },
    {
      "kind": "recipe",
      "slug": "floodfill-diecut-cutout",
      "blurb": "...",
      "recipeKind": "ffmpeg",
      "body": "## What it is ...",
      "artifact": "colorkey=0xffffff:0.1:0.0,despill=...",
      "params": { "key": "0xffffff", "similarity": 0.1 },
      "demo": { "kind": "media", "beforeUrl": "...", "afterUrl": "..." }
    },
    { "kind": "asset", "slug": "free-air-mascot", "sub": "character", "blurb": "..." }
  ],
  "reused_blocks": [{ "kind": "template", "slug": "sticker-set" }],
  "demoted_tags": ["white die-cut outline", "jelly mascot"],
  "local_template_artifact": null,
  "scenario_present": false,
  "warnings": []
}

Publish runbook (#056)

templater extracts + classifies + blueprints + de-dups; the publish to library is the #056 primitive, landing/scripts/publish-entity.ts. It has THREE independent, first-class modes — a Unit, a standalone Block, and a Blueprint each publish on their own. Print the commands below in this order (blocks → units → blueprints), each as a dry-run line then the --push line. templater never runs --push — it prints the runbook and stops; the user (or dev-publish-template) runs it.

0. DB prerequisite (once, on a fresh store)

Before ANY --push, the Supabase schema must carry the blueprints table and the additive columns blocks.recipe_kind, blocks.data, units.tags. They live in supabase/migrations/0001_init_library_v2.sql. On a fresh store, apply that migration first (psql "$SUPABASE_DB_URL" -f supabase/migrations/0001_init_library_v2.sql, or ralphy's seed path). Dry-runs touch nothing remote, so they are always safe to print/run first.

1. NEW blocks first (so unit provenance + tag facets resolve)

Each NEW block is a --block-file <spec.json>. A block spec is { kind, id, name, blurb, sub?, refs?[] }; a recipe spec ALSO carries the enriched payload { recipeKind, body, artifact, params, demo } (#082) — which pack into the blocks.recipe_kind column + the blocks.data jsonb. The recipe's demo (demo.html for HyperFrames, before/after media for ffmpeg) + any refs ride along to Storage so the library page is interactive (live recipe demo, audio player for a music asset). Tags are NOT blocks — they carry no --block-file line; they ride on the Unit.

cd landing && bun run scripts/publish-entity.ts --block-file <block-spec.json>           # dry-run (per NEW block)
cd landing && bun run scripts/publish-entity.ts --block-file <block-spec.json> --push     # push

2. Units next (carrying unit.json.tags + provenance links)

cd landing && bun run scripts/publish-entity.ts --unit .ralphy/workspaces/<ws>/projects/<id>/units/<slug>          # dry-run (per Unit)
cd landing && bun run scripts/publish-entity.ts --unit .ralphy/workspaces/<ws>/projects/<id>/units/<slug> --push    # push

The unit's media uploads to Storage; the units row + unit_blocks provenance rows upsert; the tags[] from unit.json land in the units.tags column → the feed's TAGS filter facet. A provenance block id absent from Supabase is WARN-and-skipped (never fabricated) — which is why blocks publish FIRST.

3. Blueprints last (the per-unit reproduction payload, one per Unit)

cd landing && bun run scripts/publish-entity.ts --blueprint .ralphy/workspaces/<ws>/projects/<id>/units/<slug>/blueprint          # dry-run (per Unit)
cd landing && bun run scripts/publish-entity.ts --blueprint .ralphy/workspaces/<ws>/projects/<id>/units/<slug>/blueprint --push    # push

The --blueprint dir is the units/<slug>/blueprint/ payload step 7 captured (blueprint.json + copied index.html / prompts / hard assets). It uploads the payload to Storage under blueprints/<unitId>/, upserts the 1:1 blueprints row, and appends to PUBLISHED_BLUEPRINTS.

Every mode writes to Supabase (DB + Storage) AND appends to the committed open-source landing/lib/library-v2/published.ts (idempotent by id / unitId, append-only). Default run is DRY-RUN. The maintainer one-shot that runs the whole runbook for you is dev-publish-template (#056).

Edge cases & refusals

  • No units/ AND no finished deliverables in asset-manifest.json → the project isn't done. Refuse, point at what's missing (run /producer or finish the pipeline).
  • No units/ but finished media exists → DO NOT refuse. Surface the gap, suggest ralphy unit create, classify candidate blocks from the manifest anyway. (This is the #062 fix in spirit — never hard-block scenario-less / unit-thin projects.)
  • No scenario.json → DO NOT refuse (the #062 fix). Derive the Template block's structure from index.html / slide layout, skip the scene table.
  • No postmortem/ → proceed but warn. Block classification leans harder on prompts.json + asset-manifest.json; offer to /postmortem (now a 7-file set incl. 06-units.md) first for a cleaner NEW/REUSED split.
  • A block looks like an existing one → match it, mark REUSED, do NOT create a duplicate. When genuinely unsure, prefer REUSED and flag the uncertainty in warnings. (The choose-path-xfade-master == ffmpeg-xfade-master dup is the canonical cautionary case — see references/recipe-vs-tag.md.)
  • A recipe candidate has no extractable artifact → it is a TAG, not a block. Demote it to the Unit's tags[]; never publish an empty refs:0 recipe block. See references/recipe-vs-tag.md.
  • Captured prose contains non-English (folklore, foreign brand copy, RU/other-script VO) → translate before writing it to any file. English-only on disk; the rg '\p{Cyrillic}' gate must come back empty.
  • Slug collision in templates/ (only if writing the optional local artifact) → refuse unless --force, show the diff first.

Why this skill exists

A finished project's postmortem captures the expensive lessons; units/*/unit.json captures the finished deliverables and their provenance. Without templater, those sit unindexed — the next agent re-derives the structure, the look, the effects from scratch. templater is the compression + classification layer: it factors the project into the five reusable entities and matches them against the library so the next project starts at the library's wisdom level. The publish primitive (#056) then makes those entities discoverable. This skill is the bridge between "we shipped deliverables" and "we shipped reusable blocks."

References

  • references/recipe-vs-tag.md — the #082/#083 split: the decision rule, the enriched-recipe payload to author, where to source artifacts, the de-dup cautionary example.
  • references/extraction-rules.md — per-source-file extraction details + edge cases.
  • references/slot-detection.md — LLM prompt + heuristics for {{slots}} in prompts.json.
  • references/kind-decision.md — vibe-reference vs vibe-style decision tree (for the optional local artifact).
  • references/category-classifier.md — the five segment-persona categories + LLM classification prompt.
  • references/pool-migration.md — heavy-asset migration to ralphy-assets/pool/ (for Asset blocks / the local artifact).
  • cli/lib/schemas/unit.ts — the unit.json Zod schema (the Unit source of truth, #069).
  • cli/commands/blueprint.ts — the ralphy blueprint create|show|list|use surface (#076/#079); templater runs blueprint create per Unit in step 7.
  • cli/lib/schemas/blueprint.ts — the BlueprintSchema Zod shape (#074): the six reproduction axes a captured Blueprint carries.
  • cli/lib/ffmpeg-recipes.ts — the canonical ffmpeg/encode/overlay builders (buildVhsFilter, buildColorGradeFilter, buildSidechainFilter, buildMixMusicFilter, CRF helpers); the source-of-truth for a kept recipe's artifact.
  • landing/lib/library-v2/types.ts — the five-entity shapes (Format / Unit / Block kinds), the enriched-Recipe fields (#082: recipeKind/body/artifact/params/demo), and Unit.tags.
  • supabase/migrations/0001_init_library_v2.sql — the schema (the blueprints table + blocks.recipe_kind / blocks.data / units.tags columns) that must exist before any --push.
  • landing/scripts/publish-entity.ts — the publish primitive (#056); templater hands off to it.
  • docs/skills-vs-templates.md — templater = extract/classify; #056 = the Supabase→library writer.
  • .agents/skills/dev-publish-template/SKILL.md — the maintainer one-shot that runs the publish for you.