#!/usr/bin/env node import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; export function parseThresholds(argv = process.argv.slice(2)) { const arg = argv.find((item) => item.startsWith("--thresholds=")); const raw = arg ? arg.slice("--thresholds=".length) : "2,5,10"; return Array.from(new Set( raw .split(",") .map((item) => Number(item.trim())) .filter((item) => Number.isFinite(item) && item > 0) .map((item) => Math.floor(item)), )).sort((a, b) => a - b); } function minutesAgo(minutes, now) { return new Date(now.getTime() - minutes * 60_000); } export async function buildPendingOcrSummary({ databaseReady, thresholds, countPending, now = new Date(), }) { if (!databaseReady) { return { mode: "pending-ocr-readiness", status: "SKIP", findingCount: 1, findings: [{ code: "database_not_configured", severity: "manual_or_config" }], thresholds: [], checkedAt: now.toISOString(), }; } const thresholdResults = []; for (const thresholdMinutes of thresholds) { const count = Number(await countPending({ status: "pending_ocr", created_at: { lte: minutesAgo(thresholdMinutes, now) }, })); thresholdResults.push({ thresholdMinutes, count: Number.isFinite(count) && count > 0 ? Math.floor(count) : 0, }); } const findingCount = thresholdResults.reduce((maximum, item) => Math.max(maximum, item.count), 0); return { mode: "pending-ocr-readiness", status: findingCount > 0 ? "PASS_WITH_FINDINGS" : "PASS", findingCount, findings: findingCount > 0 ? [{ code: "pending_ocr_detected", severity: "finding", count: findingCount }] : [], thresholds: thresholdResults, checkedAt: now.toISOString(), }; } export async function executePendingOcrCheck({ databaseReady = Boolean(process.env.DATABASE_URL?.trim()), thresholds = parseThresholds(), } = {}) { if (!databaseReady) { return buildPendingOcrSummary({ databaseReady: false, thresholds, countPending: async () => 0 }); } const prismaClientModule = await import("../../server/src/prismaClient.js"); const createPrismaClient = prismaClientModule.createPrismaClient ?? prismaClientModule.default?.createPrismaClient; if (typeof createPrismaClient !== "function") { throw new Error("Prisma client factory is unavailable"); } const { prisma, pool } = createPrismaClient(); try { return await buildPendingOcrSummary({ databaseReady: true, thresholds, countPending: (where) => prisma.invoicePool.count({ where }), }); } finally { try { await prisma.$disconnect(); } finally { await pool.end(); } } } function printSummary(summary, jsonOnly) { if (jsonOnly) { console.log(JSON.stringify(summary, null, 2)); return; } console.log(`[pending-ocr][${summary.status}] findings=${summary.findingCount}`); for (const item of summary.thresholds) { console.log(`[pending-ocr] thresholdMinutes=${item.thresholdMinutes} count=${item.count}`); } } async function main() { const jsonOnly = process.argv.includes("--json"); try { const summary = await executePendingOcrCheck(); printSummary(summary, jsonOnly); } catch { const summary = { mode: "pending-ocr-readiness", status: "FAIL", findingCount: 1, findings: [{ code: "pending_ocr_check_failed", severity: "blocker" }], thresholds: [], checkedAt: new Date().toISOString(), }; printSummary(summary, jsonOnly); console.error("[pending-ocr][FAIL] database check failed"); process.exitCode = 1; } } const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMain) { await main(); }