/* RFC-0891: nachweis.screenshot.process command handler — transforms a raw full-page screenshot into a 16:9 display variant (1280x720, WebP) and uploads to R2 public. Does not ingest raw screenshots — use nachweis.screenshot.ingest (RFC-0890). Does not upload pre-processed files — use nachweis.screenshot.upload (RFC-0886). Does not validate screenshot content quality or visual correctness. RFC-0891: initial nachweis.screenshot.process command handler. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1237: resolve dry-run via context.dryRun fallback — consumeCommonFlags strips --dry-run into context.dryRun before dispatch, so a flags-only read was permanently false under werkstatt run (enforced by KERNEL-FLAG-10). RFC-1254: drop unused io-migration leftovers */ import path from "node:path"; import { createHash } from "node:crypto"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { parseMarkdownFrontmatter, stringifyMarkdownFrontmatter, } from "@warpgogol/werkstatt-shared/content"; import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; import { writeFileIfChanged } from "@warpgogol/werkstatt-engine/kernel"; import { isNachweisEntitled, makeSkipResult, resolveNachweisCachePath, resolvePbpEntityDir, resolveDefaultLang, resolveNachweisScreenshotDisplayR2Path, resolveNachweisRawScreenshotLocalPath, uploadToR2, downloadFromR2, type NachweisScreenshotProcessResult, } from "./nachweis-io.ts"; function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function flagBool(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } function flagNumber(input: KernelCommandInput, key: string): number | undefined { const v = input.flags[key]; if (typeof v === "number") return v; if (typeof v === "string") { const n = Number(v); if (Number.isFinite(n)) return n; } return undefined; } export async function runNachweisScreenshotProcess( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemId = flagString(input, "system") ?? context.site?.name; const slug = flagString(input, "slug"); const dryRun = context.dryRun === true || flagBool(input, "dry-run"); const cropOffset = flagNumber(input, "crop-offset") ?? 0; if (!systemId) throw new Error("[nachweis.screenshot.process] --system is required"); if (!slug) throw new Error("[nachweis.screenshot.process] --slug is required"); const entitled = await isNachweisEntitled(workspaceRoot, systemId); if (!entitled) { return makeSkipResult( "nachweis.screenshot.process", systemId, ) as unknown as KernelCommandResult; } const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId); const lang = await resolveDefaultLang(cachePath); const evidenceDir = resolvePbpEntityDir(cachePath, lang, "evidence-source"); const evidenceFile = path.join(evidenceDir, `${slug}.md`); if (!(await context.io.exists(evidenceFile))) { throw new Error( `[nachweis.screenshot.process] NOT_FOUND: evidence-source '${slug}' not found at ${evidenceFile}`, ); } const rawEvidence = await context.io.readFile(evidenceFile); const { data: evidenceData } = parseMarkdownFrontmatter(rawEvidence); const websiteScreenshot = evidenceData.websiteScreenshot as | { rawArtifact?: { sha256?: string; mediaType?: string; originalFilename?: string; width?: number; height?: number; r2Key?: string; localPath?: string; capturedAt?: string; }; } | undefined; const rawArtifact = websiteScreenshot?.rawArtifact; if (!rawArtifact) { throw new Error( `[nachweis.screenshot.process] no rawArtifact found on websiteScreenshot for '${slug}'. Run nachweis.screenshot.ingest first.`, ); } const originalFilename = rawArtifact.originalFilename; if (!originalFilename) { throw new Error( `[nachweis.screenshot.process] rawArtifact.originalFilename is missing for '${slug}'`, ); } // Resolve raw file: check cache clone local copy first, fallback to R2 private const localRawPath = resolveNachweisRawScreenshotLocalPath(cachePath, slug, originalFilename); let rawFilePath: string; let rawBuffer: Uint8Array; if (await context.io.exists(localRawPath)) { rawFilePath = localRawPath; rawBuffer = new Uint8Array(await context.io.readFileBytes(localRawPath)); } else { // Fallback: download from R2 private const r2Key = rawArtifact.r2Key; if (!r2Key) { throw new Error( `[nachweis.screenshot.process] rawArtifact not found locally and no r2Key for fallback. Local path: ${localRawPath}`, ); } logger.info( `[nachweis.screenshot.process] raw file not in cache clone, downloading from R2: ${r2Key}`, ); rawBuffer = await downloadFromR2(r2Key); // Write to temp file for sharp to read rawFilePath = path.join( cachePath, "trust", "evidence", "screenshots", slug, "raw", originalFilename, ); await context.io.mkdir(path.dirname(rawFilePath)); await context.io.writeFile(rawFilePath, rawBuffer); } // Compute SHA-256 of raw file const rawSha256 = createHash("sha256").update(rawBuffer).digest("hex"); // Read raw image metadata via sharp const sharp = (await import("sharp")).default; const metadata = await sharp(rawFilePath).metadata(); const rawWidth = metadata.width; const rawHeight = metadata.height; if (!rawWidth || !rawHeight) { throw new Error( `[nachweis.screenshot.process] could not read image dimensions from ${rawFilePath} (format: ${metadata.format ?? "unknown"})`, ); } // Compute 16:9 crop region const cropHeight = Math.min(Math.round((rawWidth * 9) / 16), rawHeight); const cropWidth = cropHeight === rawHeight ? Math.round((rawHeight * 16) / 9) : rawWidth; const cropLeft = cropWidth < rawWidth ? Math.round((rawWidth - cropWidth) / 2) : 0; const cropTop = cropOffset; // Validate crop offset boundary if (cropTop + cropHeight > rawHeight) { const maxOffset = rawHeight - cropHeight; throw new Error( `[nachweis.screenshot.process] --crop-offset ${cropTop} places crop region beyond raw image boundary (cropOffset + cropHeight = ${cropTop + cropHeight} > rawHeight = ${rawHeight}). Maximum allowed offset: ${maxOffset}`, ); } const r2Key = resolveNachweisScreenshotDisplayR2Path(systemId, slug); const capturedAt = rawArtifact.capturedAt ?? null; if (dryRun) { return { data: { slug, systemId, rawSha256, rawDimensions: { width: rawWidth, height: rawHeight }, cropRegion: { left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight }, displaySha256: "", displayMediaType: "image/webp", displayWidth: 1280, displayHeight: 720, r2Key, capturedAt, bordbuchEventId: "", }, exitCode: 0, summary: `[nachweis.screenshot.process] ${systemId}: DRY RUN — would process screenshot for '${slug}' (crop: ${cropWidth}x${cropHeight} at offset ${cropTop})`, }; } // Crop + resize + convert to WebP const displayBuffer = await sharp(rawFilePath) .extract({ left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight }) .resize(1280, 720, { fit: "cover" }) .webp({ quality: 100 }) .toBuffer(); const displaySha256 = createHash("sha256").update(displayBuffer).digest("hex"); // Upload to R2 public (backup/archive) await uploadToR2(displayBuffer, r2Key, "image/webp"); // Copy to public/ for serving from the site const publicScreenshotDir = path.join(cachePath, "public", "nachweis-screenshots"); const publicScreenshotPath = path.join(publicScreenshotDir, `${slug}.webp`); const publicUrl = `/nachweis-screenshots/${slug}.webp`; await context.io.mkdir(publicScreenshotDir); await writeFileIfChanged(publicScreenshotPath, Buffer.from(displayBuffer)); // Update EvidenceSource.websiteScreenshot, preserving rawArtifact const { data: currentEvidence, content: evidenceContent } = parseMarkdownFrontmatter( await context.io.readFile(evidenceFile), ); const currentScreenshot = (currentEvidence.websiteScreenshot ?? {}) as Record; currentEvidence.websiteScreenshot = { ...currentScreenshot, sha256: displaySha256, mediaType: "image/webp", storage: "public", url: publicUrl, r2Key, ...(capturedAt ? { capturedAt } : {}), }; const updatedContent = stringifyMarkdownFrontmatter(evidenceContent, currentEvidence); await context.io.writeFile(evidenceFile, updatedContent); logger.info( `[nachweis.screenshot.process] processed display variant for '${slug}' to ${publicUrl}`, ); // Append Bordbuch entry const operationId = generateOperationId(); await acquireLock( workspaceRoot, `system:${systemId}`, operationId, "nachweis.screenshot.process", "agent", ); await acquireLock( workspaceRoot, `bordbuch:${systemId}`, operationId, "nachweis.screenshot.process", "agent", ); let bordbuchEventId: string; try { const { entry } = await appendAndCommitBordbuch( workspaceRoot, systemId, "nachweis-record", `Display screenshot processed for '${slug}'`, "agent", { writerRole: "nachweis", metadata: { slug, displaySha256, displayMediaType: "image/webp", displayWidth: 1280, displayHeight: 720, rawSha256, r2Key, }, }, `Bordbuch: nachweis-record ${systemId} ${slug} display-screenshot`, ); bordbuchEventId = entry.id; } finally { await releaseLock(workspaceRoot, `bordbuch:${systemId}`); await releaseLock(workspaceRoot, `system:${systemId}`); } return { data: { slug, systemId, rawSha256, rawDimensions: { width: rawWidth, height: rawHeight }, cropRegion: { left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight }, displaySha256, displayMediaType: "image/webp", displayWidth: 1280, displayHeight: 720, r2Key, capturedAt, bordbuchEventId, }, exitCode: 0, summary: `[nachweis.screenshot.process] ${systemId}: display variant for '${slug}' processed (bordbuch: ${bordbuchEventId})`, nextSteps: [ { action: `Upload the public derivative: pnpm exec werkstatt run nachweis.public-derivative --site ${systemId} --slug ${slug}`, kind: "optional", }, ], }; }