Docs/Skills and code/src/

src/stages/14-report-channel-priorities.js

Code191 lines10.3 KBsrc/

import { filterQualifiedRows } from "../analysis/qualification-scope.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getDb } from "../db.js";
import { ensureResearchSchema } from "../analysis/research-schema.js";
import { buildPriorityMarkdown } from "../analysis/priority-report-format.js";

const db = getDb();
ensureResearchSchema();

function safeJson(value, fallback) {
  try { return JSON.parse(value); } catch { return fallback; }
}
function num(value) {
  return value == null || !Number.isFinite(Number(value)) ? null : Number(value);
}
function median(values) {
  const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
  if (!sorted.length) return null;
  const mid = Math.floor(sorted.length / 2);
  return Number((sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2).toFixed(2));
}
function compactScore(row) {
  return {
    channel: row.channel_name, channelUrl: row.channel_url,
    priorityScore: row.priority_score, specialistScore: row.specialist_score,
    breakthroughScore: row.breakthrough_score, noveltyScore: row.novelty_score,
    explorationScore: row.exploration_score, evidenceQualityScore: row.evidence_quality_score,
    confidence: row.confidence, embeddedVideoCount: row.embedded_video_count,
    sampledVideoCount: row.sampled_video_count, performanceVideoCount: row.performance_video_count,
    ageAdjustedPerformanceCount: row.age_adjusted_performance_count,
    dominantNicheExample: row.dominant_niche_example,
    dominantNicheSize: row.dominant_niche_size,
    dominantNicheChannelCount: row.dominant_niche_channel_count,
    rationale: row.rationale
  };
}
const runArg = process.argv.find(value => value.startsWith("--run-id="));
const latest = runArg
  ? db.prepare(`SELECT * FROM channel_priority_runs WHERE run_id=?`).get(runArg.slice("--run-id=".length))
  : db.prepare(`SELECT * FROM channel_priority_runs ORDER BY generated_at DESC LIMIT 1`).get();
