Docs/Skills and code/skill video-script-production

.claude/skills/video-script-production/references/chain-schema.md

Markdown168 lines13.5 KBSkill: video-script-production

What the file says about itself

Script file and chain sidecar: formats the linter reads: Two files per script version, side by side:

Mentioned on: page 06, page 07, page 08, page 12

Script file and chain sidecar: formats the linter reads

Two files per script version, side by side:

Script markers

## Hook ... or any heading above the first beat
### [B0] Hook (0:00 to 1:05)
**VO:** ...
### [B1] Title (1:05 to 2:25)
**VO:** ...
### [B9] Close (12:05 to 13:10)

Beat headings must be ### [Bn] ... with n consecutive from 0. Anything before [B0] is ignored.

chain.json

{
  "version": "v4",
  "title": "Video title",
  "cdl": {
    "packaging_question": "Q0, the title/thumbnail question in one sentence",
    "question": "the core driving loop, in the viewer's terms",
    "viewer_prior": "the belief the video will break",
    "destination_hint": "the shape of the answer, under 20 words, no route",
    "destination_quote": "exact words in the hook that carry the hint",
    "question_quote": "exact words in the hook that carry the question",
    "first_payoff_quote": "exact words in the first ~30 s that confirm the click",
    "withheld": ["terms that must NOT appear in the hook"],
    "closed_in": "B7",
    "candidates": [
      {"question": "...", "scores": {"specific":1,"breaks_prior":0,"lands_on_viewer":1,"shape_hintable":1,"unspoiled":1,"deliverable":1,"scalable":1}, "chosen": true}
    ]
  },
  "ladder": [
    {"rung": 1, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B4", "payoff_quote": "..."},
    {"rung": 2, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B5", "payoff_quote": "..."},
    {"rung": 3, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B9", "payoff_quote": "..."}
  ],
  "bonuses": [ {"what": "unannounced extra", "beat": "B8", "quote": "..."} ],
  "peak_reveal": {"beat": "B7", "quote": "the biggest reveal; must fall after the midpoint"},
  "hero_numbers": [
    {"id": "N1", "beat": "B2", "value": "text exactly as spoken", "stake_quote": "line BEFORE the value in the same beat", "comparator_quote": "line with/after it, optional for scale figures"}
  ],
  "jargon": [ {"term": "bond", "gloss_quote": "exact words glossing it at first use"} ],
  "pulls": [
    {"id": "P1", "type": "clock", "beat": "B7", "signal_quote": "first words that plant it", "touches": [{"beat": "B8", "quote": "a callback"}, {"beat": "B9", "quote": "another"}], "resolve": {"beat": "B9", "quote": "where it pays off"}},
    {"id": "P2", "type": "gap", "beat": "B7", "signal_quote": "...", "touches": [{"beat": "B9", "quote": "..."}, {"beat": "B11", "quote": "..."}], "left_open": {"beat": "B7", "quote": "the line that admits the gap aloud"}}
  ],
  "loops": [
    {"id": "L0", "kind": "spine", "question": "...", "opened_in": "B0", "open_quote": "...", "closed_in": "B7", "close_quote": "..."},
    {"id": "L1", "kind": "beat", "question": "...", "opened_in": "B1", "open_quote": "...", "closed_in": "B2", "close_quote": "..."}
  ],
  "beats": [
    {
      "id": "B1", "title": "short name",
      "understand": "one idea, under 20 words",
      "care": "whose stake or what contrast",
      "care_type": "viewer | person | contrast | scale",
      "care_quote": "exact words that make the viewer care, before the beat's hero numbers",
      "change": "belief before -> belief after",
      "compel": "the question this beat ends on",
      "compel_quote": "exact words, within the beat's last two sentences",
      "link_to_next": "THEREFORE | BUT | BECAUSE",
      "link_why": "what from this beat makes the next one necessary (for BUT: what expectation was planted)",
      "next_opens_with": "exact words of the next beat's first sentence (start of it)"
    }
  ]
}

The final beat has no link_to_next, compel, compel_quote or next_opens_with; it needs closes_spine: true.

Story parts (Lesson J)

Each beat declares the part of the story it tells: "arc": ["origin"]. Allowed: hook, origin, rise, peak, turn, response, fall, people, blame, verdict, lesson (a beat may carry two, such as ["fall", "people"]). By default the script must cover origin, rise, turn, response, fall, people, blame and lesson, with the origin in the first two beats after the hook and the lesson in the last two. Set "arc_required": [...] at the top level for a story of a different shape. Without any arc the J rules warn.

Texture fields (Lessons G, H, I; references/texture.md)

{
  "cdl": { "hook_elements": {
    "person_quote": "the protagonist, named, with a want, in the hook",
    "rise_quote": "the weight of the rise", "fall_quote": "the weight of the fall",
    "viewer_quote": "a line that speaks to you", "learning_quote": "what the viewer will take away",
    "forward_quotes": ["a moment to look forward to", "another"] } },
  "hero_numbers": [ { "id": "N1", "beat": "B2", "value": "...", "stake_quote": "...", "comparator_quote": "...", "landing_quote": "after the value, within 45 words, no figure, a person or the viewer or a picture" } ],
  "humans": [
    { "id": "H1", "name": "Full Name", "role": "protagonist | counterpart | witness", "aliases": ["First", "re:(?<!Other )Surname", "the founder"],
      "want_quote": "what they wanted", "detail_quote": "one concrete, ordinary detail", "stake_quote": "what they stood to lose or lost" } ],
  "viewer_stakes": [ { "beat": "B0", "kind": "learning | mirror | stand-in | you-do-this-too", "quote": "a line containing you / your" } ],
  "persona": {
    "stance": "earnest about ..., aloof about ...",
    "earnest": [ { "beat": "B0", "quote": "a line that shows work, a gap or care" } ],
    "aloof": [ { "beat": "B1", "quote": "a line that declines to pick a side" } ],
    "sides": [ { "subject": "...", "beat": "B1", "for_quote": "the case for", "against_quote": "the case against" } ] },
  "figures": [
    { "id": "F1", "kind": "governing", "beat": "B4", "source": "a treadmill", "target": "a price war", "quote": "...", "maps": "what carries over",
      "touches": [ { "beat": "B8", "quote": "..." } ], "breaks": { "beat": "B9", "quote": "the line that says where the picture fails" } },
    { "id": "F2", "kind": "gloss", "term": "bond", "beat": "B7", "source": "a tab", "quote": "..." },
    { "id": "F3", "kind": "parable", "beat": "B4", "source": "a tea lane", "quote": "Say you ...", "end_quote": "last words of the parable" } ]
}

A re: alias is a raw regular expression (useful when a surname is shared). A person is named by name or a name-like alias, never by a role phrase ("the founder"); role phrases still count as presence for the person-gap rule. viewer_stakes[].quote must contain "you", "your" or similar. Chains with none of these sections still lint; the G, H4 and I rules warn "not declared".

pulls[].type is one of clock, irony, jeopardy, prediction, motif, gap, pattern (references/implicit-pull.md). A pull needs at least two touches. It ends with resolve or, if the script admits the gap aloud, left_open. Chains with no pulls still lint; the E rules warn.

Fields added in review round 7 (K, L, M)

All optional: a chain without them still lints (the K, L, M rules warn "not declared" where a declaration is missing, and fail only when a declaration is wrong).

{
  "audience": { "known_terms": ["crore", "CBI"], "note": "never glossed (K7); a different audience moves the term back into jargon" },
  "not_cast": ["Moser Baer", "Business Today"],
  "one_off_names": [ { "name": "Esso", "why": "his first job: shows he started as an employee" } ],
  "concepts": [ { "term": "pivot", "define_quote": "the business world has a word for that: a pivot" } ],
  "counted_sets": [
    { "claim_quote": "Three pivots worked", "kind": "back", "items": ["his first pivot", "his second pivot", "his third pivot"] },
    { "claim_quote": "three questions for any company", "kind": "forward", "items": ["...", "...", "..."] } ],
  "humans": [ { "id": "H2", "name": "Ratul Puri", "role": "counterpart", "tags": ["Deepak's son", "the founder's son"] },
              { "id": "H3", "name": "Mahesh Sharma", "role": "witness", "tags": ["union president"], "cost_quote": "had spent a full 17 years inside" } ],
  "cdl": { "hook_elements": { "turn_quote": "But one morning in November 2017", "tease_quote": "Along the way: ...", "takeaway_quote": "By the end you'll have three questions for any company, and for you" } },
  "beats": [ { "id": "B3", "headline_quote": "Here's how a latecomer ended up second in the world, even though late is usually fatal" } ]
}

tags are phrases that tell a new viewer who the person is; a mention needs one in its sentence, the one before or the one after (K1 at first mention, K2 when the person returns after 150 words or more; the protagonist is exempt from K2). not_cast lists names that are not people or organisations of the story (the company itself, publications) so the name-load rule (K3) and the one-off rule (K4) skip them; places and months are skipped by default (voice-patterns.json, places_common). one_off_names gives every name that appears once a reason of 15 characters or more. A counted_sets claim is a promise (forward: its items come after it) or a tally (back: its items came before it) and the number in the claim must match the number of items; a spoken count with no entry is a FAIL. A concepts term is first used inside its define_quote and used at least twice after it. A dense beat (7 or more figures, or 4 or more new names) needs a headline_quote within its first two sentences, 20 words or fewer, no figure in it.

Per-script override of the bar (check.js only)

{ "bar_overrides": { "hook_sec": [30, 50], "reason": "User request 2026-11-02: the hook must be immediate and energetic, over in 30 to 50 seconds." } }

The v6 bar asks for a 60 to 110 s hook (rules.json bar.hook_sec). A chain can replace that one range, and only that one, when the user asks for a different hook length. check.js gate T4 fails unless a reason of 15 characters or more is given, and the B1 line says "(chain override)". lint.js is unaffected (its own hook ceiling is 110 s). Nothing else in the bar can be overridden here; change rules.json only when data contradicts a rule twice.

What the linter does with it

CheckSource
Every quote occurs in the right beat*_quote fields
Hook: length, greeting, figures, withheld terms, first payoff in first ~30 s, question and destination presentcdl, B0 text
Ladder: three rungs, seeds in the hook, payoffs in order and in the time bandsladder
Stake before data: stake quote precedes the value in the same beathero_numbers
Junctions: only BUT/THEREFORE/BECAUSE, hinge quote present, no additive opener, shared content wordbeats[].link_to_next, next_opens_with, text
Loops: spine open to the last quarter, beat loops closed within two beats, at most three live, no uncovered stretch over 45 s, every non-final beat opens a loop in its last two sentencesloops
Load: quantities per minute and in any 30 s, jargon glossed at first usetext, jargon
Pulls (E): valid type, signal and touches in the text in order, resolved or openly left open, no stretch over 45 s with nothing pending, at least four pulls of three kinds, every middle beat carries onepulls, loops
Voice (F, text only): negation pivots, staged run-ups and script-talk, stock phrases, rhythm, contractionsspoken text, scripts/voice-patterns.json
Story parts (J1, J2): every required part of the story has a beat, the origin early and the lesson last, in a sensible orderbeats[].arc, arc_required
Hook weight (B8): a named person, rise, fall, a stake for you, at least two forward moments, the learning, all in the hookcdl.hook_elements
Persona and figures (G1 to G4): both registers early and late, contested calls told both ways, one governing metaphor that returns and states its limit, glosses by analogy, a parable without figures, a budget on comparisons, stock figures and verdict wordspersona, figures, spoken text
Rhythm (H1 to H5): no wall of hard windows, a floor of quiet windows, no two dense beats in a row, a landing after each hero number, a quiet human closespoken text, hero_numbers[].landing_quote, humans
The human element (I1 to I6): protagonist and witness declared and found, protagonist named in the hook with a want, no dangling pronoun at a hinge, nobody and no "you" absent over a minute, viewer stakes in the hook, middle and close, people recurringhumans, viewer_stakes, beats[].compel_quote, text
Clarity (K1 to K8): every person tagged at first mention and at re-entry, at most five new names in a beat, a name that appears once has a reason, every spoken count backed by items, a named idea defined once and reused, no gloss for a term the audience knows, no vague referent or verb buried behind an asidehumans[].tags, not_cast, one_off_names, counted_sets, concepts, audience, spoken text
Turns (L1 to L4): the hook states the prize then turns on a contrast word, the witness's cost carries an intensifier, every BUT junction is spoken, no neutrality disclaimercdl.hook_elements.turn_quote, humans[].cost_quote, beats[].link_to_next, spoken text
Density (M1 to M3): a dense beat opens with a headline, the hook tease and takeaway are short, no lab voicebeats[].headline_quote, cdl.hook_elements.tease_quote/takeaway_quote, spoken text

Rule thresholds live in scripts/rules.json.

Show the raw text
# Script file and chain sidecar: formats the linter reads

Two files per script version, side by side:

- `SCRIPT-vN.md`: human-readable. Spoken text is every line that starts with `**VO:**`. Everything else (headings, graphics notes in italic parentheses, fact ledger) is ignored by the linter.
- `chain-vN.json`: the declared structure. Every quoted string must occur **literally** in the spoken text (case-insensitive, whitespace and punctuation collapsed), so declaring structure is a promise the text has to keep.

## Script markers

```
## Hook ... or any heading above the first beat
### [B0] Hook (0:00 to 1:05)
**VO:** ...
### [B1] Title (1:05 to 2:25)
**VO:** ...
### [B9] Close (12:05 to 13:10)
```

Beat headings must be `### [Bn] ...` with `n` consecutive from 0. Anything before `[B0]` is ignored.

## chain.json

```json
{
  "version": "v4",
  "title": "Video title",
  "cdl": {
    "packaging_question": "Q0, the title/thumbnail question in one sentence",
    "question": "the core driving loop, in the viewer's terms",
    "viewer_prior": "the belief the video will break",
    "destination_hint": "the shape of the answer, under 20 words, no route",
    "destination_quote": "exact words in the hook that carry the hint",
    "question_quote": "exact words in the hook that carry the question",
    "first_payoff_quote": "exact words in the first ~30 s that confirm the click",
    "withheld": ["terms that must NOT appear in the hook"],
    "closed_in": "B7",
    "candidates": [
      {"question": "...", "scores": {"specific":1,"breaks_prior":0,"lands_on_viewer":1,"shape_hintable":1,"unspoiled":1,"deliverable":1,"scalable":1}, "chosen": true}
    ]
  },
  "ladder": [
    {"rung": 1, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B4", "payoff_quote": "..."},
    {"rung": 2, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B5", "payoff_quote": "..."},
    {"rung": 3, "promise": "...", "seed_quote": "(hook)", "payoff_beat": "B9", "payoff_quote": "..."}
  ],
  "bonuses": [ {"what": "unannounced extra", "beat": "B8", "quote": "..."} ],
  "peak_reveal": {"beat": "B7", "quote": "the biggest reveal; must fall after the midpoint"},
  "hero_numbers": [
    {"id": "N1", "beat": "B2", "value": "text exactly as spoken", "stake_quote": "line BEFORE the value in the same beat", "comparator_quote": "line with/after it, optional for scale figures"}
  ],
  "jargon": [ {"term": "bond", "gloss_quote": "exact words glossing it at first use"} ],
  "pulls": [
    {"id": "P1", "type": "clock", "beat": "B7", "signal_quote": "first words that plant it", "touches": [{"beat": "B8", "quote": "a callback"}, {"beat": "B9", "quote": "another"}], "resolve": {"beat": "B9", "quote": "where it pays off"}},
    {"id": "P2", "type": "gap", "beat": "B7", "signal_quote": "...", "touches": [{"beat": "B9", "quote": "..."}, {"beat": "B11", "quote": "..."}], "left_open": {"beat": "B7", "quote": "the line that admits the gap aloud"}}
  ],
  "loops": [
    {"id": "L0", "kind": "spine", "question": "...", "opened_in": "B0", "open_quote": "...", "closed_in": "B7", "close_quote": "..."},
    {"id": "L1", "kind": "beat", "question": "...", "opened_in": "B1", "open_quote": "...", "closed_in": "B2", "close_quote": "..."}
  ],
  "beats": [
    {
      "id": "B1", "title": "short name",
      "understand": "one idea, under 20 words",
      "care": "whose stake or what contrast",
      "care_type": "viewer | person | contrast | scale",
      "care_quote": "exact words that make the viewer care, before the beat's hero numbers",
      "change": "belief before -> belief after",
      "compel": "the question this beat ends on",
      "compel_quote": "exact words, within the beat's last two sentences",
      "link_to_next": "THEREFORE | BUT | BECAUSE",
      "link_why": "what from this beat makes the next one necessary (for BUT: what expectation was planted)",
      "next_opens_with": "exact words of the next beat's first sentence (start of it)"
    }
  ]
}
```

The final beat has no `link_to_next`, `compel`, `compel_quote` or `next_opens_with`; it needs `closes_spine: true`.

### Story parts (Lesson J)

Each beat declares the part of the story it tells: `"arc": ["origin"]`. Allowed: hook, origin, rise, peak, turn, response, fall, people, blame, verdict, lesson (a beat may carry two, such as `["fall", "people"]`). By default the script must cover origin, rise, turn, response, fall, people, blame and lesson, with the origin in the first two beats after the hook and the lesson in the last two. Set `"arc_required": [...]` at the top level for a story of a different shape. Without any `arc` the J rules warn.

### Texture fields (Lessons G, H, I; `references/texture.md`)

```json
{
  "cdl": { "hook_elements": {
    "person_quote": "the protagonist, named, with a want, in the hook",
    "rise_quote": "the weight of the rise", "fall_quote": "the weight of the fall",
    "viewer_quote": "a line that speaks to you", "learning_quote": "what the viewer will take away",
    "forward_quotes": ["a moment to look forward to", "another"] } },
  "hero_numbers": [ { "id": "N1", "beat": "B2", "value": "...", "stake_quote": "...", "comparator_quote": "...", "landing_quote": "after the value, within 45 words, no figure, a person or the viewer or a picture" } ],
  "humans": [
    { "id": "H1", "name": "Full Name", "role": "protagonist | counterpart | witness", "aliases": ["First", "re:(?<!Other )Surname", "the founder"],
      "want_quote": "what they wanted", "detail_quote": "one concrete, ordinary detail", "stake_quote": "what they stood to lose or lost" } ],
  "viewer_stakes": [ { "beat": "B0", "kind": "learning | mirror | stand-in | you-do-this-too", "quote": "a line containing you / your" } ],
  "persona": {
    "stance": "earnest about ..., aloof about ...",
    "earnest": [ { "beat": "B0", "quote": "a line that shows work, a gap or care" } ],
    "aloof": [ { "beat": "B1", "quote": "a line that declines to pick a side" } ],
    "sides": [ { "subject": "...", "beat": "B1", "for_quote": "the case for", "against_quote": "the case against" } ] },
  "figures": [
    { "id": "F1", "kind": "governing", "beat": "B4", "source": "a treadmill", "target": "a price war", "quote": "...", "maps": "what carries over",
      "touches": [ { "beat": "B8", "quote": "..." } ], "breaks": { "beat": "B9", "quote": "the line that says where the picture fails" } },
    { "id": "F2", "kind": "gloss", "term": "bond", "beat": "B7", "source": "a tab", "quote": "..." },
    { "id": "F3", "kind": "parable", "beat": "B4", "source": "a tea lane", "quote": "Say you ...", "end_quote": "last words of the parable" } ]
}
```

A `re:` alias is a raw regular expression (useful when a surname is shared). A person is *named* by name or a name-like alias, never by a role phrase ("the founder"); role phrases still count as presence for the person-gap rule. `viewer_stakes[].quote` must contain "you", "your" or similar. Chains with none of these sections still lint; the G, H4 and I rules warn "not declared".

`pulls[].type` is one of clock, irony, jeopardy, prediction, motif, gap, pattern (`references/implicit-pull.md`). A pull needs at least two touches. It ends with `resolve` or, if the script admits the gap aloud, `left_open`. Chains with no `pulls` still lint; the E rules warn.

## Fields added in review round 7 (K, L, M)

All optional: a chain without them still lints (the K, L, M rules warn "not declared" where a declaration is missing, and fail only when a declaration is wrong).

```json
{
  "audience": { "known_terms": ["crore", "CBI"], "note": "never glossed (K7); a different audience moves the term back into jargon" },
  "not_cast": ["Moser Baer", "Business Today"],
  "one_off_names": [ { "name": "Esso", "why": "his first job: shows he started as an employee" } ],
  "concepts": [ { "term": "pivot", "define_quote": "the business world has a word for that: a pivot" } ],
  "counted_sets": [
    { "claim_quote": "Three pivots worked", "kind": "back", "items": ["his first pivot", "his second pivot", "his third pivot"] },
    { "claim_quote": "three questions for any company", "kind": "forward", "items": ["...", "...", "..."] } ],
  "humans": [ { "id": "H2", "name": "Ratul Puri", "role": "counterpart", "tags": ["Deepak's son", "the founder's son"] },
              { "id": "H3", "name": "Mahesh Sharma", "role": "witness", "tags": ["union president"], "cost_quote": "had spent a full 17 years inside" } ],
  "cdl": { "hook_elements": { "turn_quote": "But one morning in November 2017", "tease_quote": "Along the way: ...", "takeaway_quote": "By the end you'll have three questions for any company, and for you" } },
  "beats": [ { "id": "B3", "headline_quote": "Here's how a latecomer ended up second in the world, even though late is usually fatal" } ]
}
```

`tags` are phrases that tell a new viewer who the person is; a mention needs one in its sentence, the one before or the one after (K1 at first mention, K2 when the person returns after 150 words or more; the protagonist is exempt from K2). `not_cast` lists names that are not people or organisations of the story (the company itself, publications) so the name-load rule (K3) and the one-off rule (K4) skip them; places and months are skipped by default (`voice-patterns.json`, `places_common`). `one_off_names` gives every name that appears once a reason of 15 characters or more. A `counted_sets` claim is a promise (`forward`: its items come after it) or a tally (`back`: its items came before it) and the number in the claim must match the number of items; a spoken count with no entry is a FAIL. A `concepts` term is first used inside its `define_quote` and used at least twice after it. A dense beat (7 or more figures, or 4 or more new names) needs a `headline_quote` within its first two sentences, 20 words or fewer, no figure in it.

## Per-script override of the bar (check.js only)

```json
{ "bar_overrides": { "hook_sec": [30, 50], "reason": "User request 2026-11-02: the hook must be immediate and energetic, over in 30 to 50 seconds." } }
```

The v6 bar asks for a 60 to 110 s hook (`rules.json` `bar.hook_sec`). A chain can replace that one range, and only that one, when the user asks for a different hook length. `check.js` gate T4 fails unless a `reason` of 15 characters or more is given, and the B1 line says "(chain override)". `lint.js` is unaffected (its own hook ceiling is 110 s). Nothing else in the bar can be overridden here; change `rules.json` only when data contradicts a rule twice.

## What the linter does with it

| Check | Source |
|---|---|
| Every quote occurs in the right beat | `*_quote` fields |
| Hook: length, greeting, figures, withheld terms, first payoff in first ~30 s, question and destination present | `cdl`, B0 text |
| Ladder: three rungs, seeds in the hook, payoffs in order and in the time bands | `ladder` |
| Stake before data: stake quote precedes the value in the same beat | `hero_numbers` |
| Junctions: only BUT/THEREFORE/BECAUSE, hinge quote present, no additive opener, shared content word | `beats[].link_to_next`, `next_opens_with`, text |
| Loops: spine open to the last quarter, beat loops closed within two beats, at most three live, no uncovered stretch over 45 s, every non-final beat opens a loop in its last two sentences | `loops` |
| Load: quantities per minute and in any 30 s, jargon glossed at first use | text, `jargon` |
| Pulls (E): valid type, signal and touches in the text in order, resolved or openly left open, no stretch over 45 s with nothing pending, at least four pulls of three kinds, every middle beat carries one | `pulls`, `loops` |
| Voice (F, text only): negation pivots, staged run-ups and script-talk, stock phrases, rhythm, contractions | spoken text, `scripts/voice-patterns.json` |
| Story parts (J1, J2): every required part of the story has a beat, the origin early and the lesson last, in a sensible order | `beats[].arc`, `arc_required` |
| Hook weight (B8): a named person, rise, fall, a stake for you, at least two forward moments, the learning, all in the hook | `cdl.hook_elements` |
| Persona and figures (G1 to G4): both registers early and late, contested calls told both ways, one governing metaphor that returns and states its limit, glosses by analogy, a parable without figures, a budget on comparisons, stock figures and verdict words | `persona`, `figures`, spoken text |
| Rhythm (H1 to H5): no wall of hard windows, a floor of quiet windows, no two dense beats in a row, a landing after each hero number, a quiet human close | spoken text, `hero_numbers[].landing_quote`, `humans` |
| The human element (I1 to I6): protagonist and witness declared and found, protagonist named in the hook with a want, no dangling pronoun at a hinge, nobody and no "you" absent over a minute, viewer stakes in the hook, middle and close, people recurring | `humans`, `viewer_stakes`, `beats[].compel_quote`, text |
| Clarity (K1 to K8): every person tagged at first mention and at re-entry, at most five new names in a beat, a name that appears once has a reason, every spoken count backed by items, a named idea defined once and reused, no gloss for a term the audience knows, no vague referent or verb buried behind an aside | `humans[].tags`, `not_cast`, `one_off_names`, `counted_sets`, `concepts`, `audience`, spoken text |
| Turns (L1 to L4): the hook states the prize then turns on a contrast word, the witness's cost carries an intensifier, every BUT junction is spoken, no neutrality disclaimer | `cdl.hook_elements.turn_quote`, `humans[].cost_quote`, `beats[].link_to_next`, spoken text |
| Density (M1 to M3): a dense beat opens with a headline, the hook tease and takeaway are short, no lab voice | `beats[].headline_quote`, `cdl.hook_elements.tease_quote/takeaway_quote`, spoken text |

Rule thresholds live in `scripts/rules.json`.

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.