Docs/Skills and code/skill within-channel-study

.claude/skills/within-channel-study/scripts/check-prereqs.js

Code70 lines5.0 KBSkill: within-channel-study

What the file says about itself

Gate for the within-channel study: (1) catalogue mined, (2) six sampled transcripts present. node check-prereqs.js --channel <name|handle|url> [--db data/x.db] [--transcripts-dir PATH] Exit codes: 0 ready, 2 catalogue not mined, 3…

#!/usr/bin/env node
// Gate for the within-channel study: (1) catalogue mined, (2) six sampled transcripts present.
//   node check-prereqs.js --channel <name|handle|url> [--db data/x.db] [--transcripts-dir PATH]
// Exit codes: 0 ready, 2 catalogue not mined, 3 transcripts missing/incomplete, 4 channel unknown, 5 channel too small to study.
const fs = require('fs');
const path = require('path');
const { ROOT, MIN_ELIGIBLE, parseArgs, channelFolderName, findChannel, loadVideos, rankVideos } = require('./lib');

const args = parseArgs(process.argv.slice(2));
if (!args.channel || args.channel === true) {
  console.error('usage: node check-prereqs.js --channel <name|handle|url> [--db data/x.db]');
  process.exit(1);
}
const transcriptsRoot = path.resolve(args['transcripts-dir'] || path.join(ROOT, 'transcripts'));
const sampler = (extra = '', dbPath) =>
  `${dbPath ? `ECOSYSTEM_DB_PATH=${path.relative(ROOT, dbPath).replace(/\\/g, '/')} ` : ''}npm run transcripts:sample -- --channel-url="${args.channel}"${extra}`;

const hits = findChannel(String(args.channel), args.db);
if (!hits.length) {
  console.log(`CHANNEL UNKNOWN: "${args.channel}" is not in any database under data/ (or the one given).`);
  console.log('The sampler mines the catalogue first and then saves the transcripts (slow, uses the network):');
  console.log('  npm run transcripts:sample -- --channel-url=<full channel url>');
  console.log('Prefix ECOSYSTEM_DB_PATH=data/<ecosystem>.db if the channel belongs to a non-default ecosystem database.');
  process.exit(4);
}
const scored = hits.map(h => ({ ...h, videos: loadVideos(h.db, h.channel.channel_url) })).sort((a, b) => b.videos.length - a.videos.length);
if (scored.length > 1) console.log(`NOTE: channel found in ${scored.length} databases (${scored.map(h => path.basename(h.dbPath)).join(', ')}); using the one with most saved videos. Pass --db to override.`);
const { dbPath, db, channel, videos } = scored[0];

// ---- 1. catalogue ----
const { ranked, fallback } = rankVideos(videos);
const cap = Math.min(300, channel.declared_videos || 300);
let enrichRows = null;
try { enrichRows = db.prepare('SELECT COUNT(*) n FROM channel_video_enrichment WHERE channel_url=?').get(channel.channel_url).n; } catch { /* table absent: stage 23 never ran on this DB */ }
const coverage = cap ? videos.length / cap : 1;
const catalogueOk = ranked.length >= MIN_ELIGIBLE && !fallback && coverage >= 0.5 && (enrichRows == null || enrichRows > 0);
console.log(`CHANNEL: ${channel.name} (${channel.handle || channel.channel_url})  DB: ${path.relative(ROOT, dbPath)}`);
console.log(`CATALOGUE: ${videos.length} saved videos, ${ranked.length} rankable (view count + publish date, >=14 days old); declared ${channel.declared_videos ?? 'n/a'}; coverage ${(coverage * 100).toFixed(0)}% of min(declared,300); catalogue-mine rows ${enrichRows ?? 'table missing'}${fallback ? '; FALLBACK pool (too few videos older than 14 days)' : ''}`);

// ---- 2. transcripts ----
const dir = path.join(transcriptsRoot, channelFolderName(channel.name));
const want = ['best-1', 'best-2', 'median-1', 'median-2', 'worst-1', 'worst-2'];
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => /^(best|median|worst)-\d+_.+\.txt$/.test(f)) : [];
const have = want.filter(w => files.some(f => f.startsWith(w + '_')));
const hasIndex = fs.existsSync(path.join(dir, '_index.json'));
const transcriptsOk = hasIndex && have.length === want.length;
console.log(`TRANSCRIPTS: ${path.relative(ROOT, dir)} -> ${have.length}/6 sample files (${have.join(', ') || 'none'}); _index.json ${hasIndex ? 'present' : 'missing'}`);
if (hasIndex) {
  const idx = JSON.parse(fs.readFileSync(path.join(dir, '_index.json'), 'utf8'));
  const ids = (idx.files || []).map(f => f.video_id);
  const stray = files.filter(f => !ids.some(id => f.includes(id)));
  if (stray.length) console.log(`WARNING: files not listed in _index.json (stale sample?): ${stray.join(', ')}`);
  console.log(`  sampled ${idx.generated_at}; ranking pool ${idx.ranking?.eligible} videos${idx.ranking?.used_fallback_pool ? ' (FALLBACK pool: includes videos under 14 days old)' : ''}`);
}

if (catalogueOk && transcriptsOk) { console.log('READY: both prerequisites satisfied.'); process.exit(0); }
if (!catalogueOk && coverage >= 0.9 && (fallback || ranked.length < MIN_ELIGIBLE)) {
  console.log(`CHANNEL TOO SMALL: the whole catalogue is already saved (${videos.length} videos) but only ${ranked.length} are old enough to rank. Re-mining will not help.`);
  console.log('A best/median/worst split is not meaningful here; treat any study as exploratory or wait until the channel has 10+ videos older than 14 days.');
  process.exit(5);
}
if (!catalogueOk) {
  console.log('NOT READY (catalogue): not mined well enough to rank videos fairly. Run (mines, then samples):');
  console.log('  ' + sampler('', dbPath));
  process.exit(2);
}
console.log('NOT READY (transcripts): catalogue is fine; the transcript sample is missing or incomplete. Run:');
console.log('  ' + sampler(' --skip-mine', dbPath));
process.exit(3);

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.