- 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
13 KiB
任务书 · 个人报告改为分章节生成(2026-08-30)
基线:origin/staging @ a96df754。
为什么要做
现状:createPersonalReportAgent 用一次模型调用产出整份叙述(executiveSummary + 最多 12 个 thematicNarrative),结构化输出 + Zod 校验,失败只允许一次修复重试。
两个问题:
- 没有设输出上限。 仓库里有现成的
agentGenerationSettings(答复预算 16,384 token),agent-run.ts和consultation-tools.ts都在用,只有src/mastra/personal-report.ts没用 —— 它走模型提供方的默认值。而 schema 允许的最坏输出约 224,000 字符(12 章 × 每章上限)。 - 失败是全有或全无。 任何一章写坏或被截断 → 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 绑定,本就每次相同)。
硬红线
- 任务 0 是门控。 观测必须先上线并收到真实数据,才允许调整输出上限或收紧 schema。不得凭任务书里的估算直接改参数 —— 那个 224,000 是 schema 上限推算,不是实测。
- 章节生成先串行,不得并发。 章节之间无依赖,理论上可并发,但会放大限流风险。先串行拿数据。
report_document的语义不得改变:它只存最终成品。中间结果一律不写进去,避免半成品被当成完整报告读出。- schema 只可收紧,不可放宽。 不得为了让截断的输出通过而放松校验。
- 任务生命周期在 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 函数,不得把状态机搬进应用层。 - 不得修改既有测试断言 —— 除非该断言锁的正是本轮要改的缺陷本身;那种情况必须在断言上方注明原值与原因,并在 PROGRESS 单列。
- 推 staging 前必须
./node_modules/.bin/tsc --noEmit通过。不要用npx tsc,本仓库环境下会装到空包tsc@2.0.4。 - 数据库测试必须真跑。 本轮动了表结构,
npm run test:db需要 Docker。没有 Docker 就不要推 —— 把环境缺口写进BLOCKED.md并停下,不允许"本地跑不了所以跳过"。 - 不得改
.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,注意它已经有disposition、evidenceRefs、targetCharacterssrc/lib/personal-report-contract.ts——claimStatus含blockedsupabase/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 的三个理由,实现时请守住:
report_document保持"最终成品"语义- 每个章节有自己的重试预算 —— 一个难写的主题不得烧光整个任务的
max_attempts - 续做只需查"哪些 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
验收
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(0–100)和 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。
交付物清单
logTelemetry记finishReason与 token 用量,且不含任何隐私字段- 任务 0 上线后的真实数据:失败总数、
length占比、outputTokensp50/p95 personal_report_sections迁移(事务内、幂等、RLS、显式 grant、SQL 状态函数)- 数据库测试:越权读、越权写、service_role 可写、重复插入幂等
- 分章节生成实现:N+1 次调用、按
evidenceRefs过滤 bundle、摘要最后生成 - 续做测试:中断后不重算已完成章节
- blocked 降级:单章失败报告仍
ready且有明确标注;全部失败才failed progress_percent/progress_phase真实推进 + 等待页文案- 改前改后对照:章节数、每章
outputTokens、总墙钟时间 docs/BUG_HISTORY.md条目(编号先对远端确认)BLOCKED.md:任何触发止损的项,尤其是 Docker 缺口PROGRESS-report-sectioned-20260830.mdtsc --noEmit/eslint(0 error)/tsx --test(失败清单与基线逐条比对)/npm run test:db/next build的实际输出