文件
hyb-oa/scripts/qa/check-pending-ocr.mjs
T

130 行
3.7 KiB
JavaScript

#!/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();
}