Docs/Skills and code/src/

src/analysis/rating-evidence.js

Code91 lines4.4 KBsrc/

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.