From cf6405ed88789e6860ebc62755d1778a0877e5e9 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 3 Sep 2026 09:58:06 +0800 Subject: [PATCH] fix(report): say chapters wrote when assembly never finished The 09:31 staging card had four ready sections but showed only structural validation failure. Derive the list copy from section status so a written pipeline stop is not described as an empty schema miss. Co-authored-by: Cursor --- PROGRESS-report-writer-failure-20260902.md | 2 +- docs/BUG_HISTORY.md | 2 +- frontend/src/lib/personal-report-failure-summary.ts | 5 ++++- .../tests/personal-report-writer-failure.test.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/PROGRESS-report-writer-failure-20260902.md b/PROGRESS-report-writer-failure-20260902.md index 2aee90a5..3a633e4d 100644 --- a/PROGRESS-report-writer-failure-20260902.md +++ b/PROGRESS-report-writer-failure-20260902.md @@ -99,7 +99,7 @@ - 列表 API:失败行多查 `request_id, status, last_error_code`(无正文),写入 `failureSummary`。 - 报告中心:`failureSummary ?? failureCode`。详情页摘要为主文案,等宽错误码为辅。 -本事故若用新摘要,应接近:「4 个主题中 1 个写作失败:证据引用未对齐,3 个未开始」。旧码仍会显示「证据不足」,因为历史行的 `last_error_code` 不会回写。 +本事故若用新摘要,应接近:「4 个主题中 1 个写作失败:证据引用未对齐,3 个未开始」。四章都写成但整份失败时,应接近:「4 个主题已写成,整份报告未完成装配」,不得再显示成空的「结构校验」。旧码仍会显示「证据不足」,因为历史行的 `last_error_code` 不会回写。 --- diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 9687bee3..1ffd7ff2 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -7643,7 +7643,7 @@ - 触发条件:章节 writer 输出未通过 `assertWriterOutput`(常见为 `evidenceRefs` 与 plan 集合不等),或输出在 token 预算内被截断;随后整份报告失败。 - 根因:三层叠加。旧错误分类把任何含 `evidence` 的消息打成 `section_evidence_insufficient`,`report_writer_evidence_refs_mismatch` 被误标;章节输出预算用英文「字符 ÷ 2」,standard 上限 1200 字中文只分到 1024 token;一章 blocked 或未捕获抛错可以中止整份,job 停在约 43%。失败详情只有聚合码,页面无法区分截断、引用不齐或未开始。staging 事故窗口的 writer telemetry 在容器 recreate 后丢失,本事故不能写成 `finishReason=length`。 - 修复:中文口径重算每章/摘要 `maxOutputTokens`,去掉 ÷2;length 修复要求压到字数下限且预算不低于首次。Prompt 要求逐字复制 `plan.evidenceRefs`;repair 只加失败类别。refs/identity 先于泛化 `evidence` 分类。单章失败后继续其余主题;`all_sections_blocked` 走统一失败日志。详情与列表聚合已有 section 行,展示可读摘要与错误码,不改表、不放宽 writer schema。 -- 验证:预算表锁定 concise 1218 / standard 2128 / deep 3072 / research 3072 / summary 3072。refs mismatch 分类为 `section_refs_mismatch` 且其余主题仍交付。repair 提示含类别词、不含内容。失败详情从 section 行汇总。`tsc --noEmit`、改动文件 ESLint、个人报告聚焦测试。staging `5c0bec0c` 上真实 standard 四章均 ready(无 `finishReason=length`),整份仍因 `onProgress` 抛错被标成 `report_schema_invalid`,摘要未开始;进度投影失败改为不中止已落库章节。 +- 验证:预算表锁定 concise 1218 / standard 2128 / deep 3072 / research 3072 / summary 3072。refs mismatch 分类为 `section_refs_mismatch` 且其余主题仍交付。repair 提示含类别词、不含内容。失败详情从 section 行汇总;四章 ready 但整份失败时摘要为「已写成,未完成装配」。`tsc --noEmit`、改动文件 ESLint、个人报告聚焦测试。staging `5c0bec0c` 真实 standard 四章均 ready(无 `finishReason=length`),整份因 `onProgress` 抛错被标成 `report_schema_invalid`;进度投影失败改为不中止装配。 - 防复发:输出预算不得再用字符 ÷ 2。`assertWriterOutput` 保持 id/theme 全等与 refs 集合相等。section 错误码不得把 refs mismatch 归进 evidence insufficient。一章失败不得中止其余 write 主题。用户可见失败必须有错误码级摘要,不得只展示 `report_schema_invalid`。日志与 PROGRESS 不得写入 prompt、bundle、模型原文或用户资料。 - 相关记录:BUG-352、BUG-451、BUG-486、BUG-489 - 复发自:BUG-451(分章后仍把写作失败压成整份 schema 码,截断预算与失败分类未按中文口径收口) diff --git a/frontend/src/lib/personal-report-failure-summary.ts b/frontend/src/lib/personal-report-failure-summary.ts index bd406d3b..69d7af07 100644 --- a/frontend/src/lib/personal-report-failure-summary.ts +++ b/frontend/src/lib/personal-report-failure-summary.ts @@ -40,6 +40,7 @@ export function summarizePersonalReportFailure(input: Readonly<{ const sections = input.sections; const blocked = sections.filter((row) => row.status === "blocked"); const pending = sections.filter((row) => row.status === "pending"); + const ready = sections.filter((row) => row.status === "ready"); const lastErrorCodes = blocked .map((row) => row.lastErrorCode) .filter((code): code is string => typeof code === "string" && code.length > 0); @@ -56,6 +57,8 @@ export function summarizePersonalReportFailure(input: Readonly<{ summary = `${total} 个主题中 ${blocked.length} 个写作失败:${labelText},${pending.length} 个未开始`; } else if (total > 0 && blocked.length > 0) { summary = `${total} 个主题中 ${blocked.length} 个写作失败:${labelText}`; + } else if (ready.length > 0 && input.failureCode === "report_schema_invalid") { + summary = `${ready.length} 个主题已写成,整份报告未完成装配`; } else if (input.failureCode === "report_schema_invalid") { summary = "报告未通过结构校验"; } else { @@ -65,7 +68,7 @@ export function summarizePersonalReportFailure(input: Readonly<{ let innerReason: string | null = null; if (blocked.length > 0 && pending.length === 0 && (total === 0 || blocked.length === total)) { innerReason = "all_sections_blocked"; - } else if (blocked.length > 0 && pending.length > 0) { + } else if ((blocked.length > 0 && pending.length > 0) || (ready.length > 0 && blocked.length + pending.length < total)) { innerReason = "section_generation_incomplete"; } diff --git a/frontend/tests/personal-report-writer-failure.test.ts b/frontend/tests/personal-report-writer-failure.test.ts index 81258ccf..24eca344 100644 --- a/frontend/tests/personal-report-writer-failure.test.ts +++ b/frontend/tests/personal-report-writer-failure.test.ts @@ -71,6 +71,19 @@ test("failure summary is readable and stays at error-code granularity", () => { assert.equal(incomplete.summary, "4 个主题中 1 个写作失败:证据引用未对齐,3 个未开始"); assert.equal(incomplete.innerReason, "section_generation_incomplete"); assert.deepEqual(incomplete.lastErrorCodes, ["section_refs_mismatch"]); + + const assembledChapters = summarizePersonalReportFailure({ + themeCount: 4, + failureCode: "report_schema_invalid", + sections: [ + { status: "ready", lastErrorCode: null }, + { status: "ready", lastErrorCode: null }, + { status: "ready", lastErrorCode: null }, + { status: "ready", lastErrorCode: null }, + ], + }); + assert.equal(assembledChapters.summary, "4 个主题已写成,整份报告未完成装配"); + assert.equal(assembledChapters.innerReason, "section_generation_incomplete"); }); test("writer identity and evidenceRefs assertions keep set-equality semantics", () => {