From c4c983cf2a8ded2fb1b2daa9288359ad47bdf646 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 18 Oct 2026 13:04:46 +0800 Subject: [PATCH] docs(tasks): marriage match follows upstream bde5d615 (35-point core, order-independent) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v --- .../marriage_match.py.txt | 76 ++++++++++++++++--- .../marriage_match_product_contract.md | 25 ++++-- .../test_marriage_match_product.py.txt | 63 +++++++++++++++ docs/tasks/README.md | 2 +- docs/tasks/TASK-marriage-match-p1-20261017.md | 32 ++++---- 5 files changed, 163 insertions(+), 35 deletions(-) rename docs/research/{upstream_marriage_match_3c01a214 => upstream_marriage_match_bde5d615}/marriage_match.py.txt (83%) rename docs/research/{upstream_marriage_match_3c01a214 => upstream_marriage_match_bde5d615}/marriage_match_product_contract.md (70%) rename docs/research/{upstream_marriage_match_3c01a214 => upstream_marriage_match_bde5d615}/test_marriage_match_product.py.txt (51%) diff --git a/docs/research/upstream_marriage_match_3c01a214/marriage_match.py.txt b/docs/research/upstream_marriage_match_bde5d615/marriage_match.py.txt similarity index 83% rename from docs/research/upstream_marriage_match_3c01a214/marriage_match.py.txt rename to docs/research/upstream_marriage_match_bde5d615/marriage_match.py.txt index 838eda61..8ed98f6f 100644 --- a/docs/research/upstream_marriage_match_3c01a214/marriage_match.py.txt +++ b/docs/research/upstream_marriage_match_bde5d615/marriage_match.py.txt @@ -137,7 +137,10 @@ def _reference_sign(chart: Mapping[str, Any], reference: str) -> Optional[int]: if value is not None: return int(value // 30) if reference == "lagna": - return _sign_index(chart.get("ascendant")) or _sign_index(chart.get("lagna")) + # Aries is index 0, which is a valid reference sign rather than a + # missing value. Do not use boolean fallback here. + ascendant_sign = _sign_index(chart.get("ascendant")) + return ascendant_sign if ascendant_sign is not None else _sign_index(chart.get("lagna")) return _sign_index(_planet_data(chart, "Moon" if reference == "moon" else "Venus")) @@ -296,7 +299,8 @@ def compare_kuja(person_a: Mapping[str, Any], person_b: Mapping[str, Any], profi def _moon_lon(chart: Mapping[str, Any]) -> Optional[float]: - return _planet_lon(chart, "Moon") or _lon(chart.get("moon_lon")) + longitude = _planet_lon(chart, "Moon") + return longitude if longitude is not None else _lon(chart.get("moon_lon")) def _are_natural_friends(lord_a: str, lord_b: str) -> bool: @@ -305,44 +309,96 @@ def _are_natural_friends(lord_a: str, lord_b: str) -> bool: return lord_b in FRIENDSHIP.get(lord_a, {}).get("friend", []) and lord_a in FRIENDSHIP.get(lord_b, {}).get("friend", []) -def _ashtakoota_with_exceptions(person_a: Mapping[str, Any], person_b: Mapping[str, Any]) -> Dict[str, Any]: +def _ashtakoota_with_exceptions( + person_a: Mapping[str, Any], + person_b: Mapping[str, Any], +) -> Dict[str, Any]: moon_a, moon_b = _moon_lon(person_a), _moon_lon(person_b) if moon_a is None or moon_b is None: return {"status": "not_available", "reason": "Both Moon longitudes are required for Ashtakoota."} - raw = calculate_ashtakoot(moon_a, moon_b) + forward = calculate_ashtakoot(moon_a, moon_b) + reverse = calculate_ashtakoot(moon_b, moon_a) + # The historical Varna point encodes caste hierarchy. It cannot truthfully + # be relabelled as a modern value judgement, so the international product + # excludes it rather than deriving a replacement from lunar signs. + score_names = ("Vashya", "Tara", "Yoni", "GrahaMaitri", "Gana", "Bhakoot", "Nadi") + symmetric_scores = { + name: round( + (float(forward["scores"][name]) + float(reverse["scores"][name])) / 2.0, + 2, + ) + for name in score_names + } + international_core = { + "method": "International Ashtakoota core v1 (both input orders averaged; classical Varna excluded)", + "scores": symmetric_scores, + "total_score": round(sum(symmetric_scores.values()), 2), + "max_score": 35.0, + "match_percentage": round(sum(symmetric_scores.values()) / 35.0 * 100, 1), + "person_a_details": { + "moon_sign": SIGNS[int(moon_a // 30)], + "nakshatra": get_nakshatra(moon_a)["name"], + "nakshatra_pada": get_nakshatra(moon_a)["pada"], + }, + "person_b_details": { + "moon_sign": SIGNS[int(moon_b // 30)], + "nakshatra": get_nakshatra(moon_b)["name"], + "nakshatra_pada": get_nakshatra(moon_b)["pada"], + }, + "directionality": { + "status": "symmetric", + "rule": "all included Kutas are averaged across person_a -> person_b and person_b -> person_a", + }, + "boundary": "This is a mutual-reference score, not a caste hierarchy or an automatic relationship verdict.", + } sign_a, sign_b = SIGNS[int(moon_a // 30)], SIGNS[int(moon_b // 30)] nak_a, nak_b = get_nakshatra(moon_a), get_nakshatra(moon_b) bhakoot_exceptions = [] - if raw["scores"]["Bhakoot"] == 0: + if symmetric_scores["Bhakoot"] == 0: lord_a, lord_b = SIGN_LORDS[sign_a], SIGN_LORDS[sign_b] if lord_a == lord_b: bhakoot_exceptions.append("same_sign_lord") elif _are_natural_friends(lord_a, lord_b): bhakoot_exceptions.append("mutual_natural_friend_sign_lords") nadi_exceptions = [] - if raw["scores"]["Nadi"] == 0: + if symmetric_scores["Nadi"] == 0: if nak_a["index"] == nak_b["index"] and nak_a["pada"] != nak_b["pada"]: nadi_exceptions.append("same_nakshatra_different_pada") if sign_a == sign_b and nak_a["index"] != nak_b["index"]: nadi_exceptions.append("same_rashi_different_nakshatra") return { "status": "executed", - "raw_36_point": raw, + "international_core": international_core, + "shared_life_dialogue": { + "status": "not_scored", + "storage": "no_answers_collected", + "prompts": [ + "What does a committed relationship mean to each of you?", + "How do you want to make decisions about money, home, and care responsibilities?", + "What boundaries, privacy expectations, and conflict-repair habits matter most?", + "How do you each imagine work, family, location, and personal autonomy over the next few years?", + ], + "boundary": "These are voluntary discussion prompts, not a compatibility test or a score.", + }, + "classical_varna": { + "status": "excluded_from_default", + "reason": "Historical caste hierarchy is not used in the international product or its default score.", + }, "person_a": {"moon_sign": sign_a, "nakshatra": nak_a}, "person_b": {"moon_sign": sign_b, "nakshatra": nak_b}, "exception_review": { "bhakoot": { - "raw_score": raw["scores"]["Bhakoot"], + "raw_score": symmetric_scores["Bhakoot"], "candidate_cancellations": bhakoot_exceptions, "status": "candidate_mitigation" if bhakoot_exceptions else "none", }, "nadi": { - "raw_score": raw["scores"]["Nadi"], + "raw_score": symmetric_scores["Nadi"], "candidate_cancellations": nadi_exceptions, "status": "candidate_mitigation" if nadi_exceptions else "none", }, "yoni": { - "raw_score": raw["scores"]["Yoni"], + "raw_score": symmetric_scores["Yoni"], "status": "legacy_three_band_table", "boundary": "The legacy 4/2/0 table remains visible. Intermediate 3/1 grades require a separately versioned source table before becoming a default score.", }, diff --git a/docs/research/upstream_marriage_match_3c01a214/marriage_match_product_contract.md b/docs/research/upstream_marriage_match_bde5d615/marriage_match_product_contract.md similarity index 70% rename from docs/research/upstream_marriage_match_3c01a214/marriage_match_product_contract.md rename to docs/research/upstream_marriage_match_bde5d615/marriage_match_product_contract.md index 96bed1d8..339b24cd 100644 --- a/docs/research/upstream_marriage_match_3c01a214/marriage_match_product_contract.md +++ b/docs/research/upstream_marriage_match_bde5d615/marriage_match_product_contract.md @@ -9,18 +9,25 @@ which person is labelled male or female. ## Product Layers -1. **Ashtakoota (36 points)**: retain the raw Moon-based score and each Kuta. - It is one compatibility lens, not a relationship decision. -2. **Exception review**: show Bhakoot and Nadi cancellation candidates without +1. **International Ashtakoota core (35 points)**: calculate both input orders + and average every included Kuta before display, so swapping two people + cannot change the result. The historical `Varna` point is excluded, not + renamed: it encodes caste hierarchy and cannot truthfully be inferred as a + modern value judgement from lunar signs. It remains one compatibility lens, + not a relationship decision. +2. **Shared-life dialogue (not scored)**: the product offers voluntary prompts + about commitment, money, home, care, boundaries, and autonomy. It neither + collects answers nor turns them into a compatibility score. +3. **Exception review**: show Bhakoot and Nadi cancellation candidates without silently changing the raw score. Yoni remains visibly marked as a legacy three-band table until a versioned 14x14 intermediate-score source table is admitted. -3. **Kuja / Mangal Dosha**: Mars only, inspected independently from Lagna, +4. **Kuja / Mangal Dosha**: Mars only, inspected independently from Lagna, Moon, and Venus. The product exposes every reference-frame placement and mitigation rather than creating a percentage score. -4. **Papasamya**: Mars, Saturn, Sun, Rahu, and Ketu are reported as a separate +5. **Papasamya**: Mars, Saturn, Sun, Rahu, and Ketu are reported as a separate pressure layer. It is never labelled Kuja Dosha. -5. **Relationship structure and timing**: D1, D9, UL, DK, and relationship +6. **Relationship structure and timing**: D1, D9, UL, DK, and relationship Dasha are input-dependent. Missing data is reported as unavailable rather than replaced with an inferred result. @@ -70,10 +77,12 @@ and input-dependent layers. ## Customer Presentation Rules -- Show the 36-point total as a compact summary, with the eight component rows. +- Keep the 35-point Moon/nakshatra reference layer out of the headline; show it + only as compact supporting evidence when the user requests the traditional + component rows. - Render Kuja and Papasamya as separate cards with their selected profile. - State that an active signal is neither a diagnosis nor a prediction. -- Do not make an automatic approval/rejection from 36 points, Kuja, or +- Do not make an automatic approval/rejection from the matching layer, Kuja, or Papasamya alone. - If a requested D1/D9/UL/DK/timing layer was not supplied, show it as missing instead of suppressing the section. diff --git a/docs/research/upstream_marriage_match_3c01a214/test_marriage_match_product.py.txt b/docs/research/upstream_marriage_match_bde5d615/test_marriage_match_product.py.txt similarity index 51% rename from docs/research/upstream_marriage_match_3c01a214/test_marriage_match_product.py.txt rename to docs/research/upstream_marriage_match_bde5d615/test_marriage_match_product.py.txt index 099161ba..4451dcaa 100644 --- a/docs/research/upstream_marriage_match_3c01a214/test_marriage_match_product.py.txt +++ b/docs/research/upstream_marriage_match_bde5d615/test_marriage_match_product.py.txt @@ -66,6 +66,69 @@ def test_bhakoot_and_nadi_exceptions_are_exposed_without_rewriting_raw_score(): assert "same_nakshatra_different_pada" in nadi["candidate_cancellations"] +def test_international_core_is_order_independent_and_excludes_caste_point(): + a = chart(0, 30, 60, 180) + b = chart(90, 120, 150, 270) + forward = build_marriage_match_dossier(a, b)["ashtakoota_36"]["international_core"] + reverse = build_marriage_match_dossier(b, a)["ashtakoota_36"]["international_core"] + assert forward["total_score"] == reverse["total_score"] + assert forward["scores"] == reverse["scores"] + assert forward["max_score"] == 35.0 + assert "ValuesOrientation" not in forward["scores"] + assert "Varna" not in forward["scores"] + dialogue = build_marriage_match_dossier(a, b)["ashtakoota_36"]["shared_life_dialogue"] + assert dialogue["status"] == "not_scored" + + +def test_reported_directional_reproduction_is_order_independent_without_varna(): + # The historical directional calculation yields 10 vs 9 when 45 and 200 + # degrees are swapped because it includes Varna. The product core omits + # Varna and averages every included Kuta, so input order is immaterial. + a = chart(0, 45, 80, 120) + b = chart(90, 200, 170, 210) + forward = build_marriage_match_dossier(a, b)["ashtakoota_36"]["international_core"] + reverse = build_marriage_match_dossier(b, a)["ashtakoota_36"]["international_core"] + assert forward["total_score"] == reverse["total_score"] == 9.0 + assert forward["scores"] == reverse["scores"] + assert forward["directionality"]["status"] == "symmetric" + + +def test_zero_degree_moon_is_a_valid_ashtakoota_input(): + result = build_marriage_match_dossier(chart(0, 0, 20, 180), chart(90, 120, 150, 270)) + assert result["ashtakoota_36"]["status"] == "executed" + assert result["ashtakoota_36"]["international_core"]["person_a_details"]["moon_sign"] == "Aries" + + +def test_aries_named_ascendant_is_a_valid_lagna_reference(): + # Aries is sign index 0 and must not be discarded by a boolean fallback. + person_a = {"ascendant": "Aries", "planets": {"Mars": {"longitude": 120.0}}} + person_b = {"ascendant": "Taurus", "planets": {"Mars": {"longitude": 240.0}}} + kuja = build_marriage_match_dossier(person_a, person_b)["kuja_mangal_dosha"] + lagna_frame = kuja["person_a"]["frames"][0] + assert lagna_frame["reference"] == "lagna" + assert lagna_frame["reference_sign"] == "Aries" + assert lagna_frame["mars_house"] == 5 + assert lagna_frame["status"] == "not_triggered" + + +def test_moon_only_quick_reference_stays_neutral_and_marks_missing_pressure_layers(): + result = build_marriage_match_dossier( + {"planets": {"Moon": {"longitude": 0.0}}}, + {"planets": {"Moon": {"longitude": 120.0}}}, + ) + assert result["ashtakoota_36"]["status"] == "executed" + assert result["ashtakoota_36"]["international_core"]["total_score"] == 18.5 + assert result["kuja_mangal_dosha"]["person_a"]["status"] == "not_available" + assert result["kuja_mangal_dosha"]["person_b"]["status"] == "not_available" + + +def test_shared_life_dialogue_is_not_a_score_or_questionnaire_store(): + dialogue = build_marriage_match_dossier(chart(0, 30, 60, 180), chart(90, 120, 150, 270))["ashtakoota_36"]["shared_life_dialogue"] + assert dialogue["status"] == "not_scored" + assert dialogue["storage"] == "no_answers_collected" + assert dialogue["prompts"] + + def test_non_romantic_scope_is_rejected(): with pytest.raises(ValueError, match="romantic/marriage"): build_marriage_match_dossier(chart(0, 1, 20, 180), chart(90, 4, 120, 270), relationship_type="business") diff --git a/docs/tasks/README.md b/docs/tasks/README.md index cdd2aa71..57e8e022 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -501,4 +501,4 @@ | `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` | — | 婚恋合盘第一期:独立页、任意两人、直接搬上游占星师 marriage_match.py(3c01a214)、按报告扣点(BUG-1370 起) | 排队(等上限单合入) | `codex/marriage-match-p1-20261017`;动表 | +| `TASK-marriage-match-p1-20261017.md` | — | 婚恋合盘第一期:独立页、任意两人、直接搬上游占星师 marriage_match.py(bde5d615,35 分七项、不分男女)、按报告扣点(BUG-1370 起) | 排队(等上限单合入) | `codex/marriage-match-p1-20261017`;动表 | diff --git a/docs/tasks/TASK-marriage-match-p1-20261017.md b/docs/tasks/TASK-marriage-match-p1-20261017.md index 5acad4ea..e024c168 100644 --- a/docs/tasks/TASK-marriage-match-p1-20261017.md +++ b/docs/tasks/TASK-marriage-match-p1-20261017.md @@ -5,7 +5,7 @@ - `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` 开分支。 -- 原型(v5,火星冲克按上游模块口径):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名、分数、宫位全是虚构示例。 +- 原型(v6,35 分七项、火星冲克按上游模块口径):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名、分数、宫位全是虚构示例。 ## 现状实证(按符号定位) @@ -34,31 +34,31 @@ | D1 | 合盘做成独立功能:侧栏一项「**婚恋合盘**」,独立页面,页面标题「婚恋合盘」。档案里**任意两个人**都能合(包括两个都不是本人) | | D2 | 第一版**只做恋爱 / 婚姻**,不给选关系类型;旧的 `relationshipType`(business / family / general)分支删除 | | D3 | **一次合盘按一次报告扣点**(`report.full` 的定价)。同一对人、两人出生资料都没改、规则版本没变时,再次打开直接读已保存的结果,不扣点 | -| D4 | 八项**逐项展示**,不做大号总分;历史列表不挂分数 | -| D5 | **不考虑同性**:男方女方按档案性别定;两人性别相同或有一人没填时,按选择顺序第一个人当男方,不提示、不互换平均(推翻同日早先「互换取平均」的说法) | +| D4 | 配对表**逐项展示**(第四版起为七项、满分 35,见 D12),不做大号总分;历史列表不挂分数 | +| D5 | ~~按档案性别定男方~~ **第四版撤销**:上游已改成两个方向各算一次取平均,结果与顺序、性别都无关。本单**不读性别、不分男女方**,按选人顺序传 `person_a` / `person_b` 即可;页面不出现「男方 / 女方」 | | D6 | 副标题定稿:「看这段感情往后的走向,和两人之间的助力与考验。」页面任何地方不写「合不合」「会不会在一起」 | | D7 | 新增星盘入口放在两个选人下拉的**最底一行**「+ 新增星盘」,本页弹出与星盘档案同一张新建表,保存后自动选进这一格;档案满时这一行变灰 | | D8 | 「开始合盘」按钮居中,点数说明在按钮正下方 | -| D9 | **引擎直接搬上游占星师的实现**(产品 2026-10-17 第三版):`yinduzhanxing` main `3c01a2148e82741dca62547a0ec1bab0b252766e` 的 `scripts/marriage_match.py`(默认 profile `practitioner_consensus_v1`,可选 `raman_muhurtha_v1`)原样搬入,口径以它为准。**推翻**本任务书前两版自写的规则:同日第一版的 `raman_muhurtha_v1` 细则,以及第二版自写的化解条件分档表、「未见 / 轻 / 明显」归档规则、Yoni 五档表。仍然推翻现有 `calc_kuja_dosha` 的五星合分与 `match_kuja_dosha` 的性别不对称(上游模块本身也不用它们) | +| 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 个提交)**不在本单范围**,留作第二期参考 | ### 引擎口径(上游 `marriage_match.py`,摘要) | 层 | 上游行为(照搬) | 我们在外面加的 | | --- | --- | --- | -| 36 分 | `calculate_ashtakoot(moon_a, moon_b)`,`person_a` 当男方。原始分不改;Bhakoot(同主星、互为朋友)与 Nadi(同星宿不同 pada、同星座不同星宿)只给 `candidate_cancellations` | **按档案性别把男方放 `person_a`**(D5:同性或缺性别按选择顺序)。页面表尾两个合计:原始合计、计入候选例外后的合计 | +| 配对层(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` 只报告调用方传进来了哪些层(`d1_relationship`、`d9`、`upapada`、`darakaraka`、`relationship_dasha`),不计算 | 我们用 `relationship_analysis.analyze_relationship` 算 D1 第 7 宫主、金星、D9、UL、DK,作为快照字段传入;`relationship_dasha` 第一期不传(第二期) | | 输入 | 只收已算好的盘快照(上升、各行星经度),不收出生资料 | Next 侧先并行算两张盘(沿用 BUG-1333 的并行 + 跳过外部概览),再组快照调用 | -**结论措辞**(产品层,不进上游模块):落点三档「助力多于考验」「有几处需要留意,建议结合整盘细看」「考验较多,建议请占星师细看」。规则:火星冲克 `asymmetric`,或任一方「重」→ 至少第二档;同时原始 36 分低于 18 且火星冲克 `asymmetric` → 第三档;其余第一档。36 分只作一层参考。 +**结论措辞**(产品层,不进上游模块):落点三档「助力多于考验」「有几处需要留意,建议结合整盘细看」「考验较多,建议请占星师细看」。规则:火星冲克 `asymmetric`,或任一方「重」→ 至少第二档;同时配对层原始合计低于 **17.5**(满分 35)且火星冲克 `asymmetric` → 第三档;其余第一档。配对分只作一层参考。 -**上游两个已知 bug(10-17 已报给占星师)**,搬入时在本地修掉,并在 PROGRESS 里记成「与上游差异」,等上游修好后再对齐: -1. 结果依赖 A / B 顺序(种姓等有方向的项);我们靠「男方放 `person_a`」规避,**不改上游算法**。 -2. `_moon_lon`、`_reference_sign` 里的 `or` 把 0 当缺失:月亮在 0° 时 36 分 `not_available`;上升只给星座名「Aries」时上升一处 `not_available`。本地改成显式 `is None` 判断,各补一条测试。 +**上游两个 bug(10-17 报给占星师,10-18 已在 `bde5d615` 修复,Claude 复测通过)**:顺序依赖已由「两个方向取平均」消除(随机 3000 对对调结果全一致);`or` 把 0 当缺失已改为显式 `is None`,上游各有回归测试。本单**原样搬入,不做本地改动**;若上游 main 之后有新修复,验收时由 Claude 决定是否同步。 ## 硬红线 @@ -76,13 +76,13 @@ | # | 内容 | 验收标准 | | --- | --- | --- | -| T1 | **引擎**:把上游 `scripts/marriage_match.py`(`3c01a214`,本仓副本在 `docs/research/upstream_marriage_match_3c01a214/`)原样搬入,只做「已知 bug 2」的本地修复;搬入上游 `tests/test_marriage_match_product.py`。新增本地胶水模块(建议 `scripts/marriage_match_snapshot.py`):两份已算好的盘 + 性别 → 按 D5 定 `person_a` / `person_b` → 用 `relationship_analysis.analyze_relationship` 补各自婚姻征象快照 → 默认与 Raman 两个 profile 各调一次 `build_marriage_match_dossier` | 上游 6 条测试全过;新增:0° 月亮、白羊上升(星座名)各一条;男女顺序按性别(女方选在第一格时仍放 `person_b`);两个 profile 结果都在;`diff` 上游文件只剩 bug 2 的改动(写进 PROGRESS) | +| T1 | **引擎**:把上游 `scripts/marriage_match.py`(`bde5d615`,本仓副本在 `docs/research/upstream_marriage_match_bde5d615/`)与 `tests/test_marriage_match_product.py` 原样搬入,**不做任何改动**(`scripts/ashtakoot.py` 与上游相同,不动)。新增本地胶水模块(建议 `scripts/marriage_match_snapshot.py`):两份已算好的盘 → 按选人顺序作 `person_a` / `person_b` → 用 `relationship_analysis.analyze_relationship` 补各自婚姻征象快照 → 默认与 Raman 两个 profile 各调一次 `build_marriage_match_dossier` | 上游 12 条测试全过(`test_api_handler_*` 一条若依赖上游接口形状,按我们 T2 的薄注册改写并写三栏说明);`diff` 上游两文件为空(写进 PROGRESS);新增:胶水模块两人对调后配对层、火星冲克、婚姻宫压力结果一致;两个 profile 结果都在 | | T2 | **Python 接口**:`POST /api/marriage-match`(与上游同名)薄注册,在独立模块里实现,`jyotish_api_server.py` 只加注册行;旧 `/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)选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮;点数来自定价不写死;之前的合盘列表只列「人名 × 人名 · 日期」,点开不扣点);结果页按原型 v5:三句话总述 → 各自的婚姻征象 → 八项配对(含例外绿字 / 另一派灰字、两个合计)→ 火星冲克(每人逐处列出「从哪里数、落第几宫、是否触发」,减轻条件带分档标签并注明「不改变这一处」;Raman 一派不同时并列一句;两人对比)→ 婚姻宫压力 → 脚注(口径来源、分档含义、「计算层归纳不是定论」) | 渲染测试:两个状态(两人火星冲克相当 / 一方明显)的 golden 渲染;20 人时下拉可滚动;手机 390 px 无横向滚动;`next build` 后 `/` 仍 Static,首屏 gzip ±2% | -| T7 | **三句话总述**:由服务端按结构化结果确定性拼句(不调模型),讲各自征象、相处的助力与考验、八项合计只作一层参考;落点只用「结论措辞」的三档,第二、三档讲清原因 | 单测覆盖两种状态;文案进 VOICE | +| 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)选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮;点数来自定价不写死;之前的合盘列表只列「人名 × 人名 · 日期」,点开不扣点);结果页按原型 v5:三句话总述 → 各自的婚姻征象 → 七项配对(满分 35,含例外绿字 / 另一派灰字、两个合计、「种姓项不计入」小字)→ 火星冲克(每人逐处列出「从哪里数、落第几宫、是否触发」,减轻条件带分档标签并注明「不改变这一处」;Raman 一派不同时并列一句;两人对比)→ 婚姻宫压力 → 「可以一起聊聊」四问(D12④,上游 `shared_life_dialogue.prompts` 译成中文,不收集回答)→ 脚注(口径来源、分档含义、「计算层归纳不是定论」) | 渲染测试:两个状态(两人火星冲克相当 / 一方明显)的 golden 渲染;20 人时下拉可滚动;手机 390 px 无横向滚动;`next build` 后 `/` 仍 Static,首屏 gzip ±2% | +| T7 | **三句话总述**:由服务端按结构化结果确定性拼句(不调模型),讲各自征象、相处的助力与考验、配对合计(满分 35)只作一层参考;落点只用「结论措辞」的三档,第二、三档讲清原因 | 单测覆盖两种状态;文案进 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` 真人清单(选人、新增星盘、扣点与再次打开不扣、删人连带删除、手机宽) | 文档齐 | @@ -99,8 +99,8 @@ 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 -# 上游文件(上游仓库私有,已原样放进本仓只读参照目录,固定到 3c01a214) -ls docs/research/upstream_marriage_match_3c01a214/ +# 上游文件(上游仓库私有,已原样放进本仓只读参照目录,固定到 bde5d615) +ls docs/research/upstream_marriage_match_bde5d615/ # marriage_match.py.txt → scripts/marriage_match.py;test_marriage_match_product.py.txt → tests/test_marriage_match_product.py ``` @@ -112,7 +112,7 @@ ls docs/research/upstream_marriage_match_3c01a214/ | 建议编号 | 事实 | | --- | --- | -| BUG-1370 | 合盘不看性别,本人恒为男方(实证 3) | +| BUG-1370 | 合盘结果依赖顺序,本人恒为男方(实证 3;修法:改为对调平均、不分男女) | | BUG-1371 | 网页合盘从不计算火星冲克(实证 4) | | BUG-1372 | 旧 `synastry.py` 互控写死 2.0 / 星宿下标越界(实证 7) | | BUG-1373 | 结果卡随旧面板删除,合盘只剩聊天草稿(实证 2) |