Docs/Skills and code/src/
src/analysis/channel-priority-scoring.js
import { ageInDays, isRecencyEligible } from "./temporal.js";
import { unitVector, cosineOfUnitVectors } from "./niche-clustering.js";
const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, value));
const round = (value, digits = 4) => Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
function median(values) {
const clean = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!clean.length) return null;
const mid = Math.floor(clean.length / 2);
return clean.length % 2 ? clean[mid] : (clean[mid - 1] + clean[mid]) / 2;
}
/** Scores channels from embeddings, title clusters and available performance evidence. */
export function scoreChannels({
channelRows, videoIdsByChannel, channelUrlsByVideo, videos, clusters,
membershipByVideo, performanceVideoById, minVideos = 5,
minClusterSize = 3, now = new Date(), scoringVersion = "channel-priority-v1"
}) {
const vectorById = new Map(videos.map(video => [video.video_id, video.vector]));
const clusterByNo = new Map(clusters.map(cluster => [cluster.clusterNo, cluster]));
const totalChannels = Math.max(1, channelRows.length);
for (const cluster of clusters) {
const channelsInCluster = new Set();
for (const member of cluster.members) {
for (const channelUrl of channelUrlsByVideo.get(member.video.video_id) ?? []) channelsInCluster.add(channelUrl);
}
cluster.channelCount = channelsInCluster.size;
}
const scores = [];
for (const channel of channelRows) {
const allVideoIds = [...(videoIdsByChannel.get(channel.channel_url) ?? [])];
const channelVectors = allVideoIds.map(id => vectorById.get(id)).filter(Boolean);
const embeddedCount = channelVectors.length;
const sampledCount = allVideoIds.length;
const assignedClusters = new Map();
for (const videoId of allVideoIds) {
const membership = membershipByVideo.get(videoId);
if (membership) assignedClusters.set(membership.clusterNo, (assignedClusters.get(membership.clusterNo) ?? 0) + 1);
}
let meanTitleCoherence = null;
let coherenceComponent = null;
if (embeddedCount >= minVideos) {
const sum = new Float64Array(channelVectors[0].length);
for (const vector of channelVectors) for (let i = 0; i < sum.length; i++) sum[i] += vector[i];
const centroid = unitVector(sum);
meanTitleCoherence = channelVectors.reduce((total, vector) => total + (cosineOfUnitVectors(vector, centroid) ?? 0), 0) / embeddedCount;
coherenceComponent = clamp(((meanTitleCoherence - 0.30) / 0.55) * 100);
}
const clusterCounts = [...assignedClusters.values()];
const clusterTotal = clusterCounts.reduce((sum, count) => sum + count, 0);
const dominantClusterShare = clusterTotal ? Math.max(...clusterCounts) / clusterTotal : null;
let concentration = null;
if (clusterCounts.length) {
if (clusterCounts.length === 1) concentration = 1;
else {
const entropy = clusterCounts.reduce((sum, count) => {
const p = count / clusterTotal;
return sum - p * Math.log(p);
}, 0);
concentration = clamp(100 * (1 - entropy / Math.log(clusterCounts.length))) / 100;
}
}
const specialistScore = coherenceComponent == null || concentration == null
? null : clamp(0.60 * coherenceComponent + 0.40 * concentration * 100);
let noveltyWeighted = 0;
let noveltyWeight = 0;
for (const [clusterNo, count] of assignedClusters) {
const cluster = clusterByNo.get(clusterNo);
if (!cluster || cluster.members.length < minClusterSize) continue;
const rarity = totalChannels <= 1 ? 0 : clamp(100 * (
1 - Math.log1p(Math.max(1, cluster.channelCount)) / Math.log1p(totalChannels)
));
noveltyWeighted += rarity * count;
noveltyWeight += count;
}
const noveltyScore = noveltyWeight ? noveltyWeighted / noveltyWeight : null;
// Performance evidence is independent of embedding coverage.
const channelVideoRows = allVideoIds.map(id => performanceVideoById.get(id)).filter(Boolean);
const performanceRows = channelVideoRows.filter(row => Number(row.view_count) > 0);
const ageAdjustedRows = performanceRows.map(row => {
const eligible = isRecencyEligible(row.published_at_precision);
const ageDays = eligible ? ageInDays(row.published_at_normalized, now) : null;
return ageDays ? { ...row, performanceValue: Number(row.view_count) / ageDays } : null;
}).filter(Boolean);
let performanceBasis = "insufficient_data";
let performanceMetrics = [];
if (ageAdjustedRows.length >= 3) {
performanceBasis = "lifetime_average_views_per_day";
performanceMetrics = ageAdjustedRows.map(row => ({ videoId: row.video_id, value: row.performanceValue }));
} else if (performanceRows.length >= 3) {
performanceBasis = "within_channel_view_count_snapshot";
performanceMetrics = performanceRows.map(row => ({ videoId: row.video_id, value: Number(row.view_count) }));
}
let breakthroughScore = null;
let maxRelativePerformance = null;
let outlierShare = null;
let performanceBaseline = null;
if (performanceMetrics.length >= 3) {
performanceBaseline = median(performanceMetrics.map(row => row.value));
const ratios = performanceMetrics.map(row => performanceBaseline > 0 ? row.value / performanceBaseline : 1);
maxRelativePerformance = Math.max(...ratios);
outlierShare = ratios.filter(ratio => ratio >= 3).length / ratios.length;
breakthroughScore = clamp(25 * Math.log2(Math.max(1, maxRelativePerformance)) + 30 * outlierShare);
}
const performanceCoverage = sampledCount ? performanceRows.length / sampledCount : 0;
const dateCoverage = performanceRows.length ? ageAdjustedRows.length / performanceRows.length : 0;
const embeddingCoverage = sampledCount ? embeddedCount / sampledCount : 0;
const evidenceQuality = clamp(100 * (0.55 * embeddingCoverage + 0.25 * performanceCoverage + 0.20 * dateCoverage));
const explorationScore = clamp(100 * (1 - Math.min(embeddedCount / 20, 1)));
const components = [
{ key: "specialist", value: specialistScore, weight: 0.40 },
{ key: "breakthrough", value: breakthroughScore, weight: 0.30 },
{ key: "novelty", value: noveltyScore, weight: 0.20 },
{ key: "exploration", value: explorationScore, weight: 0.10 }
].filter(component => component.value != null);
const availableWeight = components.reduce((sum, component) => sum + component.weight, 0);
const priorityScore = embeddedCount >= minVideos && availableWeight > 0
? components.reduce((sum, component) => sum + component.value * component.weight, 0) / availableWeight
: null;
const dominantClusterNo = assignedClusters.size
? [...assignedClusters.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0][0]
: null;
const missingSignals = [];
if (embeddedCount < minVideos) missingSignals.push(`needs at least ${minVideos} embedded titles for stable specialist ranking`);
if (performanceRows.length < 3) missingSignals.push("fewer than three videos have view counts; breakthrough score withheld");
else if (ageAdjustedRows.length < 3) missingSignals.push("insufficient usable publication dates for age-adjusted performance; using raw view snapshots");
if (noveltyScore == null) missingSignals.push("no supported multi-video niche cluster for novelty scoring");
scores.push({
channelUrl: channel.channel_url, channel: channel.name,
specialistScore: round(specialistScore, 2), breakthroughScore: round(breakthroughScore, 2),
noveltyScore: round(noveltyScore, 2), explorationScore: round(explorationScore, 2),
evidenceQualityScore: round(evidenceQuality, 2), priorityScore: round(priorityScore, 2),
confidence: round(evidenceQuality / 100, 3), embeddedVideoCount: embeddedCount,
sampledVideoCount: sampledCount, performanceVideoCount: performanceRows.length,
ageAdjustedPerformanceCount: ageAdjustedRows.length, dominantClusterNo,
dominantClusterShare: round(dominantClusterShare, 3), meanTitleCoherence: round(meanTitleCoherence, 4),
rationale: {
scoringVersion, rankingUsesHumanAnnotations: false,
specialist: { titleCoherenceComponent: round(coherenceComponent, 2), clusterConcentration: round(concentration, 3), formula: "0.60 * title coherence + 0.40 * cluster concentration" },
breakthrough: { score: round(breakthroughScore, 2), basis: performanceBasis, maxRelativePerformance: round(maxRelativePerformance, 2), outlierShare: round(outlierShare, 3), baseline: round(performanceBaseline, 3) },
novelty: { score: round(noveltyScore, 2), basis: "rarity of supported multi-video semantic clusters across distinct channels" },
exploration: { score: round(explorationScore, 2), basis: "higher when title coverage is low, to preserve discovery slots" },
evidenceQuality: { embeddingCoverage: round(embeddingCoverage, 3), viewCountCoverage: round(performanceCoverage, 3), usableDateCoverageAmongViewedVideos: round(dateCoverage, 3) },
priorityComponents: components.map(({ key, value, weight }) => ({ key, value: round(value, 2), configuredWeight: weight })),
availableWeightRenormalizedToOne: round(availableWeight, 3), missingSignals
}
});
}
return scores;
}
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.