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

.claude/skills/video-script-production/scripts/ledger-check.js

Code100 lines6.4 KBSkill: video-script-production

What the file says about itself

Fact-ledger gate. Every figure the script SAYS must be findable in the fact ledger, and the hand-off sections must exist. node ledger-check.js --script <SCRIPT-vN.md> [--chain chain-vN.json] [--facts FACTS.md] [--strict] [--json out.json]…

Mentioned on: page 09, page 10, page 12

#!/usr/bin/env node
// Fact-ledger gate. Every figure the script SAYS must be findable in the fact ledger, and the hand-off sections must exist.
//   node ledger-check.js --script <SCRIPT-vN.md> [--chain chain-vN.json] [--facts FACTS.md] [--strict] [--json out.json]
// What it checks (deterministic, no judgement):
//   1. each digit figure in the spoken text (₹1,100 / $150 / 2.5 / 2002 / 21 ...) appears somewhere in the ledger text
//      (the "Fact ledger" section of the script up to the "NOT use" section, plus the optional FACTS.md);
//   2. each hero number in the chain appears in the ledger;
//   3. the script file has the hand-off sections: a fact ledger, a do-not-use table, open items;
//   4. if any ledger row is [CHECK] or [V-search], the open-items section says so.
// What it cannot check: that the ledger row says what the script says, or that the source is right. That is the author's job (Step 1 of SKILL.md).
// Limits: digit figures only (spelled numbers such as "five cases" are not read); a figure that is also a year or a day number is satisfied by any match.
// Exit 1 on a missing hand-off section, or on an unmatched figure with --strict.

const fs = require('fs');

const args = {};
for (let i = 2; i < process.argv.length; i++) {
  const a = process.argv[i];
  if (!a.startsWith('--')) continue;
  const k = a.slice(2), nxt = process.argv[i + 1];
  if (nxt && !nxt.startsWith('--')) { args[k] = nxt; i++; } else args[k] = true;
}
if (!args.script) { console.error('usage: node ledger-check.js --script <SCRIPT-vN.md> [--chain chain.json] [--facts FACTS.md] [--strict] [--json out.json]'); process.exit(2); }

const clean = (s) => s
  .replace(/\*\*\[[^\]]*\]\*\*/g, ' ').replace(/\[(?:V|CHECK)[^\]]*\]/gi, ' ')
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/\*+/g, '').replace(/\s+/g, ' ').trim();

const text = fs.readFileSync(args.script, 'utf8');
const lines = text.split(/\r?\n/);

