import { createHash } from "node:crypto"; export const LONGFORM_APPENDIX_TABLE = "personal_report_longform_appendices"; export const LONGFORM_APPENDIX_MAX_ATTEMPTS = 2; export type LongformAppendixStatus = "pending" | "ready" | "unavailable"; export type LongformAppendixRow = Readonly<{ reportId: string; userId: string; requestId: string; status: LongformAppendixStatus; markdown: string | null; contentSha256: string | null; attemptCount: number; lastErrorCode: string | null; }>; export type LongformAppendixWrite = Readonly<{ status: LongformAppendixStatus; attemptCount: number; markdown: string | null; contentSha256: string | null; lastErrorCode: string | null; }>; export function hashLongformMarkdown(markdown: string): string { return createHash("sha256").update(markdown, "utf8").digest("hex"); } export function parseLongformAppendixRow(value: unknown): LongformAppendixRow | null { if (value === null || typeof value !== "object" || Array.isArray(value)) return null; const row = value as Record; const reportId = text(row.report_id ?? row.reportId); const userId = text(row.user_id ?? row.userId); const requestId = text(row.request_id ?? row.requestId); const status = row.status; if (!reportId || !userId || !requestId) return null; if (status !== "pending" && status !== "ready" && status !== "unavailable") return null; const markdown = typeof row.markdown === "string" && row.markdown.trim() ? row.markdown : null; const contentSha256 = text(row.content_sha256 ?? row.contentSha256); const attemptCount = typeof row.attempt_count === "number" && Number.isFinite(row.attempt_count) ? row.attempt_count : typeof row.attemptCount === "number" && Number.isFinite(row.attemptCount) ? row.attemptCount : 0; return { reportId, userId, requestId, status, markdown, contentSha256, attemptCount, lastErrorCode: text(row.last_error_code ?? row.lastErrorCode), }; } export function nextLongformAppendixState(input: Readonly<{ current: LongformAppendixRow | null; successMarkdown?: string | null; errorCode?: string | null; }>): LongformAppendixWrite { const successMarkdown = input.successMarkdown?.trim() ? input.successMarkdown : null; if (successMarkdown) { return { status: "ready", attemptCount: input.current?.attemptCount ?? 0, markdown: successMarkdown, contentSha256: hashLongformMarkdown(successMarkdown), lastErrorCode: null, }; } const attemptCount = Math.min( (input.current?.attemptCount ?? 0) + 1, LONGFORM_APPENDIX_MAX_ATTEMPTS, ); return { status: attemptCount >= LONGFORM_APPENDIX_MAX_ATTEMPTS ? "unavailable" : "pending", attemptCount, markdown: null, contentSha256: null, lastErrorCode: input.errorCode ?? "generation_failed", }; } function text(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }