// The content check. Read-only: it reports, it never rewrites a post. // // pnpm check:content the database rows (staged files if NEON_URL is unset) // pnpm check:content --files the staged files in content/blog // pnpm check:content --files --slug a,b only those, links resolved against the keyword map // pnpm check:content --report also print the proof tables import { getDb } from "@/db/client"; import { posts } from "@/db/schema"; import { authors, categories } from "@/lib/blog/config"; import { listPostFiles, readKeywordMap, readPostFile } from "@/lib/blog/files"; import { checkPosts, type RulePost } from "@/lib/blog/rules"; import { desc } from "drizzle-orm"; const args = process.argv.slice(2); const useFiles = args.includes("--files") || !process.env.NEON_URL; const slugArg = args.includes("--slug") ? args[args.indexOf("--slug") + 1]?.split(",") : null; const report = args.includes("--report"); async function load(): Promise { if (useFiles) { if (!args.includes("--files")) console.log( "NEON_URL is not set: checking the staged files in content/blog", ); const slugs = listPostFiles().filter( (s) => !slugArg || slugArg.includes(s), ); const out: RulePost[] = []; for (const s of slugs) { try { out.push(readPostFile(s)); } catch (e) { console.error(`valid-shape: ${e instanceof Error ? e.message : e}`); process.exitCode = 1; } } return out; } const rows = await getDb() .select() .from(posts) .orderBy(desc(posts.publishedAt)); return rows .filter((r) => !slugArg || slugArg.includes(r.slug)) .map((r) => ({ ...r, length: r.length === "short" ? "short" : "full", createdAt: r.createdAt.toISOString(), publishedAt: r.publishedAt?.toISOString() ?? null, updatedAt: r.updatedAt?.toISOString() ?? null, })); } async function main() { const all = await load(); const keywordMap = readKeywordMap(); const result = checkPosts(all, { categories, authors, full: !slugArg, plannedSlugs: keywordMap.map((r) => r.slug), keywordMap: slugArg ? undefined : keywordMap, }); if (report) { console.log("\n## Published posts per category\n"); for (const c of result.perCategory) console.log(`${c.slug.padEnd(36)} ${c.published}`); console.log( "\n## Words, floor, opening words, max section overlap, linked from\n", ); for (const p of result.perPost) console.log( `${p.slug.padEnd(60)} ${String(p.words).padStart(5)} / ${p.floor} open ${String(p.answerWords).padStart(2)} overlap ${p.maxOverlap ? `${p.maxOverlap.percent}% ${p.maxOverlap.with}` : "0%"} <- ${p.linkedFrom.join(", ") || "(none)"}`, ); } for (const f of result.findings) console.error(`${f.rule.padEnd(22)} ${f.slug}: ${f.message}`); const source = useFiles ? "files" : "database"; console.log( `\nchecked ${all.length} posts from the ${source}: ${result.findings.length ? `${result.findings.length} findings` : "PASS"}`, ); if (result.findings.length) process.exitCode = 1; } main().catch((e) => { console.error(e); process.exit(1); });