Docs/Skills and code/src/
src/stages/14-report-channel-priorities.js
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.