fix(report): classify writer failures and size CJK section budgets
Independent Staging Quality Gate / validate (push) Successful in 9m41s
Independent Staging Quality Gate / publish (push) Successful in 1m49s

Staging personal_full died as report_schema_invalid after the first
chapter blocked. Keep assertWriterOutput, stop treating refs mismatch as
missing evidence, and surface section error codes on the report APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 01:17:46 +08:00
co-authored by Cursor
parent 838d9e7687
commit eda37c1523
16 changed files with 691 additions and 66 deletions
@@ -11,6 +11,7 @@ import {
type PersonalReportService,
} from "@/lib/personal-report-service";
import { createSupabasePersonalReportJobService } from "@/lib/personal-report-job-service";
import { createPersonalReportSectionService } from "@/lib/personal-report-section-service";
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
import { createServerSupabaseClient } from "@/lib/supabase/server";
@@ -38,15 +39,24 @@ async function resolvePersistenceForUser() {
const supabase = await createServerSupabaseClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return { userId: null as string | null, persistence: null as PersonalReportService | null, jobs: null };
return { userId: null as string | null, persistence: null as PersonalReportService | null, jobs: null, listSections: undefined };
}
const persistence = createSupabasePersonalReportService(supabase);
return { userId: user.id, persistence, jobs: createSupabasePersonalReportJobService(supabase) };
const sections = createPersonalReportSectionService(supabase as never);
return {
userId: user.id,
persistence,
jobs: createSupabasePersonalReportJobService(supabase),
listSections: async (ownerId: string, requestId: string) => {
const rows = await sections.list(ownerId, requestId);
return rows.map((row) => ({ status: row.status, lastErrorCode: row.lastErrorCode }));
},
};
}
export async function GET(request: Request, context: RouteContext) {
try {
const { userId, persistence, jobs } = await resolvePersistenceForUser();
const { userId, persistence, jobs, listSections } = await resolvePersistenceForUser();
const { reportId } = await context.params;
if (!uuidPattern.test(reportId)) {
return NextResponse.json(
@@ -71,6 +81,7 @@ export async function GET(request: Request, context: RouteContext) {
// before it is returned to the browser. Client-side validation is never
// a substitute.
jobs: jobs ?? undefined,
listSections,
validateReadyDocument: (document) => {
const parsed = safeParseServerReportDocument(document);
return parsed.ok
+39 -4
View File
@@ -6,6 +6,7 @@ import {
resolveSkillSnapshot,
} from "@/lib/personal-report-generation";
import { REPORT_STABLE_CODES } from "@/lib/personal-report-codes";
import { summarizePersonalReportFailure } from "@/lib/personal-report-failure-summary";
import { isProductEnabled } from "@/lib/product-access";
import {
isPersonalReportFeatureEnabled,
@@ -54,7 +55,10 @@ function toNextResponse(response: { status: number; body: Record<string, unknown
return NextResponse.json(response.body, { status: response.status });
}
function listReportView(value: unknown) {
function listReportView(
value: unknown,
failure?: ReturnType<typeof summarizePersonalReportFailure> | null,
) {
const row = value && typeof value === "object" ? value as Record<string, unknown> : {};
return {
id: typeof row.id === "string" ? row.id : "",
@@ -71,6 +75,7 @@ function listReportView(value: unknown) {
completedAt: row.completed_at == null || row.completed_at === ""
? null
: reportListTimestamp(row.completed_at) || null,
...(failure?.summary ? { failureSummary: failure.summary } : {}),
};
}
@@ -89,9 +94,39 @@ export async function GET() {
.order("created_at", { ascending: false })
.limit(20);
if (error) throw error;
return NextResponse.json({
reports: Array.isArray(data) ? data.map(listReportView).filter((report) => report.id) : [],
});
const reports = Array.isArray(data) ? data.map((row) => listReportView(row)).filter((report) => report.id) : [];
const failed = reports.filter((report) => report.status === "failed" && report.requestId);
if (failed.length > 0) {
const { data: sections, error: sectionError } = await supabase
.from("personal_report_sections")
.select("request_id, status, last_error_code")
.eq("user_id", user.id)
.in("request_id", failed.map((report) => report.requestId));
if (sectionError) throw sectionError;
const grouped = new Map<string, Array<{ status: string; lastErrorCode: string | null }>>();
for (const row of Array.isArray(sections) ? sections : []) {
const requestId = typeof row.request_id === "string" ? row.request_id : "";
if (!requestId) continue;
const list = grouped.get(requestId) ?? [];
list.push({
status: typeof row.status === "string" ? row.status : "pending",
lastErrorCode: typeof row.last_error_code === "string" ? row.last_error_code : null,
});
grouped.set(requestId, list);
}
for (const [index, report] of reports.entries()) {
if (report.status !== "failed") continue;
const source = Array.isArray(data) ? data.find((row) => (
row && typeof row === "object" && (row as { id?: unknown }).id === report.id
)) : null;
reports[index] = listReportView(source, summarizePersonalReportFailure({
themeCount: report.themes.length,
sections: grouped.get(report.requestId) ?? [],
failureCode: report.failureCode,
}));
}
}
return NextResponse.json({ reports });
} catch (error) {
if (isSupabaseConfigurationError(error)) {
return NextResponse.json(