// ---- sections of the script file
// --ledger-from <English script>: a translation (Hinglish) takes its ledger and its hand-off sections from the English script; its spoken figures are still its own.
const srcLines = args['ledger-from'] && args['ledger-from'] !== true ? fs.readFileSync(args['ledger-from'], 'utf8').split(/\r?\n/) : lines;
const idx = (re) => srcLines.findIndex((l) => /^##\s/.test(l) && re.test(l));
const iLedger = idx(/fact ledger/i), iNot = idx(/not use|do not use|must not use/i), iOpen = idx(/open items/i), iPack = idx(/packaging/i);
const nextH2 = (from) => { for (let i = from + 1; i < srcLines.length; i++) if (/^##\s/.test(srcLines[i])) return i; return srcLines.length; };
const section = (i) => (i < 0 ? '' : srcLines.slice(i, nextH2(i)).join('\n'));
let ledger = '';
if (iLedger >= 0) ledger = srcLines.slice(iLedger, iNot > iLedger ? iNot : srcLines.length).join('\n');
if (args.facts && args.facts !== true && fs.existsSync(args.facts)) ledger += '\n' + fs.readFileSync(args.facts, 'utf8');
const openText = section(iOpen);
const notUseN = ((s) => s.replace(/(d),(?=d{3})/g, '$1'))(section(iNot));

const problems = [], warns = [];
if (iPack < 0) problems.push('no "Packaging" section (title options, thumbnail, the title/evidence tension)');
if (iLedger < 0) problems.push('no "Fact ledger" section');
if (iNot < 0) problems.push('no "Claims you must NOT use" section (rival-video claims that could not be verified)');
if (iOpen < 0) problems.push('no "Open items" section');

// ---- status counts in the ledger
const count = (re) => (ledger.match(re) || []).length;
const status = { V: count(/\[V\]/g), Vsearch: count(/\[V-search\]/g), CHECK: count(/\[CHECK\]/g), derived: count(/\bderived\b/gi), analysis: count(/\banalysis\b/gi) };
if ((status.CHECK + status.Vsearch) > 0 && !/\bCHECK\b|confirm|V-search/i.test(openText)) warns.push(`${status.CHECK} [CHECK] and ${status.Vsearch} [V-search] ledger rows, but the open-items section does not mention them`);

// ---- spoken figures
const spoken = []; let cur = null;
for (const l of lines) {
  const h = l.match(/^###\s*\[(B\d+)\]/);
  if (h) { cur = h[1]; continue; }
  if (/^#{1,3}\s/.test(l)) { cur = null; continue; }
  const m = l.match(/^\*\*VO:\*\*\s*(.*)$/);
  if (m && cur) spoken.push({ beat: cur, text: clean(m[1]) });
}
const norm = (s) => s.replace(/(\d),(?=\d{3})/g, '$1');
const ledgerN = norm(ledger);
const present = (tok) => new RegExp(`(?<![\\d.])${tok.replace(/\./g, '\\.')}(?!\\d)`).test(ledgerN);
const seen = new Set(), missing = [], rival = [];
for (const p of spoken) {
  for (const sent of p.text.split(/(?<=[.?!])\s+/)) {
    for (const m of norm(sent).matchAll(/(?<![A-Za-z\d])[₹$]?(\d+(?:\.\d+)?)/g)) {
      const tok = m[1];
      if (seen.has(tok + '|' + p.beat)) continue;
      seen.add(tok + '|' + p.beat);
      if (present(tok)) continue;
      if (new RegExp(`(?<![\d.])${tok.replace(/./g, '\.')}(?!\d)`).test(notUseN)) { rival.push({ beat: p.beat, figure: m[0] }); continue; }
      missing.push({ beat: p.beat, figure: m[0], sentence: sent.slice(0, 120) });
    }
  }
}
// ---- hero numbers
const heroMissing = [];
if (args.chain && args.chain !== true && fs.existsSync(args.chain)) {
  const chain = JSON.parse(fs.readFileSync(args.chain, 'utf8'));
  for (const h of chain.hero_numbers || []) for (const m of norm(h.value).matchAll(/\d+(?:\.\d+)?/g)) if (!present(m[0])) heroMissing.push(`${h.id}: "${h.value}" (${m[0]}) is not in the ledger`);
}

const total = [...seen].length;
console.log(`Ledger check: ${args.script}`);
console.log(`- spoken figures checked: ${total}; not found in the ledger: ${missing.length}; rival figures refuted in the script and listed in the do-not-use table: ${rival.length}`);
console.log(`- ledger statuses: [V] ${status.V}, [V-search] ${status.Vsearch}, [CHECK] ${status.CHECK}, derived ${status.derived}, analysis ${status.analysis}`);
for (const m of missing) console.log(`  MISSING ${m.beat}: ${m.figure} in "${m.sentence}"`);
for (const h of heroMissing) console.log(`  MISSING hero: ${h}`);
for (const w of warns) console.log(`  WARN ${w}`);
for (const p of problems) console.log(`  PROBLEM ${p}`);
if (args.json && args.json !== true) fs.writeFileSync(args.json, JSON.stringify({ script: args.script, spokenFigures: total, missing, rival, heroMissing, status, warns, problems }, null, 2));
const bad = problems.length > 0 || (args.strict && (missing.length || heroMissing.length));
console.log(bad ? 'LEDGER GATE: FAIL' : (missing.length || heroMissing.length || warns.length) ? 'LEDGER GATE: pass with warnings' : 'LEDGER GATE: pass');
process.exit(bad ? 1 : 0);

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.