docs(tasks): marriage match p1 fix brief (issue #79)

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-18 18:08:12 +08:00
co-authored by Claude Opus 5.5
parent a0e84845e9
commit b3ef2d8158
@@ -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 已知失败名单逐条对照)。