Files
Jyotisha/docs/tasks/TASK-report-sectioned-generation-20260830.md
T
Jesse_ChenandClaude Fable 5.1 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

242 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 个人报告改为分章节生成(2026-08-30)
基线:`origin/staging` @ `a96df754`
## 为什么要做
现状:`createPersonalReportAgent` 用**一次**模型调用产出整份叙述(`executiveSummary` + 最多 12 个 `thematicNarrative`),结构化输出 + Zod 校验,失败只允许**一次**修复重试。
两个问题:
1. **没有设输出上限。** 仓库里有现成的 `agentGenerationSettings`(答复预算 16,384 token),`agent-run.ts``consultation-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。
让步顺序:数据不损坏 > 功能与测试不回归 > 可验证的改进 > 成本 > 代码整洁。
## 开工前置
```bash
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`,注意它**已经有** `disposition``evidenceRefs``targetCharacters`
- `src/lib/personal-report-contract.ts` —— `claimStatus``blocked`
- `supabase/migrations/20260814040000_personal_report_jobs_v2.sql` —— 任务表与四个 SQL 函数
- `src/components/personal-report/personal-report-page.tsx` —— 9 个状态与轮询
---
## 任务 0(P0,门控)· 先把截断变成可观测的
### 事实
`src/mastra/personal-report.ts``logTelemetry` 只记 `resolved` / `failed`,不记 `finishReason` 也不记 token 用量。**现在无法从数据上区分"模型不听话"和"输出被截断"**,也就无法证明后续改动有效。
### 做法
- `logTelemetry` 增加 `finishReason``length` 即截断)与 `usage.outputTokens` / `usage.inputTokens`
- 两次尝试(首次与修复重试)各记一条,能区分是哪一次
- 遵守 `AGENTS.md` 的隐私红线:**只记指标名、阶段、数值**,不得记 prompt、bundle、模型原文、用户资料、案例 ID
### 验收
- 人为构造一次会被截断的生成(例如临时把输出上限调到极小),确认日志出现 `finishReason: "length"`
- 上线后至少收集一段真实数据,在 PROGRESS 里给出:失败总数、其中 `length` 占比、`outputTokens` 的 p50/p95
**这一条必须先单独上线并收到数据**,任务 2 的输出上限取值要用这批数据决定,不许拍脑袋。
---
## 任务 1(P0)· 章节中间结果落库
### 做法
新建 `public.personal_report_sections`**不要**往 `report_document` 里增量写。
字段至少包含:`user_id``request_id``section_id``payload jsonb`(单个 thematicNarrative 条目)、`status``pending` / `ready` / `blocked`)、`attempt_count``max_attempts``last_error_code``created_at``updated_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
- 显式 `grant``authenticated``select``service_role` 才有写权限
- 章节状态流转写成 SQL 函数(与 `claim/heartbeat/complete/fail_personal_report_job` 同一模式),`security definer` 并显式 `grant execute``service_role`
- 文件名沿用 `YYYYMMDDHHMMSS_描述.sql` 格式,时间戳要晚于 `20260830010000`
### 验收
```bash
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.status``ready`
### 续做
worker 拿到租约后,按 `progress_phase``personal_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` 已经是一等 `claimStatus``personal-report-contract.ts`),文档视图已有「阻塞」标签与渲染路径 —— 复用它,不要新造状态。
### 做法
- 单个章节耗尽自己的 `attempt_count` 后,标记该行 `status = 'blocked'`,记 `last_error_code`**继续下一章**
- 装配时把 blocked 章节渲染成明确的"本节未能生成",并说明原因类别(截断 / 校验失败 / 证据不足),**不得静默省略**
- 只有当**全部** write 章节都 blocked、或摘要本身失败时,整份报告才 `failed`
### 明确不要做
- 不得因为有 blocked 章节就退积分或改计费 —— 产品已定这算合格交付
- 不得把 blocked 章节伪装成正常内容
- 不得为了凑数让模型"随便写点什么"
### 验收
- 强制某一章连续失败,验证:该章标 blocked、其余章正常、报告 `ready`、页面上该节有明确标注
- 强制全部章节失败,验证报告 `failed` 且失败原因可读
---
## 任务 4(P2)· 等待页显示真进度
### 事实
`personal_report_jobs.progress_percent`0100)和 `progress_phase` 都已存在,目前基本没被用起来。等待页只有一个转圈加"已等待 N 分",8 分钟里没有任何进展信号。
### 做法
- 每完成一个章节更新 `progress_percent`(已完成 / 总 write 章节数)与 `progress_phase`
- 等待页显示"正在生成第 3/7 个主题"一类的文案
- 保留既有的 `timed-out` / 「继续等待」/ 「返回报告中心」路径,**不要动那 9 个状态的结构**
### 验收
生成一份多主题报告,录一段等待过程,说明进度是如何推进的。
---
## 不在本轮范围
- **章节并发生成**:红线 2 已禁止。等串行数据出来再单独立项。
- **提示缓存**:先做 `evidenceRefs` 过滤,量完成本再谈。
- **收紧 `thematicNarrative``max(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`。流水线是 `validate``publish` → 才派发 `deploy`,**全链路约 20 分钟**,不要十几分钟就断定失败。
- 不自行提升 main。
## 交付物清单
1. `logTelemetry``finishReason` 与 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` / `eslint`0 error/ `tsx --test`(失败清单与基线逐条比对)/ `npm run test:db` / `next build` 的实际输出