Files
Jyotisha/docs/tasks/TASK-report-sectioned-generation-20260830.md
T
Jesse_Chen 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

13 KiB
Raw Blame History

任务书 · 个人报告改为分章节生成(2026-08-30)

基线:origin/staging @ a96df754

为什么要做

现状:createPersonalReportAgent一次模型调用产出整份叙述(executiveSummary + 最多 12 个 thematicNarrative),结构化输出 + Zod 校验,失败只允许一次修复重试。

两个问题:

  1. 没有设输出上限。 仓库里有现成的 agentGenerationSettings(答复预算 16,384 token),agent-run.tsconsultation-tools.ts 都在用,只有 src/mastra/personal-report.ts 没用 —— 它走模型提供方的默认值。而 schema 允许的最坏输出约 224,000 字符(12 章 × 每章上限)。
  2. 失败是全有或全无。 任何一章写坏或被截断 → JSON 不完整 → schema 不过 → 重试一次 → 整份报告 failed。用户看到的是"生成失败",不是"少了两个主题"。

产品已确认:一份缺若干主题、缺失部分明确标注的报告,算合格交付。 这是本轮降级方案成立的前提。

这不是"上下文丢失"问题

createPersonalReportAgent 是结构性无状态的 —— 注释原文:"chat history and skill text are structurally excluded by the agent definition (no skills, no tools, no memory)"。prompt 是 (bundle, plan) 的纯函数,现在就没有跨调用的对话上下文可丢。拆开后每次仍拿到完整证据。

真正需要跨章节的只有三件,均在本任务书内解决:摘要要概括全部主题(最后生成)、章节间不重复(传已写标题列表)、断言口径一致(服务端 claimCards + evidenceRefs 绑定,本就每次相同)。


