> ## Documentation Index
> Fetch the complete documentation index at: https://ralphy.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Asset manifest

> asset-manifest.json — per-project slot pointer table, slot-id convention, and the auto-versioning archive flow.

`.ralphy/workspaces/<ws>/projects/<id>/asset-manifest.json` is the single pointer table for everything a project has generated. One key per asset slot, one record per pointer. The runtime shape is defined in `cli/commands/generate.ts` (`Manifest` type, `readManifest` / `writeManifest`). This page documents the schema, the slot-id convention, and how auto-versioning interacts with the pointer.

## Full example

```json theme={"dark"}
{
  "slots": {
    "scene-01-bg-image": {
      "kind": "image",
      "path": "artifacts/images/scene-01-bg-image.png",
      "model": "google/gemini-3-pro-image-preview",
      "costUsd": 0.15,
      "url": "https://orcdn.example/...",
      "generatedAt": "2026-05-19T08:14:22.118Z"
    },
    "scene-01-vid": {
      "kind": "video",
      "path": "artifacts/videos/scene-01-vid.mp4",
      "model": "kwaivgi/kling-v3.0-pro",
      "costUsd": 0.70,
      "generatedAt": "2026-05-19T08:18:01.501Z"
    },
    "scene-01-vo": {
      "kind": "voiceover",
      "path": "artifacts/voiceover/scene-01-vo.mp3",
      "model": "eleven_multilingual_v2",
      "costUsd": 0,
      "generatedAt": "2026-05-19T08:19:14.700Z"
    },
    "bg-music": {
      "kind": "music",
      "path": "artifacts/music/bg-music.mp3",
      "model": "music_v1",
      "costUsd": 0,
      "generatedAt": "2026-05-19T08:20:33.900Z"
    }
  }
}
```

Everything generated by `ralphy generate <kind>` lands here as a slot. The manifest is the "current pointer"; the file on disk is the latest version.

## Schema

The runtime shape from `cli/commands/generate.ts`:

```ts theme={"dark"}
type Manifest = {
  slots: Record<string, {
    kind: "image" | "video" | "voiceover" | "music" | "captions";
    path: string;
    model?: string;
    costUsd?: number;
    url?: string;
    generatedAt: string;
  }>;
};
```

### Fields

| Field                   | Type            | Nullability | Notes                                                                                                      |
| ----------------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `slots`                 | object          | required    | Map from slot id to slot record. Empty object on a fresh project.                                          |
| `slots[id].kind`        | enum            | required    | `image`, `video`, `voiceover`, `music`, `captions`.                                                        |
| `slots[id].path`        | string          | required    | Project-relative path to the current version of the asset (e.g. `artifacts/images/scene-01-bg-image.png`). |
| `slots[id].model`       | string          | optional    | Full provider model id (e.g. `google/gemini-3-pro-image-preview`).                                         |
| `slots[id].costUsd`     | number          | optional    | Cost in USD for the *latest* generation, not lifetime. Lifetime cost comes from `generations.jsonl`.       |
| `slots[id].url`         | string          | optional    | Provider URL when the asset was downloaded from a remote source. Useful for re-fetching.                   |
| `slots[id].generatedAt` | ISO 8601 string | required    | Timestamp of the latest generation.                                                                        |

There is no top-level `projectId`, `version`, or `totalAssets` field in the runtime shape. (Some older example manifests under `profiles/` have a richer shape — those are legacy and not used by the current `ralphy generate` flow.)

## Slot ID convention

Slot ids are lowercase kebab-case. The canonical pattern is:

```
{scene-id}-{type}-{descriptor?}
```

Examples:

* `scene-01-bg-image` — background image for scene 01
* `scene-01-vid` — video for scene 01
* `scene-01-vo` — voiceover for scene 01
* `scene-04-bg-image` — background image for scene 04
* `bg-music` — project-wide background music (no scene prefix)

The validator in `cli/commands/generate.ts → normalizeSlot()` accepts `[a-zA-Z0-9_-]+` and normalizes uppercase to lowercase and `_` to `-`, with a stderr warning. The canonical form is `[a-z0-9-]+`. Characters outside the relaxed set (spaces, dots, slashes, unicode) raise `E_INPUT_INVALID`.

```ts theme={"dark"}
// cli/commands/generate.ts
const SLOT_REGEX_RELAXED = /^[a-zA-Z0-9_-]+$/;
const SLOT_REGEX_CANONICAL = /^[a-z0-9-]+$/;
```

The auto-normalization exists because six of ten project postmortems flagged the previous hard-reject as their highest-frequency CLI friction.

## Auto-versioning

