fix(chart): skip unused synastry overview and doomed report subprocesses

Synastry charts now skip the unused external overview and run together. Reports skip PyJHora subprocesses when that library is missing, and a full heavy-compute slot logs which paths hold it. Splitting quotas stays a simulation.

Gate, growth, and report pytest passed, as did the frontend route tests, tsc, lint, and the A/B script.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-15 08:02:39 +08:00
co-authored by Grok (agent loop) Cursor
parent 9274491681
commit 336286e428
19 changed files with 1126 additions and 37 deletions
+60
View File
@@ -17990,6 +17990,36 @@
- 复发自:无。
- 修复版本:分支 `codex/api-disk-cache-retention-20261013`,尚未合入 staging。
## BUG-1333 | 合盘排盘漏关外部概览,而且两张盘排队算
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-15
- 来源:issue #64。任务书 `docs/tasks/TASK-api-chart-subprocess-gate-20261013.md` T1、T3、T4。
- 现象:合盘每次要排两张盘,都没关掉用不上的外部概览。每张盘多起两个后台进程,而且两张盘一张一张排。名额占满时,日志里看不出是谁占着。
- 触发条件:打开合盘。排盘接口本身默认仍走完整链,给报告和外部调用留着。
- 根因:合盘这条路由没跟上「只读盘面就关掉外部概览」的约定,也没有合同测试拦住。名额按路径一刀切,不带 skip 的排盘默认不占名额。
- 修复:合盘两次排盘都带上关闭外部概览,两张盘同时排;婚恋分支的两张分盘也同时排。前端里凡是发给排盘接口的请求体,都要带这个开关,由源码合同守着。另加一个默认关闭的开关:打开后,不带 skip 的同步排盘才占一个重计算名额。名额占满时记一行日志,只写被拒的路径和当前占用者的路径、已占秒数,不写请求正文。
- 验证:`frontend/tests/synastry-chart-skip.test.ts`、`frontend/tests/chart-request-skip-contract.test.ts`、`tests/test_api_chart_subprocess_gate.py`、`tests/test_api_heavy_compute_gate.py`。合盘耗时见 `docs/testing/api-chart-subprocess-gate-ab-20261013.md`。
- 防复发:源码合同要求 `frontend/src/app/api` 和 `frontend/src/lib` 里发往 `/api/chart` 的请求体带 `skip_vedastro_main_entry_overview: true`。`/api/chart` 仍不在重计算路径名单里。
- 相关记录:BUG-161、BUG-715、BUG-717、BUG-1302。
- 复发自:无。
- 修复版本:分支 `codex/api-chart-subprocess-gate-20261013`。编号是任务书预留的空号,夹在 BUG-1332 和 BUG-1335 之间。
## BUG-1334 | 个人报告在没有参照引擎时仍为每次报告启动子进程
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-15
- 来源:issue #64。任务书 T2。
- 现象:每份个人报告会起大约 10 个后台进程。其中参照引擎没装时,年度回放注定失败,但仍要付启动开销。另一段静态校验和用户的盘无关,每份报告都重跑。
- 触发条件:生成个人长报告,且这台机器没有安装 PyJHora。装了参照引擎的开发机上,隔离用的子进程仍然要留着。
- 根因:子进程是为了把参照引擎隔离开,这是对的;父进程却没有先问「这台机器根本没装」。静态校验文件被挂在每份报告上,没有按文件变化复用。
- 修复:父进程先看一次能不能导入参照引擎,不能就不起子进程,返回和「子进程在没装时」相同的结果。解释器查找和静态校验各在进程内记一次;校验文件变了会重算。不缓存任何出生资料。装了参照引擎时仍走子进程。
- 验证:`tests/test_api_chart_subprocess_gate.py` 对照 `tests/fixtures/api-chart-subprocess-gate/`。生产上的收益大约是每份报告 0.5 到 0.9 秒、少掉那几个注定失败的进程,不是排查时在装了参照引擎的机器上看到的 5 秒。本机没有 PyJHora,实测见 `docs/testing/api-chart-subprocess-gate-ab-20261013.md`。
- 防复发:无参照引擎时测试断言不会调用 `subprocess.run`,返回值与固定样本一致。
- 相关记录:BUG-721、BUG-1265、BUG-1333。
- 复发自:无。
- 修复版本:分支 `codex/api-chart-subprocess-gate-20261013`。
## BUG-1335 | 个人长报告的中文出稿和溯源哈希重复劳动
- 状态:investigating
@@ -36042,6 +36072,36 @@
- 复发自:无。
- 修复版本:分支 `codex/api-disk-cache-retention-20261013`,尚未合入 staging。
## BUG-1333 | 合盘排盘漏关外部概览,而且两张盘排队算
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-15
- 来源:issue #64。任务书 `docs/tasks/TASK-api-chart-subprocess-gate-20261013.md` T1、T3、T4。
- 现象:合盘每次要排两张盘,都没关掉用不上的外部概览。每张盘多起两个后台进程,而且两张盘一张一张排。名额占满时,日志里看不出是谁占着。
- 触发条件:打开合盘。排盘接口本身默认仍走完整链,给报告和外部调用留着。
- 根因:合盘这条路由没跟上「只读盘面就关掉外部概览」的约定,也没有合同测试拦住。名额按路径一刀切,不带 skip 的排盘默认不占名额。
- 修复:合盘两次排盘都带上关闭外部概览,两张盘同时排;婚恋分支的两张分盘也同时排。前端里凡是发给排盘接口的请求体,都要带这个开关,由源码合同守着。另加一个默认关闭的开关:打开后,不带 skip 的同步排盘才占一个重计算名额。名额占满时记一行日志,只写被拒的路径和当前占用者的路径、已占秒数,不写请求正文。
- 验证:`frontend/tests/synastry-chart-skip.test.ts`、`frontend/tests/chart-request-skip-contract.test.ts`、`tests/test_api_chart_subprocess_gate.py`、`tests/test_api_heavy_compute_gate.py`。合盘耗时见 `docs/testing/api-chart-subprocess-gate-ab-20261013.md`。
- 防复发:源码合同要求 `frontend/src/app/api` 和 `frontend/src/lib` 里发往 `/api/chart` 的请求体带 `skip_vedastro_main_entry_overview: true`。`/api/chart` 仍不在重计算路径名单里。
- 相关记录:BUG-161、BUG-715、BUG-717、BUG-1302。
- 复发自:无。
- 修复版本:分支 `codex/api-chart-subprocess-gate-20261013`。编号是任务书预留的空号,夹在 BUG-1332 和 BUG-1335 之间。
## BUG-1334 | 个人报告在没有参照引擎时仍为每次报告启动子进程
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-13 / 2026-10-15
- 来源:issue #64。任务书 T2。
- 现象:每份个人报告会起大约 10 个后台进程。其中参照引擎没装时,年度回放注定失败,但仍要付启动开销。另一段静态校验和用户的盘无关,每份报告都重跑。
- 触发条件:生成个人长报告,且这台机器没有安装 PyJHora。装了参照引擎的开发机上,隔离用的子进程仍然要留着。
- 根因:子进程是为了把参照引擎隔离开,这是对的;父进程却没有先问「这台机器根本没装」。静态校验文件被挂在每份报告上,没有按文件变化复用。
- 修复:父进程先看一次能不能导入参照引擎,不能就不起子进程,返回和「子进程在没装时」相同的结果。解释器查找和静态校验各在进程内记一次;校验文件变了会重算。不缓存任何出生资料。装了参照引擎时仍走子进程。
- 验证:`tests/test_api_chart_subprocess_gate.py` 对照 `tests/fixtures/api-chart-subprocess-gate/`。生产上的收益大约是每份报告 0.5 到 0.9 秒、少掉那几个注定失败的进程,不是排查时在装了参照引擎的机器上看到的 5 秒。本机没有 PyJHora,实测见 `docs/testing/api-chart-subprocess-gate-ab-20261013.md`。
- 防复发:无参照引擎时测试断言不会调用 `subprocess.run`,返回值与固定样本一致。
- 相关记录:BUG-721、BUG-1265、BUG-1333。
- 复发自:无。
- 修复版本:分支 `codex/api-chart-subprocess-gate-20261013`。
## BUG-1335 | 个人长报告的中文出稿和溯源哈希重复劳动
- 状态:investigating
@@ -0,0 +1,74 @@
# PROGRESS · 合盘排盘与报告多余子进程(2026-10-15)
- 分支:`codex/api-chart-subprocess-gate-20261013`
- 实际基线:`origin/staging` `9274491681f6fcacbc87e3797441d2c5221de874`(任务书写的 `77057a819` 更早)
- `scripts/jyotish_api_server.py`:开工 10944 行,做完 10940 行
- BUG-1333、BUG-1334。`docs/BUG_HISTORY.md` 后半是同一份历史的副本,两处都补上了。
## 做了什么
1. 合盘两次排盘都带 `skip_vedastro_main_entry_overview: true`,不改 `synastryBirthPayload`。两张盘和婚恋分支的两张九分盘都改成同时发出。任一失败仍走原来的 `catch`。
2. 前端 `app/api` 和 `lib` 里发往 `/api/chart` 的请求体必须带这个开关。白名单是空的。
3. 父进程先看一次能不能导入 PyJHora。不能就不起年度回放和 Tajika 的子进程,返回和「子进程在没装时」相同的结果。样本在 `tests/fixtures/api-chart-subprocess-gate/`,来自本机真实子进程(当时 `find_spec("jhora")` 为 `None`)。装了之后仍走子进程。
4. 解释器查找(含「没找到」)和静态 oracle 校验在进程内各记一次。校验按文件路径、mtime、大小作键。不缓存出生资料。
5. 开关 `JYOTISH_GATE_CHART_OVERVIEW` 默认关。只有把它设为 `1` 时,不带 skip、也不是异步排队的 `/api/chart` 才占一个重计算名额。`/api/chart` 没有加进 `HEAVY_COMPUTE_PATHS`。预热咨询不占名额的判断挪进了闸门模块。
6. 名额占满时打一行日志:被拒路径、当前占用者路径、已占秒数。不写请求正文。
## 自测
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 闸门、增长护栏、本单新测试 | `.venv\Scripts\python.exe -m pytest tests/test_api_chart_subprocess_gate.py tests/test_api_heavy_compute_gate.py tests/test_api_server_growth_contract.py -q` | 21 passed,退出码 0 |
| 报告三份既有测试 | 同上解释器 `-m pytest tests/test_report_reader_main.py tests/test_report_longform_parity.py tests/test_report_density_facts.py -q` | 27 passed,退出码 0 |
| 前端本单与相关单文件 | 在 `frontend` 下 `node node_modules/tsx/dist/cli.mjs --test tests/synastry-chart-skip.test.ts tests/chart-request-skip-contract.test.ts tests/chart-view-route.test.ts tests/ephemeris-route.test.ts` | 22 pass,0 fail |
| 类型检查 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
| lint | 在 `frontend` 下 `npm run lint` | 退出码 0,0 error,123 条原有 warning |
| A/B | `.venv\Scripts\python.exe scripts\research\api_chart_subprocess_ab.py` | 退出码 0。数字见下 |
`grep` 这三个名字在 `tests/` 里只命中本单新测试:`annual_pyjhora_replay`、`_oracle_progress_snapshot`、`_resolve_external_pyjhora_python`。
## A/B 数字
合盘:串行不带 skip 中位 8.0596 秒、2 次 `subprocess.run`;并发带 skip 中位 0.0166 秒、0 次。盘面摘要逐字节一致。耗时约为对照的 0.2%。
报告:强制仍起子进程中位 16.5322 秒、7 次;短路后 15.3235 秒、1 次。去掉易变字段后逐字节一致。少掉的 6 次是年度回放和 Tajika。
名额模拟(种子 64,一分钟,未改规则):
| 方案 | 咨询被拒 | 报告被拒 | 校正被拒 | 最高并发 | 碰到不得加池的那条 |
| --- | --- | --- | --- | --- | --- |
| O1 现状 | 40% | 0% | 17% | 2 | 否 |
| O2 咨询单独小池 | 0% | 0% | 17% | 3 | 是 |
| O3 咨询最多再等 1.5 秒 | 30% | 0% | 33% | 2 | 否 |
| O4 重任务最多占 1 个 | 0% | 0% | 67% | 2 | 否 |
全文在 `docs/testing/api-chart-subprocess-gate-ab-20261013.md`。
## 改既有断言
无。原值 / 新值 / 原因:没有改旧断言。
## 偏离
- `/api/health` 没有加 `heavy_compute` 字段。状态函数 `heavy_compute_status()` 在闸门模块里,接到健康检查会让主文件变长。任务书允许砍掉这一项。
- 星盘名额开关默认关。打开会让不带 skip 的外部调用可能拿到 429,任务书要求先有 A/B 再提议,本单不打开。
- O2、O3、O4 没有实现。
- 报告对照不是 staging 源码另导入,而是本分支把「已安装」强制打开。静态校验缓存两边都在,所以耗时差只反映那 6 个子进程。
- 合盘子进程是 2 → 0,不是任务书举例的 4 → 0。这台机器两张盘合计只数到 2 次 `subprocess.run`。
- 没有「装了 PyJHora」的计时。父进程强制当成已安装时,子进程仍会因为没装而失败,这一组用来代表「不短路就要付启动开销」。
## 环境缺口
- 本机 `.venv`:`sys.executable` 是工作树的 `python.exe`;`shutil.which("python3")` 是 Windows 应用商店占位;`python3.14` 没有;`find_spec("jhora")` 是 `None`。
- Docker 在,没有本地 `jyotisha-api` 镜像,镜像里的解释器路径没有核实。
- 生产装了 VedAstro、并且开了外网时,不带 skip 的排盘真实耗时本机测不到。
- 名额被占满后连带拒绝的真实频率,要等日志上了 staging 再看。
## 需拍板
轻重任务要不要分开限流,本单不改。模拟里,让咨询少被拒,校正就会多被拒;给咨询单独开池会让同时在跑的个数超过 2。要改规则需要另立一单。
## 默认值
- `JYOTISH_GATE_CHART_OVERVIEW` 默认关。
- 合盘 skip 和报告短路没有开关。
+1 -1
View File
@@ -482,7 +482,7 @@
| `TASK-consult-prep-parallel-fix-20261014.md` | `PROGRESS-consult-prep-parallel-20261013.md` | **#62 第 1 次打回**:两条源码文本检查改认新位置,对照脚本挪进测试目录 | 待验收 | `codex/consult-prep-parallel-20261013` |
| `TASK-frontend-dead-code-20261013.md` | `PROGRESS-frontend-dead-code-20261013.md` | 前端死代码 17 文件与无调用方接口删除,含未登录重计算接口(BUG-1342) | 待验收 | `codex/frontend-dead-code-20261013` |
| `TASK-frontend-dead-code-fix-20261014.md` | `PROGRESS-frontend-dead-code-20261013.md` | **#63 第 1 次打回**:类名白名单去掉已删组件留下的条目 | 待验收 | `codex/frontend-dead-code-20261013` |
| `TASK-api-chart-subprocess-gate-20261013.md` | — | 合盘不带 skip 串行排盘、报告无效子进程、名额轻重混用(只模拟)(BUG-1333~1334,A/B) | 待领取(排队:api-disk-cache-retention + report-reference-perf) | `codex/api-chart-subprocess-gate-20261013` |
| `TASK-api-chart-subprocess-gate-20261013.md` | `PROGRESS-api-chart-subprocess-gate-20261013.md` | 合盘不带 skip 串行排盘、报告无效子进程、名额轻重混用(只模拟)(BUG-1333~1334,A/B) | 待验收 | `codex/api-chart-subprocess-gate-20261013` |
| `TASK-answer-effort-ab-20261013.md` | — | 答题步推理强度开关 + A/B(需模型 key) | 待领取(排队:consult-prep-parallel) | `codex/answer-effort-ab-20261013` |
| `TASK-consult-route-split-20261013.md` | — | consult POST 拆分第一阶段:开流前加载 + 计费结算 | 待领取(排队:consult-prep-parallel、answer-effort-ab) | `codex/consult-route-split-20261013` |
| `TASK-pl9-render-split-20261013.md` | — | render_pl9_markdown 第一阶段:搬出无依赖内部函数 | 待领取(排队:api-chart-subprocess-gate) | `codex/pl9-render-split-20261013` |
@@ -0,0 +1,53 @@
{
"charts": {
"serial_seconds_median": 8.0596,
"concurrent_skip_seconds_median": 0.0166,
"serial_subprocesses_median": 2,
"concurrent_skip_subprocesses_median": 0,
"summary_bytes_match": true
},
"report_forced_subprocess": {
"seconds_median": 16.5322,
"subprocesses_median": 7
},
"report_short_circuit": {
"seconds_median": 15.3235,
"subprocesses_median": 1
},
"report_bytes_match": true,
"schemes": [
{
"scheme": "O1",
"consult_429": "0.40",
"report_429": "0.00",
"score_429": "0.17",
"peak": 2,
"touches_bug_717": false
},
{
"scheme": "O2",
"consult_429": "0.00",
"report_429": "0.00",
"score_429": "0.17",
"peak": 3,
"touches_bug_717": true
},
{
"scheme": "O3",
"consult_429": "0.30",
"report_429": "0.00",
"score_429": "0.33",
"peak": 2,
"touches_bug_717": false
},
{
"scheme": "O4",
"consult_429": "0.00",
"report_429": "0.00",
"score_429": "0.67",
"peak": 2,
"touches_bug_717": false
}
],
"jhora_installed": false
}
@@ -0,0 +1,40 @@
# 合盘排盘与报告子进程 A/B(2026-10-15)
Windows 本机,工作树 `.venv` 的 `python.exe`。`importlib.util.find_spec("jhora")` 是 `None`。`python3` 指向 Windows 应用商店占位程序,`python3.14` 不存在。Docker 守护进程在,本机没有 `jyotisha-api` 镜像,所以没有进镜像再探一次。本机没有 VedAstro SDK,外网关闭(`VEDASTRO_ENABLE_NETWORK=0`)。没有「真 SDK」一组。
脚本:`scripts/research/api_chart_subprocess_ab.py`。每组 3 次,取中位数。盘是任务书里的虚构盘(1990-01-01 12:00 与 1992-01-02 12:00,新德里)。星盘缓存 TTL 设为 0。易变字段在比对前丢掉:`generated_at`、`created_at`、`timestamp`、`elapsed_seconds`、`elapsed_ms`、`duration_ms`、`request_id`。原始数字在同目录 `api-chart-subprocess-gate-ab-20261013.json`。
对照不是另一次 `git show` 出 staging 再导入。星盘对照是本分支的 `_compute_chart_sync`:不带 skip、两张盘串行。报告对照是本分支把「参照引擎已安装」强制打开,从而仍会起子进程;静态校验缓存和解释器查找缓存两边都开着。所以报告耗时差只含那 6 个注定失败的回放进程,不含静态校验少跑的那一次。静态校验「连调两次只跑一次」由单测覆盖。
## 合盘两张盘
| | 中位耗时 | 中位 `subprocess.run` 次数 | 盘面摘要 |
| --- | --- | --- | --- |
| 对照:不带 skip,串行 | 8.0596 秒 | 2 | — |
| 本单:带 skip,并发 | 0.0166 秒 | 0 | 与对照逐字节一致 |
0.0166 / 8.0596 ≈ 0.2%,低于「不超过对照 10%」的线。子进程从 2 降到 0。任务书写的「4 → 0」是按每张盘 2 个子进程、两张合计 4 个来估的。这台机器上两张盘合计只数到 2 次 `subprocess.run`。不带 skip 的 8 秒里,大部分是没装 SDK 时子进程空等,不是盘面计算本身。
## 个人长报告(完整数据版,中英,带事实表)
| | 中位耗时 | 中位 `subprocess.run` 次数 | 中英正文与事实表 |
| --- | --- | --- | --- |
| 对照:强制仍起回放子进程 | 16.5322 秒 | 7 | — |
| 本单:没装参照引擎就不起 | 15.3235 秒 | 1 | 去掉易变字段后逐字节一致 |
少 6 次,对应年度回放 3 次和 Tajika 3 次。剩下的 1 次两边都有,短路没有把它去掉。子进程数 1,低于「模拟生产不超过 3」的线。耗时 15.32 秒,不比对照的 16.53 秒差。本机没装 PyJHora,没有再跑「装了参照引擎」的一组;那种环境仍必须走子进程,单测用桩覆盖。
生产上的收益仍按任务书:大约每份报告 0.5 到 0.9 秒,不是装了参照引擎的机器上看到的约 5 秒。
## 名额方案模拟(没改规则)
固定一分钟:2 份报告(每次 13 秒)、20 次咨询(每次 0.2 秒)、6 次校正打分(每次 8 秒)。随机种子 64。并发上限按现状 2。O2、O3、O4 只在脚本里模拟。
| 方案 | 咨询被拒 | 报告被拒 | 校正被拒 | 同时在跑的最高个数 | 是否碰到「不得加池、不得提高并发」 |
| --- | --- | --- | --- | --- | --- |
| O1 现状 | 40% | 0% | 17% | 2 | 否 |
| O2 咨询单独小池 | 0% | 0% | 17% | 3 | 是,本单不实现 |
| O3 咨询最多再等 1.5 秒 | 30% | 0% | 33% | 2 | 否,本单不实现 |
| O4 重任务最多占 1 个名额 | 0% | 0% | 67% | 2 | 否,本单不实现 |
这是一分钟的草图,不是 staging 上的真实频率。咨询少被拒的时候,校正会多被拒(O3、O4)。O2 咨询不再被拒,但同时在跑的个数到了 3,越过了现在的上限。