Files
Jyotisha/docs/tasks/TASK-report-reference-perf-20261013.md
T
Jesse_ChenandClaude Opus 5.5 2994588ad4 docs(tasks): fold product decisions into audit briefs (BUG-1344..1346)
- dasha-unify: fix Ashtottari birth balance (BUG-1344)
- rect-scoring-perf: server-side candidate cap 241 (BUG-1345)
- node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346)
- consult-prep-parallel: classifier/reserve concurrency declined
- answer-effort-ab: model key supplied by product at acceptance
- drop stricter-than-test line-count red lines (AGENTS.md section 6)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-14 00:41:02 +08:00

246 lines
28 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.
# TASK · 长报告引擎出稿提速(中文渲染、哈希、同盘复用)(issue #待定)
- 日期:2026-10-13
- 来源:「2026-10-13 全仓屎山与性能排查」P1 条目「长报告提速」(`/api/professional_report_reference`)
- 执行方:**Grok(Windows + Docker)**,分支 `codex/report-reference-perf-20261013`,只推这个分支
- 验收:Claude(Linux)
- BUG 编号:**BUG-1335~1336**(开工时核对 `docs/BUG_HISTORY.md` 最大号;写作时最大号 BUG-1317。若已被占用,顺延并在 PROGRESS 写明)(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
- 开工前置:无(第一批立即派发)
- **主会话补充的串行链(2026-10-14)**:引擎/API 文件链按此顺序逐单合入后开工(`upstream-sync7` 也改 `jyotish_engine.py` / `jyotish_api_server.py`,若在途,后合一方 rebase):`api-disk-cache-retention` + `report-reference-perf` → `api-chart-subprocess-gate` → `pl9-render-split` → `python-hygiene` → `dasha-unify` → `api-server-service-extract`。本单的前置以这条链为准(与上一行不一致时以本行为准)。
- 下游依赖:`TASK-pl9-render-split-20261013` 必须等本单合入 staging 才开工(两单都改 `scripts/jyotish_engine.py`)
## 1. 基线
- 基线 commit:`origin/staging` `77057a819`
- 生产调用链(唯一的生产 HTTP 调用方):`frontend/src/lib/personal-report-longform-generate.ts::fetchLongformMarkdown` 以 `{ include_fact_tables: true, edition: "full_data", languages: ["zh", "en"], format: "markdown" }` 调 `/api/professional_report_reference`(前端超时 `ENGINE_TIMEOUT_MS = 180_000`)→ `scripts/jyotish_api_server.py` 薄注册 → `scripts/professional_report_reference.py::build_professional_report_reference`:
1. `handler._high_rigor_birth_payload(body)` → `handler._compute_full_reading_for_thematic(birth)`(`cmd_full_reading`)
2. `jyotish_engine.build_professional_report_reference_packet`(建包、`sanitize_professional_report_reference`、`bind_result_to_profile`、`full_report_quality_gate.evaluate_full_report(delivered, render_pl9_markdown(delivered))`、`shared_full_report_authority`)
3. `_english_edition`:`copy.deepcopy(packet)` → `pl9_full_data_export.render_full_data_markdown`(英文)
4. `render_full_data_markdown(packet)`(中文)
5. `report_density_packet(packet)`、`render_dasha_applicability(packet)`
- 该路径受 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY`(默认 2)限流:一份报告占一个重计算名额 15~19 秒。
## 2. 事故实证(`77057a819`,Linux 本机,虚构盘 1990-01-01 12:00 新德里 28.6139/77.2090 +5.5,`today=2026-10-13`)
### 2.1 端到端(进程内直接调 `build_professional_report_reference`,请求体同生产)
| 条件 | 3 次 | 中位数 |
| --- | --- | --- |
| 装 PyJHora(开发机) | 18.99 / 18.38 / 18.38 s | 18.4 s |
| **屏蔽 PyJHora(=生产镜像依赖)** | 17.89 / 18.59 / 16.94 s | **17.9 s** |
- 其中 `cmd_full_reading` 只占 0.8~2.1 s;三次输出 `sha256` 全同(中文 markdown `03f4d31d…`,英文 `d52c0f69…`,整份响应同)——**同输入同输出,但每次都从头算**。
- 响应(服务端 `json.dumps(ensure_ascii=False)` 口径)2.24 MB:中文 markdown 43 万字符、英文 85 万字符;进程峰值 RSS 约 129 MB。
### 2.2 分段(屏蔽 PyJHora,进程内单独计时,2 次)
| 段 | 耗时 |
| --- | --- |
| `render_full_data_markdown` **中文** | **8.03 / 7.77 s** |
| `render_full_data_markdown` 英文 | 1.02 / 0.96 s |
| `build_professional_report_reference_packet`(含 sanitize、哈希、质量门) | 2.7~3.5 s |
| `hash_bound_result`(一次请求共 6 次调用) | 合计 1.3~1.7 s;交付包单次 0.575 s |
| `sanitize_professional_report_reference`(递归 29 万次) | 1.1~1.25 s |
| `_english_edition` 里的 `copy.deepcopy(packet)` | 0.15~0.19 s |
| `evaluate_full_report` + 其中 `render_pl9_markdown` | 0.4~0.5 s(渲染本身 0.05 s) |
cProfile(屏蔽 PyJHora,有探针开销,只看比例):中文侧 `_sanitize_pl9_ai_density_markdown` 被调 2 次(结构化附录一次、正文一次),每次内部 `normalize_zh_markdown_terms` 跑 3 遍(`pl9_full_data_export.py` 中 `_sanitize_pl9_ai_density_markdown` 内三处调用),整份中文渲染共 6 遍全文术语替换;热点:
| 热点 | 次数 | 说明 |
| --- | --- | --- |
| `pl9_language_terms.py::_thicken_zh_dasha_density` | 6 遍 × 每行 2 次 `re.search` | 模式以 `(.+?)` 开头且无锚点,长表格行上逐位置回溯;每行还先拼一次 5 行 lookahead 字符串。约 5.5 万次 search,占中文渲染最大头 |
| `pl9_language_terms.py::replace_natural_language_term` | 327 次 × 每段 1 次 `sub` | 每次 `re.compile` + 按反引号切分全文 + 逐段替换,绝大多数术语在全文里根本不存在 |
| `pl9_reader_export.py::_strip_pl9_user_engineering_markers` | 5 次 | 约 190 次整篇 `re.sub` |
| `_sanitize_pl9_ai_density_markdown` 自身 | 2 次 | 78 次整篇 `re.sub` |
| `calculation_profile_contract.py::_drop_key_recursive` | 300 万次 | `excluded_paths` 里 4 条 `**.xxx` 各做一次整包递归复制,再 `canonicalize_result_payload` 第 5 次遍历 |
### 2.3 原型(只在 scratchpad 里 monkeypatch,未改仓库)
- 只加两处「字面量预检」:`_thicken_zh_dasha_density` 全文没有「主大运中的」「次子运,会在」就跳过逐行正则;`replace_natural_language_term` 在 `raw not in text` 时原样返回。**端到端 13.65 s → 10.28 / 9.95 s(−26%),中英 markdown 与整份响应逐字节一致。**
- 哈希单遍投影(一次遍历同时去掉 4 个 `**` 键并规范化键名,`ai_and_audit.summary.elapsed_seconds` 仍按原 dotted 路径处理):交付包单次 0.575 s → 0.237 s,`result_hash` 与旧算法、与包内已有 `result_hash` 三者相等。
### 2.4 复现命令
```bash
# 屏蔽 PyJHora(模拟生产镜像;子进程也生效)
mkdir -p "$SCRATCH/nojhora/jhora" && echo 'raise ImportError("masked")' > "$SCRATCH/nojhora/jhora/__init__.py"
PYTHONPATH="$SCRATCH/nojhora" python - <<'EOF'
import time, json, hashlib
from scripts.jyotish_engine import cmd_full_reading
from scripts.professional_report_reference import build_professional_report_reference
B = {"year":1990,"month":1,"day":1,"hour":12,"minute":0,"second":0,"lat":28.6139,"lon":77.2090,"tz":5.5,
"node_mode":"mean","ayanamsa":"lahiri","today":"2026-10-13","transit_date":"2026-10-13",
"target_year":2026,"age":36,"birth_time_accuracy":"confirmed"}
class H:
def _high_rigor_birth_payload(self, b): return dict(B)
def _compute_full_reading_for_thematic(self, b): return cmd_full_reading(type("A", (), dict(b))())
body = {"format":"markdown","edition":"full_data","include_fact_tables":True,"languages":["zh","en"]}
for _ in range(3):
t = time.perf_counter(); r = build_professional_report_reference(H(), body)
s = json.dumps(r, ensure_ascii=False, default=str)
print(round(time.perf_counter()-t, 2), len(s.encode()), hashlib.sha256(s.encode()).hexdigest()[:12])
EOF
```
(Windows 下 `$SCRATCH` 用仓库外任意临时目录;跑完删掉工作树里生成的 `scratch/`。)
## 3. 根因
1. **中文擦洗是「整篇文本 × 几百条规则 × 多遍」的累加**(BUG-1335):`_sanitize_pl9_ai_density_markdown`(451 行)与 `normalize_zh_markdown_terms` 历次补丁都以「再加一条全文替换」的方式追加,从未有性能预算;同一篇正文被同一套规则跑 6 遍,其中 `_thicken_zh_dasha_density` 的无锚 `(.+?)` 正则在长表格行上回溯,`replace_natural_language_term` 对不存在的术语也做编译、切分、替换。**不是回归,是从未设计。**
2. **溯源哈希重复遍历**(并入 BUG-1335):`result_payload_for_binding` 对每条 `**.key` 排除路径各做一次整包递归复制;一次请求哈希 6 次(中间态 1 次、交付包 1 次、质量门重算 1 次等)。BUG-693 / BUG-694 只规定了「覆盖什么、排除什么」,实现没考虑大小。
3. **同盘同日同版本不复用**(BUG-1336):响应对相同输入逐字节确定,但没有任何结果缓存;任务重试(BUG-576 记录过 3 次重试都打到引擎)、同日重生成、英文版重拉都要重算 15~19 s 并占重计算名额。`scripts/consult_full_data_cache.py` 已有「按键单飞 + 落盘 + 两天清理」(BUG-1304),但只缓存咨询卡用的资料包,不覆盖本接口。
关联:BUG-693、BUG-694(哈希覆盖与排除集红线)、BUG-576(长报告引擎耗时与重试)、BUG-1300 / BUG-1304(重计算名额与单飞缓存)、BUG-610(`render_pl9_markdown` 围栏失败不得打断报告)。`docs/BUG_HISTORY.md` 检索 `_sanitize_pl9_ai_density_markdown`、`normalize_zh_markdown_terms`、`render_full_data_markdown`、`hash_bound_result` 无记录。
### 3.1 对排查结论的修正(执行方按本节,不按排查报告原文)
| 排查报告原说法 | 实测修正 |
| --- | --- |
| 「术语替换合并为预编译大正则或一次扫描」 | **不做**。这些替换是有先后依赖的链(后一条吃前一条的输出,如「强est_第」「e特殊组合ly」两条就是修前一条的产物),合并成交替正则不保证逐字节一致。改为「字面量预检 + 预编译 + 惰性构造」,见 T2 |
| 「哈希改投影」 | **只许做等值投影**:哈希载荷与覆盖范围不变、`result_hash` 数值不变,只把多次遍历合成一次。任何减少覆盖的投影都违反 BUG-693,本单不做 |
| 「`_english_edition` 整包 deepcopy 1.9 s」 | 实测 0.15~0.19 s(1.9 s 是 cProfile 探针放大),**本单不改** |
| 「`hash_bound_result` 4.2 s」 | 墙钟合计 1.3~1.7 s(同上,探针放大);仍值得做,收益约 0.8~1 s |
| 「响应 1.8 MB、峰值 170 MB」 | 本机该虚构盘 2.24 MB、峰值 RSS 约 129 MB;以 A/B 实测为准 |
## 4. 决策记录
- 2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行;本单为 P1,**必须做 A/B**(耗时 + 中英两版输出逐字节一致)。
- 本单**不推翻**任何既有红线。BUG-693 / BUG-694 的覆盖范围与排除集(`ALLOWED_BINDING_EXCLUDED_TOP_KEYS`、`ALLOWED_BINDING_EXCLUDED_PATHS` 及其字面量合同测试)原样保留。
- 观察项(**本单不做,需拍板**):生产 `full_data` + `markdown` 路径里,`build_professional_report_reference_packet` 算出的 `report_quality_gate` 与 `shared_full_report_authority`(含一次 `render_pl9_markdown`)不进入响应(中文/英文 markdown、`fact_table_packet`、`reader_dasha_applicability` 都不读它们),约 0.5 s 纯开销。跳过它等于不再在这条路径上跑质量门,属于验证边界变化,必须产品拍板后另开单。
## 5. 硬红线
1. **输出逐字节一致**:对同一输入,新代码的整份响应(`json.dumps(result, ensure_ascii=False, default=str)` 字节)、中文 `markdown`、`markdown_en`、`fact_table_packet`、`reader_dasha_applicability` 必须与基线逐字节相同;`format: "json"`、`edition: "reference"` 两条非生产路径同样不得变。差一个字节即未通过,不得以「只是空白」「只是顺序」放行。
2. **`result_hash` 数值不变**:同一包新旧算法得出同一个 `result_hash`;`binding_scope` 回执内容不变;`full_report_quality_gate` 的 `provenance:result_binding` / `provenance:result_binding_scope` 结论不变。不得改 `ALLOWED_BINDING_EXCLUDED_*` 常量或其字面量测试。
3. **不删、不合并、不重排任何一条替换规则**;不改任何文案。只允许:预编译、字面量预检(只对「每个匹配都必然包含」的大小写敏感字面量;带 `re.IGNORECASE` 或字面量不必然出现的模式不得预检)、惰性构造(例如 lookahead 只在命中时拼)、把重复的纯函数调用结果复用(必须证明输入相同)。每处预检旁写一行注释说明为何等价。
4. **不减少擦洗遍数**,除非能在 PROGRESS 里给出逐条等价论证且 A/B 全语料逐字节一致——默认不做(见让步顺序)。
5. 缓存只覆盖 `edition=full_data` + `format=markdown` 路径;其他组合(含 `format=json`,其包内有 `generated_at` 墙钟)一律不缓存。缓存命中返回的响应字节必须与未命中时相同。
6. 缓存键必须包含影响输出的**全部**输入:`_high_rigor_birth_payload` 规范化后的整份 `birth`(含 `today`、`transit_date`、`candidate_range` 等全部字段)、`format`、`edition`、`packs`、`languages`、`include_fact_tables`、`REPORT_VERSION_FULL`、代码版本(`GITHUB_SHA`)。`GITHUB_SHA` 缺失或为 `unknown` 时缓存默认关闭。文件名只用哈希;缓存文件放 `scratch/local/` 下(容器内即 `api_scratch` 卷),与 `consult_full_data_cache` 同样两天清理;日志与 BUG 历史不得写入出生资料或缓存键原文。
7. `scripts/jyotish_api_server.py`:行数只按 `tests/test_api_server_growth_contract.py` 的粗护栏(AGENTS.md §6:不另加比测试更严的行数红线,也不得压行凑数),类方法数与 `JyotishAPIHandler.__new__` 伪造点不得增长(`tests/test_api_server_growth_contract.py`);本单预期对该文件**零改动**(缓存放在 `professional_report_reference.py` 内)。
8. 不改重计算名额逻辑(`scripts/api_heavy_compute_gate.py`)、不改 `.gitea/workflows/**`、不动表结构与迁移、不升级依赖、不顺手修 warning;`frontend/src/app/(app)/page.tsx` 不碰(本单无前端改动)。
9. 改既有断言按「原值 / 新值 / 原因」三栏写进 PROGRESS;测试总数不得低于开工时。
10. 新测试的输入包必须由真实引擎现算(虚构盘,沿用 `tests/test_report_english_edition.py::CASES` / `_packet`),不得手造包形状。
11. 验收必须在**屏蔽 PyJHora** 的环境下至少跑一次(生产镜像不装 PyJHora,见 `requirements-reference-engines.txt`)。
## 6. 任务分解
### T1 · 开关与差分脚手架(先做,后面每步都靠它)
- 在一个新模块 `scripts/report_perf_flags.py` 里放两个**调用时读取**环境变量的函数(不得在 import 时缓存值,否则同进程 A/B 切不了):
- `report_render_fast_enabled()` ← `JYOTISH_REPORT_RENDER_FAST`(T2、T3 的新代码路径)
- `report_reference_cache_enabled()` ← `JYOTISH_REPORT_REFERENCE_CACHE`(T4)
- 默认值按 T5 的 A/B 结论定(预期 FAST 默认开、CACHE 默认开但 `GITHUB_SHA` 未知时关);本轮**保留旧代码路径**作为开关关闭分支,旧分支的退役列入「后续」。
- 新增 `scripts/research/report_reference_perf_ab.py`:同一进程内对每个输入先 A(两个开关关)后 B(开),各重复 3 次,输出 JSON:每次总耗时与分段耗时(`cmd_full_reading`、建包、中文渲染、英文渲染、哈希合计)、`tracemalloc` 峰值、响应字节数,以及五个输出字段各自的 `sha256`;A 与 B 任一字段 sha 不同即退出码 1 并打印第一处差异的上下文(不打印整篇)。
- 输入:`tests/test_report_english_edition.py::CASES` 的 day / night / south 三张虚构盘 + 本单 §2 的 1990-01-01 新德里虚构盘;请求体同生产。另跑 `format=json`、`edition=reference`+`markdown` 各一张盘只比 sha(这两条有墙钟字段时,用同一个 `full_reading` 并把 `datetime.utcnow` 固定后比;做不到就在 PROGRESS 写明只比除 `generated_at` 外的字节,并说明原因)。
- 脚本支持 `--mask-pyjhora`:在本进程装 import 拦截,并为子进程设置 `PYTHONPATH` 垫片(同 §2.4),保证年运回放子进程也拿不到 `jhora`。
- 验收:脚本在基线代码上(两开关都只是空壳)A==B、退出码 0;Grok 自跑。
### T2 · 中文擦洗与术语替换的等值提速(BUG-1335 主体)
改 `scripts/pl9_language_terms.py`、`scripts/pl9_full_data_export.py`、`scripts/pl9_reader_export.py`(只动下列函数):
1. `_thicken_zh_dasha_density`:两个正则改模块级预编译;每行先做字面量预检(AD 句必含「主大运中的」「子运,会把」「的自然主题带入当前阶段。」;PD 句必含「次子运,会在」「背景下呈现」「的短周期结果。」)再 `search`;`lookahead` 只在预检命中后才拼。英文侧 `_thicken_en_dasha_density` 同法处理(若其模式结构相同)。
2. `replace_natural_language_term`:`raw not in text` 时直接返回原 `text`(`split` + `join` 不改字节,故等价);按 `(raw, plural)` 缓存编译结果(`functools.lru_cache` 即可)。
3. `_sanitize_pl9_ai_density_markdown`、`_strip_pl9_user_engineering_markers`、`_clean_pl9_user_table_placeholders`、`normalize_zh_markdown_terms` 内的整篇 `re.sub`:逐条检查,可证明等价的加字面量预检、改预编译;**不改顺序、不改模式、不改替换串**。按 cProfile 的累计耗时从大到小做,做到中文渲染满足 §T5 目标即可停。
4. 上述新代码只在 `report_render_fast_enabled()` 为真时生效;关闭时走原代码(可以是同函数内 `if` 分支,或保留 `_xxx_legacy` 原函数)。
- 不改:任何替换规则的内容与先后、`normalize_zh_markdown_terms` 的调用次数、英文擦洗的规则。
- 验收:
- T1 脚本四张盘 A==B(中英全字段)、退出码 0。
- 中文渲染(单独计时,屏蔽 PyJHora)中位数较 A 下降 ≥ 40%。
- `python -m pytest tests/test_report_english_edition.py tests/test_report_full_data_fix.py tests/test_report_chart_blank_columns.py tests/test_reader_appendix_language.py tests/test_report_reader_main.py tests/test_full_data_domain_reader.py tests/test_report_longform_parity.py tests/test_report_longform_gaps2.py tests/test_report_density_facts.py -q` 全绿。
- 新测试 `tests/test_report_reference_perf.py`:对 `CASES["day"]` 的包,开关开/关两次渲染中英 markdown 逐字节相等;对 `replace_natural_language_term`、`_thicken_zh_dasha_density` 各加「命中」「不命中」「反引号段内不替换」三种小样(输入为真实渲染结果中截取的行,不手造新句式)。
- Grok 自跑 + Claude 验收复跑。
### T3 · 溯源哈希单遍化(并入 BUG-1335)
改 `scripts/calculation_profile_contract.py::result_payload_for_binding`(及其私有辅助):
- 开关开时:先按顶层排除键过滤,再按非 `**` 的 dotted 路径删除,然后**一次递归**同时完成「删除所有 `**.key` 指定的键」与 `canonicalize_result_payload` 的键规范化;`hash_bound_result` 用这份载荷做与原来完全相同的 `json.dumps(..., ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)` + sha256。
- 删除 `**` 键的比较语义与原 `_drop_key_recursive` 相同(只比字符串键;非字符串键先保留、再按原规则规范化)。
- 可选(只有 T1 脚本证明 `format=json` 响应逐字节不变时才做,否则不做并在 PROGRESS 说明):`build_professional_report_reference_packet` 中第一次 `attach_calculation_profile` 对中间态的绑定会被末尾 `bind_result_to_profile(delivered, profile)` 覆盖。**注意**:跳过它会改变交付包里 `result_hash` / `result_binding` 两个键的插入顺序,`format=json` 的字节会变,所以大概率不能做。
- 不改:`ALLOWED_BINDING_EXCLUDED_TOP_KEYS`、`ALLOWED_BINDING_EXCLUDED_PATHS`、`RESULT_BINDING_ENVELOPE_KEYS`、`binding_scope` 回执、质量门的重算逻辑、BUG-693 要求的「绑定发生在返回对象上」。
- 验收:
- 新测试(放 `tests/test_calculation_profile_contract.py` 末尾或新文件):在测试文件里保留一份旧算法的逐字副本作为参照,对真实引擎现算的交付包(`CASES["day"]`)、以及在包里人为插入多层 `elapsed_seconds` / `called_at` / 非字符串键的变体,新旧 `hash_bound_result` 相等,且等于包内 `result_hash`。
- `tests/test_calculation_profile_contract.py`、`tests/test_full_report_quality_gate.py` 全绿;BUG-694 的字面量锁测试不改。
- 交付包单次 `hash_bound_result` 中位数下降 ≥ 50%(原型 0.575 → 0.237 s)。
### T4 · 同盘同日同版本复用(BUG-1336)
- `scripts/consult_full_data_cache.py`:把 `get_or_build` 的单飞与落盘泛化——加一个可选参数指定缓存子目录(默认仍是 `consult_full_data_cache`,咨询卡行为不变),或抽出同文件内的通用函数供两处调用。**不改**咨询卡的键、两天清理、预热限流(BUG-1304)。
- `scripts/professional_report_reference.py::build_professional_report_reference`:仅当 `edition == full_data` 且 `format == markdown` 且 `report_reference_cache_enabled()` 时:
1. 用 `handler._high_rigor_birth_payload(body)` 的结果 + §5 第 6 条所列字段算键(`json.dumps(sort_keys=True, ensure_ascii=False, default=str)` → sha256),在**调用 `_compute_full_reading_for_thematic` 之前**查缓存;
2. 未命中时照旧计算,把整份响应以 `json.dumps(result, ensure_ascii=False, default=str)` 落盘(目录 `scratch/local/report_reference_cache/`);
3. 命中时 `json.loads` 返回;同键并发只算一次(复用单飞);计算抛错不落盘、不缓存错误。
4. 只打一行不含资料的日志:`[report-reference-cache] hit|miss elapsed_ms=…`。
- 不改:`jyotish_api_server.py`(零行)、重计算名额(命中仍走名额,持有时间降为毫秒级)。
- 验收:
- 新测试:同一请求两次,第二次不调用 `_compute_full_reading_for_thematic`(用计数桩),两次响应 `json.dumps` 字节相同;改 `today`、`languages`、`include_fact_tables`、`candidate_range`、`GITHUB_SHA` 任一项都不命中;`format=json` 不落盘;开关关时不落盘;两个线程同键并发只构建 1 次;构建抛错时第二个调用会重新计算。
- `tests/test_consult_card_full_source.py`、`tests/test_api_heavy_compute_gate.py`、`tests/test_api_server_growth_contract.py` 全绿。
- 命中路径端到端(同进程、屏蔽 PyJHora)中位数 ≤ 1.5 s。
### T5 · A/B 验证与默认值(P1 必做)
- A = 两开关关(等同 `origin/staging` 原代码);B = 两开关开。用 T1 脚本,四张虚构盘 × 每组 3 次取中位数,`--mask-pyjhora` 必跑一轮,另跑一轮不屏蔽作参照。
- 度量:端到端(未命中 / 命中分开)、分段(中文渲染、英文渲染、建包、哈希合计)、`tracemalloc` 峰值、响应字节数。
- 正确性:五个输出字段 + 整份响应逐字节一致(任一不一致即 B 不得默认开启)。
- 判定门槛:
- `JYOTISH_REPORT_RENDER_FAST` 默认开 ⇔ 逐字节全一致 且 未命中端到端中位数降 ≥ 35%(参照原型:仅两处预检已 −26%)且 峰值内存不升 > 10%。
- `JYOTISH_REPORT_REFERENCE_CACHE` 默认开 ⇔ 命中与未命中字节一致 且 命中中位数 ≤ 1.5 s;`GITHUB_SHA` 未知时一律关。
- 不达标:开关默认关,代码保留,回报写「未达门槛」与差距,不得写通过。
- 记录写 `docs/testing/report-reference-perf-ab-20261013.md`:机器(Windows / Linux)、Python 版本、是否屏蔽 PyJHora、每组 3 次原始数、中位数、sha 表。本单 A/B **不需要模型 key**。
- Grok 在 Windows 跑一遍并提交记录;Claude 在 Linux 复跑,两边都要逐字节一致,耗时只比各自机器上的 A/B 相对值。
### T6 · 记录
- `docs/BUG_HISTORY.md`:
- **BUG-1335** 长报告中文渲染慢:同一篇正文跑 6 遍全文术语替换、无锚正则逐行回溯、不存在的术语也编译替换;溯源哈希每条排除路径一次整包复制。
- **BUG-1336** 同盘同日同版本的长报告请求不复用,重试与重生成每次重算并占重计算名额。
- 两条都写关联 BUG-693、BUG-694、BUG-576、BUG-1304;防复发写「T1 A/B 脚本 + 新测试」。
- `docs/tasks/PROGRESS-report-reference-perf-20261013.md`:每步改动、A/B 摘要、改既有断言三栏表、未做项与原因。
- `CHANGELOG.md`:一句话「个人长报告引擎出稿提速(同盘同日重试秒回)」,不 bump Skill 版本。
- `frontend/DESIGN.md`:不改(无界面变化)。
## 7. 让步顺序(时间不够从后往前砍)
1. 先保:T1 → T2 的第 1、2 项(原型已证实,最稳的 −26%)→ T5(A/B 记录)。
2. 再做:T3 哈希单遍。
3. 再做:T4 缓存。
4. 最先砍:T2 第 3 项(其余整篇 `re.sub` 的逐条预检)、T3 的可选项。
5. 任何一步 A/B 出现字节差异且一轮内查不清:回退该步,保留开关默认关,写进 PROGRESS 与回报,不得为了过 A/B 改测试或改输出。
## 8. 验收命令分工
| 谁 | 命令 |
| --- | --- |
| Grok 自跑(工作树 `.venv`) | `python -m pytest tests/test_report_reference_perf.py tests/test_calculation_profile_contract.py tests/test_full_report_quality_gate.py tests/test_report_english_edition.py tests/test_professional_report_reference_api.py tests/test_report_full_data_fix.py tests/test_report_chart_blank_columns.py tests/test_reader_appendix_language.py tests/test_report_reader_main.py tests/test_full_data_domain_reader.py tests/test_report_longform_parity.py tests/test_report_longform_gaps2.py tests/test_report_density_facts.py tests/test_consult_card_full_source.py tests/test_api_heavy_compute_gate.py tests/test_api_server_growth_contract.py tests/test_repo_privacy_markers.py -q`;`python scripts/research/report_reference_perf_ab.py --mask-pyjhora`(退出码 0)。Python 快速门、前端全量**不跑** |
| Claude 验收 | 上述全部 + `--profile quick` 快速门 + 全量 `pytest` 与基线逐名比对(本单不应改任何引擎数值,多出一条失败即未通过)+ 在 Linux 屏蔽 PyJHora 复跑 A/B + 用 §2.4 命令对比基线 sha |
| 不需要 | 前端 `tsc` / `lint` / `npm test`(无前端改动;若意外改了前端则必须跑);`test:db`(不动数据库) |
## 9. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/report-reference-perf-20261013 .worktrees/report-reference-perf-20261013 origin/staging
cd .worktrees/report-reference-perf-20261013
git log -1 --format=%h # 应为 77057a819 或其后代
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号(写作时 BUG-1317)
wc -l scripts/jyotish_api_server.py # 记下基线行数,交付时必须 ≤ 此数
# 开工前先 grep 按源码文本断言的测试(AGENTS.md §7.8):
grep -rn "normalize_zh_markdown_terms\|_thicken_zh_dasha_density\|replace_natural_language_term\|_drop_key_recursive\|result_payload_for_binding\|get_or_build" tests
# 记录基线 sha(四张盘),作为 A 的外部对照:
python scripts/research/report_reference_perf_ab.py --mask-pyjhora --only-a # T1 完成后
```
## 10. 交付物清单
- 代码:`scripts/report_perf_flags.py`(新)、`scripts/research/report_reference_perf_ab.py`(新)、`scripts/pl9_language_terms.py`、`scripts/pl9_full_data_export.py`、`scripts/pl9_reader_export.py`(仅擦洗函数)、`scripts/calculation_profile_contract.py`、`scripts/consult_full_data_cache.py`、`scripts/professional_report_reference.py`;`scripts/jyotish_api_server.py` 零改动。
- 测试:`tests/test_report_reference_perf.py`(新)及 T3 / T4 所述用例。
- 记录:`docs/tasks/PROGRESS-report-reference-perf-20261013.md`、`docs/testing/report-reference-perf-ab-20261013.md`、`docs/BUG_HISTORY.md`(BUG-1335、BUG-1336)、`CHANGELOG.md`。
- 回报:分支 head SHA、A/B 中位数表、五字段 sha 对照、两个开关的默认值与依据、未做项。
## 11. 后续(本单不做)
- 下一轮退役开关关闭分支(旧擦洗实现、旧哈希实现),只保留新路径。
- 观察项(需拍板):`full_data` 路径跳过无人读取的质量门与 authority(约 0.5 s)。
- 擦洗函数整体退役由 `TASK-pl9-render-split-20261013` 的后续阶段处理。