/* * Scrape completeness check. * * Reads every scrape index and reports whether the walk plausibly reached the start of the * source: * - state is done, no lastError, and the message count is non-zero * - the message count matches the Discord search total when the scrape recorded one * - the oldest message is close to the channel's creation time (derived from its snowflake) * * The last check is a heuristic: a channel can legitimately sit empty for a while after it is * created, and a ranged scrape (From date) is expected to stop early. Anything flagged deserves * a look, not necessarily a re-run. * * Usage: pnpm verify:scrapes [--db ] [--grace ] */ import { existsSync } from "node:fs"; import { isoFromSnowflake, listScrapes, resolveDbRoot, ScrapeStore, scrapeDir } from "../../media-scrape/src/index.js"; const args = process.argv.slice(2); const valueOf = (flag: string) => { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; }; const dbRoot = valueOf("--db") ?? resolveDbRoot(); const graceDays = Number(valueOf("--grace") ?? 60); const graceMs = (Number.isFinite(graceDays) ? graceDays : 60) * 86_400_000; interface Verdict { id: string; channelId: string; state: string; backend: string; messages: number; estimated: number | null; oldest: string | null; created: string | null; flags: string[]; /** Another scrape of the same source that completed and walked further. */ supersededBy?: string; } const results: Verdict[] = []; for (const meta of listScrapes(dbRoot)) { if (meta.state === "deleted" || meta.state === "deleting") continue; const flags: string[] = []; let messages = 0; let oldest: string | null = null; const dir = scrapeDir(dbRoot, meta.id); if (!existsSync(dir)) { flags.push("directory missing"); } else { try { const store = new ScrapeStore(dir); try { const row = store.dbSummary(); messages = row.messages; oldest = row.oldest; } finally { store.close(); } } catch (error) { flags.push(`unreadable index: ${String(error)}`); } } if (meta.state !== "done") flags.push(`state ${meta.state}`); if (meta.lastError) flags.push(`lastError: ${meta.lastError}`); if (messages === 0) flags.push("no messages indexed"); if (meta.estimatedMessages != null && messages < meta.estimatedMessages * 0.98) { flags.push(`indexed ${messages} of ~${meta.estimatedMessages} search results`); } let created: string | null = null; try { created = isoFromSnowflake(meta.channelId); } catch { // not a snowflake; skip the age heuristic } if (!meta.fromDate && created && oldest) { const age = new Date(oldest).getTime() - new Date(created).getTime(); if (Number.isFinite(age) && age > graceMs) { flags.push(`oldest ${oldest.slice(0, 10)} vs source created ${created.slice(0, 10)}`); } } results.push({ id: meta.id, channelId: meta.channelId, state: meta.state, backend: meta.backend, messages, estimated: meta.estimatedMessages ?? null, oldest, created, flags }); } // A flagged scrape that a later full walk of the same source supersedes is stale, not a problem. for (const result of results) { if (!result.flags.length) continue; const replacement = results.find( candidate => candidate !== result && candidate.channelId === result.channelId && candidate.flags.length === 0 && candidate.messages > result.messages ); if (replacement) result.supersededBy = replacement.id; } const complete = results.filter(result => result.flags.length === 0); const superseded = results.filter(result => result.flags.length > 0 && result.supersededBy); const incomplete = results.filter(result => result.flags.length > 0 && !result.supersededBy); console.log( `scrape completeness: ${complete.length} complete, ${superseded.length} superseded, ` + `${incomplete.length} incomplete` ); console.log(`database root: ${dbRoot}\n`); for (const result of results) { const mark = !result.flags.length ? "ok " : result.supersededBy ? "old " : "FLAG"; const oldest = result.oldest ? result.oldest.slice(0, 10) : "----------"; const created = result.created ? result.created.slice(0, 10) : "----------"; console.log( `${mark} ${result.id.padEnd(44)} ${result.state.padEnd(7)} ` + `msgs ${String(result.messages).padStart(6)}/${String(result.estimated ?? "?").padStart(6)} ` + `oldest ${oldest} (source ${created})` + (result.supersededBy ? ` <- superseded by ${result.supersededBy}` : result.flags.length ? ` <- ${result.flags.join("; ")}` : "") ); } process.exitCode = incomplete.length ? 1 : 0;