Web export now calls the same full pack as the long skill report and caches an owner-only Markdown download. Appendix failure stays unavailable and does not change the main report status. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
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<string, unknown>;
|
|
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;
|
|
}
|