if (!latest) {
  console.error("[priority-report] No channel-priority run exists. Run npm run priorities:build first.");
  process.exitCode = 1;
} else {
  const config = safeJson(latest.configuration_json, {});
  const scores = db.prepare(`
    SELECT s.*, c.name AS channel_name, c.channel_url,
      COALESCE(m.subscriber_count,c.subscriber_count) AS subscribers,
      COALESCE(m.video_count,c.video_count) AS declared_video_count,
      n.representative_title AS dominant_niche_example,
      n.member_count AS dominant_niche_size,
      n.channel_count AS dominant_niche_channel_count
    FROM channel_priority_scores s
    JOIN channels c ON c.channel_url=s.channel_url
    LEFT JOIN channel_metadata m ON m.channel_url=c.channel_url
    LEFT JOIN video_niche_clusters n ON n.run_id=s.run_id AND n.cluster_no=s.dominant_cluster_no
    WHERE s.run_id=?
    ORDER BY s.priority_score DESC, s.evidence_quality_score DESC, c.name COLLATE NOCASE
  `).all(latest.run_id).map(row => ({
    ...row,
    rationale: safeJson(row.rationale_json, {}),
    priority_score: num(row.priority_score),
    specialist_score: num(row.specialist_score),
    breakthrough_score: num(row.breakthrough_score),
    novelty_score: num(row.novelty_score),
    exploration_score: num(row.exploration_score),
    evidence_quality_score: num(row.evidence_quality_score),
    confidence: num(row.confidence)
  }));

  scores.splice(0, scores.length, ...filterQualifiedRows(db, scores, "channel", { label: "14-report-channel-priorities.js" }));

  const clusters = db.prepare(`
    SELECT * FROM video_niche_clusters WHERE run_id=? AND member_count >= ?
    ORDER BY member_count DESC, mean_similarity DESC, cluster_no
  `).all(latest.run_id, Number(config.minClusterSize ?? 3));
  const channelAssociations = db.prepare(`
    SELECT channel_url, video_id FROM channel_videos
    UNION SELECT channel_url, video_id FROM videos WHERE channel_url IS NOT NULL
  `).all();
  channelAssociations.splice(0, channelAssociations.length, ...filterQualifiedRows(db, channelAssociations, "mixed", { label: "14-report-channel-priorities.js" }));
  const channelName = new Map(db.prepare(`SELECT channel_url, name FROM channels`).all().map(row => [row.channel_url, row.name]));
  const videoTitle = new Map(db.prepare(`SELECT video_id, title FROM videos`).all().map(row => [row.video_id, row.title]));
  const channelsByVideo = new Map();
  for (const row of channelAssociations) {
    if (!channelsByVideo.has(row.video_id)) channelsByVideo.set(row.video_id, new Set());
    channelsByVideo.get(row.video_id).add(row.channel_url);
  }
  const nicheCandidates = clusters.map(cluster => {
    const members = db.prepare(`
      SELECT m.video_id, m.similarity_to_centroid
      FROM video_niche_memberships m
      WHERE m.run_id=? AND m.cluster_no=?
      ORDER BY m.similarity_to_centroid DESC
    `).all(latest.run_id, cluster.cluster_no);
    members.splice(0, members.length, ...filterQualifiedRows(db, members, "video", { label: "14-report-channel-priorities.js" }));
    const channelCounts = new Map();
    for (const member of members) {
      for (const channelUrl of channelsByVideo.get(member.video_id) ?? []) {
        channelCounts.set(channelUrl, (channelCounts.get(channelUrl) ?? 0) + 1);
      }
    }
    const topChannels = [...channelCounts.entries()]
      .sort((a, b) => b[1] - a[1] || String(channelName.get(a[0]) ?? "").localeCompare(String(channelName.get(b[0]) ?? "")))
      .slice(0, 5).map(([url, count]) => ({ channel: channelName.get(url) ?? url, channelUrl: url, videos: count }));
    return {
      clusterNo: cluster.cluster_no,
      representativeTitle: cluster.representative_title,
      memberCount: cluster.member_count,
      channelCount: cluster.channel_count,
      meanSimilarity: num(cluster.mean_similarity),
      examples: members.slice(0, 6).map(member => ({
        videoId: member.video_id,
        title: videoTitle.get(member.video_id) ?? member.video_id,
        similarityToCentroid: num(member.similarity_to_centroid)
      })),
      topChannels
    };
  });

  const topPriority = scores.filter(row => row.priority_score != null).slice(0, 50);
  const topSpecialists = [...scores].filter(row => row.specialist_score != null).sort((a, b) => b.specialist_score - a.specialist_score).slice(0, 25);
  const topBreakthroughs = [...scores].filter(row => row.breakthrough_score != null).sort((a, b) => b.breakthrough_score - a.breakthrough_score).slice(0, 25);
  const topNovelty = [...scores].filter(row => row.novelty_score != null).sort((a, b) => b.novelty_score - a.novelty_score).slice(0, 25);
  const dataSummary = {
    channels: scores.length,
    rankableChannels: scores.filter(row => row.priority_score != null).length,
    channelsWithSpecialistScore: scores.filter(row => row.specialist_score != null).length,
    channelsWithBreakthroughScore: scores.filter(row => row.breakthrough_score != null).length,
    channelsWithNoveltyScore: scores.filter(row => row.novelty_score != null).length,
    embeddedVideos: db.prepare(`SELECT COUNT(*) AS n FROM video_niche_memberships WHERE run_id=?`).get(latest.run_id).n,
    allClusters: db.prepare(`SELECT COUNT(*) AS n FROM video_niche_clusters WHERE run_id=?`).get(latest.run_id).n,
    stableNicheCandidates: nicheCandidates.length,
    medianEvidenceQuality: median(scores.map(row => row.evidence_quality_score))
  };

  const report = {
    generatedAt: latest.generated_at,
    runId: latest.run_id,
    embeddingModel: latest.embedding_model,
    scoringVersion: latest.scoring_version,
    configuration: config,
    dataSummary,
    rankings: {
      overall: topPriority.map(compactScore),
      specialists: topSpecialists.map(compactScore),
      breakthroughs: topBreakthroughs.map(compactScore),
      novelty: topNovelty.map(compactScore)
    },
    nicheCandidates,
    interpretation: {
      priorityScore: "Weighted combination of available specialist, breakthrough, novelty and exploration scores; missing components have their remaining weights renormalized.",
      specialistScore: "Title embedding coherence plus concentration across discovered semantic clusters. This is a proxy for specialization, not a human-validated niche label.",
      breakthroughScore: "Within-channel outlier ratio. Where at least three usable dates exist, the basis is lifetime-average views/day; otherwise it uses raw view-count snapshots. It does not measure current growth velocity.",
      noveltyScore: "Rarity of supported multi-video clusters across distinct channels. Singletons do not count as niche novelty.",
      explorationScore: "Rewards channels with limited title coverage to preserve an exploration queue. Channels with fewer than the minimum titles are not included in the overall ranking.",
      humanAnnotationsUsedForScoring: false,
      automationPotentialIncluded: false
    },
    caveats: [
      "Clusters are heuristic, greedy cosine-to-centroid groups; they are candidate niches, not a definitive taxonomy.",
      "The clustering is deterministic for a fixed corpus, model, threshold and code version, but cluster numbers can change when the corpus changes.",
      "Title embeddings capture title semantics, not necessarily the actual video content.",
      "A single view-count snapshot cannot establish momentum or causal success.",
      "Breakthrough scores are withheld when fewer than three performance observations exist.",
      "Scores are evidence-guided prioritization heuristics, not probabilities of success.",
      "Human reference labels and automation potential are not used to calculate scores."
    ]
  };

  const here = path.dirname(fileURLToPath(import.meta.url));
  const reportDir = path.resolve(here, "../../reports");
  fs.mkdirSync(reportDir, { recursive: true });
  const stamp = latest.generated_at.replaceAll(":", "-").replaceAll(".", "-");
  const jsonPath = path.join(reportDir, `channel-priorities-${stamp}.json`);
  const mdPath = path.join(reportDir, `channel-priorities-${stamp}.md`);
  fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
  fs.writeFileSync(mdPath, buildPriorityMarkdown(report, topPriority, topSpecialists, topBreakthroughs, topNovelty, nicheCandidates));

  console.log("[priority-report] Wrote prioritization report", {
    runId: latest.run_id,
    dataSummary,
    markdown: mdPath,
    json: jsonPath,
    next: "Inspect top overall candidates, then inspect the separate specialist/breakthrough/novelty rankings before deciding what to profile or discover next."
  });
}

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.