merge origin/staging into narrower delivery (只碰文档;显示漏段 BUG-1375 顺延为 BUG-1392, issue #77)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
This commit is contained in:
Jesse_Chen
2026-10-19 09:15:55 +08:00
co-authored by Claude Opus 5.5
169 changed files with 89684 additions and 782 deletions
@@ -0,0 +1,68 @@
# 进度:full-reading 补齐上游 5 个模块(2026-10-18)
基线:`origin/staging` = `b3ef2d815`(任务书写作时写的 `47b4a2317` 已过时,开工以 fetch 后的最新 staging 为准)。分支 `codex/full-reading-module-parity-20261018`。
## 做了什么
- T3:`navamsha_dasha`、`niryaana_shoola_dasha` 用 `rashi_dashas_native.calculate_rashi_dashas` 的结果填键。profile id 仍是 `goel_method_1_v1` 和 `goel_nirayana_shoola_v1`,期间表原样放在 `periods`。外层 `status` 为 `parameter_sensitive`,给上游合盘校验器读。没有导入 `niryaana_shoola_dasha.py`。
- T2:搬入 `_calc_lagna_vargottamamsa`、`_summarize_pushkara_planets`,并在 full-reading 写入 `lagna_vargottamamsa`、`pushkara_planets`、`punarphoo`(调用已有的 `scripts/punarphoo.py`)。
- T1:按上游副本搬入 `scripts/unified_western_reading.py` 和 `scripts/classical_astrology.py`。西占排盘补了本仓没有的 `_birth_times`、`build_draconic_chart`、`build_relocation_chart`。加了 `scripts/_script_root.py`,因为搬来的文件会可选导入它,本仓原来没有这个文件。
- T4:`tests/test_full_reading_module_parity.py` 固定 86 个模块键。
- 两位公开名人用的是仓库里已有的样例(`tests/indastro_cases.json` 的 `brad_pitt`,`tests/celebrity_cases.json` 的 `jolie`),没有新抄出生时间。
## 耗时
同一进程里先跑一次预热,再各跑 3 次,取中位数。参考日 `2026-10-18`,ayanamsa `lahiri`,node `mean`。
| 样例 | 改前 3 次(秒) | 改前中位数 | 改后 3 次(秒) | 改后中位数 | 变化 |
| --- | --- | --- | --- | --- | --- |
| brad_pitt | 4.943 / 5.000 / 5.043 | 5.000 | 4.964 / 5.152 / 5.192 | 5.152 | +3.0% |
| jolie | 5.022 / 5.047 / 5.094 | 5.047 | 5.132 / 5.139 / 5.174 | 5.139 | +1.8% |
没有超过 20%。多出来的时间主要是西占各层和一次本仓七套 rashi 大运。
## Navamsha 首段
两位公开名人的首段星座序号和年数,与上游 `goel_method_1_v1` 用「D9 上升 + 宫主星在 D9 的位置」算出的首段一致,也与本仓用黄经直接取九分盘的算法一致。没有需要仲裁的差异。
## 没整份同步的西占差异
- `western_timing_engine.py` 里西占层要的函数,签名已经和上游副本一致,没有改这个文件。
- `build_tropical_natal_chart` 签名已一致。本仓这份还多了接口请求包装和岁差展示,没有用上游文件覆盖。
- 上游 `classical_astrology.py` 在导入时需要 `western_real_case_calibration._build_calibration_summary`。这个函数不在本单的上游副本里,本仓原来也没有。补了一个只返回 `blocked` 的垫片,键是调用方实际读取的那些,不编造特异性分数。完整解读里的 classical 层仍然是 `executed`。
- full-reading 不传迁居坐标,所以西占包里的 `relocation` 层是 `blocked`,整包状态是 `partial`。这和上游调用方式一样。挂到 full-reading 上的模块状态是 `parameter_sensitive`,不再是 `blocked`。
- 现有 VedAstro 适配器仍会尝试本地外部程序,失败被它自己接住。打桩测试确认这次调用栈不经过 `unified_western_reading.py` 或 `classical_astrology.py`;单独跑西占包时,`urlopen` 和 `subprocess.run` 的调用次数为 0。
## 检查
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 新测试 + 大运红线 + Punarphoo | `pytest tests/test_full_reading_module_parity.py tests/test_rashi_dashas_native.py tests/test_punarphoo_observation.py -q` | 27 passed |
| 屏蔽 PyJHora 后再跑新测试和大运红线 | 拦截 `jhora` 导入后跑上述前两个文件 | 19 passed |
| 改过的西占文件 | `pytest tests/test_western_chart_engine.py tests/test_western_real_case_calibration.py -q` | 8 passed |
| 全量 pytest | 未跑 | Windows 执行侧按循环约定不跑,留给验收 |
| 快速门 | 未跑 | 同上 |
解释器是本机 Anaconda,不是仓库 `.venv`(工作树里没有 `.venv`)。
## 修改单(产品选 A,2026-10-18)
已 `git merge origin/staging`(合并提交在功能提交之后)。合入后 `scripts/` 根目录 `.py` 为 449,其中多出来的 3 个是本单的两个上游原样文件和本地辅助文件。
- M1:删除根目录 `scripts/_script_root.py`。上游文件只接住 `ModuleNotFoundError`,名字找不到时实际抛出的是 `ImportError`,只改 `sys.path` 接不住。按修改单允许的做法,把同样的路径辅助放进 `scripts/_script_root/`(子目录,不占根目录名额)。没有改两个上游文件。full-reading 组装处仍先把仓库根放进 `sys.path`。
- M2:`ROOT_PY_BUDGET` 原值 446,新值 448。合入后 staging 仍是 446,只为本单两个上游原样文件加名额。删掉辅助文件后根目录为 448。
- M3:`niryaana_shoola_dasha` 改为占位 `execution_status=blocked`,原因「口径更正中,见 TASK-niryaana-shoola-pvr-20261018」。`navamsha_dasha` 仍用 `goel_method_1_v1`。校验器改读 `upstream_full_synastry_7a773d13`:Niryaana 这一行是 `not_applicable`,次要大运互证不因它 blocked。
- 断言三栏见 `tests/test_full_reading_module_parity.py` 与 `tests/test_python_hygiene_budget.py`。
## 修改单检查
解释器:`C:\Users\74082\anaconda3\python.exe`。工作树没有 `.venv`。
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 预算、大运红线、模块键 | `python -m pytest tests/test_python_hygiene_budget.py tests/test_rashi_dashas_native.py tests/test_full_reading_module_parity.py -q` | 21 通过 |
| 屏蔽 PyJHora 后再跑这三份 | 拦截 `jhora` 导入后同一条命令 | 21 通过 |
| 隐私标记 | `python -m pytest tests/test_repo_privacy_markers.py -q` | 通过 |
| 全量 pytest | `python -m pytest tests -q --tb=line` | 失败名单 176 条。上面三份测试都不在里面。抽样是 `python3` 退出码 9009、路径分隔符、控制台用 GBK 打不出表情符号。没有在 `origin/staging` 上把这 176 条再跑一遍做逐名差集。婚恋合盘修改轮在 Windows 上记录的也是 176 条,抽查原因同类。 |
根目录 `.py` 在删掉辅助文件后是 448,与新上限一致。静默处理只剩上游原文那一处 `scripts/classical_astrology.py:12558`。
@@ -0,0 +1,73 @@
# 进度:婚恋合盘第一期(2026-10-18)
## 做了什么
按任务书第六版,只做后端。基线 `origin/staging` = `e7bf60c1`。
- T1:`scripts/marriage_match.py` 与 `skills/jyotish-full-synastry-reading/` 从 `bde5d615` 副本原样搬入。胶水在 `scripts/synastry_full_dossier.py`:两次 `full-reading` 并行,再补 Raman。
- T2:`POST /api/marriage-match`、`POST /api/synastry_full_dossier`。证据包在有 `GITHUB_SHA` 时按两人出生资料和提交号做磁盘缓存;没有提交号则不缓存。CLI 命令 `marriage-match`。
- T3:删除 `scripts/synastry.py` 和 CLI `synastry`。`calc_kuja_dosha` / `match_kuja_dosha` 标为旧版。速查表、能力矩阵、技能命令表去掉 Papasamya / Mangal 已实现的虚报。
- T4:`synastry_reports` 加人物引用、指纹、规则版本和结构化结果。删档案人物时,引用该人物的合盘行一起删除。旧行不回填。
- T5:`frontend/src/lib/marriage-match-service.ts`。不扣点,不做路由。
- T6 / T7 / T8:按第六版不做。
## 与上游的差异
上游 `marriage_match.py` 和技能目录与副本逐字节相同,没有本地改动。
上游测试 `test_api_handler_uses_the_new_product_contract_without_legacy_kuja_score`:
| | |
| --- | --- |
| 原值 | `JyotishAPIHandler._compute_marriage_match` |
| 新值 | `marriage_match_api.compute_marriage_match` |
| 原因 | 类方法数不得增长,计算放在独立模块 |
`tests/test_open_source_integrations.py` 的附加 Kuta 测试同样改走 `ashtakoot.calculate_ashtakoot`,因为 `synastry.py` 已删除。
## 证据包截取
`frontend/tests/fixtures/synastry-full-dossier-truncated.json` 只留:schema、`delivery_readiness`、默认 profile、国际核心合计与满分、Raman profile、两边行星名。度数和模块正文不进金样。`marriage-match-snapshot.json` 是快照接口的完整引擎输出(虚构度数,不是出生资料)。
## 命令与结果
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 开工预检 | `python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` | 远端可见。碎片扫描 90 秒超时,聚焦测试 45 秒超时。未据此宣称通过。 |
| 合盘单测 | `pytest tests/test_marriage_match_product.py tests/test_marriage_match_api.py tests/test_open_source_integrations.py tests/test_ashtakoot.py tests/test_api_server_growth_contract.py tests/test_api_heavy_compute_gate.py tests/test_synastry_full_dossier.py` | 产品测试、缓存、增长合同、公开名人证据包通过。证据包测试约 27 秒,`delivery_readiness = ready_for_narrative`。 |
| 服务端函数 | `node node_modules/tsx/dist/cli.mjs --test tests/marriage-match-service.test.ts` | 6 通过,0 失败,0 取消。 |
| 类型检查 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 0 错。 |
| lint | `npm run lint`(frontend) | 0 error,123 warning(原有 warning)。 |
| 数据库 | `npm run test:db` | 86 项:73 通过,13 失败,0 取消。13 条失败名称与 `tools/agent-loop/windows-known-failures.txt` 逐条相同。婚恋合盘这一项通过。 |
| 全量 pytest / 快速门 | 未跑 | Windows 执行约定由验收方跑。 |
## 第 1 次打回(F1 / F2)
结构结果的键是 `rules_version`、`default`、`raman`。`default` 取证据包里的 `marriage_match`,`raman` 取 Raman profile。`report` 列存同一份结构结果,不存证据包。现算返回 `{ cached: false, structured, dossier }`;命中返回 `{ cached: true, structured }`,不带证据包。后续单要证据包时自己调证据包接口。旧历史列表只取 `rules_version is null` 的行。同一对人同时写入时,第二次遇到唯一冲突就重读第一次的行。
大小用 `frontend/tests/fixtures/marriage-match-snapshot.json` 当两套 profile 的金样,存进去的 JSON 小于 64 KB。证据包里的盘面快照不入库。
## 修复后的检查
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 服务端函数与旧历史过滤 | `node node_modules/tsx/dist/cli.mjs --test tests/marriage-match-service.test.ts tests/synastry-reports-history.test.ts` | 9 通过,0 失败,0 取消。 |
| 类型检查 | `tsc --noEmit` | 0 错。 |
| lint | `npm run lint` | 0 error,123 warning(原有)。 |
| 数据库 | `npm run test:db` | 86 项:73 通过,13 失败,0 取消。13 条与 Windows 已知失败清单逐条相同。`database-marriage-match.test.ts` 通过。 |
| 全量 pytest / 快速门 | 未跑 | 留给验收方。 |
## 第 2 次打回(G1 / G2)
`scripts/marriage_match.py` 仍在根目录,与上游同路径。三个胶水文件挪到 `scripts/marriage_match_service/`:`api.py`、`cli.py`、`full_dossier.py`。根目录 Python 文件数回到 446,没有放宽上限。调用清单里的 `scripts/synastry.py` 改成 `scripts/marriage_match.py`,并按合同重算了内容哈希。兼容准备度清单按生成脚本重写了一次。
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 文件数与调用清单 | `pytest tests/test_python_hygiene_budget.py tests/test_owner_case_purge_contract.py tests/test_api_server_growth_contract.py tests/test_marriage_match_api.py tests/test_marriage_match_product.py tests/test_synastry_full_dossier.py -q` | 通过。根目录 446。公开名人证据包仍可写解读。 |
| 全量 pytest | `python -m pytest tests -q --tb=line` | Windows 上 176 失败。抽查是 `python3` 不存在(退出码 9009)和路径分隔符。本单点名的文件数、调用清单、合盘测试不在失败名单里。Linux 全量仍由验收方对照基线。 |
## 偏离
- 不做页面、不扣点,与第六版一致。
- 类方法不能增加,所以没有把 `_compute_marriage_match` 加回 `JyotishAPIHandler`。
- 预检的两个超时是本机限时,不是计算失败。
@@ -0,0 +1,29 @@
# 进度:星盘档案上限 20 人(2026-10-17)
基线 `origin/staging` = `e7bf60c1f`。分支 `codex/people-archive-limit-20-20261017`。不新增 BUG 编号。
星盘档案现在最多 20 人(本人 + 19 个其他人)。第 19 个其他人可以保存,第 20 个其他人被拒绝。界面上的限额句由常量拼出。人多时,档案列表和标题旁 / 聊天里的人物菜单在自己的框里上下滚动。
生产部署前要先跑迁移 `frontend/supabase/migrations/20261017010000_chart_profiles_limit_20.sql`。旧迁移文件没有改。建档仍不扣点。
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 数据库:第 19 个他人可存,第 20 个他人被拒;并发锁仍在新门槛上生效 | `npx tsx --test --test-concurrency=1 tests/database-chart-subject.test.ts` | 4 通过,0 失败 |
| 同上两条在全量里 | `npm test` 中的对应子测试 | `ok`(全量编号 1643、1645、2114) |
| 20 人列表限高并纵向滚动 | `tests/people-archive-view.test.tsx` | 通过,含新增「twenty saved people…」 |
| 类型检查 | `npx tsc --noEmit` | 0 错误 |
| 代码规范 | `npm run lint` | 0 个错误,123 条原有警告 |
| 前端全量 | `npm test` | 5037 条:通过 4971,失败 66,取消 0,退出码 1 |
| 首页构建 | `npm run build` | 成功。`/`、`/people`、`/chart`、`/ephemeris`、`/reports` 仍是静态页 |
66 条失败都是这台 Windows 机器上的环境问题,不是这次限额断言:
- 31 条:`tsx` 解析 `tailwind-merge` 失败
- 4 条:路径被拼成重复盘符 `G:\G:\...`
- 其余:shell 脚本退出码 9009、临时目录被占用、`docker ENAMETOOLONG`
没有一条失败名是档案上限、限额句或 20 人滚动。本机没有再对 `e7bf60c1f` 单独构建一次,所以没有改动前后的首屏 gzip 对照数。这次进首屏的代码只有人物菜单的上限常量,样式只加了两段限高滚动。
`frontend/src` 里已经没有写死的「5 个人」。测试注释里的「原值」仍保留这句,用来对照改过的断言。更早的任务书仍写着当时的 5 人决定,没有改写。
真人核对仍按 `docs/testing/people-archive-p1-20260924.md` 和 `docs/testing/people-ephemeris-ui-20260926.md`:满 20 人时三处列表可滚动,手机宽 390px 不出现横向滚动。
@@ -66,3 +66,4 @@ F4 选 **b**:删掉没有调用方的 `scripts/rectification/living_display_in
- 这条显示漏段原先写成 BUG-1370。婚恋合盘保留了 BUG-1370 到 BUG-1374,上游第八轮从 BUG-1380 起。历史文件里也还没有 1375。所以改记为 BUG-1375。婚恋合盘那几个号没有动。
- `git fetch` 后 `origin/staging` 为 `edf25918b2f14a4339740be505916ba41e65acb7`(修复单所在提交 `f95eb670` 之后还有 5 个文档提交)。本轮用 merge,不 rebase。
- 临时目录 `G:\Ferti\tmp\replay-temp` 和 `G:\Ferti\tmp\baseline-712a0fd2` 不进仓库。
- 2026-10-19 Claude 验收合并时:staging 上 BUG-1375、BUG-1376 已归 #81(full-reading 补模块),本条显示漏段顺延为 **BUG-1392**(1380~1391 已归上游第八轮)。上文保留当时的编号叙述。
@@ -0,0 +1,73 @@
# PROGRESS · 上游同步第八轮(2026-10-18)
十项计算修正都在分支 `codex/upstream-sync8-20261017`,每项一个提交。校正打分的 20 条封存回放和 900 条偏移回放,与改代码之前逐条相同。冻结身份测试有 3 项失败,因为任务书要求改的三个文件就在冻结清单里。没有重新冻结,也没有改冻结记录。这一项需要产品决定:按分数相同验收,还是把冻结身份失败当成未通过。
## 基线
任务书写的基线是 `53608d8b`。开工时 `origin/staging` 已到 `e7bf60c1f`(文档提交,给 #83 加状态板一行)。本分支从 `e7bf60c1f` 拉出。
上游参照是 `codex/upstream-notice-20261016` 的 `caf1ed4e3`。没有整文件覆盖。
## 提交
| 项 | 提交 | BUG | 上游 |
| --- | --- | --- | --- |
| T1 第一大运小运 | `68762ccec` | BUG-1380 | `8643bc9a` |
| T2 星宿边界 | `d11ee6f0d` | BUG-1381 | `3c2dad95`、`8643bc9a`、`9e04f0db` 只取 `lon_to_nakshatra` |
| T3 Sandhi | `5be864a8e` | BUG-1382 | `3c2dad95` |
| T7 KP 边界 | `b5eb72343` | BUG-1383 | `8eec9a54` |
| T4 Pinda | `e8a7f3ac2` | BUG-1384 | `30b43643` |
| T5 Bhava Bala | `6cbae4ce2` | BUG-1385 | `a4d4a249` |
| T6 Argala | `aa87c0f12` | BUG-1386 | `e7301feb`、`86935c0a`、`4f1536f2` |
| T9 Saham 例外弧 | `2f8263b6a` | BUG-1387 | `ce9fed15` |
| T10 日出 | `0bd254cf1` | BUG-1388 | `9f9f16b3` 只取 `gulika.py`;`872eaecf` 只取 `saham_daynight.py` |
| T8 Avastha | `83c97a897` | BUG-1389 | `e628118d`、`7d545039`、`f75a6e77`、`ac565836` |
## 校正回放
改代码前的结果在本机临时目录,不入库。改完后跳过冻结哈希检查再跑同一套打分,只比 trials:
| 回放 | 条数 | 与改前 |
| --- | --- | --- |
| `sealed_holdout_rerun.py` | 20 | 相同 |
| `reported_offset_sweep.py` | 900 | 相同 |
直接跑这两个脚本会在出结果前退出,原因是 `frozen_record_mismatch`。变了的冻结文件只有三个:`scripts/ashtakavarga.py`、`scripts/dasha_analyzer.py`、`scripts/narayana_dasha.py`。这三份正是 T2、T4、T6 要改的。没有写新的 freeze 文件。
`tests/test_sealed_holdout_contract_freshness.py`:3 失败,其余通过。失败的是 `test_contract_tracks_actual_current_scorer_and_dataset_audit`、`test_frozen_record_matches_dataset_scorer_and_evaluator_bytes`、`test_changed_frozen_identity_fails_before_any_replay`。隐私标记测试通过。
## 定向测试
| 检查 | 结果 |
| --- | --- |
| 星宿、Sandhi、KP、Pinda、Bhava、Argala、Saham、日出、Avastha 的新增与相关旧测试 | 通过 |
| KP 20 万随机经度 | 0 处不同 |
| 星宿非边界抽样 | 0 处不同 |
| 报告事实表 `report-fact-tables.test.ts` | 11 通过 |
| 全量 pytest、快速门、前端全量 `npm test` | 未跑,留给验收方 |
## 偏离
- `9e04f0db` 只移植了 `lon_to_nakshatra`。`dasha_analyzer` 的整段时钟改写没有搬,避免改校正打分。
- Sandhi 没有照搬「跳过第一大运起点」。我方第一大运的起点本来就是出生前的真实交界。
- Argala 同数复用已有的 `select_narayana_seed_sign`,给它加了可选的对照星座。没有移植后面的强弱层。
- `determine_daytime` 默认仍是原来的视日出窗口。只有 Saham 昼夜、Gulika、Shayanadi 传入 Hindu rising。Panchanga 和校正调用点没有改。
- 年运快照没有锁住这三条公式 Saham 的旧经度,所以没有改快照数字。
- 本机没有把 `ruff` 装进这次用的虚拟环境,没有跑 ruff。
## 验收与收尾(Claude,2026-10-18)
验收基于 `44e961ef9`,基线 `e7bf60c1f`。产品同意按打分相同验收、重新冻结,并授权 Claude 直接收尾(分支 `codex/upstream-sync8-finish-20261018`,已合入当时的 `origin/staging`)。
| 检查 | 结果 |
| --- | --- |
| 封存回放 20 条(Claude 独立重跑) | 与基线逐项相同 |
| 偏移回放 900 条(Claude 独立重跑) | 与基线逐项相同 |
| 全量 pytest(两棵树并行,逐名对比) | 新增失败只有 8 条冻结身份;`test_vedastro_gateway` 一条并行偶发,单跑通过 |
| tsc / lint | 0 错 / 0 error(123 条存量 warning) |
收尾改动:
- BUG-1390:极昼极夜出生时整张 Avastha 表消失,改为只有 Shayanadi 标无法计算。
- BUG-1391:英文报告 Deeptadi 并存条件改用英文逗号。
- 重新冻结(ERR-110):标签 `upstream_sync8_2026_10_18`。两份研究脚本的 `FREEZE` / `REPORT`、密封合同当前指针、`tests/test_sealed_holdout_contract_freshness.py` 排除前缀、`frontend/tests/rectification-confirmation-gate.test.ts` 冻结路径,均按三栏改。scoring_perf 四份旧记录逐字节保留。新报告与 scoring_perf 报告的 trials、metrics、summary、historical_comparison 相同(合同更新脚本里有断言)。
+10 -4
View File
@@ -36,7 +36,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-narrower-delivery-20261016.md` | `PROGRESS-rectification-narrower-delivery-fix-20261017.md` | **交付区间收窄**:显示改按活着的簇分段。第 1 次打回只补两处显示漏段(取整差 8 分、缺区间整段退回)。对半出题和门槛 8 改 6 仍不做。基线与对照各 924 条,12 格包含率都过红线。BUG-1367 起,本轮原记 BUG-1370,因与婚恋合盘保留号冲突改为 BUG-1375 | 待验收 | `codex/rectification-narrower-delivery-20261016` |
| `TASK-rectification-narrower-delivery-20261016.md` | `PROGRESS-rectification-narrower-delivery-fix-20261017.md` | **交付区间收窄**:显示改按活着的簇分段。第 1 次打回只补两处显示漏段(取整差 8 分、缺区间整段退回)。对半出题和门槛 8 改 6 仍不做。基线与对照各 924 条,12 格包含率都过红线。BUG-1367 起,本轮原记 BUG-1370,因与婚恋合盘保留号冲突改为 BUG-1392 | 待验收 | `codex/rectification-narrower-delivery-20261016` |
| —(子代理直接执行,无任务书) | `PROGRESS-bug-1060-d9-double-transit-20260927.md` | **双重过运 D9 目标错配(BUG-1060)**:D9 第 N 宫目标误用 D9 上升、D9 宫主目标误用 D1 度数,改为标签所写的 D9 星座;D1 / CL 层逐字节不变;数据卡 golden 重采集(一张盘跨层结论 9→10);另记 BUG-1061 跨层数字比对(未修) | 已验收(待部署) | `codex/bug-1060-d9-double-transit-20260927`(未推送) |
| —(子代理直接执行,无任务书;产品 2026-09-27 授权) | `PROGRESS-bug-1061-cross-layer-pairing-20260927.md` | **双重过运跨层配对按名字数字比对(BUG-1061)**:改为结构化身份配对(D1 第 N 宫↔D9 第 N 宫;D1 第 N 宫主↔同一颗星的 D9 目标);各层结果与 D1/CL golden 逐字节不变;48 样本跨层条目 31→38;数据卡 golden 重采集(乔布斯 6→9、泰勒 9→10) | 已验收(待部署) | `codex/bug-1061-cross-layer-pairing-20260927`(未推送) |
| `TASK-rectification-fewer-probes-card-20260926.md` | [PROGRESS](PROGRESS-rectification-fewer-probes-card-20260926.md) | **减少无效追问 + 卡片区间为主**:引导补件离线新增达标 0 → 上限 6→2、定向题问完门槛未达直接出卡;卡片区间为主标题、代表分钟副标题,第一二名差距 ≥5 个百分点才显示百分比,否则写「目前区分不开」。不放宽任何置信度。先做 | 已验收(真机清单欠) | `4e6e8d87` + 验收补改 `7203d94e`(区分不开时隐藏「最可能」),deploy-staging run 2933 已部署;回放真值 ±30/±60 19→20、宽度中位 ±1 分钟、提问 11.4→7.8;Skill 10.0.30;真机清单 `docs/testing/rectification-fewer-probes-card-20260926.md` 未走 |
@@ -334,7 +334,7 @@
| `TASK-rectification-refresh-r3-r4-20260913.md` | `PROGRESS-rectification-refresh-r3-r4-20260913.md` | 刷新阶段 R3(MIN_BOUNDARY_DAYS 45→30)+ R4(pratyantar 与 D9/D10 上升 Narayana);首轮出题不变。即使放宽仍有 0 题例子,定向补事另线保留(BUG-664/665) | 已实现 `ab1ade59`,已部署;2026-09-13 验收通过(实现单由执行方自拟,无产品决策记录段;收益口径见研究文档) | `codex/rectification-refresh-r3-r4-20260913` |
| `TASK-upstream-sync6-20261007.md` | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) | **上游同步第六轮(`23be1807`)**:10-05 五个计算修正一行未合——D5/D6/D8/D11 映射(乔布斯盘四张分盘星座全不同)、Vimsopaka 权重、Yogini 顺序与出生余额、Panchadhikari 宫位参照、Narayana 并列、特殊上升、出生秒数、Neecha Bhanga 两条件、首段大运子运、岁差被覆盖;逐项对照原书核实;最先做 | 已验收(门禁补修 `2df6ee72`,已部署 staging) | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) |
| `TASK-upstream-sync7-20261014.md` | [PROGRESS-upstream-sync7-20261014.md](PROGRESS-upstream-sync7-20261014.md) | **上游同步第七轮(BUG-1318 起)**:婚恋评级断语改证据档位、DK「婚姻质量 X/100」改对;KP 宫头到宫头判宫;瑜伽宫主排序去重;api_server 大运年长 365.25;星历路径只核查。不动校正打分。Grok 执行(agent 循环) | 待验收 | `codex/upstream-sync7-20261014`(BUG-1318~1321 已改;BUG-1322 星历路径核查中,未改代码) |
| `TASK-upstream-sync8-20261017.md` | — | **上游同步第八轮(BUG-1380 起)**:按作者修复分支 caf1ed4e 移植——星宿拆解第一大运小运、星宿边界、Sandhi、Pinda、Bhava Bala、Argala 裁决、KP 边界、Avastha 一批、Saham 弧方向、Gulika / Hindu 日出。不动校正打分(回放逐字节相同)。Grok 执行(agent 循环) | 待领取 | issue #80 |
| `TASK-upstream-sync8-20261017.md` | `PROGRESS-upstream-sync8-20261017.md` | **上游同步第八轮(BUG-1380 起)**:十项计算已在 `codex/upstream-sync8-20261017`。校正 20 条与 900 条打分与改前相同。冻结身份测试因任务书要求修改的三个冻结文件而失败,未重新冻结,待产品决定是否按分数相同验收 | 待拍板 | issue #80 |
| `TASK-beta-main-path-20261014.md` | `PROGRESS-beta-main-path-20261014.md` | **内测主路径**:BUG-1063 他人报告 worker 读档案(逐列只读授权 + test:db);对话与报告「AI 生成,仅供参考」标识与免责;BUG-1080/283 状态;状态板按事实清账;冒烟清单 `docs/testing/beta-smoke-20261014.md`。与发布单并行 | 第 1 次打回后已修,待验收 | `codex/beta-main-path-20261014` |
| `TASK-beta-main-path-fix-20261014.md` | `PROGRESS-beta-main-path-20261014.md` | **#73 第 1 次打回**:`fca62fda` 内容通过;与 #63 删除的旧报告视图有代码冲突,接受删除即可 | 待验收 | `codex/beta-main-path-20261014` |
| `TASK-release-path-production-20261014.md` | `PROGRESS-release-path-production-20261014.md` | **发布通路 + 生产首发**:产品 10-14 授权改生产工作流(发布 SHA = 最近门禁成功提交 + 其后纯文档);63 个迁移清单与演练脚本、首发 runbook;生产拒绝测试验证码。Grok 不 dispatch 生产工作流 | 第 2 次打回后续修 | `codex/release-path-production-20261014`(BUG-1348、BUG-1349) |
@@ -500,5 +500,11 @@
| `TASK-api-server-service-extract-20261013.md` | — | API 单体第一阶段:拆出不带 HTTP 的计算类,生产伪造 handler 归零 | 待领取(排队:dasha-unify) | `codex/api-server-service-extract-20261013` |
| `TASK-data-layer-types-20261013.md` | `PROGRESS-data-layer-types-20261013.md` | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1351;任务书预留的 1343 已被更大编号越过) | 待验收 | `codex/data-layer-types-20261013` |
| `TASK-contracts-helpers-dedupe-20261013.md` | — | 前后端契约与小工具去重第一阶段 | 待领取(排队:frontend-dead-code、consult-route-split) | `codex/contracts-helpers-dedupe-20261013` |
| `TASK-people-archive-limit-20-20261017.md` | — | 星盘档案上限 5→20 人(触发器新迁移 + 常量 + 文案;婚恋合盘前置单) | 待领取 | `codex/people-archive-limit-20-20261017`;动表,需 test:db |
| `TASK-marriage-match-p1-20261017.md` | — | 婚恋合盘第一期:独立页、任意两人、从业者共识口径(分档并列)、按报告扣点(BUG-1370 起) | 排队(等上限单合入) | `codex/marriage-match-p1-20261017`;动表 |
| `TASK-people-archive-limit-20-20261017.md` | `PROGRESS-people-archive-limit-20-20261017.md` | 星盘档案上限 5→20 人(触发器新迁移 + 常量 + 文案;婚恋合盘前置单) | 待验收 | `codex/people-archive-limit-20-20261017` |
| `TASK-marriage-match-p1-20261017.md` | #79 | 婚恋合盘引擎(搬上游 bde5d615:marriage_match + 合盘证据包;35 分七项、不分男女;第六版不扣点、不做页面)(BUG-1370 起) | 待验收 | `codex/marriage-match-p1-20261017`;动表 |
| `TASK-marriage-match-p1-fix-20261018.md` | #79 | 只存两个 profile 的配对结果;旧历史列表不读新行;并发重复写入改为重读 | 待验收 | `codex/marriage-match-p1-20261017` |
| `TASK-marriage-match-p1-fix-20261018-2.md` | #79 | 胶水挪进子包,根目录文件数回到 446;调用清单改指向 marriage_match.py | 待验收 | `codex/marriage-match-p1-20261017` |
| `TASK-full-reading-module-parity-20261018.md` | #81 | full-reading 补齐上游 5 个模块(西占层文件没搬致永远 blocked、Navamsha / Niryaana 用我们自己的实现填键、Lagna Vargottamamsa / Pushkara / Punarphoo)(BUG-1375 起) | 待执行(可与 #78 并行) | `codex/full-reading-module-parity-20261018` |
| `TASK-niryaana-shoola-pvr-20261018.md` | #84 | Niryaana Shoola 改 PVR 口径(我们的表与 Shoola 逐行相同;推翻 BUG-1283 所选 profile;占星师 7a773d13) | 排队(等 #81) | `codex/niryaana-shoola-pvr-20261018` |
| `TASK-synastry-full-report-20261018.md` | #82 | 婚恋合盘:选人页 + 完整合盘报告(关系画像九节:结论+折叠的分析逻辑+关系时间线+技术依据摘要;英文按需;计费键 synastry.full) | 排队(等 #78、#79、#81) | `codex/synastry-full-report-20261018`;动表 |
| `TASK-synastry-report-chat-20261018.md` | #83 | 从合盘报告某一节进入对话问依据(绑定报告与节、对话里可用术语、只依据已存计算结果并逐条核对、计费照普通对话) | 排队(等 #82) | `codex/synastry-report-chat-20261018`;动表 |
@@ -0,0 +1,85 @@
# 任务书:full-reading 补齐上游 5 个模块(2026-10-18)
## 基线与顺序
- `origin/staging` = `47b4a2317`(开工前 `git fetch origin --prune`,以最新为准)。
- 分支:`codex/full-reading-module-parity-20261018`;工作树:`.worktrees/full-reading-module-parity-20261018`。
- 顺序:纯 Python,和 #78(档案上限,只改前端与迁移)没有文件冲突,可以先做。和 #79(婚恋合盘引擎)都可能动 `scripts/jyotish_engine.py`,两边都只加不改,后合入的一方解决冲突。**必须在 `TASK-synastry-full-report-20261018.md` 开工前合入**。
- 上游参照:占星师仓库 `yinduzhanxing` 提交 `bde5d615`。上游仓库私有,本单需要的文件副本在 `docs/research/upstream_full_reading_parity_bde5d615/`(`.py.txt` 搬入时去掉 `.txt`)。
## 现状实证
Claude 10-18 用两位公开名人(布拉德·皮特、安吉丽娜·朱莉,出生资料来自公开星盘库)在两边各跑一次 `full-reading`(`--ayanamsa lahiri --node-mode mean --today 2026-10-18`):我们 81 个模块,上游 86 个。
| # | 模块(`modules.<key>`) | 我们现状 | 上游 | 定位 |
| --- | --- | --- | --- | --- |
| 1 | `unified_western_reading` | **永远 `blocked`**:`No module named 'unified_western_reading'`。引擎里有调用,文件从没搬过来 | `parameter_sensitive`,有原始结果 | 我们 `scripts/jyotish_engine.py` 里 `from unified_western_reading import build_unified_western_reading`(full-reading 内);上游 `scripts/unified_western_reading.py` |
| 2 | `lagna_vargottamamsa` | 没有这个键 | D1 / D9 上升是否同星座 | 上游引擎 `_calc_lagna_vargottamamsa` |
| 3 | `pushkara_planets` | 没有这个键(原始 `pushkara` 有) | 落在 Pushkara 的行星简表 | 上游引擎 `_summarize_pushkara_planets` |
| 4 | `punarphoo` | 没有这个键;`scripts/punarphoo.py` 与上游**逐字相同**,只是 full-reading 没调用 | `observation_only` | 上游 full-reading 里 `detect_punarphoo(planets)` |
| 5 | `navamsha_dasha` | 没有这个键 | Goel Method 1(`goel_method_1_v1`) | 上游 `scripts/navamsha_dasha.py`(依赖上游 `dasha_profile_arbitration`、`rashi_dasha_explicit_contract`,我们都没有) |
| 6 | `niryaana_shoola_dasha` | 没有这个键 | 上游 `scripts/niryaana_shoola_dasha.py` | 见下条 |
**我们已有更好的两套**:Navamsha Dasha 与 Niryaana Shoola 在 `scripts/rashi_dashas_native.py`(`_navamsha` 用 `goel_method_1_v1`,Niryaana Shoola 用 `goel_nirayana_shoola_v1`),完整数据版报告经 `scripts/rashi_dasha_report.py` 在用。BUG-1283 已定:上游那份 `niryaana_shoola_dasha.py`(按 365.25636 日一年、上升与第 7 宫平手就停)**不得再被新路径调用**;上游 `bde5d615` 的版本仍是 `YEAR_DAYS = 365.25636`。
影响:上游合盘证据包的「次要大运互证」一项因 `navamsha_dasha` 缺失整项 `blocked`;西占层在个人报告与合盘里都拿不到。
## 根因
前几轮上游同步只按提交范围挑,这几处是上游 7~9 月加进 full-reading 的(`fad247d4`、`568d8d20`、`2ff139de`、`680f94e5` 等),我们同步时漏了;`unified_western_reading` 则是调用搬了、文件没搬。
## 决策记录(产品负责人 2026-10-18)
| # | 决定 |
| --- | --- |
| F1 | 比上游少的模块都补上,让我们的 full-reading 是上游的超集 |
| F2 | `navamsha_dasha`、`niryaana_shoola_dasha` 两个键**用我们自己的 `rashi_dashas_native` 结果填**,不搬上游那两份文件(BUG-1283 的红线不变)。键名与上游一致,形状按上游合盘校验器能识别的方式包一层(见 T3) |
| F2b | **10-18 更正(占星师 `7a773d13`)**:我们 `rashi_dashas_native` 里的 Niryaana Shoola(`goel_nirayana_shoola_v1`)其实是 Shoola 的规则,正确口径另立单 `TASK-niryaana-shoola-pvr-20261018.md`。所以本单 **`niryaana_shoola_dasha` 键不再用它填**,只写占位 `{"execution_status": "blocked", "reason": "口径更正中,见 TASK-niryaana-shoola-pvr-20261018"}`;合盘校验器对这一行本来就固定判「不适用」,不影响合盘。`navamsha_dasha` 照原计划 |
| F3 | 西占层(`unified_western_reading`)按上游搬入。它在报告里的用途与上游一致:只作补充,合盘报告不得出现方法名(合盘任务书红线 3) |
| F4 | 个人报告里如果因为西占层从 `blocked` 变成有结果而多出内容,这是预期的修复,不是回归;改动的断言写三栏说明 |
## 硬红线
1. `scripts/jyotish_api_server.py` 不动。`scripts/jyotish_engine.py` 只在 full-reading 组装处加键,不改既有键的值。
2. 不得 import `scripts/niryaana_shoola_dasha.py`(BUG-1283 的语法树断言 `tests/test_rashi_dashas_native.py` 必须仍然绿)。
3. 生产不 import PyJHora。验收会屏蔽 PyJHora 模拟生产(见 `feedback-acceptance-prod-deps`)。
4. 运行时不得联网、不得调外部程序(T1 测试②守住)。
5. full-reading 耗时:两位名人各跑 3 次取中位数,前后对比写进 PROGRESS。增加超过 20% 要说明来源,超过 50% 停下报 Claude。
6. 全量 pytest 与开工基线逐条比对,无新增失败。改既有断言写「原值 / 新值 / 原因」三栏。
## 任务分解
| # | 内容 | 验收标准 |
| --- | --- | --- |
| T1 | **西占层**:搬上游 `scripts/unified_western_reading.py`。它在导入时需要 `scripts/classical_astrology.py`(上游约 1.3 万行,我们**没有这个文件**)以及 `western_chart_engine.py`、`western_timing_engine.py` 里的若干函数(这两份与上游有差异)。做法:`classical_astrology.py` 原样整份搬入(Claude 已用语法树核对:`build_classical_astrology_packet` 走不到任何 `urlopen` / `subprocess.run`;联网和调外部程序的只有 OCR、Astrolog、公开案例抓取那几个研究用函数,full-reading 不调用);`western_*` 只补 `unified_western_reading` 与 `classical_astrology` 需要的函数,其余差异记进 PROGRESS,不顺手同步 | 两位名人 `modules.unified_western_reading.status` 不再是 `blocked`;新增测试:① full-reading 里该模块能导入(防止再出现「调用搬了、文件没搬」);② 把 `urllib.request.urlopen` 和 `subprocess.run` 打桩成抛错后跑一次 full-reading,仍然成功(证明运行时不联网) |
| T2 | **三个简单键**:从上游引擎搬 `_calc_lagna_vargottamamsa`、`_summarize_pushkara_planets` 两个函数,并在 full-reading 里写 `lagna_vargottamamsa`、`pushkara_planets`、`punarphoo`(调用现有 `scripts/punarphoo.py`,包法照上游,异常时写 `{"status": "blocked", "reason": …}`) | 每个键各一条测试;名人结果与上游同一输入逐字段一致 |
| T3 | **两个大运键**:用 `rashi_dashas_native.calculate_rashi_dashas` 的 Navamsha 与 Niryaana Shoola 结果填 `navamsha_dasha`、`niryaana_shoola_dasha`。保留我们的 profile id 和期间表;外层加上游校验器读的状态字段(以上游 `skills/jyotish-full-synastry-reading/scripts/validate_full_synastry_packets.py` 的 `_status` 为准:不能是 `blocked / not_available / unavailable / skipped`,也不能只有 `error`) | 用上游 `validate_full_synastry_packets.py` 跑两位名人:`Navamsha Dasha`、`Niryaana Shoola Dasha` 两行都是 `executed` 或 `parameter_sensitive`;合盘任务「secondary-dasha convergence」不再是 `blocked`;名人的 Navamsha 大运首段与上游 `goel_method_1_v1` 一致(不一致写进 PROGRESS 交 Claude 仲裁) |
| T4 | **超集断言**:新增测试固定 full-reading 的模块键清单(至少包含上游 86 个键名),以后少了会红 | 测试绿 |
| T5 | **记录**:BUG_HISTORY(下节编号)、CHANGELOG(报告里西占层恢复)、PROGRESS(耗时对比、未同步的西占差异、上游与我们 Navamsha 首段对比) | 文档齐 |
## 让步顺序
T3 → T2 → T1 → T4。T1 若因依赖差异过大做不完,先交 T2–T4,T1 写进 BLOCKED 并单独回报,不得为了搬它而整份覆盖我们的 `western_*` 文件。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/full-reading-module-parity-20261018 .worktrees/full-reading-module-parity-20261018 origin/staging
cd .worktrees/full-reading-module-parity-20261018
ls docs/research/upstream_full_reading_parity_bde5d615/
grep -n "from unified_western_reading" scripts/jyotish_engine.py
grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
```
验证:全量 `pytest tests -n 4` 与开工基线逐条比对;`run_quality_gate.py --profile quick`;屏蔽 PyJHora 后再跑一遍 `tests/test_rashi_dashas_native.py` 与新增测试。
## BUG 编号
#79 已预留 BUG-1370~1374。本单从 **BUG-1375** 起(开工时核对最大号,冲突整体顺延):
| 建议编号 | 事实 |
| --- | --- |
| BUG-1375 | full-reading 的西占层永远 `blocked`:引擎调用了 `unified_western_reading`,文件没搬(实证 1) |
| BUG-1376 | full-reading 比上游少 5 个模块键,合盘证据包「次要大运互证」整项 `blocked`(实证 2~6) |
@@ -0,0 +1,60 @@
# TASK · full-reading 补齐上游模块 · 修改单(issue #81)
- 日期:2026-10-18
- 被看提交:`900581068e06df14ab1f3de6bf5e7bbfe2ad1dee`(分支 `codex/full-reading-module-parity-20261018`)
- 原任务书:`docs/tasks/TASK-full-reading-module-parity-20261018.md`
- 执行方:Grok,同一分支追加提交;`origin/staging` 前进时用 `git merge origin/staging`,不 rebase
- 性质:**不是打回**。验收中发现一条与既有合同冲突的规则,产品已拍板;本单写清怎么按决定改,连同 issue 里的 F2b 补充一起做
- BUG 编号:沿用 BUG-1375、BUG-1376,不新开号
## 决策记录(产品负责人 2026-10-18,issue #81「我选 A」)
`tests/test_python_hygiene_budget.py` 的 `ROOT_PY_BUDGET = 446` 规定 `scripts/` 根目录 `.py` 文件数只许减不许增(`TASK-python-hygiene-20261013`)。本分支根目录 449 个。
产品选 **A**:**上游原样文件**可以放在 `scripts/` 根目录(与上游同路径,方便以后同步),预算为这几个文件相应调高;**我们自己写的文件仍不许增加**。
## 已核对(Claude)
- `scripts/classical_astrology.py`、`scripts/unified_western_reading.py` 与 `docs/research/upstream_full_reading_parity_bde5d615/` 副本逐字一致(只差 CRLF / BOM)。
- `scripts/western_chart_engine.py`、`scripts/western_real_case_calibration.py` 只有新增函数,没有整份覆盖。
## 要改的
### M1 · 删掉 `scripts/_script_root.py`
这是本地新建的文件,不是上游原样文件(上游副本里没有它),按决定不许占根目录名额。两个上游模块对它的引用都包在 `try / except ModuleNotFoundError: pass` 里,是可选的。
- 删掉该文件(它还带了 UTF-8 BOM)。
- 若删掉后两个上游模块有 import 失败,**不要**改上游文件;改为在调用方(`jyotish_engine.py` full-reading 组装处或测试)保证仓库根在 `sys.path` 上,或把辅助放进子目录,在进度里写明。
- 验收:`tests/test_full_reading_module_parity.py` 全绿,且其中「模块能导入」那条在没有 `_script_root.py` 时仍成立。
### M2 · 预算按决定调高,并写明例外
`tests/test_python_hygiene_budget.py`:
- `ROOT_PY_BUDGET` 由 446 改为 **446 + 本单新增的上游原样文件数**(预期 448),常量旁写三栏:
- 原值:446
- 新值:448
- 原因:产品 2026-10-18(issue #81 选 A):上游原样文件按上游路径放在 `scripts/` 根目录,`classical_astrology.py`、`unified_western_reading.py` 两个;本地新写的文件仍不得增加。
- 合并时若 `origin/staging` 的根目录计数已因其他单变化(例如 #79 合入),按合并后的实际数重算,只为本单的上游原样文件加名额。
### M3 · F2b:`niryaana_shoola_dasha` 改为占位
照 issue #81 2026-10-18 20:40 的补充:本单的 `niryaana_shoola_dasha` 键不再用 `rashi_dashas_native` 的 Niryaana Shoola 结果填,只写
```json
{"execution_status": "blocked", "reason": "口径更正中,见 TASK-niryaana-shoola-pvr-20261018"}
```
`navamsha_dasha` 照原计划。相应测试与进度同步改;原任务书 T3 对 Niryaana 那一行的验收改为「校验器判为不适用,合盘任务不因它 blocked」。
### M4 · 交回前必跑
- 全量 `pytest tests` 与开工时 `origin/staging` 逐名比对(预算这条正是全量才抓得到的)。
- `python3 -m pytest tests/test_python_hygiene_budget.py tests/test_rashi_dashas_native.py tests/test_full_reading_module_parity.py -q`。
- 屏蔽 PyJHora 后再跑新增测试(原任务书已要求)。
## 不要做
- 不改两个上游原样文件;不整份覆盖 `western_*`。
- 不把预算调到超过上游原样文件所需的数。
+40 -48
View File
@@ -4,8 +4,9 @@
- `origin/staging` = `f95eb670b`(开工前 `git fetch origin --prune`,以最新 `origin/staging` 为准)
- 分支:`codex/marriage-match-p1-20261017`;工作树:`.worktrees/marriage-match-p1-20261017`
- **前置**:`TASK-people-archive-limit-20-20261017.md` 必须先合入 staging。两单都改 `components/people/people-page.tsx`,本单在它之后从最新 `origin/staging` 开分支。
- 原型(v4,规则已按共识口径改):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名、分数、宫位全是虚构示例。
- **前置**:~~档案上限单先合入~~ 第六版起本单不碰前端页面(D16),**不再依赖 #78,可以直接开工**。
- **后续单**:`TASK-synastry-full-report-20261018.md`(#82,完整合盘报告)在本单与 #78、#81 合入后开工,复用本单的引擎、证据包接口、存储与服务端函数;选人页和所有入口改动都在那一单。证据包不入库。本单只存两个 profile 的配对结果(`structured_result.default` / `raman`)。#82 要证据包时自己调证据包接口;已保存命中时服务端函数不返回证据包。
- 原型:https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe (v8;本单没有任何页面,原型给的是后续单的样子)
## 现状实证(按符号定位)
@@ -33,76 +34,65 @@
| --- | --- |
| D1 | 合盘做成独立功能:侧栏一项「**婚恋合盘**」,独立页面,页面标题「婚恋合盘」。档案里**任意两个人**都能合(包括两个都不是本人) |
| D2 | 第一版**只做恋爱 / 婚姻**,不给选关系类型;旧的 `relationshipType`(business / family / general)分支删除 |
| D3 | **一次合盘按一次报告扣点**(`report.full` 的定价)。同一对人、两人出生资料都没改、规则版本没变时,再次打开直接读已保存的结果,不扣点 |
| D4 | 八项**逐项展示**,不做大号总分;历史列表不挂分数 |
| D5 | **不考虑同性**:男方女方按档案性别定;两人性别相同或有一人没填时,按选择顺序第一个人当男方,不提示、不互换平均(推翻同日早先「互换取平均」的说法) |
| D3 | ~~按 `report.full` 扣点~~ **第五版改**:扣点移到后续单(单独计费键 `synastry.full`,随完整报告一起预留 / 结算)。**本单不扣点**;同一对人、两人出生资料没改、规则版本没变时读已保存的结构结果 |
| D4 | 配对表**逐项展示**(第四版起为七项、满分 35,见 D12),不做大号总分;历史列表不挂分数 |
| D5 | ~~按档案性别定男方~~ **第四版撤销**:上游已改成两个方向各算一次取平均,结果与顺序、性别都无关。本单**不读性别、不分男女方**,按选人顺序传 `person_a` / `person_b` 即可;页面不出现「男方 / 女方」 |
| D6 | 副标题定稿:「看这段感情往后的走向,和两人之间的助力与考验。」页面任何地方不写「合不合」「会不会在一起」 |
| D7 | 新增星盘入口放在两个选人下拉的**最底一行**「+ 新增星盘」,本页弹出与星盘档案同一张新建表,保存后自动选进这一格;档案满时这一行变灰 |
| D8 | 「开始合盘」按钮居中,点数说明在按钮正下方 |
| D9 | **规则口径定为「从业者共识 + 分歧并列」`practitioner_consensus_v1`**(2026-10-17 第二版)。占星师复核意见:这些规则不能只按一本书定,参与的信息很多,不能粗暴下断语,不要刻板。所以不再以 Raman《Muhurtha》一书为准,改为按印度占星从业者公开资料的共识分档:**广泛公认的作默认,常见的、有争议的都列出来并注明分档**。**推翻**同日第一版的 `raman_muhurtha_v1`,以及现有 `calc_kuja_dosha` 的五星合分、旺弱打分表、`match_kuja_dosha` 的性别不对称阈值。旧打分与性别不对称不得出现在新功能里,也不作为并列口径展示(产品 10-17 同意) |
| D10 | **规则有分歧时并列展示**:每条命中的规则都标出分档(广泛公认 / 常见 / 有争议),并写明按另一派结果会变成什么,不悄悄只选一个。轻重一律用文字,不出百分比,也不出伪精确分数 |
| D9 | **引擎直接搬上游占星师的实现**(产品 2026-10-17 第三版):`yinduzhanxing` 提交 `bde5d615`(分支 `codex/personal-report-export-r9`;第三版曾固定 `3c01a214`,第四版改为此提交)的 `scripts/marriage_match.py`(默认 profile `practitioner_consensus_v1`,可选 `raman_muhurtha_v1`)原样搬入,口径以它为准。**推翻**本任务书前两版自写的规则:同日第一版的 `raman_muhurtha_v1` 细则,以及第二版自写的化解条件分档表、「未见 / 轻 / 明显」归档规则、Yoni 五档表。仍然推翻现有 `calc_kuja_dosha` 的五星合分与 `match_kuja_dosha` 的性别不对称(上游模块本身也不用它们) |
| D10 | **规则有分歧时并列展示**:默认 profile 的结果在前;`raman_muhurtha_v1` 与默认结果不同的地方,用「按 Raman 一派:……」并列写出,不悄悄替换。化解条件按调研记录 `docs/research/marriage_match_practitioner_consensus_2026_10_17.md` 加上分档标签(只影响展示,不改上游结果) |
| D11 | 第二期(本单不做、不留占位):两人大运同步「什么时候顺」、就这对关系开对话、导出报告、非婚恋关系 |
| D12 | **第四版(产品 2026-10-18)跟随上游修复**:① 配对层改为上游的「国际化核心」——Varna(种姓)从默认结果里去掉,剩七项、满分 **35**;每项两个方向平均,会出现 0.5 分;② 本地不再有任何与上游不同的改动(0° 月亮、白羊上升两个 bug 上游已修);③ 「考验较多」的分数线按比例换算为 **低于 17.5 分**;④ 上游新增的四个「可以一起聊聊」的问题(不打分、不收集回答)放在结果页脚注前 |
| D13 | 上游 main 10-17~18 新增的「完整合盘解读」流程(双语报告、D9 互动证据、章节覆盖门槛等 26 个提交)**不在本单范围**,留作第二期参考 |
| D14 | **第五版(产品 2026-10-18):完整报告就是合盘结果**。本单的结果页只是后续报告里的最后一节「传统配对参考」:七项配对、火星冲克、婚姻宫压力。**删掉**「助力多于考验」三档落点、三句话总述、「各自的婚姻征象」一节(由报告第一、二节代替;上游也要求开头不放分数和结论档位)。本单合入后 staging 上能看到选人页和这一节;生产保持 `compatibility` 开关关闭,直到后续单合入 |
| D15 | 证据包接口在本单做:两人出生资料 → 两次 `full-reading` → 上游 `build_full_synastry_dossier`(它内部已调用默认 profile 的 `marriage_match`),再对它的 `chart_snapshots` 补调一次 Raman profile。本单只用其中的 `marriage_match` 部分;其余写作素材留给后续单 |
| D16 | **第六版(产品 2026-10-18):报告正文是关系画像,技术层不给用户看**。35 分、火星冲克、婚姻宫压力是后续单的写作底稿,并进入后续单默认折叠的「技术依据摘要」(格式由产品另给)。因此本单**不做任何页面**:T6 结果页取消、选人页移到后续单;T5 由 Next 路由改为服务端函数;T8 的入口改动移到后续单。本单只做引擎、证据包接口、存储、退役旧模块。D10 的「并列展示」改为两个 profile 都算、都存,供后续单写作时使用 |
### 规则口径 `practitioner_consensus_v1`
### 引擎口径(上游 `marriage_match.py`,摘要)
调研记录:`docs/research/marriage_match_practitioner_consensus_2026_10_17.md`(与本任务书同批推送,逐条附来源链接)。公开资料多为商业站互相转录,经典出处多是二手转引。所以分档只说明「从业者有多常用」,不说明「对不对」。
**火星冲克(Kuja / Mangal Dosha)**:只看火星。
| 项 | 默认 | 同时列出 |
| 层 | 上游行为(照搬) | 我们在外面加的 |
| --- | --- | --- |
| 参照点 | 从上升、月亮、金星三处分别数,逐处列出「从哪里数、落第几宫」。**不给权重** | — |
| 犯宫 | 1、2、4、7、8、12(多数采用) | 每处触发都注明「不计 2 宫的版本(1/4/7/8/12)下是否仍算」「不计 1 宫的版本(2/4/7/8/12,Raman)下是否仍算」 |
| 化解:广泛公认 | 火星本垣(白羊、天蝎)或入旺(摩羯);木星与火星同宫或木星相位火星 | — |
| 化解:常见 | 水星 / 月亮 / 金星与火星同宫或相位;宫 × 星座例外(2 宫双子/处女、4 宫白羊/天蝎、7 宫巨蟹/摩羯、8 宫射手/双鱼、12 宫金牛/天秤,各家星座略有出入);巨蟹或狮子上升(火星为 Yogakaraka);火星在动星座 | 写明「常见,各家写法不一」 |
| 化解:有争议 | 火星落巨蟹(落陷)、逆行、燃烧;火星在狮子或水瓶;28 / 30 岁后减弱(多处明说无经典依据) | 写明「有争议」,不参与轻重判断,只列出 |
| 单人轻重(文字) | **未见**:三处都没触发,或每处触发都被广泛公认的条件化解;**轻**:触发只被常见条件化解,或只在某一宫位版本下成立;**明显**:至少一处触发没有任何广泛公认或常见的化解 | 页面脚注写明:这是计算层的归纳,不是定论,要结合整盘看 |
| 两人比较 | 两人都「明显」或都「轻」→ 相互抵消(广泛公认,前提是程度相当);一方「明显」、另一方「未见」→ 需要留意,并提示看婚姻宫压力是否平衡。**不分男女** | — |
| 配对层(35 分) | `_ashtakoota_with_exceptions` 对 `calculate_ashtakoot` 正反各算一次,七项(互控 Vashya、星宿 Tara、性情 Yoni、月主友谊 GrahaMaitri、气质 Gana、月宫距离 Bhakoot、脉 Nadi)各自平均,结果在 `international_core`;Varna 标 `excluded_from_default`。Bhakoot(同主星、互为朋友)与 Nadi(同星宿不同 pada、同星座不同星宿)只给 `candidate_cancellations`,不改分 | 按选人顺序传入,不读性别。页面七行,分数按上游原样显示(含 .5),表尾两个合计:原始合计 / 35、计入候选例外后的合计 / 35;表下一行小字「传统第八项种姓(Varna)不计入」 |
| 性情 Yoni | 保留旧 4 / 2 / 0 三档,标 `legacy_three_band_table`;中间档要等有正式出处的完整表 | 页面注明「沿用三档旧表,中间档待定」 |
| 火星冲克 | 只看火星;从上升、月亮、金星三处;默认犯宫 1/2/4/7/8/12(Raman:2/4/7/8/12);宫 × 星座例外表直接取消该处;本垣 / 入旺、木星同宫、月亮同宫列为 mitigation,**不改变该处是否触发**;狮子 / 水瓶默认只标 disputed(Raman 下取消)。轻重 = 触发处数 0/1/2/3 → none / mild / material / strong;两人比较 = 处数差 ≤ 1 为 broadly_balanced,否则 asymmetric,不分男女 | 中文:无 / 轻 / 明显 / 重;相当 / 一方偏重。mitigation 中文名 + 分档标签:本垣、入旺、木星同宫(广泛公认),月亮同宫、宫 × 星座例外(常见),狮子 / 水瓶(有争议)。每处写「减轻,不改变这一处」 |
| 婚姻宫压力 | 火星、土星、太阳、罗睺、计都,从三处数,用 profile 的宫位集;只列出 | 两人处数的一句比较(大致均衡 / 一方明显偏重,差 ≥ 3 处或两倍以上),不出加权分 |
| 各自婚姻征象 | `relationship_structure` 只报告调用方传进来了哪些层 | 第五版:不再用 `relationship_analysis` 另算;证据包(D15)已含两人完整命盘,各自婚姻征象由后续单的报告第一、二节写 |
| 输入 | `marriage_match` 只收已算好的盘快照,不收出生资料 | 证据包接口(T2)收两人出生资料、算两次 `full-reading`,再把快照交给 `marriage_match`(沿用 BUG-1333 的并行做法) |
**婚姻宫压力(Papasamya)**:多为南印做法。火星、土星、太阳、罗睺落在从上升、月亮、金星数的 1、2、4、7、8、12 宫,逐处列出;计都单列一行,标「部分派别计入」。两人比较只写「大致均衡 / 一方明显偏重」。**不出加权总分**(各派权重不同,经典没有公布分表)。南印习惯里「女方不得多于男方」的比较带性别不对称,不采用,脚注说明。单独一节,不叫火星冲克。
~~**结论措辞**三档落点~~:第五版删除(D14)。
**八项配对(36 分)**
| 项 | 默认 | 同时列出 |
| --- | --- | --- |
| 性情 Yoni | 同类 4、七对死敌 0(牛—虎、象—狮、马—水牛、犬—鹿、猫—鼠、蛇—獴、羊—猴),广泛公认。中间档 3 / 2 / 1 用一张版本化 14 × 14 表计分(以 PyJHora `YoniArray` 对照写入、注明出处),页面用文字写「友好 / 中性 / 略不合」 | 注明「中间档各家表不同」;南印只判是否死敌,并列一行「南印:是 / 否死敌」 |
| 月宫距离 Bhakoot | 原始分照算照显示,并标类型(2/12、5/9、6/8)。两人月宫同一主星、或两主星互为朋友 → 例外成立(广泛公认 / 多数采用),单独标出;**不改原始分** | 九分盘主星友好、其他项满分可抵:标「部分占星师接受」 |
| 脉 Nadi | 原始分照算。例外(广泛公认):同一月亮星座不同星宿;同一星宿不同月亮星座。多数采用:同一星宿不同 pada | 月宫主星友好:标「少数」;豁免星宿名单:标「有争议」,并写明出自哪份名单 |
| 合计 | 表尾并列两个数:原始合计、计入广泛公认例外后的合计。**不显示及格分档**:常见及格线是 18,但各家分别用 18 / 25 / 27 / 28,脚注写「满分 36,常见及格线 18,各家不一」 | — |
`ashtakoot.py` 现有的附加项(Mahendra、Stree Deergha、Vedha、Rajju、BadConstellations)保留展示,例外按上面同样的分档标注。
**结论措辞**:三句话总述的落点只用三档:「助力多于考验」「有几处需要留意,建议结合整盘细看」「考验较多,建议请占星师细看」。火星冲克一方「明显」另一方「未见」时,至少落到第二档。总述以各自婚姻征象(D1 第 7 宫、D9、UL、DK)为主,36 分只作一层参考。
**上游两个 bug(10-17 报给占星师,10-18 已在 `bde5d615` 修复,Claude 复测通过)**:顺序依赖已由「两个方向取平均」消除(随机 3000 对对调结果全一致);`or` 把 0 当缺失已改为显式 `is None`,上游各有回归测试。本单**原样搬入,不做本地改动**;若上游 main 之后有新修复,验收时由 Claude 决定是否同步。
## 硬红线
1. `scripts/jyotish_api_server.py` 不增长(类方法数、`__new__` 伪造点、行数护栏):新计算放新模块(建议 `scripts/marriage_match.py`),主文件只做薄注册。
2. `app/(app)/page.tsx` 的 `Home()` 状态数不得增长;本单删掉 `SynastryDeepLink` 与开机拉合盘历史,只会减少。
2. 本单不改前端页面与 `app/(app)/page.tsx`(第六版,入口清理在后续单)。
3. 新功能里不得出现:五星合分、任何百分比或 100/80/50 之类的火星冲克分数、Papasamya 加权总分、按性别判轻重、「合 / 不合」「相合」「会不会在一起」「适合 / 不宜结婚」「分离」「寿命」。
4. 生产运行时不得 import PyJHora(只作测试对照,许可证边界);验收会屏蔽 PyJHora 模拟生产。
5. 合同测试的 fixture 用真实引擎输出(golden)。所有示例与测试只用虚构人物或公开名人。
6. 扣点:先预留、算失败或超时**释放不扣**;读已保存结果不预留。用现有 `authorizeUsage` / `completeUsage` 及其释放路径,记账 metadata 标明 `kind: "marriage_match"`。
7. 一次等待一次揭幕:等待只有一屏,结果整页一起出现;揭幕后无 spinner / 骨架。
6. ~~扣点~~:第五版移到后续单(D3)。本单不得调用 `authorizeUsage`。
7. ~~一次揭幕~~:本单无页面。
8. 加表 / 加列可以同轮,不得删旧列(AGENTS §7.6);动表必须真跑 `npm run test:db`。
9. 删人:删档案里的人时,这个人参与的合盘一并删除;删前确认句在「N 个对话和 M 份报告」后加上「K 份合盘」。
9. 删人:删档案里的人时,这个人参与的 `synastry_reports` 行一并删除(数据库层);删前确认句的文案改动在后续单。
## 任务分解
| # | 内容 | 验收标准 |
| --- | --- | --- |
| T1 | **引擎**:新模块实现 `practitioner_consensus_v1`:火星冲克(三参照点 × 三种宫位版本 × 命中的化解条件及其分档)、婚姻宫压力、八项(Yoni 表、Bhakoot / Nadi 例外分档)、单人婚姻征象(复用 `relationship_analysis.analyze_relationship`:7 宫主、金星、D9、UL、DK)、D9 对照(两人 D9 上升、D9 金星、D9 上升相隔几宫)。输入两份出生资料 + 性别,一次请求内算完(占重计算并发位)。每条规则带 `consensus: broad / common / disputed` 和来源键,来源键对应调研记录里的条目 | 新 pytest:每条化解条件至少一例且分档正确;宫位三版本差异各一例;单人「未见 / 轻 / 明显」与两人比较各至少一例;有争议的条件不改变轻重;Yoni 14 × 14 与 PyJHora 对照全等(测试里可 import PyJHora,缺时 skip 并记录),七对死敌为 0;Bhakoot / Nadi 例外不改原始分;至少 20 对虚构出生资料的八项原始分与 PyJHora North 方法对照,差异逐条解释 |
| T2 | **Python 接口**:新端点(如 `POST /api/marriage_match`)薄注册;旧 `/api/synastry` 保留给 MCP / CLI 兼容但不再被网页调用 | `tests/test_api_server_growth_contract.py` 绿;端点 golden 一份进 `frontend/tests/fixtures/` |
| T1 | **引擎**:把上游 `scripts/marriage_match.py` 与 `tests/test_marriage_match_product.py`(`bde5d615`,副本在 `docs/research/upstream_marriage_match_bde5d615/`)原样搬入;把上游 `skills/jyotish-full-synastry-reading/` 整个目录(副本在 `docs/research/upstream_full_synastry_bde5d615/skill/`,`.txt` 后缀去掉)原样搬到同路径。**都不做改动**(`scripts/ashtakoot.py` 与上游相同)。新增本地胶水模块(建议 `scripts/synastry_full_dossier.py`):两份出生资料 → 并行两次 `full-reading`(复用 `_compute_full_reading_for_thematic` 那条路径)→ `build_full_synastry_dossier` → 对 `chart_snapshots` 补调 Raman profile | 上游 `test_marriage_match_product.py` 12 条过(调 `_compute_marriage_match` 那条按 T2 改写,三栏说明);`diff` 上游文件为空;新增:两人对调后配对层、火星冲克、婚姻宫压力一致;两个公开名人的证据包 `delivery_readiness = ready_for_narrative` |
| T2 | **Python 接口**:`POST /api/synastry_full_dossier`(收两份出生资料,返回证据包 + Raman 结果)与 `POST /api/marriage-match`(与上游同名,只收快照)薄注册,实现在独立模块;证据包接口进 `HEAVY_COMPUTE_PATHS`,按两人出生资料 + 提交号做磁盘缓存(照 `report_reference_cache` 的做法);旧 `/api/synastry` 保留给 MCP / CLI 兼容但不再被网页调用;CLI 加 `marriage-match` 命令 | `tests/test_api_server_growth_contract.py` 绿;两个接口各一份 golden 进 `frontend/tests/fixtures/`(证据包可截取,写明截取规则) |
| T3 | **退役与文档纠错**:删 `scripts/synastry.py`,CLI `synastry` 改走新模块或删除(同步 `SKILL.md` 命令表、`tests/run_all.py`、`tests/test_open_source_integrations.py`);`ashtakoot.py` 的 `calc_kuja_dosha` / `match_kuja_dosha` 标为旧版、不被新功能调用;删掉三处 Papasamya / Mangal 匹配虚报 | 全量 pytest 与基线逐条比对无新增失败;`git grep "synastry.calc_ashtakoot"` 0 命中 |
| T4 | **数据**:`synastry_reports` 加列——两人的人物引用(本人用约定值,他人用 `chart_profiles.id`)、两人出生资料指纹、规则版本 `rules_version`、结构化结果;旧行不迁移、不显示 | `npm run test:db`:同一对人同指纹第二次读到旧行;任一人改出生资料后指纹不同;删人后其合盘行消失 |
| T5 | **Next 接口**:重写 `app/api/synastry/route.ts`(或新路由,旧的删):收两个人物 id → `loadSubjectBirth` 两次 → 查已保存(命中即返回,不扣点)→ 预留 `report.full` → 调 T2 → 存 → 结算;性别按 D5;保留产品开关 `compatibility` 与限流桶;删掉 `relationshipType` 与 business / family / general 分支 | 路由测试覆盖:命中缓存不扣点;计算失败释放不扣;两个 id 相同 400;不属于本人的 id 拒绝;同性 / 缺性别按顺序定男方 |
| T6 | **页面**:侧栏「婚恋合盘」;`/marriage-match`(路由名可议,写进 DESIGN)选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮;点数来自定价不写死;之前的合盘列表只列「人名 × 人名 · 日期」,点开不扣点);结果页按原型 v4:三句话总述 → 各自的婚姻征象 → 八项配对(含例外绿字 / 另一派灰字、两个合计)→ 火星冲克(每人逐处列出触发与化解,化解条件带分档标签;两人对比)→ 婚姻宫压力 → 脚注(口径来源、分档含义、「计算层归纳不是定论」) | 渲染测试:两个状态(两人火星冲克相当 / 一方明显)的 golden 渲染;20 人时下拉可滚动;手机 390 px 无横向滚动;`next build` 后 `/` 仍 Static,首屏 gzip ±2% |
| T7 | **三句话总述**:由服务端按结构化结果确定性拼句(不调模型),讲各自征象、相处的助力与考验、八项合计只作一层参考;落点只用「结论措辞」的三档,第二、三档讲清原因 | 单测覆盖两种状态;文案进 VOICE |
| T8 | **清旧入口**:档案页「和我合盘」改跳新页并预选两人;删 `SynastryDeepLink`、`use-synastry.ts` 中无人用的部分、`buildSynastryQuestion` 草稿路径、孤儿 CSS、`page.tsx` 开机拉合盘历史 | 按 AGENTS §7.8 grep 源码文本合同测试同轮改,三栏说明 |
| T9 | **记录**:调研记录已由 Claude 写好,执行方只需在 PROGRESS 引用;DESIGN.md、VOICE.md(新文案、删人确认句、侧栏名)、CHANGELOG、BUG_HISTORY(下节编号)、`docs/testing/marriage-match-p1-20261017.md` 真人清单(选人、新增星盘、扣点与再次打开不扣、删人连带删除、手机宽) | 文档齐 |
| T5 | **服务端函数**(第六版):建议 `frontend/src/lib/marriage-match-service.ts`:收两个人物 id → `loadSubjectBirth` 两次 → 查已保存(同一对人不分先后 + 两人出生资料指纹 + `rules_version` 命中即返回)→ 调 T2 证据包接口 → 存结构结果(两个 profile 的 `marriage_match`)并返回证据包。**不扣点、不做路由**;旧 `app/api/synastry/route.ts` 暂不删(后续单删入口时一起删) | 单测覆盖:命中已保存不再调引擎;两个 id 相同报错;不属于本人的 id 拒绝;两人对调命中同一行 |
| T6 | ~~页面~~ 第六版移到后续单(D16) | — |
| T7 | ~~三句话总述~~ 第五版删除(D14),由后续单报告第一节代替 | — |
| T8 | ~~清旧入口~~ 第六版移到后续单(D16) | — |
| T9 | **记录**:调研记录已由 Claude 写好,执行方只需在 PROGRESS 引用;CHANGELOG、BUG_HISTORY(下节编号)、PROGRESS(与上游的差异:无;改写的那条上游测试三栏说明) | 文档齐 |
## 让步顺序
先保 T1–T5(算对、存对、扣对);T6 不可省;T7 若来不及可先用固定模板句但不得出现红线 3 的词;T3 的 CLI 改写可降为「删 `synastry.py` + CLI 命令删除」。任何一项过不了红线就停下写进 PROGRESS,不得放宽口径。
先保 T1、T2、T4、T5(算对、存对);T3 的 CLI 改写可降为「删 `synastry.py` + CLI 命令删除」。任何一项过不了红线就停下写进 PROGRESS,不得放宽口径。
## 开工前置命令
@@ -110,9 +100,11 @@
git fetch origin --prune
git worktree add -b codex/marriage-match-p1-20261017 .worktrees/marriage-match-p1-20261017 origin/staging
cd .worktrees/marriage-match-p1-20261017
git log --oneline -5 -- frontend/src/lib/subject-birth.ts # 确认上限 20 人那单已在
grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
# 上游文件(上游仓库私有,已原样放进本仓只读参照目录,固定到 bde5d615)
ls docs/research/upstream_marriage_match_bde5d615/ docs/research/upstream_full_synastry_bde5d615/skill/
# marriage_match.py.txt → scripts/marriage_match.py;test_marriage_match_product.py.txt → tests/test_marriage_match_product.py
```
验证:全量 `pytest tests -n 4` 与开工基线逐条比对;`run_quality_gate.py --profile quick`;前端 `tsc --noEmit`、`npm run lint`、`npm test`(含 grep 源码文本合同测试)、`npm run test:db`、`npm run build`。
@@ -123,8 +115,8 @@ python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
| 建议编号 | 事实 |
| --- | --- |
| BUG-1370 | 合盘不看性别,本人恒为男方(实证 3) |
| BUG-1370 | 合盘结果依赖顺序,本人恒为男方(实证 3;修法:改为对调平均、不分男女) |
| BUG-1371 | 网页合盘从不计算火星冲克(实证 4) |
| BUG-1372 | 旧 `synastry.py` 互控写死 2.0 / 星宿下标越界(实证 7) |
| BUG-1373 | 结果卡随旧面板删除,合盘只剩聊天草稿(实证 2) |
| BUG-1373 | 结果卡随旧面板删除,合盘只剩聊天草稿(实证 2;本单只登记 investigating,由后续单 #82 修复并改 resolved) |
| BUG-1374 | 文档虚报 Papasamya / Mangal 匹配已实现(实证 8) |
@@ -0,0 +1,47 @@
# TASK · 婚恋合盘第一期 · 第二份修复单(issue #79,第 2 次打回)
- 日期:2026-10-18
- 被验提交:`cf5a62c684b0523dbc33406bbdc5067eb513bb29`(分支 `codex/marriage-match-p1-20261017`)
- 前一份修复单:`docs/tasks/TASK-marriage-match-p1-fix-20261018.md`(F1、F2 已通过)
- 原任务书:`docs/tasks/TASK-marriage-match-p1-20261017.md` 第六版
- 状态:**第 2 次打回,按循环规则等产品负责人拍板**(选项见 issue #79)。下文是改法,谁来做由产品定。
- BUG 编号:不新开号
## 验收结论(Claude,Linux,合并最新 `origin/staging` `0d92f586` 后)
| 项 | 结论 | 证据 |
| --- | --- | --- |
| 修复单 F1:只存两套配对结果 | 通过 | `structured_result` 与 `report` 都只有 `rules_version` / `default` / `raman`;旧历史接口加了 `rules_version is null` 过滤并有测试 |
| 修复单 F2:并发重读 | 通过 | `23505` 时重读已保存行 |
| 合并 staging | 只碰文档 | `CHANGELOG.md`、`docs/BUG_HISTORY.md` 冲突;`scripts/jyotish_engine.py` 与 sync8 自动合并,合并后相对 staging 只有本单的段落 |
| **全量 pytest** | **未通过** | 与 staging `0d92f586` 基线逐名比对,新增 2 个文件、3 条失败(见 G1、G2) |
## G1 · scripts 根目录 Python 文件数超预算
`tests/test_python_hygiene_budget.py::test_scripts_root_py_count_does_not_grow`:`449 <= 446` 失败。本单在 `scripts/` 根目录新增 4 个(`marriage_match.py`、`marriage_match_api.py`、`marriage_match_cli.py`、`synastry_full_dossier.py`),删 1 个(`synastry.py`),净增 3。预算由 `TASK-python-hygiene-20261013` 定下(「新研究脚本放 `scripts/audit` 或 `scripts/research`」),早于本单任务书;任务书建议的放置位置与它冲突,按既有合同为准,**不改预算**。
改法:
- `scripts/marriage_match.py` 留在根目录(与上游同路径,T1 要求逐字相同)。
- 三个本地胶水模块搬进新子包 `scripts/marriage_match_service/`(名字可议,写进进度):`api.py`、`cli.py`、`full_dossier.py`,加 `__init__.py`。
- 同步改调用处:`scripts/jyotish_api_server.py` 两处 `_load_local_module('marriage_match_api')`(改为能加载子包模块的写法,保持主文件只做薄注册、不增方法)、`scripts/jyotish_engine.py` 的 `cmd_marriage_match` import、`tests/test_marriage_match_api.py`、`tests/test_synastry_full_dossier.py`、`tests/test_marriage_match_product.py` 里改写过的那条(三栏注释里的「新值」同步更新)。
- 验收:根目录计数 ≤ 446;`tests/test_api_server_growth_contract.py` 绿。
## G2 · 删掉的 `scripts/synastry.py` 还被调用清单引用
`tests/test_owner_case_purge_contract.py` 两条失败(`test_effective_view_matches_overlay_without_replacement_oracle`、`test_invocation_matrix_paths_counts_hash_and_boundaries`):`references/oracle/full_technique_invocation_matrix_2026_07_22.json` 仍列着 `scripts/synastry.py`,文件已删,`is_file()` 为假。AGENTS §7.8:删文件前先 grep `tests/` 与 `references/`。
改法:
- 在该清单里把这一项改指向 `scripts/marriage_match.py`(或删除该项),并按该合同测试要求同步计数 / 哈希(看测试里怎么校验,按它的规则更新,不放宽断言)。
- `references/oracle/compatibility_skill_readiness_dashboard_2026_07_20.json` 由 `scripts/compatibility_skill_readiness_dashboard.py` 生成,脚本里已改成 `marriage_match.py`;重新生成一次让 JSON 与脚本一致。
- 验收:`python3 -m pytest tests/test_owner_case_purge_contract.py tests/test_python_hygiene_budget.py -q` 全绿;全量 pytest 失败名单与 staging 基线逐名一致。
## 不要做
- 不改 `ROOT_PY_BUDGET`;不改 `scripts/marriage_match.py`、`skills/jyotish-full-synastry-reading/**`。
- 不动 F1/F2 已通过的存储逻辑。
## 交回前必跑
全量 `pytest tests` 与开工时 `origin/staging` 逐名比对(这两条正是全量才抓得到的)。
@@ -0,0 +1,57 @@
# TASK · 婚恋合盘第一期 · 修复单(issue #79,第 1 次打回)
- 日期:2026-10-18
- 被验提交:`cf063834ca9f9f62dc8559913dddb5ccb8578999`(分支 `codex/marriage-match-p1-20261017`)
- 原任务书:`docs/tasks/TASK-marriage-match-p1-20261017.md` 第六版(下称「原单」)
- 执行方:Grok,同一分支追加提交;`origin/staging` 前进时用 `git merge origin/staging`,不 rebase(合并时 `CHANGELOG.md`、`docs/tasks/README.md` 会有纯文档冲突,按两边都保留处理)
- BUG 编号:沿用 BUG-1370~1374,不新开号
## 验收结论(Claude,Linux)
引擎与上游搬运部分**通过**;存储层(T4/T5)与原单不符,打回。
| 项 | 结论 | 证据 |
| --- | --- | --- |
| T1 上游原样搬入 | 通过 | `scripts/marriage_match.py` 与 `docs/research/upstream_marriage_match_bde5d615/marriage_match.py.txt` 逐字相同;`skills/jyotish-full-synastry-reading/` 全部文件与副本(去 `.txt`)逐字相同;上游测试只改了调用那一处并写了三栏 |
| T1 生产依赖 | 通过 | 屏蔽 `jhora` 后两位公开名人的证据包 `delivery_readiness = ready_for_narrative`,截取结果与 `frontend/tests/fixtures/synastry-full-dossier-truncated.json` 相同 |
| T2 接口 | 通过 | 主文件只加 14 行薄注册;证据包接口进了 `HEAVY_COMPUTE_PATHS`,按两人出生资料 + 提交号做磁盘缓存 |
| T3 退役 | 通过 | `scripts/synastry.py` 已删;CLI 改为 `marriage-match`;`calc_kuja_dosha` / `match_kuja_dosha` 标为旧版 |
| **T4/T5 存储** | **未通过** | 见 F1、F2 |
| 全量 pytest / 前端 | 见 issue 评论里的数字 | — |
## F1 · 存了整份证据包,而且存了两份(必做)
`frontend/src/lib/marriage-match-service.ts::readResolvedPair` 把引擎返回的整个包(`rules_version` + `dossier` + `raman_marriage_match`)同时写进 `report` 和 `structured_result` 两列。实测(爱因斯坦 × 乔布斯,公开盘):整包约 **490 KB**,其中两个 profile 的 `marriage_match` 合计只有约 **19 KB**。
问题:
1. **与原单不符。** 原单 T5:「存结构结果(**两个 profile 的 `marriage_match`**)并返回证据包」;T4:加列存「结构化结果」。证据包是写作素材,不是要长期存的结构结果。
2. **会过期。** 证据包里有 `parallel_timing_context` 等随「今天」变化的内容,而命中条件只有「两人出生资料指纹 + 规则版本」,不含日期。整包存下来,以后每次命中都会读到建档那天的时间背景。`marriage_match` 本身只依赖两张盘,没有这个问题。
3. **一行近 1 MB。** 旧的合盘历史接口 `app/api/synastry-reports/route.ts` 的 GET 会 `select("id, partner_name, report, created_at")` 取最近 10 行;新行的 `report` 是整包,开了 `compatibility` 后一次就可能拉近 10 MB,而且形状不是旧卡片能读的。
修法:
- `structured_result` 只存 `{ rules_version, default: <dossier.marriage_match>, raman: <raman_marriage_match> }`(键名可定,写进进度)。
- `report` 列(旧列,`not null` 且必须是对象)**不放证据包**:放同一份结构结果,或一个只含 `rules_version` 的小对象,二选一写进进度。
- 函数返回值区分两种:现算时返回 `{ cached: false, structured, dossier }`;命中时返回 `{ cached: true, structured }`,**不返回旧证据包**。后续单 #82 需要证据包时自己调证据包接口(Python 侧有磁盘缓存)。在原单「后续单」一节或进度里写明这个约定。
- 旧历史接口 GET 加过滤 `rules_version is null`,只列旧行;新行不进旧列表。
验收:
- 单测:存进去的 `structured_result` 不含 `dossier` / `chart_snapshots` 键;大小断言(如 < 64 KB)用 golden 结构结果。
- 单测:命中时不调引擎,返回值不含证据包。
- 旧历史接口有一条测试证明新行不出现在 GET 结果里(可用源码文本断言或现有 route 测试模式,按 AGENTS §7.8 不弱化旧断言)。
## F2 · 并发同一对人时第二个写入报「存储失败」(必做,小)
唯一索引 `synastry_reports_pair_fingerprint_uidx` 会让同时发起的第二次插入失败,现在直接抛 `store_failed`。改为:插入遇到唯一冲突(PostgreSQL `23505`)时重读一次已保存行并返回;重读仍为空才报 `store_failed`。加一条单测。
## 不要做
- 不改 `scripts/marriage_match.py`、`skills/jyotish-full-synastry-reading/**`(上游原样)。
- 不做页面、不扣点(原单第六版)。
- 迁移文件可以在同一份里改(还没上过 staging),不要另开第二份迁移去补救本单自己的列。
## 回报
issue #79 回报新 SHA,写明:结构结果的键名与大小、`report` 列选了哪种、旧历史接口的过滤和测试、`npm run test:db` 中 `database-marriage-match.test.ts` 的结果(Windows 已知失败名单逐条对照)。
@@ -0,0 +1,63 @@
# 任务书:Niryaana Shoola 改为 PVR 口径(2026-10-18)
## 基线
- `origin/staging` 以开工时最新为准(`git fetch origin --prune`)。
- 分支:`codex/niryaana-shoola-pvr-20261018`;工作树:`.worktrees/niryaana-shoola-pvr-20261018`。
- 顺序:与 #81(full-reading 补齐模块)都碰 `niryaana_shoola_dasha` 这个键。#81 先合入;本单在它之后开分支,把 #81 留下的「暂不填」占位换成真结果。
- 上游参照:占星师 `yinduzhanxing` main `7a773d13`(10-18)「correct niryaana shoola」。副本:`docs/research/upstream_full_synastry_7a773d13/niryaana_shoola_dasha.py.txt` 及同目录 `tests/test_niryaana_shoola_dasha.py.txt`。
## 事故实证
| # | 事实 | 位置 |
| --- | --- | --- |
| 1 | 我们完整数据版报告里的「Niryaana Shoola」与「Shoola」**是同一套算法**:两者都调 `_nine_year`,从上升与第七宫中取强、每宫 9 年、顺行,只是 profile 名不同(`shoola_forward_md_direct_ad_v1` / `goel_nirayana_shoola_v1`)。报告里这两张大运表逐行相同 | `scripts/rashi_dashas_native.py` 的 `calculate_rashi_dashas`、`_nine_year`;报告节表 `scripts/rashi_dasha_report.py` |
| 2 | 占星师 10-18 裁定:「上升 / 七宫取强、固定 9 年」属于 Shoola Dasha;Niryaana Shoola 应按 PVR:从**第二宫与第八宫**取强起运,奇数星座顺行、偶数星座逆行,动 / 固 / 双星座分别 7 / 8 / 9 年。上游已把 `goel_nirayana_shoola_v1` 标为误归类并禁止运行,新 profile 为 `pvr_niryaana_shoola_v1` | 上游 `scripts/niryaana_shoola_dasha.py`(`PROFILE_ID`、`LEGACY_MISCLASSIFIED_PROFILE_ID`) |
| 3 | 上游同时把 Niryaana Shoola 从恋爱婚姻合盘的应期里排除(寿元类大运,不用于关系时间) | 上游 `validate_full_synastry_packets.py`:该行固定 `not_applicable` |
| 4 | 只在 staging:`origin/main` 还没有 `rashi_dashas_native.py`,生产不受影响 | — |
另:上游 `cba6a031` 一度用 Bhava Bala 总分比二宫 / 八宫,Claude 复测发现两张名人盘起运都与 Jaimini 星座力量相反并提出质疑;占星师随后在 `9f08b57b` 改为寿命类星座力量规则(H4),不再读 Bhava Bala,平手明确 `blocked`。Claude 10-18 复测 `9f08b57b`:专项测试全过;两张名人盘一张起射手、一张起水瓶,奇顺偶逆与 7 / 8 / 9 年都对。
## 根因
BUG-1283(10-09)把旧 `niryaana_shoola_dasha.py` 换成 `goel_nirayana_shoola_v1` 时,选的规则其实是 Shoola 的,而且没有和同文件里的 Shoola 对比输出,两张表一样没被发现。
## 决策记录(产品负责人 2026-10-18)
| # | 决定 |
| --- | --- |
| H1 | Niryaana Shoola 采用占星师 10-18 的 PVR 口径(`pvr_niryaana_shoola_v1`)。**推翻 BUG-1283 选定的 `goel_nirayana_shoola_v1`**;BUG-1283 的另一半(旧 `niryaana_shoola_dasha.py` 不得被新路径调用)保持不变 |
| H2 | Shoola 保持现状(上升 / 七宫取强、每宫 9 年)——那本来就是 Shoola 的规则 |
| H3 | 合盘不用 Niryaana Shoola 做关系应期(与上游一致);本单只影响个人完整数据版报告和 full-reading 的 `niryaana_shoola_dasha` 键 |
| H4 | **起运星座按上游 `9f08b57b` 的寿命类星座力量选**(`pvr_niryaana_shoola_ayur_rasi_strength_v1`,占星师 10-18 裁定,推翻本单上一版「用我们的 `stronger_sign`」与上游 `cba6a031` 的 Bhava Bala):比较第二宫与第八宫,依次看 ① 宫内行星数 ② 所有行星对它的 Jaimini 星座相位数(宫主不加权)③ 宫内有无入旺行星;三层都平手则「本轮不上表」并写明「二宫与八宫力量相同,原书没有给出进一步的裁决规则」,不得借用 Narayana 的宫主规则硬选。计数的行星**固定为九曜**:七曜 + 罗睺 + 计都(占星师 10-18 确认,依据 PVR 赋予两者星座相位、寿命章节视其为实际影响;上游 `258e08e3`);天王、海王、冥王与 Upagraha 不计,节点不加额外权重。在 `rashi_dashas_native.py` 里按上游函数逐条实现,副本见 `docs/research/upstream_niryaana_shoola_9f08b57b/` |
| H5 | **只出主运,不出子运**(与上游一致:上游说明 PVR 没有独立给出子运规则,子运标为 blocked)。报告里该表只列主运并写一句「子运规则未定,暂不列出」 |
## 硬红线
1. 在 `scripts/rashi_dashas_native.py` 内改,不 import 旧的 `scripts/niryaana_shoola_dasha.py`(BUG-1283 语法树断言保持绿);不整份搬上游文件(上游版本依赖我们没有的 `dasha_profile_arbitration` 等)。
2. 力量比较只用 H4 的三层规则,**不用** `rashi_strengths` / `stronger_sign`(那套带宫主规则,PVR 说寿命类大运不适用);平手就「本轮不上表」,不得默认挑一个。
3. 全量 pytest 与基线逐条比对;改既有断言写三栏。
## 任务分解
| # | 内容 | 验收标准 |
| --- | --- | --- |
| T1 | `niryaana_shoola` 改为 PVR:第二宫与第八宫取强起运;起运星座奇数顺行、偶数逆行;动 / 固 / 双星座 7 / 8 / 9 年(12 宫一轮 96 年);**只出主运**(H5);profile 名 `pvr_niryaana_shoola_v1`,出处写 PVR《Vedic Astrology: An Integrated Approach》 | 新测试:① 同一张盘 Shoola 与 Niryaana Shoola 的首运或年数不再全同;② 白羊上升样例起运在金牛 / 天蝎中较强者;③ 奇偶方向各一例;④ 动 / 固 / 双年数各一例;⑤ 在上游测试样例上(样例直接给定二宫 / 八宫力量)与上游 `pvr_niryaana_shoola_v1` 的主运序列一致(把期望值抄过来作 golden,写明出处);⑥ 两位公开名人各一条,起运星座与主运序列必须与上游 `9f08b57b` 一致(Claude 10-18 实测:一张起射手、一张起水瓶);⑦ 构造一张三层都平手的盘,结果为「本轮不上表」;⑧ 节点占宫与节点星座相位各一条(照上游 `258e08e3` 的测试);⑨ 传入外行星时不计数 |
| T2 | full-reading 的 `niryaana_shoola_dasha` 键改用 T1 结果(替换 #81 的占位) | 上游合盘校验器读这一行仍为 `not_applicable`(合盘不用);个人报告节表显示新表 |
| T3 | 记录:BUG_HISTORY(新编号,关联 BUG-1283,写明「推翻其所选 profile」)、CHANGELOG(报告里 Niryaana Shoola 大运表变了)、PROGRESS | 文档齐 |
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/niryaana-shoola-pvr-20261018 .worktrees/niryaana-shoola-pvr-20261018 origin/staging
cd .worktrees/niryaana-shoola-pvr-20261018
grep -n "goel_nirayana_shoola_v1\|_nine_year" scripts/rashi_dashas_native.py
grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
```
验证:全量 `pytest tests -n 4` 与开工基线逐条比对;`run_quality_gate.py --profile quick`;屏蔽 PyJHora 后跑 `tests/test_rashi_dashas_native.py`。
## BUG 编号
开工时最大号 + 1(10-18 staging 最大为 BUG-1391,#80 在用 1380 段)。
@@ -0,0 +1,145 @@
# 任务书:婚恋合盘第二步——完整合盘报告(2026-10-18)
## 基线与顺序
- `origin/staging` 以开工时最新为准(`git fetch origin --prune`)。
- 分支:`codex/synastry-full-report-20261018`;工作树:`.worktrees/synastry-full-report-20261018`。
- **前置**:#78(档案上限 20 人)、#79(引擎、证据包接口、存储;第六版起不做页面)、#81(full-reading 补齐 5 个模块)三单都合入后开工。上游参照以 `7a773d13` 为准(见实证 8b)。三单彼此不冲突,可并行。
- **上线**:#79 与本单要一起提升到 `main`(#79 单独上线时旧入口还在、新入口不存在)。本单从 #79 合入后的 `origin/staging` 开分支,复用 #79 的引擎、证据包接口、选人页和 `synastry_reports` 新列。
- **正文写作首要准则**:`docs/research/synastry_relationship_language_guide_2026_10_18.md`(产品负责人 10-18 提供,「从星曜语言到关系语言」)。凡与上游写作规范冲突处,以它为准。
- 原型(v13,关系画像 + 折叠的分析逻辑 + 时间线 + 技术依据摘要):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名和正文全是虚构示例。
- 上游参照:占星师仓库 `yinduzhanxing` 提交 `bde5d615` 的 `skills/jyotish-full-synastry-reading/`。上游仓库私有,原样副本在 `docs/research/upstream_full_synastry_bde5d615/`(`skill/` 下是 Skill 本体,`tests/` 下是五个测试文件;`.py.txt` 搬入时去掉 `.txt`)。
## 现状实证(按符号定位)
| # | 事实 | 位置 |
| --- | --- | --- |
| 1 | 个人报告已经**不用模型写**:`PERSONAL_REPORT_WRITER_ENABLED = false`,正文是 Python `professional_report_reference` 渲染的 Markdown,中英两版同一次引擎调用产出 | `frontend/src/lib/personal-report-writer-flag.ts`、`personal-report-longform-generate.ts` 的 `generatePersonalReportLongform`、`scripts/professional_report_reference.py` |
| 2 | 旧的模型写作路径还在但不调用:按节写、节状态 `pending / ready / blocked` | `frontend/src/mastra/personal-report.ts`(`createPersonalReportAgent`、`buildReportPrompt`)、`personal-report-generation.ts`、`personal-report-section-service-core.ts` |
| 3 | 报告任务基础设施可复用:`personal_report_jobs`(`queued / running / suspended / retrying / ready / failed / cancelled`,`max_attempts` 3)、租约 600 s、心跳 20 s、阶段进度、删报告释放预留 | `personal-report-worker-core.ts`、`personal-report-worker.ts`、迁移 `20260814040000_personal_report_jobs_v2.sql`、`20260925040000_deleted_report_release.sql` |
| 4 | `personal_reports.report_type` 只允许 `personal_full / personal_thematic`;同一枚举在 zod 请求、v2 文档 schema、列表标签里各有一份 | 迁移 `20260806010000_personal_reports.sql`、`lib/contracts/personal-report-request.ts`、`contracts/personal-report/report-document.v2.schema.json`、`PersonalReportCenter` |
| 5 | 计费键写死在多处:SQL CHECK(`feature_pricing.feature_key`、`resolve_feature_pricing`、`admin_save_feature_pricing_draft`、`usage_reservations.feature_key`、权益表)与 TS(`consultation-billing.ts` 的 `featureKey`、后台定价 / 商品 / 用量聚合路由的 zod、`feature-pricing-labels.ts`、`pricing-simulator.tsx`、`membership.ts`)。`feature_pricing` 不带价格,价格由后台发布 | 迁移 `20260831010000_feature_pricing.sql` 等 |
| 6 | 阅读页轮询 `/api/reports/:id`,有等待面板与阶段文案;语言切换写 `?lang=en` | `components/personal-report/personal-report-page.tsx`、`personal-report-progress.ts`、`report-language.ts` |
| 7 | 上游证据包在我们引擎上能直接跑:Claude 10-18 用两位公开名人(布拉德·皮特、安吉丽娜·朱莉,出生资料来自公开星盘库)在 `origin/staging` 上跑 `full-reading` 两次再跑上游 `build_full_synastry_dossier.py`,`delivery_readiness = ready_for_narrative`,九节写作素材齐全。整个证据包约 56 万字符;九节素材 3k~96k 字符不等 | 本地实测,未入库 |
| 8 | 与上游引擎相比我们缺 5 个模块(上游同一对人能算出):Lagna Vargottamamsa、Navamsha Dasha、Niryaana Shoola Dasha、Punarphoo、西占对照层。结果是「次要大运互证」一项 `blocked`,其余照常 | 由前置单 `TASK-full-reading-module-parity-20261018.md` 补齐 |
| 8b | 上游 10-18 `7a773d13` 已修:D4 键名统一为 `D4_Turyamsa`,「共同生活」那条双人比对不再恒为 blocked;另加了单语言交付、把 Niryaana Shoola 从恋爱婚姻应期里排除。#79 搬的是 `bde5d615` 版本,本单开工第一步把 Skill 同步到 `7a773d13`(改动的 6 个文件与测试副本在 `docs/research/upstream_full_synastry_7a773d13/`;`marriage_match.py` 两版相同) | 上游 main `7a773d13` |
| 9 | 上游五个测试在我们这边 46 / 49 通过。3 条失败:① `scripts/aspects.py` 比上游旧(上游 09-24 改为按方向角和速度判相位,我们没同步);② 一条读上游桌面端 `jyotish-app/main.js`,我们没有该文件;③ 一条调上游 API 主文件里的 `_compute_marriage_match`,我们按薄注册另写 | 同上 |
## 决策记录(产品负责人 2026-10-18)
| # | 决定 |
| --- | --- |
| E1 | **完整报告就是合盘结果**:点「开始合盘」即生成整份报告(要等几分钟),走报告任务的后台生成与等待面板;没有先出的即时结果页 |
| E2 | **先写中文,切到英文时再写**:英文不翻译中文稿,从同一份证据包按同一准则重新写;第一次切换时生成,不另扣点 |
| E3 | **单独计费项 `synastry.full`**,默认价格同个人完整报告(`report.full`)。价格由后台发布,代码里不写死。同一对人、两人出生资料没改、规则版本没变,再次打开读已有报告,不扣点 |
| E4 | 报告写作**重新启用模型**,但只用于合盘报告;个人报告保持引擎渲染不变(`PERSONAL_REPORT_WRITER_ENABLED` 不动) |
| E5 | **正文是关系画像**(产品 10-18 第三版,推翻第一版「九节 + 传统配对参考」):按准则文件把星曜语言翻译成关系经验语言。结构九节:一、关系基调(底层天气);二、情感节奏;三、沟通与理解;四、安全感与亲密;五、冲突与修复;六、生活节律与现实协作;七、长期方向;八、关系时间线(E6);九、共同练习(两三条可操作的建议)。之后是「对哪一段想再问清楚」入口(E5d)、「可以一起聊聊」四问、折叠的「技术依据摘要」(E5b)、脚注。开头不放分数、不放结论档位 |
| E5b | **正文最后一个默认折叠的「技术依据摘要」**(标题就叫「技术依据摘要」,英文 Technical basis,不加「给懂占星的人看」之类的副标题;产品 10-18;格式照上游:`full-technique-execution-matrix.md` 要求报告最后有技术附录、每项技法标明执行状态,`SKILL.md` 要求传统配对只能放附录)。**全部由程序从证据包生成,不经模型**,六块:① 计算参数(岁差、交点、宫制、两人出生时间精度,不显示出生日期地点);② 各自的婚恋结构(上升、7 宫与宫主、金星、月亮、DK、UL / UL 二宫、A7、D9 上升与 7 宫主、关键功能吉凶星);③ 两盘交感(各节分析逻辑里用到的那几条);④ 大运对照(时间线覆盖期间两人的大运 / 子运与起止);⑤ 传统配对参考(七项 35 分含例外候选、两人逐处的火星冲克与 Raman 一派差异、婚姻宫压力,标「只作一层参考,不决定结论」);⑥ 技法执行情况(按上游执行矩阵逐行:已执行 / 参数敏感 / 无法计算 / 不适用;最后一行固定写「外部资料与真实案例核对:未进行,本报告是计算层归纳」) |
| E5e | **每节一个默认收起的「分析逻辑」**(产品 10-18 转达占星师:「原因写成分析逻辑,想看的人点开,不看的可以不点」;推翻同日「原因直接展示不折叠」)。第一到七节结论段落之后放一个折叠块「分析逻辑」,展开后两到三步,每步两行:「盘面」一行写盘面事实(可以出现星曜、宫位、分盘、大运,必须对得上证据包里的具体条目),「所以」一行写由此得出的关系判断(白话,不含术语)。时间线每段也带一个同样的折叠块。结论段落本身不出现术语 |
| E5d | **依据可以去对话里问**(产品 10-18):不在每节挂按钮。报告末尾一个入口「对哪一段想再问清楚?」,列出第一到八节的标题,选一节进入一个绑定这份合盘报告的对话,由对话用印度占星的语言细讲。对话侧另立任务书(Claude 写),与本单、#79 一起上线;对话侧合入前这个入口不显示 |
| E5c | 写法守五条:说「你们之间」不说「你 / 他是什么人」;说倾向不说必然;说需求不说缺点;给条件不给结论;保留双方主体性。可以写「一方……另一方……」或点名谁需要什么,但不得把一方写成问题源 |
| E6 | **关系时间线**(产品 10-18,采纳占星师「要加入大运:这段关系在哪段时间会发生什么」,推翻同日「只写大致阶段、不提大运」):取两人接下来约五年的 Vimshottari 大运 / 子运分界,合并成三到五段(短于 6 个月的段并入相邻段)。每段:起止年月、阶段名(如「靠近、了解」「两人同步,适合确认」「现实考验」)、一段关系语言的描述、一个折叠的「分析逻辑」(盘面:两人各自的大运 / 子运与起止、相关行运)。按上游规则,两人时间重叠本身不算共同事件:每段写成「两人各自走到什么阶段 → 这段关系在这期间会是什么样子」,不写「某年结婚 / 领证 / 分开」。不收集两人现在的关系阶段与经历 |
| E7 | 上游的发版前检查(`pre_work_check`)、全网资料核验(MEVG)、书库检索是写稿代理的开发流程,**不进产品运行时**;报告脚注照常写「计算层归纳,不是定论」 |
| E8 | 缺的 5 个模块全部补上(产品 10-18):`TASK-full-reading-module-parity-20261018.md`,**必须在本单开工前合入** |
## 引擎与写作链路
```
选人页「开始合盘」
→ POST /api/reports(reportType = synastry_full,两个人物 id)
→ 预留 synastry.full → 建报告行 + 任务
→ worker:取两人出生资料
→ Python POST /api/synastry_full_dossier(#79 已做):两次 full-reading → 上游证据包 + 两个 profile 的 marriage_match
→ 时间线分段(确定性:两人大运 / 子运分界合并成三到五段,「盘面」行由程序生成)
→ 九节中文:每节一次模型调用,只喂这一节对应的证据(映射见下表)与公共上下文,并发 ≤ 3
→ 中文质检(Python,本单自己的关系语言质检)
不过:只重写不过的节,最多 1 轮,把失败原因附进提示
仍不过:任务失败、释放预留
→ 存:中文九节(含分析逻辑)、时间线分段、证据包快照、质检回执
→ 结算 synastry.full
阅读页切到英文(第一次)
→ POST /api/reports/:id/english → 任务(不扣点)→ 九节英文 + 英文质检 → 存
```
### 九节与证据映射
| 节 | 英文标题 | 喂给模型的证据(上游证据包字段) |
| --- | --- | --- |
| 一、关系基调 | Relationship Climate | `reader_section_brief` 的 `relationship_thesis`、`what_each_person_brings`;`relationship_lenses` 总览 |
| 二、情感节奏 | Emotional Rhythm | `emotional_safety` |
| 三、沟通与理解 | Communication and Understanding | `communication_conflict_and_repair`(沟通部分) |
| 四、安全感与亲密 | Safety and Intimacy | `attraction_and_closeness` + `mutual_affection_activation`(两个方向都要写:什么让对方感到被重视、什么让对方退缩) |
| 五、冲突与修复 | Conflict and Repair | `communication_conflict_and_repair`(冲突与修复部分)、`can_it_stay_steady` 的修复部分 |
| 六、生活节律与现实协作 | Daily Life and Practical Partnership | `shared_life_gains_and_pressures` |
| 七、长期方向 | Long-term Direction | `relationship_progression`、`can_it_stay_steady`(D9 双人互动已执行时必须用上,结论段落不提 D9,「盘面」行可以提) |
| 八、关系时间线 | Relationship Timeline | 程序分好的三到五段(两人各自的大运 / 子运、起止)+ `parallel_timing_context`、`event_resonance_gate`;模型只写每段的阶段名和描述,「盘面」行由程序生成 |
| 九、共同练习 | Practices to Share | 前八节的依据;每条建议必须能回指一条具体依据 |
`marriage_match`(35 分、火星冲克、婚姻宫压力)作为背景放进第二、四、五、六节的输入;结论与白话推理里只能翻译成关系语言(例如「压力下一个追、一个退」),「盘面」行可以写名称,但不写分数。
### 模型提示(新文件,建议 `frontend/src/mastra/synastry-report.ts`)
- 系统提示以准则文件为主体(原文要点全部进提示,含五条写法与示例);上游 `chinese-synastry-writing-contract.md`、`customer-report-shape.md` 只取「证据怎么用」的部分:每个判断要有两条同向依据、冲突要降级、时间要有条件、补充方法(宿曜、西占、问卦)不得点名。上游要求在正文里写术语、写「七宫主落……」句式、点名 D9 的部分**不采用**。英文提示同理,以准则文件为主体,参考 `english-synastry-writing-contract.md` 的证据部分。
- 每节输入:两人称呼(档案里的称呼,不给出生日期、出生地)、这一节的标题与要求、上表对应的证据。
- 第一到七节输出 JSON:`{ text, reasons: [{ plain, basis, evidence, label }] }`,`reasons` 两到三条,渲染成「分析逻辑」的步骤:`basis` 是「盘面」行(可含术语),`plain` 是「所以」行(白话,不含术语),`evidence` 指向这一节输入里的具体条目,`label` 取上游六个证据标签之一(只存不显示)。第八节输出 `{ segments: [{ id, phase, text }] }`(`id` 对应程序分好的段);第九节输出 `{ items: [{ text, evidence }] }`。
### 中文 / 英文质检(本单自己的,`scripts/synastry_full_reader_gate.py`)
上游校验器(`validate_bilingual_synastry_delivery.py`)原样搬入不改,但它的读者规则与准则冲突(要求正文出现 D9、要求首句含特定靠近感词),所以**不用它判正文**;只复用它的 `PRIVATE_EVIDENCE_TERMS`、`SUPPLEMENTARY_METHOD_TERMS` 两张表。本单自己的质检,中英各一套,逐条:
1. 九个标题齐全、顺序对;第一到七节结论段落中文至少 120 字(英文 80 词),全文中文至少 1800 字(英文 1100 词);第一到七节每节有两到三条原因;时间线三到五段。
2. **术语禁表**(只查结论段落、「所以」行、时间线描述、共同练习;**「盘面」行不查**)(中文):行星名(太阳、月亮、水星、金星、火星、木星、土星、罗睺、计都)、「宫」、星座名、星宿、上升、分盘 / 九分盘 / 婚姻分盘、D1 / D9 / D 加数字、大运、小运、行运、Dasha、Koota、Kuja、火星冲克、Nadi、Yoni、Varna、Ashtakoota、Papasamya、UL、DK、「数字 + 分」、满分。英文对应一套(planet names、house、sign names、nakshatra、ascendant、Navamsa、D9、dasha、transit、koota、points/score 等)。
3. **判断禁表**:克、煞、凶、吉、注定、必然、命中、一定会、合不合、不合、相合、不适合、适合结婚、不宜、灵魂伴侣、宇宙安排、百分百、会不会在一起;「年份 / 月份 + 结婚 / 领证 / 分手 / 离婚」。
4. 上游私有词表、补充方法名表一个都不能出现。
5. 首句不得是分析腔(「这段关系的核心不是……」「不是有没有……」)。
6. 每条原因的 `evidence` 能在这一节的输入里找到,`basis` 里出现的星曜 / 宫位 / 大运与该条证据一致(逐字段比对,不一致即失败);第九节每条建议都有 `evidence`;时间线的「盘面」行由程序生成,模型不得改写。
不过的节带着失败原因重写一次,仍不过则任务失败、释放预留。
## 硬红线
1. `scripts/jyotish_api_server.py` 不增长(类方法数、`__new__` 伪造点、行数护栏);新接口在独立模块,主文件只加注册行。`app/(app)/page.tsx` 的 `Home()` 状态数不增长。
2. 上游 `skills/jyotish-full-synastry-reading/` 的脚本与参考文件**原样搬入**,`diff` 为空;不得为了让我们这边的测试通过去改上游文件。
3. 结论段落、白话推理、时间线描述、共同练习不得出现任何分数、Koota、星曜、宫位、分盘、大运等术语;判断禁表、补充方法名、私有证据词全文都不得出现。术语只允许出现在「分析逻辑」的「盘面」行、折叠的「技术依据摘要」和对话里。质检必须拦住这些。
4. 模型提示不得带出生日期、时间、出生地和经纬度;证据包快照只存在报告自己的行里,随报告删除;日志不打证据包和正文。
5. 扣点:任务失败、超时、质检两轮不过、用户中途删报告 → 释放不扣;读已有报告不预留;英文版不扣点。模型 token 用量照常记账(`kind: "synastry_full"`),进成本统计。
6. 动表的轮次真跑 `npm run test:db`;只加列、加枚举值、加键,不删旧列(AGENTS §7.6)。
7. 一次等待一次揭幕:等待用报告现有等待面板;正文整份出现,揭幕后无 spinner / 骨架。
8. 合同测试 fixture 用真实引擎输出:两位公开名人的证据包(可截取到测试需要的字段,写明截取规则),不得手造形状。
## 任务分解
| # | 内容 | 验收标准 |
| --- | --- | --- |
| T1 | **数据**:`personal_reports.report_type` 加 `synastry_full`;加两人引用列(另一人 `partner_chart_profile_id` 或等价字段,本人用约定值)与 `synastry_report_id`(指向 #79 的 `synastry_reports` 行,带两人出生资料指纹与 `rules_version`)。证据包快照、中文九节(含分析逻辑)、质检回执存进 `personal_report_sections`(与个人报告快照同一套)。zod、v2 schema、列表标签同步 | `test:db`:同一对人同指纹第二次找到已有报告;任一人改资料后找不到;删任一人后报告与合盘行一并消失 |
| T2 | **计费键 `synastry.full`**:一个迁移把键加进实证 5 列出的所有 SQL CHECK 与函数;TS 侧同步所有枚举与后台标签(「完整合盘报告」);不写价格 | `test:db`:未发布价格时创建报告返回与个人报告同样的「暂未定价」错误;发布后预留、结算、释放三条路径都绿;后台定价页能看到新键 |
| T3 | **创建入口**:`POST /api/reports` 接受 `reportType: "synastry_full"` + 两个人物 id(同一人 400,不属于本人的 id 拒绝);命中已有报告直接返回不预留;`compatibility` 开关与 `report_center` 开关都要开;沿用每日上限、限流、同时只能生成一份 | 路由测试覆盖以上每条 |
| T4 | **worker 分支**:按上面的链路实现;阶段进度 `loading_context` → `computing_charts` → `writing_report` → `checking_report` → `persisting_report`;Python 429 走现有 `upstream_busy` 重试;每节模型调用有超时,单节失败只重试该节 | 单测:一节失败只重写这一节;质检两轮不过任务失败并释放预留;删报告释放预留 |
| T5 | **质检** `scripts/synastry_full_reader_gate.py`(中英两套规则)+ 薄注册接口(或 worker 直接调 CLI,二选一写进 PROGRESS) | pytest:准则文件第四节的示例(补够字数、补上原因的版本)通过;「盘面」行写错一处宫位(与证据不符)失败一次;分别塞进「金星」「七宫」「22 分」「注定」「2027 年结婚」「宿曜」「execution_receipt」、首句改成「这段关系的核心不是……」、删掉一节、去掉某节 claims,各失败一次;英文同理 |
| T6 | **英文版按需生成**:`POST /api/reports/:id/english` 建任务(不扣点);同一报告只会有一个英文任务;失败可重试 | 路由与 worker 测试;英文提示里不出现中文稿内容(断言提示文本不含中文九节正文) |
| T7 | **阅读页**:合盘报告标题「A × B · 婚恋合盘」;九节正文(第一到七节结论后一个默认收起的「分析逻辑」;时间线每段同样带一个)→「对哪一段想再问清楚?」入口(E5d,对话侧未合入时隐藏)→「可以一起聊聊」四问 →「技术依据摘要」(默认折叠,E5b 六块,程序生成)→ 脚注(「合盘描述的是互动的质地,走不走、怎么走,仍然是你们两个人的选择」)。切英文时若还没生成,原地显示等待面板(一次揭幕),生成好整份替换 | 渲染测试用名人 golden;结论段落与「所以」行过一遍质检第 2、3 条禁表为空;分析逻辑与技术摘要默认收起;技术摘要六块齐全,第 ⑥ 块状态与证据包 `execution_receipt` 逐行一致;手机 390 px 无横向滚动;`next build` 后 `/` 仍 Static,首屏 gzip ±2% |
| T8 | **婚恋合盘页(从 #79 移来)与入口**:侧栏「婚恋合盘」;选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮,按钮下点数说明读 `synastry.full` 定价);「开始合盘」创建报告并跳到阅读页;「之前的合盘」列出合盘报告;档案页「和我合盘」改跳本页并预选两人;报告中心列表里合盘报告的卡片写「A × B 婚恋合盘」;删人确认句加「K 份合盘」 | 渲染与路由测试;20 人时下拉可滚动 |
| T9 | **上游同步与测试搬入**:先把 `skills/jyotish-full-synastry-reading/` 同步到 `7a773d13`(见实证 8b),`diff` 为空;搬入上游合盘测试(以 `7a773d13` 副本为准)+ #79 已有的 `test_marriage_match_product.py`;`aspects.py` 同步上游版本(方向角与速度判相位),跑全量 pytest 与基线逐条比对(相位值变了,报告类测试可能受影响,逐条写原因);依赖上游独有文件的测试改写为我们的等价断言(三栏说明) | 上游合盘测试全过或有三栏说明;全量 pytest 无新增失败;Niryaana Shoola 一行在名人证据包里为 `not_applicable` |
| T10 | **记录**:BUG-1373(#79 登记为 investigating,本单修复后改 resolved);DESIGN.md(选人页、阅读页、英文按需生成的等待态)、VOICE.md(引用准则文件;报告禁表、标题格式)、CHANGELOG、PROGRESS(与上游的差异:质检包装层、两条改写的测试)、`docs/testing/synastry-full-report-20261018.md` 真人清单(生成一份、再次打开不扣点、切英文、删人连带删除、失败退点、手机宽) | 文档齐 |
## 让步顺序
先保 T1–T5(存对、扣对、写得出、拦得住);T7 不可省;T6 若来不及,英文切换先显示「英文版即将提供」并写进 BLOCKED,不得用机器翻译中文稿顶替;T9 的 `aspects.py` 同步若牵动过多既有测试,可拆成单独小单先行,但必须在本单合入前完成。任何一项过不了红线就停下写进 PROGRESS,不得放宽口径。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/synastry-full-report-20261018 .worktrees/synastry-full-report-20261018 origin/staging
cd .worktrees/synastry-full-report-20261018
git log --oneline -5 -- scripts/marriage_match.py # 确认 #79 已合入
ls docs/research/upstream_full_synastry_bde5d615/skill docs/research/upstream_full_synastry_bde5d615/tests
grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
```
验证:全量 `pytest tests -n 4` 与开工基线逐条比对;`run_quality_gate.py --profile quick`;前端 `tsc --noEmit`、`npm run lint`、`npm test`(含 grep 源码文本合同测试)、`npm run test:db`、`npm run build`。真实模型输出(无模型凭据时)写成环境缺口,部署后由 Claude 用名人盘在 staging 生成一份复核。
## BUG 编号
本单是产品需求。只有在执行中发现并修复的真实缺陷才新增编号,起点为开工时 `docs/BUG_HISTORY.md` 最大号 + 1。
@@ -0,0 +1,73 @@
# 任务书:从合盘报告进入对话问依据(2026-10-18)
## 基线与顺序
- `origin/staging` 以开工时最新为准(`git fetch origin --prune`)。
- 分支:`codex/synastry-report-chat-20261018`;工作树:`.worktrees/synastry-report-chat-20261018`。
- **前置**:`TASK-synastry-full-report-20261018.md`(#82)合入后开工。本单读 #82 存下的合盘报告与每节分析逻辑。
- **上线**:与 #79、#82 一起提升到 `main`。#82 的「对哪一段想再问清楚?」入口在本单合入前隐藏,本单负责打开它。
- 原型(v13,报告末尾的入口):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe
## 现状实证(按符号定位)
| # | 事实 | 位置 |
| --- | --- | --- |
| 1 | 对话只能绑一个人:`chat_sessions.chart_profile_id / chart_profile_name / chart_profile_role`;人物选择器单选 | 迁移 `20260830010000_chat_session_chart_profile_binding.sql`;`components/chat-profile-picker.tsx`、`lib/chat-profile-picker-model.ts` |
| 2 | 会话已有「类型 + 外键」的先例:`session_type`(`consultation` / `birth_time_rectification`)+ `rectification_case_id`,非普通会话锁定人物选择器 | 迁移 `20260721110000_chat_session_surface_metadata.sql`;`lib/rectification-in-chat*.ts`、`chatSessionSubjectLocked` |
| 3 | 建会话:`POST /api/sessions`,`chatSessionCreateSchema` 为 `.strict()`、`session_type` 是枚举;服务端用 `authoritativeSessionBinding` 校验绑定 | `app/api/sessions/route.ts`、`lib/chat-session-write-contract.ts`、`lib/consultation-subject-resolver.ts` |
| 4 | 「新会话 + 预填问题」只有 `continueInNewChat` 一处(`setDraft(prompt.question)`);地址栏只认 `c=` 与 `new=1` | `hooks/use-session-management.ts`、`lib/chat-session-url.ts` |
| 5 | 每轮对话固定先调 `run-jyotish-consultation`(单人本命),再按同源卡回答;证据查阅工具只认固定的 `EVIDENCE_LOOKUP_SECTIONS` | `mastra/index.ts`(`jyotishInstructions`)、`mastra/consultation-tools.ts`、`lib/consultation-evidence-card.ts` |
| 6 | 入口枚举 `consultationEntrypointSchema`(`daily_starlanguage` / `birth_time_rectification` / `guided_topic`);有入口时跳过闲聊分类 | `lib/consultation-entrypoint.ts`、`lib/consultation-smalltalk.ts` |
| 7 | 计费:每轮 `POST /api/consult` 预留 `chat.standard`;开会话不扣点 | `lib/consult/settlement.ts` |
| 8 | 护栏:`Home()` 的 `useState` ≤ 36、`useRef` ≤ 37;普通对话提示词 + 卡 ≤ 20,000 字符(含方法论 ≤ 26,000) | `frontend/tests/home-shell-growth-contract.test.ts`;`consult-card-full-source-20261007.test.ts` 等 |
## 决策记录(产品负责人 2026-10-18)
| # | 决定 |
| --- | --- |
| G1 | 合盘报告正文是关系语言,**印度占星的依据要能讲给用户听,引导到对话里**(产品 10-18)。入口只在报告末尾一处:「对哪一段想再问清楚?」列出第一到八节标题(含关系时间线),选一节进入对话 |
| G2 | 点一节 → 新建一个**绑定这份合盘报告和这一节**的对话,输入框预填「合盘报告里『<节名>』这一段,在我们两人的盘里依据是什么?」(英文报告用英文),**由用户自己点发送**,不自动发出 |
| G3 | 对话里**可以用印度占星术语**(星曜、宫位、分盘、大运),这是它和报告正文的分工;仍守 VOICE 的口吻,判断禁词照旧(合不合、注定、必然、灵魂伴侣等);不替两人做决定 |
| G4 | 回答**只依据这份报告已经存下的计算结果**(证据包快照、每节分析逻辑、时间线分段、技术依据摘要),不在对话里重新排盘、不新增结论;问到报告没覆盖的内容,如实说明这份合盘没算到,并建议回到各自的盘单独问 |
| G5 | 计费照普通对话:每轮 `chat.standard`,开会话不扣点 |
| G6 | 对话侧栏里这类会话标题写「A × B · 合盘追问」;当前人物是两人中任意一个时都显示;人物选择器锁定显示「A × B」 |
| G7 | 报告被删或任一人被删:报告随之删除(#82),这些会话保留文字记录,但不能再继续提问,输入框处显示「这份合盘已删除」 |
| G8 | **不许瞎说**(产品 10-18 追问「对话能拿到合盘数据吗,不是在瞎说吧」):今天的普通对话**拿不到**合盘数据,本单就是把数据接进来。三道保证:① 每轮回答前**必须**先调「读取合盘报告某节材料」工具,没拿到材料不回答;② 回答里提到的每一个盘面事实(某星落某宫 / 某星座、某两星相位、某段大运与起止)都要能在本轮工具返回的材料里找到;③ 回答生成后由程序逐条核对②,对不上就带着差异重写一次,仍对不上则改为固定答复「这一点在这份合盘里没有算到,我不能确定」,不输出未核实的说法 |
## 硬红线
1. `scripts/jyotish_api_server.py` 不动;`app/(app)/page.tsx` 的 `Home()` 状态数不增长(新逻辑进 hook / lib)。
2. 普通单人对话的提示词、同源卡、工具集**一字不改**;合盘追问用独立的提示与工具,独立的长度上限测试(提示 + 本节材料 ≤ 20,000 字符)。
3. 证据只从本人拥有的报告里读(服务端按 `user_id` 校验,行级安全照旧);不把出生日期、出生地、经纬度放进提示。
4. 改既有断言写三栏;动表必须真跑 `npm run test:db`;只加列、加枚举值,不删旧列。
5. 合盘追问会话不进入单人对话的记忆与上下文继承(`continued_from_session_id`、`context_summary` 不跨类型)。
## 任务分解
| # | 内容 | 验收标准 |
| --- | --- | --- |
| T1 | **数据**:`chat_sessions.session_type` 加 `synastry_report`;加 `synastry_report_id`(指向 `personal_reports.id`,删除时置空并标记会话只读)与 `synastry_section`(节键)。`chatSessionCreateSchema`、`authoritativeSessionBinding` 同步:该类型必须带本人拥有的 `synastry_full` 报告 id 与合法节键 | `test:db`:他人报告 id 被拒;节键非法被拒;删报告后会话仍在、标记只读 |
| T2 | **入口**:#82 阅读页「对哪一段想再问清楚?」的节按钮 → 建会话 → 跳到 `c=<id>` 并预填问题(G2);去掉 #82 里的隐藏开关 | 渲染测试;点按钮不调用 `/api/consult`、不扣点 |
| T3 | **对话链路**:`POST /api/consult` 遇到 `synastry_report` 会话,走独立分支:不调 `run-jyotish-consultation`;用新的合盘追问提示(建议 `mastra/synastry-report-chat.ts`)+ 一个只读工具「读取合盘报告某节材料」(返回该节结论、分析逻辑、对应证据条目;时间线节返回分段与两人大运;可另取技术依据摘要),**每轮强制先调用**(G8①);入口枚举加 `synastry_report`,跳过闲聊分类;计费照 `chat.standard`。**回答核对**(G8②③):新增 `lib/synastry-chat-fact-check.ts`,从回答里抽出星曜、宫位、星座、相位、大运与日期的组合,逐条在本轮工具材料里查找;对不上 → 重写一次 → 仍对不上走固定答复;核对结果记进本轮遥测 | 单测:不调用单人排盘;工具只能读本会话绑定的报告;工具没被调用时不输出回答;用名人报告材料造三条回答样例(全对 / 编了一个不存在的「金星在 7 宫」/ 大运起止写错一年),核对分别判通过、失败、失败;失败两次时用户看到的是固定答复;问报告外的内容时回答里有「这份合盘没算到」类说明 |
| T4 | **侧栏与选择器**:标题「A × B · 合盘追问」;两人任一为当前人物时显示;选择器锁定显示「A × B」;只读会话的输入框状态(G7) | 渲染测试 |
| T5 | **记录**:DESIGN.md(入口与只读态)、VOICE.md(合盘追问的口吻与术语边界)、CHANGELOG、`docs/testing/synastry-report-chat-20261018.md` 真人清单(从报告进对话、预填不自动发、追问两轮、问报告外问题、删报告后只读、英文报告进对话) | 文档齐 |
## 让步顺序
T1–T3 必须一起交,T3 的回答核对不可省(没有它本单不得合入);T4 若来不及,侧栏先按第一个人显示,写进 PROGRESS;不得为赶工复用单人对话的提示或工具。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/synastry-report-chat-20261018 .worktrees/synastry-report-chat-20261018 origin/staging
cd .worktrees/synastry-report-chat-20261018
git log --oneline -5 -- frontend/src/components/personal-report # 确认 #82 已合入
grep -n "BUG-1[0-9][0-9][0-9]" docs/BUG_HISTORY.md | tail -3
```
验证:前端 `tsc --noEmit`、`npm run lint`、`npm test`(含 grep 源码文本合同测试与 Home 增长护栏)、`npm run test:db`、`npm run build`(`/` 仍 Static,首屏 gzip ±2%)。真实模型回答(无模型凭据时)写成环境缺口,部署后由 Claude 在 staging 用名人合盘走一遍。
## BUG 编号
本单是产品需求;只为执行中发现的真实缺陷新增编号,起点为开工时最大号 + 1。