Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
134 lines
14 KiB
Markdown
134 lines
14 KiB
Markdown
# PROGRESS · 上游同步 3:返照时区本地化 / 年运面板核实 / PL9 紧凑写出(2026-09-29)
|
||
|
||
任务书:`TASK-upstream-sync3-20260929.md`。分支 `codex/upstream-sync3-20260929`,基线 `origin/staging` = `15be6a06`。上游本机检出 `/workspace/yinduzhanxing` @ `db8cdf4a`。执行方:Claude fork 子代理;只 commit 不 push。
|
||
|
||
## 一句话结论
|
||
|
||
- **T1 做了,且比任务书写的更严重**:本仓返照的 `dt_local` 从来就是 UT 原样(`find_solar_return_ut` 注释「调用方另行加 tz」,但没有任何调用方加过)。北京盘读者版报告里的「Varshapravesha:2026-06-15 08:04:52」实际是 UT,应为 16:04:52;2028 年那行连日期都错(06-14 → 06-15)。所以**硬红线 1「北京盘逐字节相同」无法满足,也不应满足**——差异恰好只有被修正的三处(见 §T1 等价比对表),已把改前改后每个差异路径列出,请验收方按语义判断。
|
||
- **T2 不适用**:12 个键里 10 个在本仓从来没有生产者(`annual_tajika_pack.py` 不产出面板类键,只有 `pl9_reader_export.py` 防御性读取),不是装配时丢失。BUG-1096 记 `closed_not_applicable`。
|
||
- **T3 做了,但没抄上游的实现**:实测 `json.dump` 流式在 CPython 走纯 Python 编码器,比 `dumps` 慢 45%;改为紧凑 `dumps` 一次写出,保留上游 `stream=` 签名。文件 9.57 MB → 4.61 MB、序列化峰值 46.9 → 22.1 MB、0.41 → 0.30 s。
|
||
|
||
## 上游对照与取舍
|
||
|
||
| 上游 SHA | 内容 | 本仓落点 | 取舍 |
|
||
| --- | --- | --- | --- |
|
||
| `41d1c740` | `_localize_return_datetime`(ZoneInfo 按返照瞬间取偏移)、`annual_location`、返照地点 `current_location` 模式与 CLI 参数、外部 PyJHora 回放在异地时 blocked | `scripts/solar_return.py`(helper、`calc_solar_return_chart` / `solar_return_full_report` 四个新 kwargs、`dt_local` 真正本地化、Sahams 用 `annual_location`);`scripts/cmd_solar_return.py`(新增 `solar_return_location_kwargs`,同时接受年运包命名空间 `return_*` 与 CLI 命名空间 `--solar-return-location-mode` / `--current-*` / `--timezone-id` / API `timezone`);`scripts/annual_tajika_pack.py`(`_args_from_payload` 读 `settings.current_location`,出生 `timezone`/`timezone_id` 是 IANA 时传 `return_timezone_id`;新增 `_iana_timezone_id`);`scripts/jyotish_engine.py`(`_annual_payload_from_args` 写 `current_location` 与 `birth.timezone_id`;`_build_pl9_full_annual_section` 异地时回放 blocked;full-reading Step 4.12 透传;`_add_solar_return_location_args` 挂到 `solar-return` / `full-reading` / `pl9-export`) | 全抄,另加 `--timezone-id`(出生地 IANA 区)——上游只在 `current_location` 模式带 IANA 区,出生地模式仍是固定偏移;本仓把出生地 IANA 区也接上,否则 T1 对默认模式没有意义。`tajika_monthly.py` / `_local_eight_year_overview` / `_annual_special_points` 本仓不存在,对应块不抄 |
|
||
| `089858c0` | `_preserve_annual_report_surface`:全量报告装配时把 12 个年运面板键从快照补回 | 无 | **不适用**(§T2 核实表)。同批 `full_report_quality_gate.py` +86 行(`80756803`)内容是逐年 Muntha / Year Lord 一致性与 Ashtakavarga 12 宫映射校验,与面板无关,任务书假设的「面板覆盖检查」不存在,未接 |
|
||
| `59c9139f` | `output_json(data, *, stream=None)` 流式写出,PL9 紧凑 | `scripts/jyotish_engine.py::output_json` 与 `main()` 的 pl9 `--output` 落盘 | 签名照抄,实现改为紧凑 `dumps`(§T3 数据) |
|
||
|
||
## T1 · 返照时刻本地化(BUG-1095)
|
||
|
||
### 事实
|
||
|
||
- 改前 `find_solar_return_ut` 返回 `'dt_local': dt_ut`(`scripts/solar_return.py:221`),`calc_solar_return_chart` 原样透传;`solar_return_full_report` 把它当本地时间交给 `calc_all_sahams(..., tz=birth_tz)` 做昼夜判定(`saham_daynight.determine_daytime` 再减一次 tz → 判定时刻比真实返照早 8 小时)。
|
||
- 读者版 `pl9_reader_export.py:1830/2201` 直接打印 `dt_local`。北京虚构盘(1990-06-15 10:30,39.9042/116.4074,+8,目标 2026)reader_main 渲染:
|
||
|
||
| 行 | 改前 | 改后 |
|
||
| --- | --- | --- |
|
||
| `- Varshapravesha:` | 2026-06-15 08:04:52 | 2026-06-15 16:04:52 |
|
||
| 2027 表行 | 2027-06-15 14:07:50 | 2027-06-15 22:07:50 |
|
||
| 2028 表行 | 2028-06-14 20:25:46 | 2028-06-15 04:25:46 |
|
||
|
||
- 昼夜判定时刻:`sahams.daynight_evidence.julian_day_ut` 2461206.5034(=06-15 00:04 UT)→ 2461206.8367(=真实返照 08:04:52 UT)。这张盘两者恰好都是白天,判定没翻;其他盘会翻。
|
||
|
||
### 等价比对(硬红线 1)
|
||
|
||
方法:改前用基线树(`git worktree add --detach <scratch> 15be6a06`,用完已删)、改后用本分支,同一命令、`PYTHONHASHSEED=0`,JSON 解析后递归比对,剔除 `generated_at` / `elapsed_seconds` / `*_hash` 等易变键。
|
||
|
||
| 命令 | 差异路径数 | 差异内容 |
|
||
| --- | --- | --- |
|
||
| `solar-return` 北京盘 | 6 | `solar_return.dt_local` 08:04:52 → 16:04:52;`solar_return.annual_location` / 顶层 `annual_location` 新增;`sahams.daynight_evidence.julian_day_ut` 改为真实返照时刻;`sunrise_jd_ut` / `sunset_jd_ut` 第 9 位小数浮动(rise_trans 起点变了) |
|
||
| `tajika --age 36` 北京盘 | **0** | — |
|
||
| `pl9-export --pack full` 北京盘 | 83 | 上面三项在 `full_report_pack.sections.annual.*`、`personal_report_producer.source_pack_contracts.annual_tajika_pack.*`、`raw_module_index[35].field_names` 的传播;其余全是易变项(VedAstro 快照 `called_at`、`slowest_stages` 顺序、authority / quality-gate 哈希 id——两次改后运行之间同样有 30 处这类差异) |
|
||
| `solar-return` 北京盘 固定 +8 vs `--timezone-id Asia/Shanghai` | **0** | 无夏令时地区 IANA 区与固定偏移等价 |
|
||
|
||
**结论:北京盘不是逐字节相同,差异恰为修正项本身;这一条需要验收方确认接受。**
|
||
|
||
### 夏令时与异地验证(虚构纽约盘 1985-07-04 08:00,40.7128/-74.006,`--tz -5`,目标 2026)
|
||
|
||
| 模式 | `dt_ut` | `dt_local` | `annual_location.utc_offset` | 返照盘上升 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 固定偏移(改后默认) | 2026-07-05 01:08:03 | 2026-07-04 20:08:03 | −5.0 | Capricorn |
|
||
| `--timezone-id America/New_York` | 同 | 2026-07-04 **21:08:03** | **−4.0**(EDT) | Capricorn(盘按 UT 起,不变) |
|
||
| `--solar-return-location-mode current_location --current-lat 51.5074 --current-lon -0.1278 --current-tz 0 --current-timezone-id Europe/London` | 同 | 2026-07-05 02:08:03 | +1.0(BST) | **Taurus** |
|
||
|
||
### 时区 ID 能否传到年运路径(任务书要求核查)
|
||
|
||
- 引擎侧已通:CLI `--timezone-id`;API `birth.timezone` / `timezone_name`(`calculation_profile_contract._normalize_timezone` 认这两个键)→ profile `timezone.name` → `_annual_payload_from_args` 写 `birth.timezone_id` → `annual_tajika_pack._args_from_payload` → `return_timezone_id`;full-reading Step 4.12 走 `solar_return_location_kwargs(args)` 读 `timezone_id` / `timezone` / `timezone_name`。
|
||
- **前端缺口(未修,前端不在本单范围)**:`frontend/src/lib/personal-report-longform-birth.ts:79` 只传数字 `tz: profile.timezoneOffset`;账户资料其实存了 IANA 区(`account-profile-patch.ts:55` `timezone_id`,全球出生地必填)。网页报告因此仍走固定偏移:中国用户无影响;夏令时地区用户只有当资料里的偏移恰好等于返照瞬间的偏移时才对。补一行 `timezone: profile.timezone_id` 即可接上,建议另开前端小单。
|
||
|
||
## T2 · 年运面板核实表(BUG-1096,closed_not_applicable)
|
||
|
||
北京盘 `pl9-export --pack full --format json`(`selected_report_scope = full`),`worksheets.timing_and_predictive_systems.annual_tajika_pack`:
|
||
|
||
| 键 | 状态 | 说明 |
|
||
| --- | --- | --- |
|
||
| `profile` | 有 | |
|
||
| `patyayini_dasha` | 有 | 同时出现在 `external_engine_comparison.pyjhora` 与 `exports.json.field_statuses` |
|
||
| `monthly_chart_pack` | 缺 | 全包任何路径都没有;本仓 `annual_tajika_pack.py` 无生产者 |
|
||
| `normalized_tables` | 缺 | 同上 |
|
||
| `lifetime_annual_chart_pages` | 缺 | 同上 |
|
||
| `annual_chart_series_pack` | 缺 | 同上 |
|
||
| `pl9_annual_visual_panels` | 缺 | 同上 |
|
||
| `pl9_annual_page_map` | 缺 | 同上(本仓源码 0 处引用) |
|
||
| `pl9_current_year_results` | 缺 | 同上 |
|
||
| `pl9_annual_dasha_results` | 缺 | 同上 |
|
||
| `annual_special_points` | 缺 | 同上 |
|
||
| `tripataki_chakra` | 缺 | 同上 |
|
||
|
||
本仓年运包实际键:`annual_chart, audit, exports, external_engine_comparison, monthly_windows, mudda_dasha, muntha, patyayini_dasha, profile, report_sections, sahams, schema, solar_return, tajika_yogas, year_lord`(15 个)。「缺」不是装配丢失(`_apply_pl9_pack_selection` 在 `full` 时原样返回 packet),而是从未生成;上游的补回逻辑没有可补的来源。不抄代码。
|
||
|
||
## T3 · PL9 紧凑写出(BUG-1097)
|
||
|
||
### 序列化单步基准(改后 4.61 MB 语义包,`tracemalloc`,5 次取中位)
|
||
|
||
| 变体 | 耗时 | 峰值内存 | 文件 |
|
||
| --- | --- | --- | --- |
|
||
| A 旧:`dumps(indent=2)` + 写 | 0.410 s | 46.9 MB | 9.57 MB |
|
||
| B 上游:`json.dump(stream, compact)` | 0.593 s | 0.1 MB | 4.61 MB |
|
||
| C 采用:紧凑 `dumps` 一次写出 | **0.303 s** | 22.1 MB | 4.61 MB |
|
||
|
||
B 慢是因为 `json.dump` 只能走 `iterencode` 纯 Python 路径(C 编码器只在 `dumps` 且 `indent=None` 时启用)。本仓生产 API 路径(`jyotish_api_server.py`)不经过 `output_json`,本项只影响 CLI 与 `--output` 落盘,所以选耗时最优的 C,保留 `stream=` 签名。三种变体解析后与源对象相等。
|
||
|
||
### 整条 CLI 交错 A/B(`pl9-export --pack full --format json`,基线树 / 本分支交替各 3 次,`resource.getrusage` 子进程)
|
||
|
||
| 轮 | 基线耗时 | 基线 RSS | 改后耗时 | 改后 RSS |
|
||
| --- | --- | --- | --- | --- |
|
||
| 1 | 6.92 s | 197.9 MB | 6.58 s | 191.3 MB |
|
||
| 2 | 6.62 s | 169.2 MB | 6.42 s | 159.4 MB |
|
||
| 3 | 6.42 s | 169.6 MB | 6.38 s | 159.0 MB |
|
||
|
||
输出文件 9.57 MB → 4.61 MB;非 PL9 命令(`solar-return` / `tajika`)仍 `indent=2`,输出逐字节不变(tajika 0 差异)。
|
||
|
||
## 测试
|
||
|
||
- 新增 `tests/test_solar_return_timezone.py`(10 条):ZoneInfo 夏令 / 标准偏移;北京 `dt_local = dt_ut + 8h` 与昼夜判定时刻回归;`Asia/Shanghai` 等价固定 +8;纽约 IANA 区差 1 小时且盘不变;伦敦异地返照盘上升不同;返照地点不全时报错;两种命名空间的 kwargs;年运包参数映射与缺字段报错;`output_json` 只对 PL9 紧凑且可解析。
|
||
- 任务书点名:`tests/test_tajika.py tests/test_annual_tajika_pack.py tests/test_full_report_quality_gate.py tests/test_professional_report_reference_api.py tests/test_api_server_growth_contract.py tests/test_cli_smoke.py` → 118 passed。
|
||
- 快速门(`run_quality_gate.py --profile quick`,系统 `python3` 3.13.5 + swisseph,`/workspace/Jyotisha/.venv` 在本机不存在):
|
||
- 基线 15be6a06 第一次(无 Node):Python 步 1001 passed / 1 skipped,`npm test` exit 127(PATH 无 node)。
|
||
- 基线第二次(`PATH=/exec-daemon` Node 22.14):Python 步 **1 failed** `tests/test_consultation_native_layers.py::test_annual_route_attaches_the_annual_pack_verbatim`(`'blocked' == 'parameter_sensitive'`),门在 Python 步停下、未到 npm。该测试改后单独跑 3/3 通过(exit 0);当时同机另有两个子代理在跑全量测试,判为负载相关的瞬时失败,不是本单改动(基线树、改动前)。
|
||
- 改后(Node 22):见文末「快速门改后」一节。
|
||
|
||
## 未完成 / 缺口
|
||
|
||
- 前端 IANA 区未传(见 T1 末节)。
|
||
- `full_report_quality_gate.py` 未改(上游同批改动与面板无关)。
|
||
- `saham_daynight` 的昼夜判定输入现在是真实返照本地时刻;其他盘的昼夜结论可能因此翻转(这是修正,不是回归),未逐盘统计。
|
||
|
||
## 快速门改后(`PATH=/exec-daemon` Node 22.14,系统 python3 3.13.5)
|
||
|
||
- Python 步:**1001 passed / 1 skipped / 3 subtests passed**(149.84 s),与基线第一次逐条一致;基线第二次瞬时失败的 `test_annual_route_attaches_the_annual_pack_verbatim` 本次通过。
|
||
- `npm test` 步:`sh: 1: tsx: not found` → exit 127。原因是本 worktree 没有 `frontend/node_modules`(基线两次同样在这一步 exit 127,逐条一致)。本单不改前端,另用临时软链到主检出的 `frontend/node_modules` 单独跑了一次 `npm test`(见下一节;软链跑完即删,不入库)。
|
||
|
||
## 前端 `npm test`(本单零前端改动;临时软链主检出 `frontend/node_modules`,Node 22.14,跑完即删)
|
||
|
||
`# tests 4357 / # pass 4302 / # fail 24 / # cancelled 0 / # skipped 31`,exit 1。24 条失败全是需要 Docker / 本地 PostgreSQL / 部署套件的用例(`local PostgreSQL applies the reviewed business schema…`、`migration runner…`、`staging backups…`、`Better Auth…` 等),与 09-29 已知基线 4302/24 的环境失败清单一致(无 Docker),无新增失败。
|
||
|
||
## Claude 验收(2026-09-29)
|
||
|
||
- 测试:`tests/test_solar_return_timezone.py` + 任务书点名 5 个文件 + `test_api_server_growth_contract.py` + `test_repo_privacy_markers.py` = 156 passed。
|
||
- 等价比对(独立重做):基线 `15be6a06` 独立 worktree vs 本分支,虚构北京盘 1990-06-15 10:30,`PYTHONHASHSEED=0`:`solar-return` 6 处差异(`dt_local` 08:04:52→16:04:52、`annual_location` ×2 新增、`daynight_evidence.julian_day_ut` 改为真实返照时刻、sunrise/sunset 第 9 位小数);`tajika --age 36` 0 差异。与执行方表逐条一致。
|
||
- 硬红线 1 裁定:旧代码 `solar_return.py:221/249` `'dt_local': dt_ut # 调用方另行加 tz`,全仓无调用方加过,报告里的 Varshapravesha 时刻一直是 UT。「北京盘逐字节相同」的前提不成立,差异恰为修正项本身,**接受**。
|
||
- 快速门:Python 步 1001 passed / 1 skipped(与执行方基线一致);`npm test` 步 exit 127 为本 worktree 无 `node_modules` 的环境缺口,本单零前端改动,执行方软链跑出 4302/24 与基线一致。
|
||
- 结论:T1 / T3 通过,T2 不适用成立。遗留另开小单:前端 `personal-report-longform-birth.ts:79` 只传数字 `tz`,需把账户 IANA `timezone_id` 送到引擎。
|