- 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
246 lines
28 KiB
Markdown
246 lines
28 KiB
Markdown
# 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` 的后续阶段处理。
|