Docs/Skills and code/src/
src/analysis/rating-evidence.js
import { isRecencyEligible } from "./temporal.js";
/** Pure helpers shared by rating-evidence audit and snapshot collection. */
export function parseCalendarDay(value) {
if (value == null || value === "") return null;
const match = String(value).trim().match(/^(\d{4})-(\d{2})-(\d{2})(?:$|[T\s])/);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const timestamp = Date.UTC(year, month - 1, day);
const date = new Date(timestamp);
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null;
return date;
}
export function calendarAgeDays(value, now = new Date()) {
const date = value instanceof Date ? value : parseCalendarDay(value);
if (!date) return null;
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const age = Math.floor((today - date.getTime()) / 86400000);
return age < 0 ? null : age;
}
export function classifyDateWindow(value, now = new Date(), recentDays = 90, maxDays = 730) {
const age = calendarAgeDays(value, now);
if (age == null) return "unknown";
if (age <= recentDays) return "recent";
if (age <= maxDays) return "baseline";
return "outside";
}
export function isSnapshotDue(observedAt, minAgeHours = 24, now = new Date(), force = false) {
if (force || !observedAt) return true;
const timestamp = Date.parse(observedAt);
if (!Number.isFinite(timestamp)) return true;
return now.getTime() - timestamp >= Math.max(0, Number(minAgeHours) || 0) * 3600000;
}
/** Select a stable per-channel cohort first, then filter for snapshot freshness. */
export function selectSnapshotCohort(videos, {
scope = "recent", perChannel = 5, limit = 100, minAgeHours = 24,
now = new Date(), force = false
} = {}) {
if (!["recent", "baseline", "all"].includes(scope)) throw new Error(`Unsupported snapshot scope: ${scope}`);
const eligible = videos.map(video => {
const day = parseCalendarDay(video.published_at_normalized) ?? parseCalendarDay(video.published_at);
const precision = String(video.published_at_precision ?? "").trim();
// Legacy rows with no precision or the older label "date" remain usable but
// are not assumed exact elsewhere. Explicit month/year estimates are too wide
// to assign safely to the 90-day or 730-day windows.
const precisionUsable = !precision || precision === "date" || isRecencyEligible(precision);
const window = day && precisionUsable ? classifyDateWindow(day, now) : "unknown";
return { ...video, _day: day, _window: window, _precisionUsable: precisionUsable };
}).filter(video => {
if (scope === "all") return true;
return video._window === scope;
});
const groups = new Map();
for (const video of eligible) {
const key = video.channel_url || "__unassigned__";
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(video);
}
const cohort = [];
for (const group of groups.values()) {
group.sort((a, b) => {
const dayA = a._day?.getTime() ?? -Infinity;
const dayB = b._day?.getTime() ?? -Infinity;
return dayB - dayA || String(a.video_id).localeCompare(String(b.video_id));
});
const cap = Number.isFinite(Number(perChannel)) && Number(perChannel) > 0 ? Math.floor(Number(perChannel)) : Infinity;
cohort.push(...group.slice(0, cap));
}
const due = cohort.filter(video => isSnapshotDue(video.latest_snapshot_at, minAgeHours, now, force));
due.sort((a, b) => {
if (force) return (b._day?.getTime() ?? -Infinity) - (a._day?.getTime() ?? -Infinity) || String(a.video_id).localeCompare(String(b.video_id));
if (!a.latest_snapshot_at && b.latest_snapshot_at) return -1;
if (a.latest_snapshot_at && !b.latest_snapshot_at) return 1;
const ageOrder = String(a.latest_snapshot_at ?? "").localeCompare(String(b.latest_snapshot_at ?? ""));
return ageOrder || (b._day?.getTime() ?? -Infinity) - (a._day?.getTime() ?? -Infinity) || String(a.video_id).localeCompare(String(b.video_id));
});
return { cohortCount: cohort.length, dueCount: due.length, selected: due.slice(0, Math.max(1, Number(limit) || 1)) };
}
export function breakoutEligibility(recentCount, baselineCount, minRecent = 3, minBaseline = 3) {
return { eligible: recentCount >= minRecent && baselineCount >= minBaseline, missingRecent: Math.max(0, minRecent - recentCount), missingBaseline: Math.max(0, minBaseline - baselineCount) };
}
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.