Files
Jyotisha/frontend/src/lib/personal-report-longform-appendix.ts
T
Jesse_ChenandCursor bab0718700
Independent Staging Quality Gate / validate (push) Failing after 9m41s
Independent Staging Quality Gate / publish (push) Has been skipped
feat(report): ship full-mode longform appendix beside the five-chapter report
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>
2026-09-05 11:10:44 +08:00

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;
}