When you regenerate a slot that already has a file, `cli/lib/providers/media.ts → protectExistingAsset()` archives the existing file before the new one lands:

```
artifacts/images/scene-01-bg-image.png        # before regen
artifacts/images/scene-01-bg-image.png        # after regen — new file
artifacts/images/scene-01-bg-image.v1.png     # archived original
```

A second regen archives v1 → v2:

```
artifacts/images/scene-01-bg-image.png        # latest
artifacts/images/scene-01-bg-image.v2.png     # second-latest
artifacts/images/scene-01-bg-image.v1.png     # original
```

The pattern is `<base>.v{N}<ext>` where `N` is the next free version number. The archiver scans siblings, finds the highest existing `N`, and uses `N+1`.

Bypass with `--force-overwrite` only when the user explicitly asks for legacy destructive behavior.

### Manifest interaction

The manifest's `path` field always points at the **latest** version — the unversioned file. The archive files (`.v1.png`, `.v2.png`, ...) are *not* listed in the manifest. To enumerate them, walk the assets directory.

Every generation also appends a row to `generations.jsonl` (see [Memory schemas](/advanced/memory-schemas)). The full history of a slot lives in those rows; the manifest is the current pointer only.

## Promote workflow

If the user prefers v1 over the latest, the promote flow is **manual and user-driven** — Ralphy does not flip the pointer without explicit consent.

```bash theme={"dark"}
# inspect the versions
ls .ralphy/workspaces/<ws>/projects/<id>/artifacts/images/scene-01-bg-image*

# promote v2 → latest
mv .ralphy/workspaces/<ws>/projects/<id>/artifacts/images/scene-01-bg-image.png \
   .ralphy/workspaces/<ws>/projects/<id>/artifacts/images/scene-01-bg-image.v3.png
mv .ralphy/workspaces/<ws>/projects/<id>/artifacts/images/scene-01-bg-image.v2.png \
   .ralphy/workspaces/<ws>/projects/<id>/artifacts/images/scene-01-bg-image.png
```

The manifest's `path` does not change (the unversioned filename is the same). The new file *is* the new latest. A future `ralphy project doctor` verb may automate this, but until then the swap is by hand.

## Manifest vs generations.jsonl

| File                     | Purpose                                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `asset-manifest.json`    | Current pointer per slot. "What is the latest version of `scene-01-bg-image`?"                    |
| `logs/generations.jsonl` | History. "How many times has `scene-01-bg-image` been generated? At what cost? With what prompt?" |

The manifest is the read-cache for the editor and renderer — it knows where every slot's file is right now. The log is the audit trail.

The two never disagree about the *current* file on disk: every successful `ralphy generate <kind>` writes the file, appends to the log, and updates the manifest in the same call.

## Writer behaviour

```ts theme={"dark"}
// cli/commands/generate.ts
async function readManifest(projectId: string): Promise<Manifest>;
async function writeManifest(projectId: string, m: Manifest): Promise<void>;
```

`readManifest` returns `{slots: {}}` when the file is absent or unparseable — empty-slot recovery is silent so a corrupted manifest does not block a fresh generation.

`writeManifest` does a full rewrite of the file with `JSON.stringify(m, null, 2)`. The file is small (a few hundred bytes to a few KB per project), so the rewrite cost is negligible.

This is the one place in the project memory where rewrites happen. The JSONL logs are append-only; the manifest is rewrite-allowed because it tracks current pointers, not history.

## Sister files: render output

`render/final.mp4` and friends are written by `ralphy render <id>` and do not appear in the manifest. The renderer reads the manifest to find slot files, composes the video, and writes the output to `render/` directly. Render outputs are tracked separately by `ralphy project show <id>`.

## Doctor verb

```bash theme={"dark"}
ralphy project doctor <id>
```

The doctor verb walks the manifest slot-by-slot and ffprobes every file, flagging divergences (missing file, wrong duration, wrong dimensions, broken codec). It exits non-zero on any red. Postmortems (tokyo, kbo) flagged that the manifest claims drift from the actual on-disk file on long sessions; the doctor is the recovery path.

## Related

* [Memory schemas](/advanced/memory-schemas) — the sibling JSONL logs.
* [Concepts: generation log](/concepts/generation-log) — narrated overview.
* [Working: reviewing and iterating](/working/reviewing-iterating) — how to read versions and pick the keeper.
* [`cli/commands/generate.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/commands/generate.ts) — `Manifest` type and writer.
* [`AGENTS.md`](https://github.com/alecs5am/ralphy/blob/main/AGENTS.md) — invariant #13 (regen → new version, never overwrite).
