CLAUDE.md · diff
git:20260903.c31138d to git:20260903.4f53f2c
7 added, 592 removed. Audit A to A.
- # CLAUDE.md: authoring videos for this engine
-
- This repo turns **one self-describing JSON → one rendered video** (30fps mp4, at any of **five**
- canvases: `16:9` 1920×1080 · `9:16` 1080×1920 · `1:1` 1080×1080 · `4:5` 1080×1350 · `4:3` 1440×1080,
- the table at `core/safe.js:35`; a ratio it does not name is still honoured, sized to fit the long edge
- at 1920). There is exactly **one module: `scene`**, an open canvas of **18 composable layer types**
- (`ls core/layers/`: beam · board · canvas · clip · component · composition · count · cursor · doc ·
- glow · group · html · image · lottie · rect · svg · text · video) plus camera · cuts · stings ·
- captions. `html` counts as a picture, and that matters: it is what makes brew's 46% further down.
- **No templates.** You do not pour data into a canned layout; you compose each video from the vocabulary in
- `docs/PRIMITIVES.md`. Your job when asked to "make a video about X" is to **write a scene JSON**
- (and capture the real assets it needs), then render it. You do **not** edit `scene.html` or the Go
- renderer unless explicitly asked.
-
- > **Reflecting a brand/website?** `make sections` + `make palette` (eyedrop) builds the colours pack + fonts +
- > favicon; `make sections URL=… NAME=…` inventories every real section to reflect. Design knowledge
- > lives in **`docs/DESIGN-DATABASE.md`**; the primitive vocabulary in **`docs/PRIMITIVES.md`**; motion
- > rules in **`docs/MOTION-CRAFT.md`**. **Use ONLY the site's colours** and respect dominance
- > (white-first vs dark).
-
- ## The loop `[ref: make list]`
-
- Three targets, and you will spend nearly all of your time in the first two. They are named nowhere else
- in this file, so read them here.
-
- ```bash
- make list # shows the scene module + its schema/sample
- make dev D=path/to/video.json # THE ITERATION LOOP. Build, draft-render, open. No gates, no audit.
- make studio D=path/to/video.json # the same scene with a frame scrubber + a timeline. Edit, reload, watch.
- make check D=path/to/video.json # every gate, every finding, ZERO consequence. Nothing blocks.
- make ship D=path/to/video.json # the ladder with its teeth in: author-check → render → audit → seams.
- ```
-
- `make ship` is the one command that says a film is done: author-check, render, audit, seams, in order.
- It DECLARES its own ladder before it runs, so what each step reads and whether it can stop you is
- printed by the thing that does it, not restated here. Start at `make preflight D=<file>`, though: the
- decisions that belong before the JSON are the ones this file kept mis-ordering.
-
- Single-shot forms, when you want one thing and not the ladder:
-
- ```bash
- ./bin/vawe path/to/video.json # module read from JSON → out/<name>.mp4 (--draft = fast, no grain)
- make video D=path/to/video.json # author-check → render → audit. NOCHECK=1 / NOAUDIT=1 skip a half.
- ```
-
- `make video` already runs `make audit` for you (`Makefile:149`) unless `NOAUDIT=1`. `MODE=iterate`
- (`Makefile:163`, what `make check` sets) is the see-everything-block-on-nothing mode.
-
- Every JSON **must** start with `"module": "scene"`. Save new videos as
- `formats/scene/<topic>.json` (siblings of `sample.json`). Always read `sample.json` and an existing
- video (e.g. `linear-launch.json`) first as working references, then compose, never copy a structure wholesale
- (that would re-introduce a template; the ledger flags it).
-
- > **Planning any fan-out? Read [`docs/CRAFT/SUBAGENT-BUDGET.md`](docs/CRAFT/SUBAGENT-BUDGET.md) first.**
- > It carries the measured cost of a real run here (87 agents, 5.66M tokens, 65k for each agent) and the
- > rules that follow from it: fewer and larger agents, batches of 5 to 10, file contents in the prompt,
- > never two agents on the same files, always a structured schema. One agent with a better prompt beats a
- > fan-out for sequential work, and costs less.
-
- > **Shipping a real video? USE DEDICATED SUBAGENTS.** Authoring well needs several different kinds of
- > judgement, and one agent doing all of them in one context does all of them worse: it grades its own
- > work, and its reading of six sheets crowds out the room to fix anything. Give each critic ONE job, ONE
- > input path and a fixed verdict shape, and launch them in PARALLEL in a single message. Critics report,
- > the main thread fixes; a critic is evidence, never a ruling. The standing roster (beat · bg-motion ·
- > reveal · fidelity · copy · seam) is **[`docs/CRAFT/SUBAGENTS.md`](docs/CRAFT/SUBAGENTS.md)**. Overkill for
- > a one-line tweak; required for a full pass, a recreation, or anything you intend to ship.
-
- > **Making something good?** Read **[`docs/TASTE.md`](docs/TASTE.md)** first, the front door to the
- > taste system (house-style · composition · motion · story-spine), the block registry (`make catalog`),
- > and the author→gate→render quality loop. Everything below is the doctrine it indexes.
-
- ## "LET'S MAKE A VIDEO" IS A REQUEST TO ASK QUESTIONS, NOT A REQUEST TO START `[eye]`
-
- **Load `vawe-video-planning` and follow it. Do not open a JSON file first.** The skill exists because
- authoring without a locked plan produces the same generic video for everyone, and it states the contract
- in one line: *nothing is rendered until the plan is LOCKED and the user signs off*, and *if you find
- yourself trying things in the JSON, the plan wasn't locked*.
-
- That is written here rather than left to the skill because the failure it prevents is a failure of the
- FIRST MOVE, and by the time you have opened the scene file you have already made it. It has happened:
- a launch film went eight renders deep with the requester watching each one, because a storyboard was
- written, the LOCK SHEET was skipped, and the JSON became the place decisions got made. Every render
- after that was exploration performed in public.
-
- **Four questions belong to the requester and one of them is the one that gets skipped.** Audience,
- payoff and feeling are easy to remember to ask. The one that decides whether the film looks like
- anything is the STYLE ANCHOR: a real site to study, or a named reference to manufacture one from. Skip
- it and "cinematic" becomes your guess at cinematic, which is a dark gradient and some type.
-
- **The lock sheet is the artefact, not the storyboard.** Per-beat copy in exact words, the theme's
- colours and fonts, the layout archetype, the coordinate band, the treatment per beat, the motion
- personality, the cuts, the CTA. Present it and WAIT. Then authoring is transcription, and a render is
- something you show once rather than something you iterate in front of somebody.
-
- ## THE BRIEF: what the person asking is allowed to say, and what is YOUR job `[eye]`
-
- The person asking for a film should never have to name a colour, an easing, a layer type or a preset.
- If they find themselves reaching for one, that is your failure, not their prompt. Their whole request is
- five lines, and any of them may be missing:
-
- ```
- SUBJECT what it is about, in one line
- DATA where the facts come from (a URL, a file, an API, or "here they are")
- PAYOFF the one thing to remember, and it lands LAST
- AUDIENCE who watches, and where they see it
- FEELING one reference, or one word ("brew act 1", "cinematic", "loud")
- ```
-
- **Everything else is yours to decide and defend:** palette, typography, layout, motion, camera, cuts,
- backdrop, sound, pacing, structure. If a missing field would change the film, ASK BEFORE BUILDING, not
- after: one small forced-choice question with real consequences beats a rebuild.
-
- **Two more lines, and they are YOURS to fill, not theirs to supply.** Write them into the storyboard
- before you author, every time, even when the requester says nothing about either:
-
- ```
- SPECTACLE the ONE exaggerated moment, named: which beat, which layer, which device
- NOT what this film explicitly does not do (no narration, no stock photos, no gradient hero)
- ```
-
- Both are borrowed from the reference system whose films measurably read better than ours, where every
- worked example ends with a spectacle clause and every one carries an exclusion line. The spectacle line
- is TWO-SIDED and that is the whole point: naming the loud moment is also a promise that **every other
- beat stays restrained**. A film where four beats shout has no loud moment, it has a volume setting.
- `effect-soup` and `plain-slideshow` gate the two extremes and neither asks you to NOMINATE the peak,
- which is why a film can sit safely between them and still be shapeless.
-
- The `NOT` line is cheaper than it looks. Most generic output is not a wrong decision, it is an
- un-excluded default: nothing said "no gradient hero", so a gradient hero was free.
-
- **And DO NOT hand-author a film from a blank JSON.** That is the #1 failure, and `make arsenal` exists for it,
- and it has been committed here: a 28s film of 31 hand-written layers, 74% of them text, `anim:"fade"`
- on nearly every one, one backdrop window for the whole runtime, and one hand-keyed motion track. It was
- rejected twice by the person who asked for it, and it was not below the house standard, the library
- median is **8% picture and 0% hand-keyed motion**, so it was AT it. Compose from `make blueprints`.
- Measured against the two films this repo is proudest of. **Every cell is a percentage of that film's
- top-level layers**, because the row below used to mix counts and percentages and read as nonsense
- either way:
-
- | | brew-launch-act1 | higgsfield-recreation | that film | library median |
- |---|---|---|---|---|
- | pictorial LAYERS (see the warning below) | 46% (16/35) | 38% (3/8) | 3% (unverified) | **6%** |
- | layers with a hand-keyed `motion` track | 11% (4/35) | **75%** (6/8) | 10% (unverified) | **0%** |
- | `bg` windows | **6** in 19.6s, bound to `cut@0..cut@3` | 1 in 5s | 1 in 28s | 1 in 82% of films |
-
- **Every library figure below comes from `node scripts/dev/library-stats.mjs`, and none is typed by
- hand.** That script measures all of them in one run against one population, prints the definition it
- used for "pictorial", and is the thing to quote. It exists because these numbers were hand-measured
- once and then decayed in place: three had drifted by the time `docs-drift` learned to check them (566
- effects against a real 638, the arsenal census 337 against a real 445, the library 134 against 148). Re-run it before you
- cite anything here, and correct the sentence if it moved.
-
- **The population, named once so every figure here can be re-run.** "The library" means the **134
- gate-visible scenes** the gates themselves reason over: `formats/scene/*.json` with `module=="scene"`,
- minus derivatives and `schema.json`, `_`-prefixed scratch included. `node scripts/gates/waiver-drift.mjs`
- prints that count on its first line, so it is one command away and it is the number to quote. Three
- other populations exist (98 without scratch, 154 raw files in the directory) and mixing them is how this
- file once cited 93, 130 and 144 as the size of the same library in three sentences. **"that film" is not
- named anywhere and I could not identify it, so its two cells are unverified and marked so.**
-
- **A FRESH CLONE SEES A THIRD OF 149, AND NOTHING IS BROKEN.** Films are gitignored on purpose
- (`.gitignore:61`: a video instance is not the framework), with an allowlist for the handful the site
- needs. So `waiver-drift.mjs` prints `WAIVER CENSUS · 149 scenes` on this machine and about a third of
- that on a clean checkout, and EVERY census in this file behaves the same way. If your count is smaller,
- the gates are fine and the number here is not stale: you are looking at a smaller library.
-
- Say the cost plainly, because it is real. **The two films this file argues from,
- `brew-launch-act1` and `higgsfield-recreation`, do not ship.** They are recreations of other companies'
- sites, carrying captured UI and real brand marks, and publishing those is the thing the asset rules two
- sections down already forbid. So the strongest claims here are measured against work a contributor
- cannot open, and that is a deliberate trade, not an oversight. Treat the percentages as the direction
- to author in, and re-derive any number you intend to QUOTE against the library you actually have.
-
- The backdrop line is the strongest single lever: brew inverts the whole tone of the world on four of its
- five cuts and spends its one accent window on the logo reveal. A pictorial beat on a dead backdrop is
- still a slide.
-
- > **THE 46% IS A COUNT OF LAYERS AND IT IS NOT WHAT IT SOUNDS LIKE.** brew carries **35** layers: 13
- > text, 11 image, 5 rect, 5 html, 1 count. The 46% is 16 of 35 with `html` counted as pictorial. (An
- > earlier attempt to correct this line said "11 image layers of 30" and was wrong twice over, which is
- > the hazard exactly: a number in this file gets quoted downstream faster than it gets checked.)
- > higgsfield's 38% is 3 of 8, and every one of those three is `html`: it has NO image layers at all.
- > Measured by the share of the FRAME carrying real pictorial detail, brew was **10.3%, the lowest of nine
- > films compared**, against 23.8% for the reference films and 20.6% for ours. **THAT AREA FIGURE CANNOT
- > BE RE-RUN TODAY.** It came from a one-off comparison, and the only tool that ever measured layer area
- > was `visual-vocabulary`, which was deleted for measuring it wrongly. Nothing in the repo measures area
- > now, so treat 10.3% as a recorded observation, not a live metric, and do not quote it as one.
- > Eleven small marks is still not a picture. This is the same layer-count-versus-area
- > error that killed the `visual-vocabulary` gate, whose size helper squared a 590x18 rule into 590x590
- > and credited a hairline with a tenth of the frame (`docs/TASTE.md`). Read the row above as "brew
- > places many pictorial ELEMENTS", never as "brew's frame is half picture", and when you want the second
- > thing, measure area.
-
- ## Content philosophy (what makes these good: follow it) `[eye]`
-
- Every video is built on **hook → suspense → payoff**. The data must earn attention:
-
- - **Never spoil the payoff.** The hook poses a question / open loop; the answer lands at the end.
- - **Build to a shocker.** Order beats so the most counterintuitive, "no way" moment is last.
- - **Be honest.** The on-screen copy must be true. No clickbait the video can't pay off.
- - **Stakes + a human line.** A surprising, specific fact beats a dry number.
- - **Numbers:** use real, accurate figures. The `count` layer compacts ≥1e6 (`2500000000` → `2.5B`);
- use a unit suffix for small numbers (`unit: "$B"`, value `880` → `$880B`).
-
- ## Hard rules `[built: core/validate.mjs:764]`
- - **No em-dashes (U+2014) in any on-screen text**: the validator rejects them. Use a comma, period, or ·.
- - First-frame hook ≤ ~12 words, front-load the strong word, ≤ 1 emoji.
- - Text may contain `<b>…</b>` / `<em>…</em>` (rendered as HTML). Keep names short (they sit in cards).
-
- ## Launch-video rules (standing, asked for directly: apply to every launch film) `[live: .claude/hooks/craft-live.mjs]`
-
- 1. **Check EVERY page, not the homepage.** `make sections` inventories one URL. Crawl the whole site
- (routes, view modes, empty states) before storyboarding. Tpot's real product turned out to be
- `/dir` with its LIST/CARD/BUBBLE toggle, which the homepage never shows, and the payoff shot came
- from a view no landing page links to.
- 2. **Give the logo prominence.** A mark sized like a bullet next to a headline reads as punctuation.
- It should be a deliberate element (~100px+ beside a title, 150px+ on the end card), not a marker.
- 3. **Pair entrances with their exits, directionally.** A layer that enters from the right should leave
- to the left. One continuous direction of travel per beat, never enter-and-retreat. Use
- `anim:"slide-right"` + `out:"slide-left"`.
- 4. **Blur out when moving would fight the content.** `out:"defocus"` leaves through focus instead of
- through space. Correct for faces, cards and dense grids, where sliding 50 elements reads as chaos.
- 5. **A changing word belongs in a fixed box.** If one word swaps mid-sentence, put it in a fixed-width
- chip so nothing after it reflows, and the chip is the natural place for the brand colour.
-
- ## Reflecting a real website (capture-first: the taste is already on the page) `[ref: make sections]`
-
- Never rewrite a site's sections by hand; you'll lose its taste and ignore half its assets. **Capture the
- real blocks instead**, and the ordered procedure lives in
- [`docs/CRAFT/RECREATION.md`](docs/CRAFT/RECREATION.md) step 2: inventory every section and LOOK at the
- shots, storyboard one beat per section in the site's order, target the UI cluster rather than the whole
- block, when a screenshot is the only option, and the one job hand-written HTML still has.
-
- > **Reflecting a real FILM? `make study VIDEO=refs/ref.mp4 NAME=ref`.** `make sections` reads a website;
- > nothing here read a film, so every reference this repo argues from was studied by eye once and the study
- > was lost. `study` probes the file, detects the shot boundaries, cuts a sheet with the in/mid/out frame of
- > every shot, and writes `refs/<name>/study.md` with four judgement columns for you to fill. It measures
- > duration, resolution, fps and the cuts; it never guesses what moves or what triggers the next shot.
- > Hard cuts come back exact, dissolves score nothing and it says so instead of inventing a list. `refs/`
- > is gitignored on purpose: take the grammar, never the frames. **[`docs/CRAFT/REFERENCE-STUDY.md`](docs/CRAFT/REFERENCE-STUDY.md)**.
-
- ## REACH FOR HTML FIRST. A LAYER IS FOR WHERE IT HELPS. `[eye]`
-
- **If you are hunting for the prop that does the thing CSS already does, stop and write the CSS.** A
- gradient-filled word with a bloom behind it is four declarations, and it took three failed attempts
- through the layer vocabulary to not get it. Reach for a layer TYPE when it does something you would
- otherwise hand-roll: `text` measures and fits, `count` counts, `component` captures a real product
- surface, `group` scopes a box AND a clock.
-
- **EVERY LAYER EFFECT WORKS ON AN `html` LAYER, and that is the architecture, not a coincidence.** So
- the choice is never "HTML or effects". It is HTML for what the frame LOOKS like, and the engine for
- what it DOES over time. Two refusals stand in the way and both are deliberate: no CSS `animation` or
- `transition` (they run on a clock the renderer does not own, so a seeked frame would be wrong), and no
- `opacity` or `filter` in `css` (the engine writes both every frame).
-
- Which effects were verified stacked on one fragment, what to use instead of each refusal, the four ways
- a fragment moves, and the traps that cost a render each:
- [`docs/CRAFT/HTML-FRAGMENTS.md`](docs/CRAFT/HTML-FRAGMENTS.md).
-
- ## BLACK MEANS `#000000` `[built: core/backgrounds.js:429]`
-
- When a brief says black, it means black. Every OTHER dark preset in `core/backgrounds.js` carries a tint
- or a wash: rendered on the vawe theme, `dark`, `deep` and `ink` all sample `rgb(12,18,26)` at the corner,
- which is right for a film with a lit world and wrong for one whose only light is the subject. So write
- `{"preset": "black"}`. It is a solid `#000000` with no grain, it renders `rgb(0,0,0)` at every corner,
- and the engine reads its lightness off that colour, so you do not type a tone. This used to be two lines
- of hand-authored HTML plus a `tone: "dark"` you had to remember, because the engine cannot read lightness
- out of your CSS. A rule that existed to work around a missing preset is now the preset.
-
- ## NAME THE EFFECT BEFORE YOU BUILD IT `[eye]`
-
- When a reference shows a look you cannot immediately construct, **find out what it is called and read
- its recipe.** Do not approximate it by eye and iterate. An effect a designer has made before has a name,
- the name leads to a sequence of steps, and one of those steps is always the thing you would never have
- guessed. That cost four rejected renders once: a stack of coloured, offset, blurred copies of a word
- cannot make what a **thermal blur** makes, because its colour is a gradient map on luminance and not
- paint (`docs/MISTAKES.md` #505).
-
- **Why the pull toward guessing is real**, so you can watch for it: iterating looks like progress,
- because each attempt is a render you can show, and searching produces nothing to show.
-
- **Load the `vawe-name-the-effect` skill** the moment a reference arrives and you cannot name what you
- are looking at. It carries the three questions in order, the plain-words to After-Effects vocabulary
- table, and where the effect goes afterwards so the next author inherits the name rather than the guess.
- That second half is not optional.
-
- ## Hand-writing HTML? Beat the AI slop (see `AGENTS.md`) `[eye]`
-
- Hand-authored HTML regresses to the mean: centered text, Inter, blue/purple gradient, equal card grid.
- And a fragment animated with CSS renders as a **dead still**, because the engine refuses `animation` and
- `transition` at boot. `parts` is the answer almost nobody reaches for: a CSS selector into your own
- markup, and every matched element gets an engine-driven, SEEKED entrance with a stagger, plus a paired
- exit. 8 of 164 scenes use it, against 62 that carry an `html` layer.
-
- **Load [`docs/CRAFT/HTML-FRAGMENTS.md`](docs/CRAFT/HTML-FRAGMENTS.md) before you write one by hand.** It
- carries the four ways a fragment moves, the three refusals and what to use instead, the traps that each
- cost a render, the defaults to reach past, and which skills to load. **Then gate it two ways, and know
- which one sees what:** `make preview HTML=<frag>` reads the FRAGMENT in a real browser, `make
- designspec-check D=<file>` reads the SCENE. Both must be clean before you render.
-
- ## Icons & images (real assets first, emoji last) `[live: .claude/hooks/craft-live.mjs]`
-
- **Always prefer a real image**, and captured real UI (`make capture`) is the highest-taste source. The
- rest of the ladder, and the treatment every image needs so it does not read as slop:
- [`docs/CRAFT/IMAGERY.md`](docs/CRAFT/IMAGERY.md).
-
- **Use `make assets` for logos. A bare `curl` writes a zero-byte file on a 404 and you get an invisible
- hole**, which is what two shipped assets were in until `core/boot.js` learned to refuse an asset that
- never loaded. IMAGERY.md §0 has the incident and the `curl -f` form if you must do it by hand.
-
- **Never embed copyrighted material** into a published video: movie/TV posters, album covers, film
- stills, news photos, paid stock. They trigger Content ID claims. Capture the real product UI instead.
-
- ## THE PROCESS HAS ONE OWNER, AND IT IS NOT THIS FILE `[ref: make ship]`
-
- `make ship D=<file>` is the process. It **declares its own ladder before it runs**: every step, in
- order, what that step reads, and whether it can stop you. Twenty steps today, and the number moves
- without this paragraph having to be edited, which is the entire reason it is stated there and not here.
-
- ```bash
- make preflight D=<file> # BEFORE the JSON: the nine decisions, and the arsenal aimed at this film
- make dev D=<file> # the iteration loop: build, draft-render, open. No gates, no audit
- make check D=<file> # every gate, every finding, ZERO consequence. Nothing blocks
- make ship D=<file> # the ladder with its teeth in, then the seams
- make judge D=<file> # MANDATORY, post-render, and the only step that SEES
- ```
-
- This file used to carry its own copy of that ladder: twenty-two numbered steps whose numbering did not
- sort, in which `0` and `0a` (compose from blueprints · see the whole arsenal) were printed **eleventh
- and twelfth**, inside a section titled "After writing a JSON", when both must happen before one exists.
- Three descriptions of one process, and the two that were prose went stale. The gates now say what they
- check, and a finding **names the doc that settles it**, so what is left here is the part no gate can
- hold: why, and what to do when the gate is quiet.
-
- ### Where to start, every time
-
- **`make preflight D=<file>`.** It puts the nine-step decision chain from
- [`docs/CRAFT/README.md`](docs/CRAFT/README.md) in front of you for THIS film, ranks the arsenal against
- what the film says it is, and records a receipt that goes stale the moment the scene changes. The chain
- is ordered because each decision constrains the next: beats → the anchor → the per-beat effect →
- type/colour/layout/imagery → density → show-or-tell → what holds it across cuts → restraint → sound.
-
- **`make arsenal Q="<what you mean, in plain english>"`** searches all 722 named things at once and
- prints the snippet with the key it goes in (`make effects` regenerates the full reference,
- `docs/EFFECTS.md`, 647 effects across 53 families). Reach for it before you invent anything. The measured cost
- of not doing so: the `{type:"beat"}` blueprint mechanism is used by **3 of the 149 gate-visible scenes**, and 12 of its
- 19 beats have never been used once.
-
- **`make schema AT="layers[].motion[]"`** answers the other half: not what the engine can DO, but what you
- may WRITE at one path, with every field's type and its written label, read live from
- `formats/scene/schema.json`. Ask it instead of guessing a field name. It exists because a guess
- (`in`/`out` for a keyframe's two bezier handles, which are `easeIn`/`easeOut`) shipped a refusal that
- rejected three correct films, while the schema had the answer all along and served it to nobody. No
- `AT` prints the top-level shape; a partial path (`AT=motion`) is found rather than refused.
-
- **`make track SHAPE=pan|blast|drift|enter|exit`** emits a hand-keyed `motion` track from a shape
- measured off the two reference films. Use it instead of naming a preset. See the next section for why.
-
- ## AUTHOR THE MOTION. DO NOT NAME IT. `[gated: scripts/gates/author-check.mjs#no-authored-motion]`
-
- The one measurement that separates the two films this file argues from and everything else:
-
- | | higgsfield | brew | a launch film that passed every gate |
- |---|---|---|---|
- | layers with a hand-keyed `motion` track | **6 of 8** | 4 of 35 | **0 of 16** |
- | cuts between beats | **0** | 0 (camera fx instead) | 4 |
- | text share of layers | 38% | 37% | **75%** |
-
- higgsfield is five seconds, eight layers, **no transitions at all**, and its subject travels
- `x: 30 → 0 → -155 → -288 → -447 → -542 → -600` across seven hand-placed keys whose interior is
- `linear`. brew's punctuation is four keys, `scale 1.5 → 1 → 1.04 → 1.9`, arriving over-size and leaving
- THROUGH the frame. Neither is reachable from a preset, because a preset animates ONE layer over ONE
- span with ONE curve, and what makes both films read as directed is the opposite of that.
-
- **Why this keeps happening, so it can be watched for: a preset is one word and a track is seven lines.**
- Every time both are available the cheap one wins. The ambition floor cannot tell them apart because it
- COUNTS techniques, and a preset is a technique, so the floor stays green while the film stays
- undirected. `no-authored-motion` closes that hole and BLOCKS new work.
-
- ## SHOW, DO NOT ONLY TELL. NOTHING ENFORCES THIS. `[eye]`
-
- Every beat that makes a claim must be asked what it could SHOW instead of set in type. **DECORATION**
- dresses the frame and carries no information. **EXPLANATION** does work the words cannot. A film can
- drown in the first and have none of the second.
-
- **There is no show floor, and you should know why.** `visual-vocabulary` measured a layer's area and
- its size helper squared any layer that declared one axis, so the one gate whose job was to tell a mark
- from a picture handed a pass to a hairline. It was deleted. **You are the check now**, with `make
- judge` and your eyes. No green tick will tell you a film is only type.
-
- Measured over the 149 gate-visible scenes: **64 carry zero pictorial layers of any size.** That was
- nobody's decision. It is debt, not a pattern to copy. What counts as explanation, the three questions
- the deleted gate asked and where to get the graphic:
- [`docs/CRAFT/SHOW-DONT-TELL.md`](docs/CRAFT/SHOW-DONT-TELL.md).
-
- ## A SLIDESHOW IS A FAILURE, AND A RESIZING BOX IS NOT THE ONLY WAY OUT `[eye]`
-
- The failure is easy to feel: every beat is born and dies inside its own window, so each cut is a jump
- between unrelated shots and the film is a stack of cards read aloud.
-
- The prescription used to be a CONTINUOUS OBJECT: one layer that survives a cut and changes across it.
- That is one device and it is the cheapest one, and **Murch ranks it last** of the six things a cut must
- serve, so it is the first thing you sacrifice. `no-continuous-object` measures it and cannot see a
- match cut, a oner, a sound bridge, a metric cut rate, a motif or an escalation. A film held by a motif
- and an escalation is properly structured and fails that rule every time. The catalogue of what else can
- hold a film, Murch's ranking with its percentages, and a six-question decision aid:
- [`docs/CRAFT/FILM-STRUCTURE.md`](docs/CRAFT/FILM-STRUCTURE.md).
-
- **Name three ways this film could hold its subject, and reject the first one.** If the answer is always
- "the layer resizes", you are writing a gate's minimum rather than a film. Three consecutive films here
- were one rectangle changing size, and each passed everything.
-
- ## A DEMO IS A TEN-SECOND FILM ABOUT ONE THING `[ref: make demo]`
-
- **27 of the 35 `formats/scene/_*.json` scratch scenes are contact-sheet shaped**, and none of the 35
- paints a second `bg` window. They are the worst-looking work in the repo and they are the ones we end up
- showing people, because a demo's job is "prove the mechanism works" and no step of the taste ladder
- fires for a throwaway. A shared theme was never the missing piece: all 35 already declare one. The
- missing piece was the archetype, and a blank file has none.
-
- **`make demo Q="…" [NAME=…] [FX=…] [SUBJECT=…]`** writes the archetype and runs the dev loop on it.
- Why the subject must be a PICTURE, why every one of its constants is fixed, and when the honest answer
- is `make catalog` instead: [`docs/CRAFT/SPECIMEN.md`](docs/CRAFT/SPECIMEN.md).
-
- ## THE BACKGROUND MUST MOVE, AND YOU MUST WATCH IT MOVE `[live: .claude/hooks/scene-live.mjs]`
-
- `bg` is required, so the backdrop is always your decision. A static field is a choice you have to
- justify, never a default. **A backdrop that changes PER BEAT is the cheap thing to write**: list the
- windows in the order the film turns, give none of them a `from`/`to`, and the engine binds window i to
- the joint after it (`core/junctions.js`), so the cuts you already wrote own the numbers.
-
- This is the strongest single lever in the file. brew inverts the tone of the world on four of its five
- cuts and spends its one accent window on the logo reveal, while **121 of the 149 gate-visible scenes,
- 81%, paint ONE window for the whole runtime**. A pictorial beat on a dead backdrop is still a slide.
-
- **HALF OF THIS IS LIVE NOW, AND IT IS THE CHEAPER HALF.** `.claude/hooks/scene-live.mjs` fires on every
- save of a scene JSON and says so when a film paints one `bg` window for its whole runtime, with the 82%
- beside it. What it CANNOT see is whether the window that is there actually moves, at what speed, or at
- what scale: it counts windows, and a window can hold a dead field. So the second paragraph is still
- yours.
-
- Then **judge it across frames, never on one still**: pull 4+ timestamps and compare them as a strip. A
- still hides speed, scale and direction. A background was once "matched" on one frame and was, in
- motion, twice too fast with folds half the size (`docs/MISTAKES.md` #155).
-
- ## NO RULED GRID UNLESS SOMEBODY ASKED FOR ONE `[gated: scripts/gates/designspec-check.mjs#ruled-grid]`
-
- A ruled line grid is a design tool's canvas. Put one behind a film and the film reads as a mock-up of
- itself, which is why it never arrives as a default any more. `blobs` used to bake `grid: true` into the
- preset, so `"preset": "blobs"` painted a blueprint the author never wrote: silent substitution, the
- exact failure the harvest table names. It does not now.
-
- **Want one? Write it: `grid: true` on a `softwash` fx**, with `gridColor`, `gridAlpha` and
- `gridSpacing` beside it, and be able to say in one clause what the grid is doing. Hand-written CSS has
- no single write site, so `make designspec-check` warns on `ruled-grid`: two `repeating-linear-gradient`
- rules crossing axes in one fragment. One axis is scanlines and stays quiet. Waive it in the scene with
- a `_why`. **Four films rule a grid by hand today, and none of them wrote it down.**
-
- ## SILENCE IS A DEVICE, NOT A DEFAULT `[gated: scripts/gates/audio-check.mjs#silence-without-a-reason]`
-
- **123 of the 149 gate-visible scenes ship mute, 83%**: 17 carry no `audio` key at all and 106 declare
- `silent: true`. Of those 106, **only 26 say why**. So the sentence to remember is not "nobody declares
- the silence", it is "nearly everybody declares it and almost nobody justifies it". That closes the
- whole aural family of structural device: the sound bridge, music-led structure, the unfinished
- sentence. A sound bridge is also a continuous object the picture never has to carry.
-
- Give every film sound, or state the silence: `"audio": {"silent": true, "_why": "…"}`.
- [`docs/CRAFT/SOUND.md`](docs/CRAFT/SOUND.md).
-
- ## IS THE EMPTY PART OF THE FRAME DOING A JOB? `[eye]`
-
- Whitespace is ACTIVE (isolating the subject, directing the eye) or PASSIVE (what merely happened
- between two things placed independently). Passive space does not read as minimal, it reads as
- unfinished. Two tests, and the second decides: name what the emptiness is doing in one clause, then ask
- whether enlarging the subject removes it and improves the frame. If it does, that space was never
- working.
-
- **NO GATE SEES THIS.** `make audit` fires on things COLLIDING, never on a frame that is half empty
- because nobody decided anything. Same doc carries lead room, visual weight and three-plane depth:
- [`docs/CRAFT/LAYOUT.md`](docs/CRAFT/LAYOUT.md).
-
- ## What the gates cannot do, so you must `[eye]`
-
- - **Read the sheets.** `make beats` (where each beat lands) and `make reveal` (how it arrives) are
- scored by nothing. `make beats` signs the look off with a receipt; skipping it is visible.
- - **QA the seams, not the centres.** `make seam-check` pulls the frames straddling every transition out
- of the rendered mp4. Cheapest catch for the worst class of bug.
- - **Audit every canvas you ship.** `make audit M=<file> ASPECT=all`. A scene passes at its own aspect
- and is wrong at every other: `pin` centres a *box*, so a text layer needs `w` (+ `align`). Going to a
- phone feed? Set `"destination": "tiktok"|"reels"|"shorts"`: the safe area is not a property of the
- shape (`core/safe.js`).
- - **`make judge`, and read the sheet.** The static gates cannot see composition or fidelity. If your
- eye catches a flaw, it is a FIX, never a rationalisation (`docs/JUDGE.md`).
- - **Eyeball real frames**: `make look D=<file>` / `make frame D=<file> N=<n>`. Never ship a film whose
- hook, reveal and end card you have not actually looked at.
- - **Narrated?** Pace the picture to the voice: `make pace-from-vo VO=<file>.words.json`.
- - **`make ledger`** before shipping, `make ledger-add` after the user approves.
-
- ## Waivers, legacy, and the difference `[gated: scripts/gates/author-check.mjs]`
-
- A rule you deliberately break is waived IN THE SCENE, with a reason, and **a waiver with no `_why`
- blocks**:
-
- ```json
- "authoring": {
- "allow": ["dead-air"],
- "_why": { "dead-air": "the held frame IS the beat: the room empties and nothing replaces it" }
- }
- ```
-
- **LEGACY IS NOT A WAIVER.** A waiver says somebody looked and decided. Legacy says nobody has looked
- yet: it is a debt, it warns on every run, and it disappears the moment the film complies
- (`make legacy` for the board, `make legacy STAMP=1` to pay one off). Edit a legacy film without fixing
- it and it BLOCKS. Nothing is excused for life.
-
- ## WHAT THIS ENGINE TOOK FROM THE AGENT HARNESS, AND WHERE IT LIVES `[ref: make rung]`
-
- An agent works here through Claude Code, and several of that harness's mechanisms answer problems this
- repo also has. They were adopted one at a time, by different people, without anything writing down the
- correspondence, so the same idea kept being re-invented under a new name. This is the map. **When you
- adopt another one, add its row and give it a rung**, or the next author will build a third copy.
-
- **The organising idea is the RUNG LADDER**, and it is the harness's own lesson: a rule enforced by the
- system beats a rule an agent is asked to remember. `make rung` prints the current
- distribution and `node scripts/gates/rung.mjs --list` prints the worklist, so the numbers are one command away and are not repeated
- here. Highest rung wins:
-
- ```
- [built] the engine makes it true a wrong value cannot be written
- [gated] a gate refuses it the push stops
- [live] something says it while you write a hook speaks at the keystroke
- [ref] a command answers it on demand you have to ask
- [eye] nothing but the sentence you have to remember
- ```
-
- | the harness does | this engine does | where |
- |---|---|---|
- | PostToolUse hooks that speak mid-task | the `[live]` rung: a hook reads what you just saved and answers | `.claude/hooks/*.mjs`, wired in `.claude/settings.json` |
- | deferred tools, fetched by search rather than all loaded | `make arsenal Q="…"` over every named thing, and `make schema AT=…` for what is legal at one path | `scripts/author/arsenal.mjs`, `scripts/author/schema-at.mjs` |
- | skills: instructions loaded only when the task needs them | `docs/CRAFT/*.md`, and a finding NAMES the doc that settles it | `docs/TASTE.md` indexes them |
- | structured tool results instead of scraped prose | gate findings as data, not regex over a message | `scripts/lib/findings.mjs` |
- | a subagent's context stays out of the main thread | one critic, one job, one input path, a fixed verdict shape | `docs/CRAFT/SUBAGENTS.md`, budget in `docs/CRAFT/SUBAGENT-BUDGET.md` |
- | refusing an invalid call at the tool boundary | refusing at the WRITE SITE, so the bad state is unrepresentable | `core/registry.js`: `checkBlurb`, `checkCovered`, `checkCatalog` |
-
- **Three rules that keep this from drifting, and each one was paid for.**
-
- **A gate is the LAST resort, and a ratchet is what you write when zero is not reachable.** If the bad
- value has a write site, the refusal goes there and the class ends; a gate that runs afterwards only
- promises to notice. Where a clean state genuinely cannot be reached today, the number goes in
- `verify/*-ratchet.json` and may fall but never rise. Lower one deliberately with `--stamp`, never to
- quiet a complaint.
-
- **A search that says ABSENT about something present is worse than one that stays quiet**, because it
- ends the looking. Two of these shipped: 131 registry entries carried no blurb, so they were reachable
- only by someone who already knew the name; and the block library was not in the search corpus at all,
- so `make arsenal Q="a terminal window"` answered "assume the engine does not have it" about 95
- families the engine has. Both are why `checkCovered` refuses at load now.
-
- **A blurb is not optional, and there is no exemption to ask for.** Every registry entry and every block
- row must carry one, refused at load by `checkCovered` and `checkBlurb` in `core/registry.js`. The one
- opt-out that existed, for the 42 easings, is gone along with the mechanism: it was a reason to write
- them well, not a licence to leave 42 capabilities reachable only by someone who already knew the name.
-
- **A threshold is a property of the corpus, not a constant.** `CONFIDENT` in `scripts/author/arsenal.mjs`
- is the midpoint between two measured query sets, and both sets are asserted in `lib-test`. Change what
- the engine contains and it must be re-measured, THROUGH that file's own `toks`: a pass that used a
- hand-rolled tokenizer instead produced numbers that were all wrong in the same direction.
-
- ## Changing the ENGINE, not a film? The doctrine is one file away `[live: .claude/hooks/craft-live.mjs]`
-
- Five rules govern any change to `core/`, `internal/`, a gate or the capture path, and they live in
- **[`docs/CRAFT/ENGINE-CHANGES.md`](docs/CRAFT/ENGINE-CHANGES.md)** rather than here, because this file
- is addressed to somebody authoring a film. Their triggers stay, because a rule you do not know exists
- is a rule you cannot go and read. Two of the five are decidable from a path, so
- `.claude/hooks/craft-live.mjs` says them at the keystroke: a save under `internal/scene` or
- `internal/render`, and a new file under `scripts/gates/`. The other three are not decidable from a
- filename, and the hook says nothing about them rather than guessing:
+ # CLAUDE.md: pointer to the real doctrine
- - **About to write a gate?** A gate is the LAST resort. If the bad value has a write site, the refusal
- goes there and the whole class of bug ends. A gate that runs afterwards only promises to notice.
- - **Adding an authoring convenience?** Sugar either resolves at boot or its absence fails loudly.
- Silence is never the third option: a field written and never read is the failure this repo pays for most.
- - **Adding an effect, a layer type, a block or a camera move?** Use one of the three existing extension
- primitives rather than inventing a fourth, and check whether an owner for the fact already exists.
- - **Touched the capture path?** `internal/scene`, `internal/render`, a Chrome flag, the worker count,
- anything on `.hs-layer` or `#cam`: render one film before and after and put both wall-clock times in
- the commit body. A correctness fix may cost speed. Not knowing what it cost is the failure.
- - **After EVERY render**, list every problem you hit and classify each one: framework bug (fix the
- engine now, delete the workaround), gate gap (sharpen the gate or its message), or authoring choice
- (fix the JSON). A workaround is a bug report. Log every framework-class finding to `docs/MISTAKES.md`.
+ The doctrine for authoring videos with this engine lives in `AGENTS.md`, which is now canonical and
+ tool-neutral. Read it, not this file.
+ @AGENTS.md
- > **Editing `scene.html`?** Read the `vawe-scene-authoring` skill first (render-frame purity,
- > tokens, motion primitives, image/capture system, QA loop). System map: `docs/CODEMAPS/ARCHITECTURE.md`.
- > Run `make probe` after scene-logic changes and `make review` for a fast health snapshot.
+ Claude Code specific mechanics (hooks, the Skill tool, vendored skills) are labelled inline in
+ `AGENTS.md`, each with a neutral note for agents that lack them. Do not duplicate doctrine here: add a
+ new rule to `AGENTS.md`, not to this shim.