Checks and balances

Every gate a script passes: the linter, the damage test, the ledger, the bar, the read-aloud pass and the readers.

TL;DR

A script passes three layers of checks. Mechanical (run by check.js): file integrity, 70 lint rules, a damage test that proves the rules fire, a ledger check that every spoken figure is sourced, timecodes, and the numeric "v6 bar". Judgement (done by the author): a 26-item read-aloud checklist and 13 judgement questions. External (needs the user's permission): two blind reader subagents. A lint score of 100 means the declared structure matches the text. It does not mean the script is good; the Moser Baer v4 readers found real problems in a script that scored 100.

  • Verdicts: HARD GATES FAILING (exit 1), HARD GATES PASS, BAR NOT YET MET, BAR MET (exit 0).
  • Hard gates: T1 integrity, T3 documentation links, L1 lint with no FAIL, D1 damage test, F1 fact ledger. Bar gates: T2 timecodes, L2 zero warnings, B1 bar numbers, M1 read-aloud, M2 readers, M3 unproven statement.
  • Never edit rules.json to pass a script; never say a gate passed unless the tool printed it.

The gate runner, check.js

node .claude/skills/video-script-production/scripts/check.js --dir research/<slug>/script --version vN [--prev vM] [--fix] [--no-selftest] [--facts FACTS.md]

It runs, in order: tool and file integrity, the chain JSON, the timecodes, lint, the damage test, the ledger check, the bar, the comparison with the previous version, and the manual gates read from HANDOFF-vN.md. It writes STATUS-vN.md and lint/. --fix rewrites stale heading timecodes first. --no-selftest skips the damage test while drafting; run it before you stop.

GateKindPasses when
T1 Tool and file integrityhardThe chain is valid JSON and no tool file, script or chain contains a stray control character (a swallowed backslash shows up as one).
T3 Documentation linkshardEvery references/..., scripts/... or examples/... path named in SKILL.md and the reference files exists. A renamed file would leave a stale instruction.
T2 TimecodesbarHeading time ranges equal the word counts at 150 wpm.
L1 LinthardNo FAIL across all 70 rules.
L2 Lint warningsbarZero WARN. Read each; a false alarm is fixed by rewording, not by raising a budget.
B1 Bar numbersbarSee the table below.
D1 Damage testhardEvery mutation is caught by the rule meant to catch it. When skipped, the gate shows "NOT YET".
F1 Fact ledgerhardEvery digit figure in the spoken text is in the ledger (or listed as a refuted rival figure); the packaging, ledger, do-not-use and open-items sections exist.
M1 Read-aloudbarA line starting Read-aloud pass: done in the hand-off file.
M2 ReadersbarA line Reader pass: done or Reader pass: skipped: <reason>.
M3 UnprovenbarAn Unproven: line of at least 15 characters stating what is not proven.

"Hard" gates decide the exit code. "Bar" gates decide whether BAR MET is printed.

B1: the v6 bar numbers

CheckThreshold (from rules.json bar)
Contractions per 100 wordsat least 1.5
Quantities per minuteat most 3
Hook length60 to 110 seconds
Stretch with no person or "you"at most 60 seconds
Hard 30-second windowsat most 40%
Comparison markers4 to 16
Lint warnings0

These are tighter than the linter so that a script that merely passes is not mistaken for a finished one.

The linter, lint.js (70 rules)

node scripts/lint.js --script SCRIPT-vN.md --chain chain-vN.json --label vN --out lint/

Without a chain it runs in text-only mode. Output: lint-<label>.json and .md; exit 1 on a FAIL. Thresholds are in scripts/rules.json; reasons in references/principles.md. The rule list as emitted on Moser Baer v7:

RuleWhat it checks
A1Quantities per minute at most 5
A2No 30-second window holds more than 4 quantities
A3Hook has at most 1 figure
A4No naked numbers (heuristic: each figure has a pair, comparator, person or a question before it)
A5At most 7 hero numbers
A6Every hero number has a stake line before it (same beat) and a findable comparator
A7, A8Declared jargon glossed at first use; no undeclared jargon from the watch list
B1A core driving loop with a viewer prior and a destination hint, both in the hook
B2Hook within length, no greeting, destination without direction (no withheld term appears)
B3The click is confirmed with a concrete payoff in the first ~30 seconds
B4Promise ladder: three rungs, seeded in the hook, paid in ascending order and in the time bands
B5At least one unannounced bonus, present in the text and absent from the hook
B6The loop was chosen from at least three scored candidates
B7The biggest reveal comes after the midpoint
B8The hook carries a named person, a viewer stake, the weight of the rise and of the fall, forward moments and the learning
C1, C2No beat opens with an additive marker; few additive connectives inside beats
C3Dependency test: the next beat picks up a word the last two sentences created (heuristic)
C4Every junction is BUT/THEREFORE/BECAUSE, hinge quotes are in the text, the beat ends on its compel line
C5BUT share of junctions between 0.2 and 0.65
D1Every beat declares understand / care / change / compel with a link and a reason
D2, D3The care line is in the beat and before its first figure; at least 40% of beats care via the viewer or a person
D4Middle beats run 90 to 260 words
D5 to D8Loop ledger quotes exist and the spine stays open into the last quarter; at most 3 live loops; no stretch over 60 s without a pending beat-level question; nearly every beat opens a beat-level loop
E1 to E4Pulls declared, typed, touched and resolved or left open; no stretch over 45 s without a loop or a touched pull; at least 3 kinds in use; every middle beat carries a pull signal, touch or loop opening
F1 to F5Voice budgets: negation pivots at most 1; staged run-ups and script-talk at most 3; stock phrases at most 2 and extremes under 6 per 1,000 words; rhythm (no fragment drumrolls, few triads, varied sentence length); speakable (contractions, no dashes, short sentences)
G1 to G4Both persona registers early and late; contested calls get both sides; one governing metaphor with its limit, glosses by analogy and a parable; figure budget (4 to 16 comparisons, at most 2 a minute)
H1 to H5No wall of numbers (at most 3 hard windows in a row); hard windows at most 40% and quiet at least 30%; no two consecutive dense beats; a landing after each hero number; a quiet human close
I1 to I6People declared; protagonist named in the hook with a want; no hinge leaning on an unnamed pronoun; nobody and no "you" absent for over 60 s; viewer stakes in hook, middle and end; people recur
J1, J2All story parts have a beat, in a sensible order
K1 to K8Cast tags at first mention and re-entry; at most 5 new names in a beat; one-off names have a reason; counts are backed; named ideas defined once; no gloss for known terms; nothing to decode
L1 to L4Prize then contrast word in the hook; intensifier where the witness pays; every BUT heard; no neutrality disclaimer where the record supports a conclusion
M1 to M3Headline on dense beats; hook trailer and takeaway length; no lab voice

Key thresholds from rules.json: speaking pace 150 wpm; hook cap 110 seconds with at most 1 quantity; ladder rung 1 by 60% of runtime, rung 2 between 50% and 90%, rung 3 after 80%; at most 4 quantities in 30 seconds and 5 a minute; at most 7 hero numbers; beats 90 to 260 words; at most 3 live major loops; spine closes after 70%; pulls warm for 60 seconds, minimum 4 pulls and 3 kinds; rhythm windows of 30 seconds, a hard window is 2 or more quantities, a landing is at most 45 words; humans: protagonist named by word 160; person gap warn at 60 and fail at 100 seconds; dense beat is 7 quantities or 4 new names; headline at most 20 words, tease at most 30, takeaway at most 14.

How to treat a rule that fires

The damage test, selftest.js (56 checks on v7)

node scripts/selftest.js --script SCRIPT-vN.md --chain chain-vN.json

It breaks one property at a time (adds a greeting or a route term to the hook, stacks six figures in one breath, types a junction AND_THEN, moves a care line after its figure, strips a pull's touches, stacks negation pivots, and so on) and checks the matching rule fires. It tests the linter, not the script, and is the safety net when a rule or lint.js changes. A mutation counts only if the named rule fires, not a neighbour. A "MISS" means: find which text the test cut, and fix the text, not the rule. Mutations touch only **VO:** lines, because a quote can also occur in the notes sections. A chain that does not declare a section gets those mutations skipped (v6: 46 behave, 10 skipped).

The reason it exists: the damage test found loopholes in the project's own new rules. A role phrase ("its founder") counted as naming the protagonist; a name from three paragraphs earlier satisfied the dangling-pronoun test; the parable test looked only at the first and last line; two regex word boundaries had been written as backspace characters by the shell. Write the damage test for a new rule before trusting it, and try to pass the rule with a bad script.

The ledger check, ledger-check.js

Checks that every spoken digit figure is in the ledger as spoken (so "14 cents" requires the ledger to hold the words "14 cents", not only $0.14), that the four hand-off sections exist, and that [CHECK] rows reach the open items. It reads digit figures only; spelled numbers are not read. On its first run it found real gaps in v5 and v6: "14 cents" and "₹110 crore" were spoken while the ledger held $0.14 and ₹109.8 crore, and two refuted rival figures lived only in the do-not-use table.

The other deterministic tools

retime.js
Writes heading time ranges from word counts at 150 wpm (--write, --check).
extract-vo.js
Extracts the spoken text, numbered. Modes: timed (for the read-aloud pass), blind (for readers), plain (for copying into a TTS tool).
compare.js
Side-by-side text-level measures and beat openers across versions.
calibrate.js
Measures voice, rhythm, comparisons, admissions, "you" and name rates on a folder of winner transcripts, to set thresholds on data.
session-log.js
Writes every search, fetch and subagent of a Claude Code session as Markdown (the research record).
scaffold.js
Blank script, chain, FACTS and hand-off; --from vP copies the previous version.

The reference profile (Moser Baer v6)

The bar was set from the approved v6. Winners column: 8 English "why X died" transcripts (auto-captions of scripted voice-overs). Reproduce with calibrate.js --dir <winners> --script SCRIPT-vN.md.

Measurev68 English winners (median, range)
Rules passed55 of 55 (0 FAIL, 0 WARN)n/a
Length / words / beats19.6 min / 2,940 / 1419.9 min (11.6 to 30.4)
Quantities per minute / busiest 30 s / in the hook2.0 / 4 / 12.4 (0.9 to 5.3)
Hook length110 snot measured
Contractions per 100 words1.840.75 (0.13 to 1.88)
Negation pivots / staged run-ups0 / 02.5 / 1.5
Comparison markers per 1,000 words1.70.73 (0 to 2.3)
First-person admissions per 1,000 words2.40 (0 to 1.5)
"You" per 100 words1.00.33
Hard / quiet 30 s windows26% / 46%29% / 45%
Longest run of hard windows23 (1 to 13)
Pulls (kinds) / loops8 (7) / 31n/a

v7: 70 of 70 rules, 56 of 56 damage checks, 20.2 minutes, 3,029 words, hook 107 s, 2.0 quantities a minute, 5 comparison markers, headlines on beats 3, 7, 9, 10 and 12. The v6 profile is kept because v6 is the script the user approved.

The contraction median differs by sample: 12 mixed English winners had about 1.5 per 100 words, the 8 captioned "why X died" winners 0.75, so the F5 floor of 1.0 and the bar of 1.5 are design choices.

The judgement bar

These cannot be checked by a machine. The author answers them in the read-aloud pass and readers answer them if authorised. A script that passes the mechanical bar and fails any of these is not finished.

  1. The whole story is told: origin, rise with its mechanism, the first sign, the response, the fall, the people who paid, the blame question answered by the record, the lesson. Rule J1 forces a beat for each; it cannot judge whether the beat is rich.
  2. The thesis is verified, not only the numbers. Every "X caused Y" has a source for the link; if not, the script says so aloud and states the weaker thesis.
  3. A reader who has never seen the chain can say the argument back and knows who the main person is, what they wanted, and why a decision of theirs is a puzzle.
  4. The first two minutes are not a menu.
  5. The persona is likeable, not smug, is fair to both sides, and is never aloof about the people who paid.
  6. Every figure of speech earns its place.
  7. Every "I" is true of whoever will say it.
  8. The title and the evidence agree, or the disagreement is stated to the user with alternates.
  9. Nothing about a living person goes beyond the record; allegations are allegations.
  10. Nobody is a stranger. (K)
  11. Nothing needs decoding. (K)
  12. The prize is felt before the fall, the turn is heard, the cost is said, and each beat is a small arc of doubt, edge and proof. (L)
  13. A dense stretch opens with a headline, and the hook ends as a trailer. (M)

The read-aloud checklist (S9)

Print the spoken text (extract-vo.js --mode timed), read it in order as if saying it to one person, and answer each item. Each exists because a real defect of that kind passed a green linter. Record Read-aloud pass: done (<date>; what you found). A pass that found nothing is suspicious; look again at items 1 to 3.

What the reader cannot follow

  1. Every pronoun and "the same thing" has its person or thing in the previous sentence. (v5's hook ended on "the city he left" with "he" last named three paragraphs earlier; v6 beat 3 had "the same thing" pointing at nothing.)
  2. Every name, term and acronym is explained the first time it is spoken, in words a stranger would use.
  3. No stretch is a pile-up of new names, figures and concepts, even if the figure count is within budget.

Claims

  1. Nothing is asserted about what someone thought, felt or intended unless a source says it. Hedge or cut ("I think each step looked sensible").
  2. No link is built on an adjacency the script itself says it cannot support.
  3. A comparison shows scale, not innocence or guilt.
  4. Hedges are true: every "I couldn't find" describes a check that was made, by whoever will say it.
  5. Allegations and findings are kept apart; nobody is called a villain, crook or fraudster without a conviction.

Promises and flow

  1. A tease is paid at the size it was sold.
  2. Nothing is said twice (a recap after the thesis read as a third ending).
  3. The join into every beat feels like "therefore" or "but". If "and then" fits, the flow is broken.
  4. The hook promises experiences, not the route, and the learning it promises is paid in the close.

Voice

  1. Say each sentence aloud in one breath. A sentence you would not say to a friend is written English.
  2. The persona is fair and warm where it counts: aloof about verdicts, never about the workers; no sarcasm at a person; no claim to effort that was not made.
  3. Each figure of speech helps: it explains, comes back, and its limit is honest.
  4. The close is quiet and human and returns to the opening image.

Facts one more time

  1. Units and magnitudes: crore against million, rupee against dollar, "doubled" against the arithmetic (1,620 to 3,500 is 2.2 times). Check the 7 hero numbers against the ledger by eye.
  2. Dates: day, month and year of every dated event in the hook and pulls (the 21 June 2012 clock must be right).
  3. Names: spelling, and whether a private person is named on the strength of one source.

Cast, turns and dense stretches (round 7)

  1. The opening list is what the audience actually did, including the slightly guilty association.
  2. The decode test: say each sentence to someone who has not read the research; rewrite any line they would have to work out.
  3. The hook's last line is a person's act and a puzzle, not a place or a mechanism, with no manner (calm, cold) that no source gives.
  4. Each beat is a small arc: the doubt, the person's edge, the proof. The edge must be one a source supports.
  5. Restate the height before the fall point.
  6. A cliffhanger lands in the viewer ("you'd do a double take"), not in an unnamed crowd.
  7. A dense beat's headline is a trailer line, not a spoiler or a summary of the numbers; if it and the last line say the same thing, one goes.

After the pass, fix, run check.js again (edits can break a declared quote), and write the result in the hand-off. The round-7 pass found five defects, two of them created by the changes themselves (a "This pattern" with no antecedent once a line moved; a pair of "too").

The reader pass (S10; needs the user's permission)

Two Sonnet subagents get only the blind file. The viewer reads as a first-time viewer: per paragraph, the question in their head and their urge to keep watching 0 to 10; a confusion log; which figures they remember; for each topic shift, whether it felt like therefore, but or and-then; what they expect after 60 seconds; the argument back from memory; who the narrator is in three adjectives and whether they would trust them. The editor reads as a skeptical editor: the thesis as the script supports it, each causal claim rated supported-in-the-script, asserted-only or leaps; the swap and and-then tests on every boundary; load; every promise in the first 150 words and where it is paid; padding; claims about people. Readers must return their report as text. Run once on Moser Baer v4; v5, v6, v7 and Cafe Coffee Day v1 have had none.

What the v4 readers found that a 100/100 linter had not

What each layer can and cannot prove

LayerProvesDoes not prove
Lint (70 rules)The structure the author declared is present and consistent with the text; budgets are met.That a care line lands, a BUT truly reverses an expectation, the persona is likeable, or anyone would keep watching. A declaration can be gamed.
Damage testEach rule fires when its property is broken on this script.That the rules measure the right thing.
Ledger checkSpoken digit figures are in the ledger.That the ledger figures are right; that spelled-out numbers are sourced.
Read-aloud passDefects the author can hear.Anything the author cannot hear in their own draft.
ReadersWhat two model readers of the same family report, with honour-system blindness.How people will react. No viewer has seen any of these scripts.
ThresholdsCalibrated on at most 8 English winner transcripts.That they predict retention. They are design budgets.
Do not do these
  • Edit rules.json, lint.js, selftest.js, check.js, ledger-check.js or voice-patterns.json to pass a script (the launch prompt forbids it outright).
  • Claim a gate passed unless the tool printed it; paste the real output line.
  • Claim BAR MET unless check.js printed it.
  • After five honest attempts at a hard gate, stop and write exactly what fails and why in the hand-off. A truthful partial result is the right outcome; a faked pass is the worst one.

Written 2026-10-27 from the repository files and the project memory notes. If a page and a file disagree, the file is right: fix the page.