From a75929c1397e4846763d46da467d5fa71fcc6498 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 4 Sep 2026 22:22:38 +0800 Subject: [PATCH] fix(report): keep actionNotes within the document cap at assemble Five-theme personal_full still failed final parse after the charts cap fix because writer output exceeded actionNotes.max(24). Truncate at assemble; do not raise the schema cap. Co-authored-by: Cursor --- CHANGELOG.md | 2 +- docs/BUG_HISTORY.md | 4 +-- .../PROGRESS-report-chart-cap-20260904.md | 13 +++++++--- frontend/src/lib/personal-report-contract.ts | 3 ++- .../src/lib/personal-report-generation.ts | 4 +++ .../personal-report-generation-v2.test.ts | 25 ++++++++++++++++++- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3a5164..f94a41e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ## 2026-09-04 — 五主题完整报告不再因分盘张数上限装不出来 -默认五章(含健康)会带上全部正式分盘。以前文档最多只许 6 张图,五章写完后整份报告仍会被判失败。现在张数上限跟着分盘枚举走。Skill 版本未变。 +默认五章(含健康)会带上全部正式分盘。以前文档最多只许 6 张图,五章写完后整份报告仍会被判失败。现在张数上限跟着分盘枚举走;各章行动条超过文档上限时按上限收下,不再整份作废。Skill 版本未变。 ## 2026-09-04 — 开场打招呼不再被 set-focus 清掉 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index f5bd157c..f3e29f19 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -8270,8 +8270,8 @@ - 用户现象:报告中心显示生成失败。错误码 `report_schema_invalid`,日志 `innerReason=final_parse_rejected`。五章其实都已写完。 - 触发条件:accepted 生时、默认五主题、staging @ `e27d5dc5`。干净 attempt 1 与此前被租约打断后恢复的 attempt 都复现。 - 根因:`c2f23131` 把 `CHART_IDS` 扩到 9 个(加 D6/D8/D30),文档 v2 仍 `charts.max(6)`。分盘提取修好后,五主题装配 9 张图,终稿 parse 拒绝。四主题一直 ≤6 所以从未踩中。 -- 修复:v2 `charts.max(CHART_IDS.length)`;JSON Schema / Python 合同同步;装配允许集改为 `CHART_IDS`。`final_parse_rejected` 日志增加 `parsePaths`(仅 path + code)。 -- 验证:合同测试 9 图过 parse;五主题分章夹具 READY;四主题不回归;超限仍 `too_big`。staging 真实 standard personal_full 待任务 3。 +- 修复:v2 `charts.max(CHART_IDS.length)`;JSON Schema / Python 合同同步;装配允许集改为 `CHART_IDS`。`final_parse_rejected` 日志增加 `parsePaths`(仅 path + code)。装配把 `actionNotes` 截到合同上限 24,不放宽 schema。 +- 验证:合同测试 9 图过 parse;五主题分章夹具 READY;每章 6 条行动仍 READY 且 notes=24。staging @ `d4627a50` request `31025c49`:五章 ready 后 `parsePaths=actionNotes/too_big`(charts 已不是原因)。截断修复待再部署。 - 防复发:上限必须绑定 `CHART_IDS.length`,不得再写裸 6/9。扩枚举必须同时改 Zod / JSON Schema enum / Python `CHART_IDS`。终稿失败必须带 parse path。 - 相关记录:BUG-526、BUG-534;引入半拉子改动的提交 `c2f23131` - 复发自:无 diff --git a/docs/tasks/PROGRESS-report-chart-cap-20260904.md b/docs/tasks/PROGRESS-report-chart-cap-20260904.md index f0ed8acf..d054f9a2 100644 --- a/docs/tasks/PROGRESS-report-chart-cap-20260904.md +++ b/docs/tasks/PROGRESS-report-chart-cap-20260904.md @@ -10,12 +10,13 @@ | --- | --- | --- | | 1 上限绑定 + 消费点核对 + 回归 | 完成 | BUG-535 | | 2 终稿 parse path 日志 | 完成 | BUG-535 | -| 3 部署后真实 personal_full | 待 health SHA 对齐后做 | BUG-535 | +| 3 部署后真实 personal_full | 进行中:第一次仍失败;parsePaths 指出 `actionNotes.too_big`,装配已按合同上限截断,待再部署 | BUG-535 | ## 实现要点 - v2 文档 `charts.max(CHART_IDS.length)`,单一真源,不写裸数字 9。 - 装配侧 `assembleReportDocumentV2` 的允许分盘集合改为 `new Set(CHART_IDS)`;`DOCUMENT_VARGA_CHART_IDS` 从 `CHART_IDS` 去掉 `D1` 派生。 +- 装配把 `actionNotes` 截到 `REPORT_DOCUMENT_V2_ACTION_NOTES_MAX`(24),**不放宽** schema。staging 第一次五章写完后又 `final_parse_rejected`,`parsePaths` 只有 `actionNotes`/`too_big`。 - 终稿 `safeParseServerReportDocument` 失败时,`generation_failed` 增加 `parsePaths: [{path, code}]`。只记路径与 zod/guard 代号,不记 `message`、不记正文或 hash 值。 - JSON Schema `maxItems` / chart `enum`、Python `CHART_IDS` 与 `len(CHART_IDS)` 与 TS 对齐(合同三份真源)。冻结 skill 包 `6.9.14` / `6.9.15` 未动。 @@ -42,6 +43,7 @@ - 超过 `CHART_IDS.length` 仍 `too_big` - 四主题既有用例不回归 - 五主题(含 health)分章管线 READY,文档 charts 正好 9 张、blocked 为空 +- 五主题每章 6 条行动仍 READY,`actionNotes.length === 24` - `final_parse_rejected` 日志含 `thematicNarrative.0.title` / `too_big`,不含超长标题正文 - Python `test_v2_accepts_full_chart_ids_set` @@ -51,9 +53,14 @@ - `npm run lint`:0 error / 74 warning(既有,未动) - 定向:`personal-report-contract` + `personal-report-generation-v2` + `personal-report-api`:**110 pass / 0 fail**(含新增 9 图 parse、五主题 READY、超限 too_big、parsePaths 日志) - `.venv/bin/python -m pytest tests/test_personal_report_contract.py`:**40 pass**(含 `test_v2_accepts_full_chart_ids_set`) +- 定向补跑 generation-v2 + contract:**56 pass**(含 actionNotes 截断) - 全量 `npm test`:Docker 库测争用超时(`database-*` 与部分会起 Postgres 的套件),与既有环境缺口同类;与本单无关 -- `next build`:本单未改 UI/路由,未跑;任务 3 部署后用 health SHA 验收 +- `next build`:本单未改 UI/路由,未跑 + +### 任务 3 第一次(health `d4627a50`,含 `61194690`) + +request `31025c49`:5× `consultation_workflow` 200;五章 ready;摘要 `stop`。终稿仍 `final_parse_rejected`。日志 `parsePaths: [{path:"actionNotes", code:"too_big"}]`——charts 上限已不是这条失败原因。未放宽 `actionNotes.max(24)`,装配截断到该上限后再验。 ## 让步 -无。JSON Schema 的 `maxItems` 只能写数字,用合同测试把它锁到 `CHART_IDS.length`。 +无。JSON Schema 的 charts `maxItems` 只能写数字,用合同测试把它锁到 `CHART_IDS.length`。`actionNotes` 合同上限未放宽。 diff --git a/frontend/src/lib/personal-report-contract.ts b/frontend/src/lib/personal-report-contract.ts index 880d70f5..931b43e4 100644 --- a/frontend/src/lib/personal-report-contract.ts +++ b/frontend/src/lib/personal-report-contract.ts @@ -46,6 +46,7 @@ export const CONFLICT_STATUSES = ["unresolved", "partial", "resolved"] as const; export const REPORT_ACTION_PRIORITIES = ["now", "next", "watch"] as const; export const REPORT_DOCUMENT_V1_CHART_IDS = ["D1", "D9", "D10"] as const; export const CHART_IDS = ["D1", "D2", "D6", "D8", "D9", "D10", "D11", "D24", "D30"] as const; +export const REPORT_DOCUMENT_V2_ACTION_NOTES_MAX = 24; const claimStatusSchema = z.enum(CLAIM_STATUSES); const themeIdSchema = z.string().regex(/^[a-z][a-z0-9_.-]{0,95}$/, "invalid theme id"); @@ -281,7 +282,7 @@ export const reportDocumentV2Schema = z.strictObject({ executiveSummary: executiveSummaryV2Schema, natalFoundation: natalFoundationSchema, currentPhase: currentPhaseSchema.nullable(), - actionNotes: z.array(actionNoteSchema).min(1).max(24), + actionNotes: z.array(actionNoteSchema).min(1).max(REPORT_DOCUMENT_V2_ACTION_NOTES_MAX), charts: z.array(reportDocumentV2ChartSchema).min(1).max(CHART_IDS.length), thematicNarrative: z.array(thematicSectionV2Schema).max(12), blockedConflictDisclosure: z.array(blockedConflictDisclosureSchema).max(12), diff --git a/frontend/src/lib/personal-report-generation.ts b/frontend/src/lib/personal-report-generation.ts index cb907a68..ee7bae22 100644 --- a/frontend/src/lib/personal-report-generation.ts +++ b/frontend/src/lib/personal-report-generation.ts @@ -5,6 +5,7 @@ import { } from "./personal-report-contract.server-core.ts"; import { CHART_IDS, + REPORT_DOCUMENT_V2_ACTION_NOTES_MAX, REQUIRED_THEME_CHARTS, type ClaimStatus, type EvidenceAppendix, @@ -2773,6 +2774,9 @@ export function assembleReportDocumentV2( evidenceRefs: [...executiveRefs], }); } + if (actionNotes.length > REPORT_DOCUMENT_V2_ACTION_NOTES_MAX) { + actionNotes.length = REPORT_DOCUMENT_V2_ACTION_NOTES_MAX; + } const usedEvidenceRefs = new Set([ ...executiveRefs, diff --git a/frontend/tests/personal-report-generation-v2.test.ts b/frontend/tests/personal-report-generation-v2.test.ts index ccd7d0a9..ba099485 100644 --- a/frontend/tests/personal-report-generation-v2.test.ts +++ b/frontend/tests/personal-report-generation-v2.test.ts @@ -13,7 +13,7 @@ import { type ReportEvidenceBundleV2, type TechniqueExecutionReceipt, } from "../src/lib/report-evidence-bundle-v2.ts"; -import { CHART_IDS } from "../src/lib/personal-report-contract.ts"; +import { CHART_IDS, REPORT_DOCUMENT_V2_ACTION_NOTES_MAX } from "../src/lib/personal-report-contract.ts"; import { classifyFinalParseInnerReason, classifyReportSchemaInnerReason, @@ -392,6 +392,29 @@ test("five-theme sectioned pipeline with the full CHART_IDS set finishes READY", assert.deepEqual(sectionCalls, ["career", "health", "marriage", "timing", "wealth"]); }); +test("five-theme assemble keeps actionNotes within the document cap", async () => { + const bundle = makeBundle({ + themes: standardFiveThemes, + charts: CHART_IDS.map((id, offset) => chart(id, offset)), + }); + const inner = sectionedAgent({}); + const agent: ReportAgentPort = { + ...inner, + async generateSection(sectionBundle, section, completedTitles, options) { + const output = await inner.generateSection!(sectionBundle, section, completedTitles, options); + return { + ...output, + actions: Array.from({ length: 6 }, (_, index) => ( + `围绕${output.title}记录可验证的现实反馈 ${index + 1}` + )), + }; + }, + }; + const document = readyV2(await runSectioned(bundle, agent, inMemorySectionService())); + assert.equal(document.actionNotes.length, REPORT_DOCUMENT_V2_ACTION_NOTES_MAX); + assert.equal(document.blockedConflictDisclosure.length, 0); +}); + test("generatePersonalReport passes the worker lease signal to the writer and settles when aborted", async () => { const bundle = makeBundle({ themes: [{