Checks and balances
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.jsonto 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.
| Gate | Kind | Passes when |
|---|---|---|
| T1 Tool and file integrity | hard | The 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 links | hard | Every references/..., scripts/... or examples/... path named in SKILL.md and the reference files exists. A renamed file would leave a stale instruction. |
| T2 Timecodes | bar | Heading time ranges equal the word counts at 150 wpm. |
| L1 Lint | hard | No FAIL across all 70 rules. |
| L2 Lint warnings | bar | Zero WARN. Read each; a false alarm is fixed by rewording, not by raising a budget. |
| B1 Bar numbers | bar | See the table below. |
| D1 Damage test | hard | Every mutation is caught by the rule meant to catch it. When skipped, the gate shows "NOT YET". |
| F1 Fact ledger | hard | Every 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-aloud | bar | A line starting Read-aloud pass: done in the hand-off file. |
| M2 Readers | bar | A line Reader pass: done or Reader pass: skipped: <reason>. |
| M3 Unproven | bar | An 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
| Check | Threshold (from rules.json bar) |
|---|---|
| Contractions per 100 words | at least 1.5 |
| Quantities per minute | at most 3 |
| Hook length | 60 to 110 seconds |
| Stretch with no person or "you" | at most 60 seconds |
| Hard 30-second windows | at most 40% |
| Comparison markers | 4 to 16 |
| Lint warnings | 0 |
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:
| Rule | What it checks |
|---|---|
| A1 | Quantities per minute at most 5 |
| A2 | No 30-second window holds more than 4 quantities |
| A3 | Hook has at most 1 figure |
| A4 | No naked numbers (heuristic: each figure has a pair, comparator, person or a question before it) |
| A5 | At most 7 hero numbers |
| A6 | Every hero number has a stake line before it (same beat) and a findable comparator |
| A7, A8 | Declared jargon glossed at first use; no undeclared jargon from the watch list |
| B1 | A core driving loop with a viewer prior and a destination hint, both in the hook |
| B2 | Hook within length, no greeting, destination without direction (no withheld term appears) |
| B3 | The click is confirmed with a concrete payoff in the first ~30 seconds |
| B4 | Promise ladder: three rungs, seeded in the hook, paid in ascending order and in the time bands |
| B5 | At least one unannounced bonus, present in the text and absent from the hook |
| B6 | The loop was chosen from at least three scored candidates |
| B7 | The biggest reveal comes after the midpoint |
| B8 | The hook carries a named person, a viewer stake, the weight of the rise and of the fall, forward moments and the learning |
| C1, C2 | No beat opens with an additive marker; few additive connectives inside beats |
| C3 | Dependency test: the next beat picks up a word the last two sentences created (heuristic) |
| C4 | Every junction is BUT/THEREFORE/BECAUSE, hinge quotes are in the text, the beat ends on its compel line |
| C5 | BUT share of junctions between 0.2 and 0.65 |
| D1 | Every beat declares understand / care / change / compel with a link and a reason |
| D2, D3 | The care line is in the beat and before its first figure; at least 40% of beats care via the viewer or a person |
| D4 | Middle beats run 90 to 260 words |
| D5 to D8 | Loop 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 E4 | Pulls 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 F5 | Voice 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 G4 | Both 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 H5 | No 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 I6 | People 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, J2 | All story parts have a beat, in a sensible order |
| K1 to K8 | Cast 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 L4 | Prize 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 M3 | Headline 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
- FAIL: fix the script, never
rules.json.references/lint-fixes.mdlists what each rule usually means and the fix that worked. - WARN: read it and decide. Heuristic rules (A4 naked numbers, C3 shared noun, F1 negation pivots) can be false alarms. Reword; do not raise the budget.
- Known false positives: "I couldn't find what it earned. It was still..." looks like a negation pivot; "moving on" matches the additive list even when it means leaving; "L&T" normalises to "lt", which sits inside "built" (rule A7); the sentence splitter ends sentences at "V.G."; D2 reads only digit figures, so number-words hide a first figure; comparison markers are a list and one phrase can count twice.
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 vPcopies 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.
| Measure | v6 | 8 English winners (median, range) |
|---|---|---|
| Rules passed | 55 of 55 (0 FAIL, 0 WARN) | n/a |
| Length / words / beats | 19.6 min / 2,940 / 14 | 19.9 min (11.6 to 30.4) |
| Quantities per minute / busiest 30 s / in the hook | 2.0 / 4 / 1 | 2.4 (0.9 to 5.3) |
| Hook length | 110 s | not measured |
| Contractions per 100 words | 1.84 | 0.75 (0.13 to 1.88) |
| Negation pivots / staged run-ups | 0 / 0 | 2.5 / 1.5 |
| Comparison markers per 1,000 words | 1.7 | 0.73 (0 to 2.3) |
| First-person admissions per 1,000 words | 2.4 | 0 (0 to 1.5) |
| "You" per 100 words | 1.0 | 0.33 |
| Hard / quiet 30 s windows | 26% / 46% | 29% / 45% |
| Longest run of hard windows | 2 | 3 (1 to 13) |
| Pulls (kinds) / loops | 8 (7) / 31 | n/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.
- 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.
- 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.
- 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.
- The first two minutes are not a menu.
- The persona is likeable, not smug, is fair to both sides, and is never aloof about the people who paid.
- Every figure of speech earns its place.
- Every "I" is true of whoever will say it.
- The title and the evidence agree, or the disagreement is stated to the user with alternates.
- Nothing about a living person goes beyond the record; allegations are allegations.
- Nobody is a stranger. (K)
- Nothing needs decoding. (K)
- 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)
- 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
- 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.)
- Every name, term and acronym is explained the first time it is spoken, in words a stranger would use.
- No stretch is a pile-up of new names, figures and concepts, even if the figure count is within budget.
Claims
- Nothing is asserted about what someone thought, felt or intended unless a source says it. Hedge or cut ("I think each step looked sensible").
- No link is built on an adjacency the script itself says it cannot support.
- A comparison shows scale, not innocence or guilt.
- Hedges are true: every "I couldn't find" describes a check that was made, by whoever will say it.
- Allegations and findings are kept apart; nobody is called a villain, crook or fraudster without a conviction.
Promises and flow
- A tease is paid at the size it was sold.
- Nothing is said twice (a recap after the thesis read as a third ending).
- The join into every beat feels like "therefore" or "but". If "and then" fits, the flow is broken.
- The hook promises experiences, not the route, and the learning it promises is paid in the close.
Voice
- Say each sentence aloud in one breath. A sentence you would not say to a friend is written English.
- 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.
- Each figure of speech helps: it explains, comes back, and its limit is honest.
- The close is quiet and human and returns to the opening image.
Facts one more time
- 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.
- Dates: day, month and year of every dated event in the hook and pulls (the 21 June 2012 clock must be right).
- Names: spelling, and whether a private person is named on the strength of one source.
Cast, turns and dense stretches (round 7)
- The opening list is what the audience actually did, including the slightly guilty association.
- The decode test: say each sentence to someone who has not read the research; rewrite any line they would have to work out.
- 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.
- Each beat is a small arc: the doubt, the person's edge, the proof. The edge must be one a source supports.
- Restate the height before the fall point.
- A cliffhanger lands in the viewer ("you'd do a double take"), not in an unnamed crowd.
- 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
- Two paragraphs too dense in names and terms (the linter counted figures, not people or jargon). K3, K4 and M1 were later built from this.
- A tease that was oversold ("something so strange you can't unsee" was paid by an ordinary pair of results).
- A link built on an adjacency the script could not support ("alongside all this, the company owed a bond").
- A comparison that showed scale and not innocence.
- A recap after the thesis that read as a third ending.
- Unglossed Philips, gigawatt, tribunal, Alchemist.
- That the title's "Killed" outran the evidence.
What each layer can and cannot prove
| Layer | Proves | Does 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 test | Each rule fires when its property is broken on this script. | That the rules measure the right thing. |
| Ledger check | Spoken digit figures are in the ledger. | That the ledger figures are right; that spelled-out numbers are sourced. |
| Read-aloud pass | Defects the author can hear. | Anything the author cannot hear in their own draft. |
| Readers | What two model readers of the same family report, with honour-system blindness. | How people will react. No viewer has seen any of these scripts. |
| Thresholds | Calibrated on at most 8 English winner transcripts. | That they predict retention. They are design budgets. |
- Edit
rules.json,lint.js,selftest.js,check.js,ledger-check.jsorvoice-patterns.jsonto 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 METunlesscheck.jsprinted 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.