Docs/Skills and code/src/
src/analysis/channel-evidence.js
import crypto from "node:crypto";
import { config } from "../../config.js";
import { isRecencyEligible } from "./temporal.js";
const labels = new Set(config.qualification.classification.labels);
const clean = value => String(value ?? "").replace(/\s+/g, " ").trim();
const numberOrNull = value => value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value)) ? Number(value) : null;
function uniqueById(rows) {
const seen = new Set();
return rows.filter(row => {
const key = clean(row.video_id || row.title).toLowerCase();
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
}
/** A deterministic, bounded evidence packet shared by the LLM stage and queue scorer. */
export function buildQualificationInput(channel) {
const titleLimit = config.qualification.llm.maxVideoTitles;
const description = clean(channel.description).slice(0, config.qualification.llm.descriptionChars);
const keywords = Array.isArray(channel.keywords)
? channel.keywords.map(clean).filter(Boolean).slice(0, 30)
: parseJsonArray(channel.keywords_json);
const videos = (channel.videos ?? []).map(video => ({
video_id: clean(video.video_id) || null,
title: clean(video.title).slice(0, 240),
views: numberOrNull(video.view_count ?? video.views),
published_at: clean(video.published_at_normalized ?? video.published_at) || null,
published_at_precision: clean(video.published_at_precision) || "unknown"
})).filter(video => video.title);
// Always break ties deterministically. The qualification stage and queue scorer
// obtain the same videos through different SQL paths, so relying on input order
// makes equal-date/equal-view titles produce different fingerprints.
const stableTitleOrder = (a, b) => a.title.localeCompare(b.title) || String(a.video_id ?? "").localeCompare(String(b.video_id ?? ""));
const byPopularity = (a, b) => (b.views ?? -1) - (a.views ?? -1) || stableTitleOrder(a, b);
const popular = [...videos].sort(byPopularity).slice(0, Math.ceil(titleLimit / 2));
const dated = [...videos].filter(v => v.published_at && /^\d{4}-\d{2}-\d{2}/.test(v.published_at))
.sort((a, b) => String(b.published_at).localeCompare(String(a.published_at)) || stableTitleOrder(a, b))
.slice(0, Math.floor(titleLimit / 2));
const selected = uniqueById([...popular, ...dated]);
if (selected.length < titleLimit) {
selected.push(...uniqueById([...videos].sort(byPopularity).filter(v => !selected.some(s => s.video_id && s.video_id === v.video_id || s.title === v.title))).slice(0, titleLimit - selected.length));
}
const boundedVideos = selected.slice(0, titleLimit).map(v => ({
video_id: v.video_id, title: v.title,
published_at: v.published_at, published_at_precision: v.published_at_precision
}));
const packet = {
channel_url: clean(channel.channel_url),
name: clean(channel.name),
description,
keywords,
sample_note: "Bounded sample of captured linked-video titles selected across high-view and recent-dated candidates where available; not guaranteed to represent the full catalogue.",
representative_videos: boundedVideos,
sampled_video_count: videos.length
};
return { packet, inputHash: crypto.createHash("sha256").update(stableStringify(packet)).digest("hex") };
}
export function stableStringify(value) {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
function parseJsonArray(value) {
if (Array.isArray(value)) return value.map(clean).filter(Boolean).slice(0, 30);
try { const parsed = JSON.parse(value ?? "[]"); return Array.isArray(parsed) ? parsed.map(clean).filter(Boolean).slice(0, 30) : []; }
catch { return []; }
}
export function normalizeQualification(row, { source = "rule_based", minimumConfidence = config.qualification.minimumConfidenceToQualify } = {}) {
const classification = String(row?.classification ?? row?.ecosystem_classification ?? "unknown").toLowerCase();
const validClass = labels.has(classification) ? classification : "unknown";
const rawConfidence = numberOrNull(row?.confidence ?? row?.qualification_confidence);
const confidence = rawConfidence == null ? (source === "llm" ? 0 : 0.35) : rawConfidence > 1 ? rawConfidence / 100 : rawConfidence;
const resolvedSource = row?.source ?? source;
const accepted = (validClass === "core" || validClass === "adjacent") && confidence >= minimumConfidence;
const eligibility = validClass === "irrelevant" ? "ineligible_irrelevant"
: accepted ? (validClass === "core" ? "eligible_core" : "eligible_adjacent")
: "qualification_pending";
return {
classification: validClass,
confidence: Math.max(0, Math.min(1, confidence)),
relevance_score: numberOrNull(row?.relevance_score),
focus_specificity: numberOrNull(row?.focus_specificity),
source: resolvedSource,
prompt_version: row?.prompt_version ?? null,
rationale: clean(row?.rationale ?? row?.reason) || null,
focus_summary: clean(row?.focus_summary) || null,
channel_type: clean(row?.channel_type) || null,
supporting_evidence: arrayValue(row?.supporting_evidence ?? row?.supporting_evidence_json),
counter_evidence: arrayValue(row?.counter_evidence ?? row?.counter_evidence_json),
topic_tags: arrayValue(row?.topic_tags ?? row?.topic_tags_json),
formats: arrayValue(row?.formats ?? row?.formats_json),
uncertainty_flags: arrayValue(row?.uncertainty_flags ?? row?.uncertainty_flags_json),
eligibility,
eligible: accepted,
minimum_confidence: minimumConfidence
};
}
function arrayValue(value) {
if (Array.isArray(value)) return value;
if (value == null || value === "") return [];
try { const parsed = typeof value === "string" ? JSON.parse(value) : value; return Array.isArray(parsed) ? parsed : []; }
catch { return []; }
}
export function buildSharedChannelEvidence(channel, now = new Date()) {
const videos = (channel.videos ?? []).map(video => {
const views = numberOrNull(video.view_count ?? video.views);
const dateText = video.published_at_normalized ?? video.published_at;
const precision = String(video.published_at_precision ?? "").trim().toLowerCase();
const precisionUsable = !precision || precision === "date" || isRecencyEligible(precision);
const date = precisionUsable && dateText && /^\d{4}-\d{2}-\d{2}/.test(String(dateText)) ? new Date(dateText) : null;
const ageDays = date && Number.isFinite(date.getTime()) ? (now.getTime() - date.getTime()) / 86400000 : null;
return { video_id: video.video_id ?? null, title: video.title ?? null, views, date: ageDays != null && ageDays >= 0 ? date : null,
age_days: ageDays != null && ageDays >= 0 ? ageDays : null,
views_per_day: views != null && ageDays != null && ageDays > 0 ? views / ageDays : null,
published_at_precision: video.published_at_precision ?? "unknown" };
}).filter(v => v.views != null && v.views >= 0);
const sortedViews = videos.map(v => v.views).sort((a, b) => a - b);
const median = medianOf(sortedViews);
const p90 = percentile(sortedViews, 0.9);
const dated = videos.filter(v => v.age_days != null && v.age_days >= config.queueScoring.breakout.minimumAgeDaysForAgeAdjustedViews);
const medianViewsPerDay = medianOf(dated.map(v => v.views_per_day).filter(Number.isFinite));
const subs = numberOrNull(channel.subscriber_count);
const prior = config.queueScoring.breakout.smallChannelSubscriberPrior;
const smoothedReach = median != null && subs != null ? median / (subs + prior) : null;
const threshold = Math.max(1000, subs == null ? 0 : subs * 0.25);
const repeatableCount = videos.filter(v => v.views >= threshold).length;
const repeatability = videos.length ? repeatableCount / videos.length * 100 : null;
const consistency = median != null && p90 != null && p90 > 0 ? median / p90 * 100 : null;
const channelAgeYears = channel.joined_date ? (now.getTime() - new Date(channel.joined_date).getTime()) / (365.2425 * 86400000) : null;
const qualification = normalizeQualification(channel.qualification_assessment ?? {
classification: channel.ecosystem_classification,
confidence: channel.ecosystem_classification_confidence,
rationale: channel.ecosystem_reason,
source: channel.ecosystem_classification_source ?? "rule_based"
}, { source: channel.qualification_assessment ? "llm" : (channel.ecosystem_classification_source ?? "rule_based") });
return {
qualification,
video_count: (channel.videos ?? []).length,
videos_with_views: videos.length,
dated_video_count: dated.length,
estimated_date_count: videos.filter(v => String(v.published_at_precision).startsWith("estimated_")).length,
median_video_views: median,
p90_video_views: p90,
median_views_per_day_proxy: medianViewsPerDay,
smoothed_views_per_subscriber: smoothedReach,
repeatable_count: repeatableCount,
repeatability_percent: repeatability,
median_to_p90_percent: consistency,
channel_age_years: Number.isFinite(channelAgeYears) && channelAgeYears >= 0 ? channelAgeYears : null,
videos,
topic_tags: qualification.topic_tags,
focus_specificity: qualification.focus_specificity
};
}
function medianOf(values) {
const cleanValues = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!cleanValues.length) return null;
const mid = Math.floor(cleanValues.length / 2);
return cleanValues.length % 2 ? cleanValues[mid] : (cleanValues[mid - 1] + cleanValues[mid]) / 2;
}
function percentile(values, p) {
if (!values.length) return null;
return values[Math.min(values.length - 1, Math.max(0, Math.ceil(values.length * p) - 1))];
}
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.