# Project Layout

This doc describes the canonical on-disk structure of a `videoclaw`
project — the unit `vclaw video init <slug>` creates and every
downstream stage operates on.

## Slug rules

Enforced at `vclaw video init` time:

| Rule | Constraint |
|---|---|
| Allowed chars | `[a-z0-9-]` |
| Must start with | `[a-z0-9]` (not `-`) |
| Must end with | `[a-z0-9]` (not `-`) |
| Length | 3–64 chars |
| Disallowed substrings | `--`, leading `.`, reserved names (`history`, `artifacts`, `checkpoints`, `events`, `state`, `outputs`, `assets`, `obsidian`, `characters`, `notes`, `tmp`) |
| Argv guard | Reject anything that looks like a flag (`^-`). Prevents the historical bug where `vclaw video init --project foo` parsed `--project` as the slug. |

Recommended slug template when the user doesn't provide one:
`<yyyy-mm-dd>-<noun>-<noun>` (e.g., `2026-05-25-disco-monster`).

## Canonical layout

```
projects/<slug>/
│
├── project.json                # MANIFEST (see "project.json shape" below)
├── storyboard.md               # Director-mode human-readable approval doc
│                                 (absent in storyboard-mode until storyboard
│                                 stage)
│
├── artifacts/                  # CANONICAL machine-readable outputs.
│   │                             JSON only. Every file MUST have a schema
│   │                             in schemas/video/artifacts/.
│   ├── brief.json
│   ├── storyboard.json
│   ├── story-bible.json        # Deterministic continuity bible (cast,
│   │                             settings, props, scene timeline, continuity
│   │                             notes) auto-emitted at storyboard-write time.
│   ├── asset-manifest.json
│   ├── scene-candidates.json
│   ├── scene-selection.json
│   ├── reference-sheets.json
│   ├── execution-plan.json
│   ├── execution-report.json
│   ├── review-report.json
│   ├── publish-report.json
│   ├── analyze-output.json
│   ├── clone-plan.json
│   ├── multi-shot-prompt.json  # Written only when `vclaw video multi-shot
│   │                             --project <slug>` is used; absent for
│   │                             standalone (no --project) invocations.
│   ├── filmmaking-prompts.json # Character sheet, 9-panel storyboard grid,
│   │                             reference map, and Seedance prompt packets.
│   └── history/                # Snapshots of artifacts/ files on overwrite.
│                                 Append-only. One subdir per artifact:
│                                 history/brief/<ts>.json.
│
├── checkpoints/                # ONE FILE per stage. Tracks approval state,
│   ├── brief.json                who approved when, retry count, verdict.
│   ├── storyboard.json
│   ├── assets.json
│   ├── review.json
│   └── publish.json
│
├── characters/                 # CANONICAL identity store for this project.
│   └── characters.json
│
├── events/                     # Append-only timeline. JSONL.
│   └── events.jsonl              Payloads use project-relative paths only.
│
├── notes/                      # Human-authored or model-authored MD.
│   │                             Free-form. Anything NOT a canonical
│   │                             JSON artifact lives here.
│   └── (markdown files)
│
├── outputs/                    # DERIVED. Final encoded media only.
│   ├── final/<slug>-<mode>.mp4   Created by publish stage.
│   ├── scene-0.mp4               Per-scene renders.
│   ├── scene-1.mp4
│   └── ...                       (Gitignored at project level.)
│
├── preview.html                # DERIVED. Final portal showcase.
├── edit.html                   # DERIVED. Editor edit/check portal.
├── review.html                 # DERIVED. Editor review portal.
├── client-review.html          # DERIVED. Client review portal.
├── compare.html                # DERIVED. Version/run comparison portal.
├── project-audit.jsonl         # Append-only preview portal generation/publish audit.
│
├── assets/                     # DERIVED. Intermediate visual assets.
│   ├── storyboard-grid.png       Deterministic 3x3 production board from
│   │                             `vclaw video storyboard-grid`.
│   ├── storyboard/               Per-scene stills.
│   ├── upscaled/                 Upscaled variants.
│   └── ...                       (Gitignored.)
│
├── obsidian/                   # DERIVED. Mirror of canonical artifacts
│                                 in Obsidian-friendly MD. Created by
│                                 `vclaw video export-obsidian`.
│                                 (Gitignored.)
│
└── state/                      # Derived state cache. Created on init by
                                  ensureProjectWorkspace.
```

## Directory disposition

| Directory | Always-present on init | Schema-enforced | Gitignored at project level |
|---|---|---|---|
| `project.json` | yes | yes | no (commit) |
| `storyboard.md` | director-mode only | n/a (free-form) | no |
| `artifacts/` | yes (empty) | **yes — every file** | no |
| `artifacts/history/` | yes (mkdir at init) | yes | no |
| `checkpoints/` | yes (empty) | yes (per-stage shape) | no |
| `characters/` | yes (empty) | yes (characters.json shape) | no |
| `events/` | yes (empty) | line shape enforced | no |
| `state/` | yes (empty) | internal cache | no |
| `notes/` | on demand | no (MD) | no |
| `outputs/` | on demand | n/a (media) | **yes** |
| `preview.html` / `edit.html` / `review.html` / `client-review.html` / `compare.html` | on portal generation | n/a (static HTML) | no |
| `project-audit.jsonl` | on portal generation/publish | JSONL event shape | no |
| `assets/` | on demand | n/a (media) | **yes** |
| `obsidian/` | opt-in command | n/a (MD mirror) | **yes** |

## Git tracking

`vclaw video init` does **not** write a per-project `.gitignore` —
`handleVideoInit` only writes `project.json`, the pending `brief`
checkpoint, and the `project.initialized` event. Any gitignore handling
is at the repo root, not per-project.

