← back to Crazy News Channel

scripts/verify-tags.mjs

72 lines

#!/usr/bin/env node
// scripts/verify-tags.mjs — TK-12165 proof script.
// Loads stories-data.js, real-news-data.js, and cartoons/manifest.js in a
// node vm Context (same "plain <script src> globals" shape the pages use,
// no DOM/browser needed) and asserts every post carries a non-empty `tags`
// array. Prints tag-vocabulary stats (distinct tags, top 15 by count,
// singleton count) so drift is visible at a glance.
//
// Usage: node scripts/verify-tags.mjs
import fs from "node:fs";
import path from "node:path";
import vm from "node:vm";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..");

const ctx = { window: {}, console };
vm.createContext(ctx);
for (const f of ["stories-data.js", "real-news-data.js", "cartoons/manifest.js"]) {
  vm.runInContext(fs.readFileSync(path.join(ROOT, f), "utf8"), ctx, { filename: f });
}

const buckets = [
  ["stories-data.js (window.P24_EXTRA_STORIES)", ctx.window.P24_EXTRA_STORIES || []],
  ["real-news-data.js (window.P24_REAL_STORIES)", ctx.window.P24_REAL_STORIES || []],
  ["cartoons/manifest.js (window.P24_CARTOONS)", ctx.window.P24_CARTOONS || []],
];

let totalPosts = 0;
let missing = [];
const freq = new Map();

for (const [label, arr] of buckets) {
  console.log(`${label}: ${arr.length} entries`);
  for (const post of arr) {
    totalPosts++;
    const tags = post.tags;
    if (!Array.isArray(tags) || tags.length === 0) {
      missing.push(`${label} :: ${post.id}`);
      continue;
    }
    for (const t of tags) {
      const k = String(t).toLowerCase();
      freq.set(k, (freq.get(k) || 0) + 1);
    }
  }
}

console.log("");
console.log(`Total posts checked: ${totalPosts}`);
console.log(`Posts with missing/empty tags: ${missing.length}`);
if (missing.length) {
  console.log("MISSING:");
  missing.forEach((m) => console.log("  - " + m));
}

const entries = [...freq.entries()].sort((a, b) => b[1] - a[1]);
const singletons = entries.filter(([, c]) => c === 1);
console.log("");
console.log(`Distinct tags: ${entries.length}`);
console.log(`Singleton tags (used exactly once): ${singletons.length}`);
console.log("");
console.log("Top 15 tags by count:");
for (const [t, c] of entries.slice(0, 15)) console.log(`  ${String(c).padStart(3)}  ${t}`);

if (missing.length > 0) {
  console.error("\nFAIL: one or more posts have no tags.");
  process.exit(1);
}
console.log("\nPASS: every post has a non-empty tags array.");