fix(report): bind v2 chart cap to CHART_IDS.length

Five-theme personal_full assembled nine charts after health vargas landed, but the document schema still capped at six. Bind the count to the enum, align JSON/Python contracts, and log final-parse paths without values.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 21:33:27 +08:00
co-authored by Cursor
parent d2955c6280
commit 6119469069
13 changed files with 279 additions and 21 deletions
@@ -38,6 +38,7 @@ export function safeParseServerReportDocument(input: unknown): ReportDocumentPar
{
path: "provenance.evidenceHash",
message: `does not match recomputed evidence hash ${recomputed}`,
code: "evidence_hash",
},
],
};
+4 -2
View File
@@ -282,7 +282,7 @@ export const reportDocumentV2Schema = z.strictObject({
natalFoundation: natalFoundationSchema,
currentPhase: currentPhaseSchema.nullable(),
actionNotes: z.array(actionNoteSchema).min(1).max(24),
charts: z.array(reportDocumentV2ChartSchema).min(1).max(6),
charts: z.array(reportDocumentV2ChartSchema).min(1).max(CHART_IDS.length),
thematicNarrative: z.array(thematicSectionV2Schema).max(12),
blockedConflictDisclosure: z.array(blockedConflictDisclosureSchema).max(12),
evidenceAppendix: evidenceAppendixSchema,
@@ -310,6 +310,7 @@ export type BlockedConflictDisclosureV2 = ReportDocumentV2["blockedConflictDiscl
export type ReportDocumentParseError = Readonly<{
path: string;
message: string;
code: string;
}>;
export class ReportDocumentValidationError extends Error {
@@ -592,6 +593,7 @@ export function safeParseReportDocument(input: unknown): ReportDocumentParseResu
errors: parsed.error.issues.map((issue) => ({
path: issue.path.join(".") || "(root)",
message: issue.message,
code: issue.code,
})),
};
}
@@ -600,7 +602,7 @@ export function safeParseReportDocument(input: unknown): ReportDocumentParseResu
if (guardErrors.length > 0) {
return {
ok: false,
errors: guardErrors.map((message) => ({ path: "(guard)", message })),
errors: guardErrors.map((message) => ({ path: "(guard)", message, code: "guard" })),
};
}
return { ok: true, document };
+36 -8
View File
@@ -4,10 +4,12 @@ import {
safeParseServerReportDocument,
} from "./personal-report-contract.server-core.ts";
import {
CHART_IDS,
REQUIRED_THEME_CHARTS,
type ClaimStatus,
type EvidenceAppendix,
type ReportDepth,
type ReportDocumentParseError,
type ReportDocumentV1,
type ReportDocumentV2,
} from "./personal-report-contract.ts";
@@ -318,8 +320,10 @@ function readHouses(
return houses;
}
const DOCUMENT_VARGA_CHART_IDS = ["D2", "D6", "D8", "D9", "D10", "D11", "D24", "D30"] as const;
type DocumentVargaChartId = (typeof DOCUMENT_VARGA_CHART_IDS)[number];
type DocumentVargaChartId = Exclude<(typeof CHART_IDS)[number], "D1">;
const DOCUMENT_VARGA_CHART_IDS = CHART_IDS.filter(
(id): id is DocumentVargaChartId => id !== "D1",
);
const VARGA_KEY_ALIASES: Readonly<Record<DocumentVargaChartId, readonly string[]>> = {
D2: ["D2_Hora", "D2"],
@@ -2691,7 +2695,7 @@ export function assembleReportDocumentV2(
const thematicRefs = uniqueInOrder(thematicNarrative.flatMap((section) => section.evidenceRefs));
const executiveRefs = thematicRefs.length > 0 ? thematicRefs : d1Refs;
const chartIds = new Set(["D1", "D2", "D6", "D8", "D9", "D10", "D11", "D24", "D30"]);
const chartIds = new Set<string>(CHART_IDS);
const charts: ReportDocumentV2["charts"] = [];
for (const chart of bundle.charts) {
if (!chartIds.has(chart.id)) continue;
@@ -3220,15 +3224,39 @@ export function isPersonalReportGenerationAbort(error: unknown, signal?: AbortSi
return error instanceof Error && error.name === "AbortError";
}
function failSchema(innerReason: ReportSchemaInnerReason): GeneratePersonalReportResult {
console.info("[personal-report]", JSON.stringify({
function failSchema(
innerReason: ReportSchemaInnerReason,
parseErrors?: readonly ReportDocumentParseError[],
): GeneratePersonalReportResult {
const payload: {
event: "generation_failed";
failureCode: "report_schema_invalid";
innerReason: ReportSchemaInnerReason;
parsePaths?: readonly Readonly<{ path: string; code: string }>[];
} = {
event: "generation_failed",
failureCode: "report_schema_invalid",
innerReason,
}));
};
if (parseErrors && parseErrors.length > 0) {
payload.parsePaths = parseErrors.map((error) => ({
path: error.path,
code: error.code,
}));
}
console.info("[personal-report]", JSON.stringify(payload));
return { status: "failed", failureCode: "report_schema_invalid", innerReason };
}
function failFinalParse(
parsed: ReturnType<typeof safeParseServerReportDocument>,
): GeneratePersonalReportResult {
if (!parsed.ok) {
return failSchema(classifyFinalParseInnerReason(parsed.errors), parsed.errors);
}
return failSchema("final_parse_rejected");
}
function rethrowIfAborted(error: unknown, signal?: AbortSignal): void {
if (isPersonalReportGenerationAbort(error, signal)) throw error;
}
@@ -3473,7 +3501,7 @@ async function generateSectionedPersonalReport(
const guarded = applyReportGuard(candidate, buildLegacyPacketFromBundle(bundle));
if (!guarded.ok) return { status: "failed", failureCode: "report_guard_rejected" };
const parsed = safeParseServerReportDocument(guarded.document);
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") return failSchema(parsed.ok ? "final_parse_rejected" : classifyFinalParseInnerReason(parsed.errors));
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") return failFinalParse(parsed);
return { status: "ready", document: parsed.document, evidenceHash: computeEvidenceHash(parsed.document.evidenceAppendix), usage: deps.agent.getUsage?.() };
} catch (error) {
rethrowIfAborted(error, deps.signal);
@@ -3575,7 +3603,7 @@ export async function generatePersonalReport(
}
const parsed = safeParseServerReportDocument(guarded.document);
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") {
return failSchema(parsed.ok ? "final_parse_rejected" : classifyFinalParseInnerReason(parsed.errors));
return failFinalParse(parsed);
}
return {
status: "ready",