Docs/Skills and code/src/

src/channel-qualification.js

Code391 lines7.2 KBsrc/

function normalizeText(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/\s+/g, " ")
    .trim();
}

function matchesTerm(text, term) {
  return normalizeText(text).includes(
    normalizeText(term)
  );
}

function collectMatches(text, terms) {
  return terms.filter((term) =>
    matchesTerm(text, term)
  );
}

function classifyChannel(
  channel,
  popularVideos,
  newestVideos,
  ecosystem
) {
  const metadataText = [
    channel.name,
    channel.description,
    ...(channel.keywords || [])
  ]
    .filter(Boolean)
    .join(" ");

  const videoTexts = [
    ...popularVideos,
    ...newestVideos
  ]
    .map((video) => video.title)
    .filter(Boolean);

  const positiveMetadata =
    collectMatches(
      metadataText,
      ecosystem.includeTerms
    );

  const negativeMetadata =
    collectMatches(
      metadataText,
      ecosystem.excludeTerms
    );

  const positiveVideos = [];

  for (const title of videoTexts) {
    const matches = collectMatches(
      title,
      ecosystem.includeTerms
    );

    for (const match of matches) {
      if (!positiveVideos.includes(match)) {
        positiveVideos.push(match);
      }
    }
  }

  const negativeVideos = [];

  for (const title of videoTexts) {
    const matches = collectMatches(
      title,
      ecosystem.excludeTerms
    );

    for (const match of matches) {
      if (!negativeVideos.includes(match)) {
        negativeVideos.push(match);
      }
    }
  }

  let score = 0;

  score += positiveMetadata.length * 3;
  score += positiveVideos.length;
  score -= negativeMetadata.length * 4;
  score -= negativeVideos.length;

  let classification = "unknown";

  const strongPositive =
    positiveMetadata.length >= 2 ||
    (
      positiveMetadata.length >= 1 &&
      positiveVideos.length >= 2
    );

  if (
    strongPositive &&
    score >= 3
  ) {
    classification = "core";
  } else if (score <= -3) {
    classification = "irrelevant";
  } else if (score !== 0) {
    classification = "adjacent";
  }

  const evidence = [];

  if (positiveMetadata.length) {
    evidence.push(
      `positive metadata: ${positiveMetadata.join(", ")}`
    );
  }

  if (positiveVideos.length) {
    evidence.push(
      `positive video titles: ${positiveVideos.join(", ")}`
    );
  }

  if (negativeMetadata.length) {
    evidence.push(
      `negative metadata: ${negativeMetadata.join(", ")}`
    );
  }

  if (negativeVideos.length) {
    evidence.push(
      `negative video titles: ${negativeVideos.join(", ")}`
    );
  }

  let reason =
    "no strong ecosystem signal";

  if (evidence.length) {
    reason = evidence.join(" | ");
  }

  return {
    classification,
    score,
    reason,
    evidence: {
      positiveMetadata,
      positiveVideos,
      negativeMetadata,
      negativeVideos
    }
  };
}

function classifyVideos(
  videos,
  ecosystem
) {
  return videos.map((video) => {
    const title =
      normalizeText(video.title);

    const positiveMatches =
      collectMatches(
        title,
        ecosystem.includeTerms
      );

    const negativeMatches =
      collectMatches(
        title,
        ecosystem.excludeTerms
      );

    let score =
      positiveMatches.length -
      negativeMatches.length;

    let classification =
      "unknown";

    if (
      negativeMatches.length > 0 &&
      positiveMatches.length === 0
    ) {
      classification = "irrelevant";
    } else if (score > 0) {
      classification = "core";
    } else if (score < 0) {
      classification = "adjacent";
    }

    const evidence = [];

    if (positiveMatches.length) {
      evidence.push(
        `positive title terms: ${positiveMatches.join(", ")}`
      );
    }

    if (negativeMatches.length) {
      evidence.push(
        `negative title terms: ${negativeMatches.join(", ")}`
      );
    }

    const reason =
      evidence.length
        ? evidence.join(" | ")
        : "no strong ecosystem signal";

    return {
      ...video,
      classification,
      score,
      reason,
      method: "rules"
    };
  });
}

function saveQualification(
  db,
  channelUrl,
  ecosystem,
  result
) {
  const reviewedAt =
    new Date().toISOString();

  const updatedAt =
    reviewedAt;

  const ecosystemName =
    typeof ecosystem === "string"
      ? ecosystem
      : ecosystem?.name;

  if (!ecosystemName) {
    throw new Error(
      "Cannot save qualification: ecosystem name is missing."
    );
  }

  if (!result?.classification) {
    throw new Error(
      "Cannot save qualification: classification is missing."
    );
  }

  const channelExists =
    db.prepare(`
      SELECT 1
      FROM channels
      WHERE channel_url = ?
    `).get(channelUrl);

  if (!channelExists) {
    throw new Error(
      `Cannot save qualification: channel does not exist in channels table: ${channelUrl}`
    );
  }

  const statement = db.prepare(`
    INSERT INTO channel_qualifications (
      channel_url,
      ecosystem,
      classification,
      score,
      reason,
      method,
      reviewed_at,
      updated_at
    )
    VALUES (
      ?, ?, ?, ?, ?, ?, ?, ?
    )

    ON CONFLICT(channel_url)
    DO UPDATE SET
      ecosystem = excluded.ecosystem,
      classification = excluded.classification,
      score = excluded.score,
      reason = excluded.reason,
      method = excluded.method,
      reviewed_at = excluded.reviewed_at,
      updated_at = excluded.updated_at
  `);

  const values = [
    channelUrl,
    ecosystemName,
    result.classification,
    result.score,
    result.reason,
    "rules",
    reviewedAt,
    updatedAt
  ];

  console.log(
    "[qualification] Saving channel qualification:",
    {
      channelUrl,
      ecosystem: ecosystemName,
      classification:
        result.classification,
      score:
        result.score,
      parameterCount:
        values.length
    }
  );

  statement.run(...values);
}

function saveVideoQualifications(
  db,
  videos,
  ecosystem
) {
  const ecosystemName =
    typeof ecosystem === "string"
      ? ecosystem
      : ecosystem?.name;

  if (!ecosystemName) {
    throw new Error(
      "Cannot save video qualifications: ecosystem name is missing."
    );
  }

  const statement = db.prepare(`
    INSERT INTO video_qualifications (
      video_id,
      ecosystem,
      classification,
      score,
      reason,
      method,
      reviewed_at,
      updated_at
    )
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)

    ON CONFLICT(video_id)
    DO UPDATE SET
      ecosystem = excluded.ecosystem,
      classification = excluded.classification,
      score = excluded.score,
      reason = excluded.reason,
      method = excluded.method,
      reviewed_at = excluded.reviewed_at,
      updated_at = excluded.updated_at
  `);

  const now =
    new Date().toISOString();

  const transaction =
    db.transaction(() => {
      for (const video of videos) {
        statement.run(
          video.video_id,
          ecosystemName,
          video.classification,
          video.score ?? 0,
          video.reason,
          video.method,
          now,
          now
        );
      }
    });

  transaction();

  console.log(
    `[qualification] Saved ${videos.length} video qualifications.`
  );
}

export {
  classifyChannel,
  classifyVideos,
  saveQualification,
  saveVideoQualifications
};

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.