Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
97 lines
5.4 KiB
TypeScript
97 lines
5.4 KiB
TypeScript
import { z } from "zod";
|
|
import { hashLongformMarkdown } from "./personal-report-longform-appendix";
|
|
import { CHART_IDS, reportDocumentV2Schema } from "./personal-report-contract";
|
|
import { reportFactTablesSchema } from "./report-fact-table-schema";
|
|
import type { AppendixClient } from "./personal-report-longform-generate";
|
|
|
|
/** Reserved server-owned cache, never a writer chapter or reader progress row. */
|
|
export const LONGFORM_SNAPSHOT_SECTION_ID = "longform-calculation-snapshot-v1";
|
|
export const LONGFORM_SNAPSHOT_TABLE = "personal_report_sections";
|
|
export type LongformSnapshotIdentity = { reportId: string; userId: string; requestId: string };
|
|
const snapshotFields = {
|
|
kind: z.literal("longform_calculation_snapshot"),
|
|
reportId: z.string().uuid(), userId: z.string().uuid(), requestId: z.string().uuid(),
|
|
markdown: z.string().min(1), contentSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
factTables: reportFactTablesSchema.refine(tables => tables.length === 8),
|
|
charts: reportDocumentV2Schema.shape.charts.refine(charts =>
|
|
CHART_IDS.every(id => charts.filter(chart => chart.id === id).length === 1)
|
|
&& charts.every(chart => chart.houses.length === 12 && chart.houses.some(house => house.occupants.length > 0))),
|
|
readerDashaApplicability: z.string().min(1),
|
|
};
|
|
|
|
/**
|
|
* The English edition, rendered by the engine from the same packet as the
|
|
* Chinese one (TASK-report-english-edition-20260929). Subordinate: when it is
|
|
* missing the snapshot carries `englishUnavailable` instead and the report is
|
|
* still ready in Chinese.
|
|
*/
|
|
export const longformEnglishEditionSchema = z.strictObject({
|
|
markdown: z.string().min(1),
|
|
contentSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
factTables: reportFactTablesSchema,
|
|
});
|
|
export type LongformEnglishEdition = z.infer<typeof longformEnglishEditionSchema>;
|
|
|
|
/** v1: Chinese only (every report before the English edition). v2: both editions, or the reason English is missing. */
|
|
const snapshotSchema = z.union([
|
|
z.strictObject({ ...snapshotFields, version: z.literal(1) }),
|
|
z.strictObject({
|
|
...snapshotFields,
|
|
version: z.literal(2),
|
|
english: longformEnglishEditionSchema.optional(),
|
|
englishUnavailable: z.string().regex(/^[a-z_]{1,64}$/).optional(),
|
|
}).refine(value => (value.english === undefined) !== (value.englishUnavailable === undefined), {
|
|
message: "a v2 snapshot carries exactly one of english / englishUnavailable",
|
|
}),
|
|
]);
|
|
export type LongformSnapshot = z.infer<typeof snapshotSchema>;
|
|
|
|
export function validateLongformSnapshot(value: unknown, identity: LongformSnapshotIdentity): LongformSnapshot {
|
|
const snapshot = snapshotSchema.parse(value);
|
|
if (snapshot.reportId !== identity.reportId || snapshot.userId !== identity.userId
|
|
|| snapshot.requestId !== identity.requestId || snapshot.contentSha256 !== hashLongformMarkdown(snapshot.markdown)) {
|
|
throw new Error("longform_snapshot_binding_invalid");
|
|
}
|
|
if (snapshot.version === 2 && snapshot.english
|
|
&& snapshot.english.contentSha256 !== hashLongformMarkdown(snapshot.english.markdown)) {
|
|
throw new Error("longform_snapshot_binding_invalid");
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
type SnapshotFilter = {
|
|
eq(column: string, value: unknown): SnapshotFilter;
|
|
maybeSingle(): PromiseLike<{ data: unknown; error: unknown }>;
|
|
};
|
|
export type LongformSnapshotReader = { from(table: string): { select(columns: string): { eq(column: string, value: unknown): SnapshotFilter } } };
|
|
|
|
export async function readLongformSnapshot(client: LongformSnapshotReader, identity: LongformSnapshotIdentity) {
|
|
const result = await client.from(LONGFORM_SNAPSHOT_TABLE)
|
|
.select("user_id,request_id,section_id,status,payload")
|
|
.eq("user_id", identity.userId).eq("request_id", identity.requestId)
|
|
.eq("section_id", LONGFORM_SNAPSHOT_SECTION_ID).maybeSingle();
|
|
if (result.error) throw new Error("longform_snapshot_read_failed");
|
|
if (result.data === null) return null;
|
|
const row = result.data as Record<string, unknown>;
|
|
if (!row || row.user_id !== identity.userId || row.request_id !== identity.requestId
|
|
|| row.section_id !== LONGFORM_SNAPSHOT_SECTION_ID) throw new Error("longform_snapshot_binding_invalid");
|
|
if (row.status === "pending" && row.payload === null) return { status: "pending" as const };
|
|
if (row.status !== "ready") throw new Error("longform_snapshot_invalid");
|
|
return { status: "ready" as const, snapshot: validateLongformSnapshot(row.payload, identity) };
|
|
}
|
|
|
|
export async function persistLongformSnapshot(client: AppendixClient, identity: LongformSnapshotIdentity, snapshot: LongformSnapshot | null) {
|
|
if (snapshot) validateLongformSnapshot(snapshot, identity);
|
|
const args = { p_user_id: identity.userId, p_request_id: identity.requestId, p_section_id: LONGFORM_SNAPSHOT_SECTION_ID };
|
|
// Existing SQL: INSERT ON CONFLICT DO NOTHING; completion UPDATE ... WHERE status='pending'.
|
|
// Neither an in-flight ensure nor a late completion can overwrite a committed winner.
|
|
const result = snapshot
|
|
? await client.rpc("complete_personal_report_section", { ...args, p_payload: snapshot })
|
|
: await client.rpc("ensure_personal_report_section", { ...args, p_max_attempts: 2 });
|
|
if (result.error) throw new Error("longform_snapshot_persist_failed");
|
|
const stored = await readLongformSnapshot(client, identity);
|
|
if (!stored || (snapshot && stored.status !== "ready")) throw new Error("longform_snapshot_persist_failed");
|
|
// Always return the committed winner, never the losing worker's local calculation.
|
|
return stored;
|
|
}
|