The derived directories — `outputs/`, `assets/`, `obsidian/` — are
re-generable from canonical artifacts and are the candidates to ignore
if you wire up repo-root gitignore rules. `artifacts/`, `checkpoints/`,
`characters/`, `events/`, `notes/`, `project.json`, and `storyboard.md`
are **canonical by design** so a project is reproducible from its
on-disk state.

## `project.json` shape

The manifest is `VideoProjectManifest` (`src/video/workspace.ts`). The
pipeline manifest is **embedded inline** — there is no versioned
reference and no `schemaVersion`. On init, `handleVideoInit` writes:

```json
{
  "slug": "fresh-proof",
  "productionMode": "director",
  "createdAt": "2026-05-06T02:20:19.421Z",
  "updatedAt": "2026-05-06T02:20:19.421Z",
  "pipeline": { "name": "director", "stages": ["..."] },
  "currentStage": "brief",
  "lastCompletedStage": null,
  "lastCheckpointStatus": "pending"
}
```

Required fields:

- `slug` — the project slug.
- `productionMode` — `"storyboard"` or `"director"`.
- `createdAt` / `updatedAt` — ISO timestamps.
- `pipeline` — the **full embedded `VideoPipelineManifest` object**
  (not a `pipelineRef` string). In practice only `pipeline.name` is read
  back by downstream code.
- `currentStage` / `lastCompletedStage` / `lastCheckpointStatus` —
  stage-machine cursor. On init these are `"brief"`, `null`, and
  `"pending"`.

Optional fields, written only by `updateProjectManifestMetadata`
(`owner`, `priority`, `dueDate`, `tags`, `blockedBy`, `blockedReason`)
and `updateProjectManifestCinemaProfile` (`cinemaProfile`). There is no
`schemaVersion`, no `pipelineRef`, no `createdBy`, and no nested
`metadata` object.

### `routePreference` (optional)

An ordered list of provider route ids that overrides the built-in
per-mode default ordering when `buildExecutionPlan` (`plan`/`produce`)
picks a route:

```json
{
  "routePreference": ["dreamina-useapi", "seedance-direct", "veo-useapi"]
}
```

- Valid ids: `veo-useapi`, `runway-useapi`, `dreamina-useapi`,
  `seedance-direct` (the `ProviderRouteId` union). An unknown id makes
  `plan`/`produce` **fail at plan time** — free, before any submission.
- Listed routes are tried first, in order; any built-in default route not
  listed is appended as fallback, so a **partial** preference is safe (a
  preferred route that turns out unavailable falls through to the rest).
- **Absent/empty ⇒ byte-identical** to the built-in defaults
  (`storyboard`: `veo-useapi` → `seedance-direct`; `director`:
  `seedance-direct` → `veo-useapi`).

Use it when a project must pin a route the default order wouldn't pick —
e.g. an image-to-video project whose references are remote URLs needs
`dreamina-useapi` (which uploads the bytes) rather than `seedance-direct`
(which requires the provider to fetch the URL itself).

## Event log shape

`events.jsonl` lines follow the `VideoProjectEvent` envelope
(`src/video/events.ts`): `{ type, recordedAt, payload? }`. There is no
`id`/ULID and no `source` field.

```jsonl
{"type":"artifact.review-report.written","recordedAt":"2026-05-06T21:54:52.404Z","payload":{"artifactPath":"artifacts/review-report.json","verdict":"pass"}}
```

Readers split on newlines and `JSON.parse` each non-empty line;
`appendProjectEvent` fills `recordedAt` if absent. Event-payload paths
are project-relative by convention.

## Artifact schema coverage guardrail

The build pipeline includes `check:artifact-schema-coverage`
(`scripts/check-artifact-schema-coverage.mjs`), which asserts:

1. Every artifact name passed to `writeArtifact(workspace, '<name>', ...)`
   in `src/video/**/*.ts` has a matching
   `schemas/video/artifacts/<name>.schema.json`.
2. Every schema in `schemas/video/artifacts/` has either a matching
   `writeArtifact()` call OR is in the script's
   `KNOWN_ALTERNATE_WRITERS` allowlist (audit-pending; some artifacts
   are written via specialized helpers rather than the typed
   `writeArtifact()` shim).

**Modes:**

- Default (advisory) — prints any drift but always exits 0. This is
  what `check:release-readiness-lite` invokes, so the guardrail
  reports status inline without blocking the release-readiness
  pre-flight while the allowlist is being burned down.
- `--strict` — exits 1 on drift. Wire into CI gates / pre-commit hooks
  when you want hard enforcement. Currently zero unexpected drift
  with the allowlist applied.

Current allowlist (9 schemas needing per-artifact audit): `analyze-output`,
`clone-plan`, `execution-plan`, `multi-shot-prompt`, `publish-report`,
`reference-sheets`, `review-report`, `scene-candidates`, `scene-selection`.
The audit work is tracked in `MERGE_PLAN.md` §A2.

## Slug validation implementation

The slug rules above are enforced by `validateInitSlug()` in
`src/cli/vclaw.ts`, called from the `init` command before any
filesystem operation. Test coverage in
`src/tests/cli-init-slug-validation.test.ts` (6 cases):

1. rejects `--project` as the slug (the historical argv-as-slug bug)
2. rejects uppercase / whitespace / dots / underscores / leading dots / leading dashes
3. rejects reserved per-project directory names
4. rejects consecutive `--`
5. rejects too-short / too-long
6. accepts a well-formed slug (`2026-05-25-disco-monster`)

Each failure mode has a distinct error message; the argv-as-slug case
explicitly names the bug to make the fix discoverable.
