Files
agent-desirecore/web/solver-report/scripts/validate-screenshot-privacy-ocr.mjs
2026-08-30 11:02:51 -04:00

81 lines
3.3 KiB
JavaScript

import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { assertPublicText, canonical, sha256 } from './public-release-policy.mjs'
const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd())
const planFile =
process.env.SOLVER_REPORT_SCREENSHOT_PLAN_FILE ??
join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json')
const ocrFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_FILE
if (!ocrFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_FILE is required')
const validationFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE
if (!validationFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE is required')
const plan = JSON.parse(readFileSync(planFile, 'utf8'))
const ocr = JSON.parse(readFileSync(ocrFile, 'utf8'))
if (ocr.schemaVersion !== 'solver.screenshot-privacy-ocr/v1' || !Array.isArray(ocr.entries)) {
throw new Error('unsupported screenshot OCR result')
}
const planned = plan.scenarios.flatMap((scenario) =>
scenario.screenshots.map((screenshot) => ({ scenarioId: scenario.scenarioId, ...screenshot }))
)
if (planned.length !== 174 || ocr.entries.length !== planned.length) {
throw new Error(`screenshot OCR coverage mismatch: expected ${planned.length}, received ${ocr.entries.length}`)
}
const failures = []
const screenshotRootEntries = []
let recognizedLines = 0
let screenshotsWithText = 0
for (const [index, expected] of planned.entries()) {
const actual = ocr.entries[index]
const imageFile = join(inputRoot, expected.file)
if (!existsSync(imageFile)) failures.push({ file: expected.file, issue: 'missing-image' })
const actualSha256 = existsSync(imageFile) ? sha256(readFileSync(imageFile)) : null
if (
actual?.scenarioId !== expected.scenarioId ||
actual?.file !== expected.file ||
actualSha256 !== expected.sha256 ||
!Array.isArray(actual?.lines) ||
actual.lines.some((line) => typeof line?.text !== 'string' || !Array.isArray(line?.boundingBox))
) {
failures.push({ file: expected.file, issue: 'plan-or-hash-mismatch' })
continue
}
const text = actual.lines.map((line) => line.text).join('\n')
recognizedLines += actual.lines.length
if (actual.lines.length > 0) screenshotsWithText += 1
try {
assertPublicText(text, `${expected.scenarioId} screenshot OCR`)
} catch (error) {
failures.push({ file: expected.file, issue: 'sensitive-text-policy', detail: error.message })
}
screenshotRootEntries.push({
scenarioId: expected.scenarioId,
file: expected.file,
sha256: expected.sha256,
width: expected.width,
height: expected.height,
})
}
const report = {
schemaVersion: 'solver.screenshot-privacy-validation/v1',
auditedAt: new Date().toISOString(),
screenshotPlanSha256: sha256(canonical(plan)),
screenshotContentRootSha256: sha256(canonical(screenshotRootEntries)),
screenshots: planned.length,
ocr: {
engine: ocr.engine,
screenshotsWithText,
recognizedLines,
sensitiveMatches: failures.filter((failure) => failure.issue === 'sensitive-text-policy').length,
},
passed: failures.length === 0 && screenshotsWithText === planned.length,
failures,
}
writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
if (!report.passed) process.exitCode = 1