硬红线

  1. 任务 0 是门控。 观测必须先上线并收到真实数据,才允许调整输出上限或收紧 schema。不得凭任务书里的估算直接改参数 —— 那个 224,000 是 schema 上限推算,不是实测。
  2. 章节生成先串行,不得并发。 章节之间无依赖,理论上可并发,但会放大限流风险。先串行拿数据。
  3. report_document 的语义不得改变:它只存最终成品。中间结果一律不写进去,避免半成品被当成完整报告读出。
  4. schema 只可收紧,不可放宽。 不得为了让截断的输出通过而放松校验。
  5. 任务生命周期在 Postgres 函数里claim_personal_report_job / heartbeat_personal_report_job / complete_personal_report_job / fail_personal_report_job,见 supabase/migrations/20260814040000_personal_report_jobs_v2.sql)。章节级状态必须沿用同一模式加 SQL 函数,不得把状态机搬进应用层。
  6. 不得修改既有测试断言 —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
  7. 推 staging 前必须 ./node_modules/.bin/tsc --noEmit 通过。不要用 npx tsc,本仓库环境下会装到空包 tsc@2.0.4
  8. 数据库测试必须真跑。 本轮动了表结构,npm run test:db 需要 Docker。没有 Docker 就不要推 —— 把环境缺口写进 BLOCKED.md 并停下,不允许"本地跑不了所以跳过"。
  9. 不得改 .gitea/workflows/**。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。

让步顺序:数据不损坏 > 功能与测试不回归 > 可验证的改进 > 成本 > 代码整洁。

开工前置

git fetch origin --prune
git worktree add -b codex/report-sectioned-20260830 \
  ../.worktrees/report-sectioned-20260830 origin/staging

基线必须是 origin/staging。读 pre_work_error_ledger.md,跑 scripts/pre_work_check.py,读 frontend/AGENTS.md。改前在 docs/BUG_HISTORY.md 检索同类记录。

先读这几个文件再动手,本任务书的判断都基于它们:

  • src/mastra/personal-report.ts —— agent、prompt、schema、重试
  • src/lib/personal-report-plan.ts —— ReportSectionPlanEntry,注意它已经有 dispositionevidenceRefstargetCharacters
  • src/lib/personal-report-contract.ts —— claimStatusblocked
  • supabase/migrations/20260814040000_personal_report_jobs_v2.sql —— 任务表与四个 SQL 函数
  • src/components/personal-report/personal-report-page.tsx —— 9 个状态与轮询

任务 0(P0,门控)· 先把截断变成可观测的

事实

src/mastra/personal-report.tslogTelemetry 只记 resolved / failed,不记 finishReason 也不记 token 用量。现在无法从数据上区分"模型不听话"和"输出被截断",也就无法证明后续改动有效。

做法

  • logTelemetry 增加 finishReasonlength 即截断)与 usage.outputTokens / usage.inputTokens
  • 两次尝试(首次与修复重试)各记一条,能区分是哪一次
  • 遵守 AGENTS.md 的隐私红线:只记指标名、阶段、数值,不得记 prompt、bundle、模型原文、用户资料、案例 ID

验收

  • 人为构造一次会被截断的生成(例如临时把输出上限调到极小),确认日志出现 finishReason: "length"
  • 上线后至少收集一段真实数据,在 PROGRESS 里给出:失败总数、其中 length 占比、outputTokens 的 p50/p95

这一条必须先单独上线并收到数据,任务 2 的输出上限取值要用这批数据决定,不许拍脑袋。


任务 1P0)· 章节中间结果落库

做法

新建 public.personal_report_sections不要report_document 里增量写。

字段至少包含:user_idrequest_idsection_idpayload jsonb(单个 thematicNarrative 条目)、statuspending / ready / blocked)、attempt_countmax_attemptslast_error_codecreated_atupdated_at。主键或唯一约束用 (user_id, request_id, section_id)

选这个方案而不是复用 report_document 的三个理由,实现时请守住:

  1. report_document 保持"最终成品"语义
  2. 每个章节有自己的重试预算 —— 一个难写的主题不得烧光整个任务的 max_attempts
  3. 续做只需查"哪些 section 还没有 payload"

迁移要求

20260814040000_personal_report_jobs_v2.sql 的既有写法:

  • 整个迁移包在 begin; ... commit;
  • 所有 DDL 用 if not exists,可重复执行
  • 必须 enable row level security,并给 authenticated 只读自己的行的 policy
  • 显式 grantauthenticatedselectservice_role 才有写权限
  • 章节状态流转写成 SQL 函数(与 claim/heartbeat/complete/fail_personal_report_job 同一模式),security definer 并显式 grant executeservice_role
  • 文件名沿用 YYYYMMDDHHMMSS_描述.sql 格式,时间戳要晚于 20260830010000

验收

npm run db:migrate:check      # 幂等性
npm run test:db               # 需要 Docker

新增数据库测试,至少覆盖:authenticated 读不到别人的章节;authenticated 写不进去;service_role 能写;同一 (user_id, request_id, section_id) 重复插入不产生第二行。

止损

如果执行环境没有 Docker停在这里,把缺口写进 BLOCKED.md,不要带着未验证的迁移继续做任务 2。


任务 2(P0)· 把一次生成切成 plan → 章节 → 摘要 → 装配

阶段模型

沿用已存在但基本没用起来personal_report_jobs.progress_phase

plan  →  section:<id> × N  →  summary  →  assemble  →  ready
 ↑确定性        ↑每章节一次调用        ↑最后一次      ↑确定性

每个章节一次调用

输入:

  • 按该章节 evidenceRefs 过滤后的 bundle 子集 —— ReportSectionPlanEntry.evidenceRefs 已经有这个字段。这条是成本关键:不过滤就是把完整证据包重发 N 次。过滤后既省钱也让模型更聚焦。
  • plan 只保留这一个 section
  • 已完成章节的标题列表(只要标题,不要正文)——用于避免重复
  • 显式输出上限,用 agentGenerationSettings;具体数值由任务 0 的数据决定,并参考该 section 的 targetCharacters.max

输出 schema 收成单个 thematicNarrative 条目,不再是 12 条数组。

摘要最后生成

输入是各已完成章节的标题 + claimStatus,不是正文全文。这样既概括到全部主题,又不会把 prompt 撑大。

装配

确定性合并成 report_document,此时才写 report_document 并把 personal_reports.statusready

续做

worker 拿到租约后,按 progress_phasepersonal_report_sections 的已有行判断从哪继续,已完成章节不得重算

验收

  • 单元测试:给定 N 个 write 章节,验证发生 N+1 次调用,且第 k 次的 bundle 只含该章节 evidenceRefs 引用到的证据
  • 续做测试:模拟在第 3 章后中断,重新领取任务,验证前 3 章不重算、最终文档完整
  • 端到端至少跑一份真实报告,PROGRESS 给出:章节数、每章 outputTokens、总墙钟时间,与改前单次调用对照

止损

  • 如果过滤 bundle 导致某类章节缺证据写不出来,不要退回发全量:先确认是 plan 的 evidenceRefs 不全还是过滤逻辑错了。真是 plan 不全就修 plan,并在 PROGRESS 说明。
  • 如果总墙钟时间超过现有 8 分钟轮询预算(POLL_BUDGET_MS),不要顺手调大预算,先把数据摆出来再决定。

任务 3(P1)· 章节写不出时降级为 blocked,报告照常交付

前提

产品已确认:缺若干主题、缺失部分明确标注的报告算合格交付。

blocked 已经是一等 claimStatuspersonal-report-contract.ts),文档视图已有「阻塞」标签与渲染路径 —— 复用它,不要新造状态。

做法

  • 单个章节耗尽自己的 attempt_count 后,标记该行 status = 'blocked',记 last_error_code继续下一章
  • 装配时把 blocked 章节渲染成明确的"本节未能生成",并说明原因类别(截断 / 校验失败 / 证据不足),不得静默省略
  • 只有当全部 write 章节都 blocked、或摘要本身失败时,整份报告才 failed

明确不要做

  • 不得因为有 blocked 章节就退积分或改计费 —— 产品已定这算合格交付
  • 不得把 blocked 章节伪装成正常内容
  • 不得为了凑数让模型"随便写点什么"

验收

  • 强制某一章连续失败,验证:该章标 blocked、其余章正常、报告 ready、页面上该节有明确标注
  • 强制全部章节失败,验证报告 failed 且失败原因可读

任务 4P2)· 等待页显示真进度

事实

personal_report_jobs.progress_percent0100)和 progress_phase 都已存在,目前基本没被用起来。等待页只有一个转圈加"已等待 N 分",8 分钟里没有任何进展信号。

做法

  • 每完成一个章节更新 progress_percent(已完成 / 总 write 章节数)与 progress_phase
  • 等待页显示"正在生成第 3/7 个主题"一类的文案
  • 保留既有的 timed-out / 「继续等待」/ 「返回报告中心」路径,不要动那 9 个状态的结构

验收

生成一份多主题报告,录一段等待过程,说明进度是如何推进的。


不在本轮范围

  • 章节并发生成:红线 2 已禁止。等串行数据出来再单独立项。
  • 提示缓存:先做 evidenceRefs 过滤,量完成本再谈。
  • 收紧 thematicNarrativemax(12):等任务 0 的数据。
  • 换模型或改模型选择逻辑:不在本轮。
  • 前端 UI 的其它优化:见 TASK-frontend-interaction-20260830.md,两份任务书互不冲突。

收尾

  • PROGRESS 文件名写 PROGRESS-report-sectioned-20260830.md不要写成 PROGRESS.md —— 根目录已有受版本控制的 progress.md,大小写不敏感文件系统上会互相覆盖。
  • 任务 0 单独一次推送并收数据;任务 1–4 可合成第二次推送。staging push 触发全量构建+部署,没有路径过滤。
  • 数据库迁移走既有流程(.gitea/workflows/migrate-staging-database.yml),不要手工连库改表
  • 推送后核对 https://staging.jyotisha.chat/api/health.deployment.gitCommit。流水线是 validatepublish → 才派发 deploy全链路约 20 分钟,不要十几分钟就断定失败。
  • 不自行提升 main。

交付物清单

  1. logTelemetryfinishReason 与 token 用量,且不含任何隐私字段
  2. 任务 0 上线后的真实数据:失败总数、length 占比、outputTokens p50/p95
  3. personal_report_sections 迁移(事务内、幂等、RLS、显式 grant、SQL 状态函数)
  4. 数据库测试:越权读、越权写、service_role 可写、重复插入幂等
  5. 分章节生成实现:N+1 次调用、按 evidenceRefs 过滤 bundle、摘要最后生成
  6. 续做测试:中断后不重算已完成章节
  7. blocked 降级:单章失败报告仍 ready 且有明确标注;全部失败才 failed
  8. progress_percent / progress_phase 真实推进 + 等待页文案
  9. 改前改后对照:章节数、每章 outputTokens、总墙钟时间
  10. docs/BUG_HISTORY.md 条目(编号先对远端确认)
  11. BLOCKED.md:任何触发止损的项,尤其是 Docker 缺口
  12. PROGRESS-report-sectioned-20260830.md
  13. tsc --noEmit / eslint0 error/ tsx --test(失败清单与基线逐条比对)/ npm run test:db / next build 的实际输出