Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bafa296829 |
@@ -0,0 +1,411 @@
|
||||
#!/usr/bin/env python3
|
||||
"""User-facing romantic/marriage compatibility contract.
|
||||
|
||||
This module deliberately does not reuse the historical ``calc_kuja_dosha``
|
||||
score. That function combines Mars with five pressure planets and contains a
|
||||
male/female asymmetric comparison. It remains available for legacy reports,
|
||||
but is not a valid default for a new relationship product.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, Iterable, Mapping, Optional
|
||||
|
||||
from ashtakoot import (
|
||||
FRIENDSHIP,
|
||||
NADI,
|
||||
SIGN_LORDS,
|
||||
SIGNS,
|
||||
calculate_ashtakoot,
|
||||
get_nakshatra,
|
||||
)
|
||||
|
||||
|
||||
PRODUCT_VERSION = "marriage_match_product_v1"
|
||||
DEFAULT_PROFILE = "practitioner_consensus_v1"
|
||||
VALID_RELATIONSHIP_TYPES = {"romantic_or_marriage", "romantic", "marriage"}
|
||||
KUJA_PLANET = "Mars"
|
||||
PAPASAMYA_PLANETS = ("Mars", "Saturn", "Sun", "Rahu", "Ketu")
|
||||
|
||||
# These profiles are deliberately qualitative. The consensus did not support
|
||||
# an invented severity percentage or a sex-specific tolerance threshold.
|
||||
RULE_PROFILES = {
|
||||
"practitioner_consensus_v1": {
|
||||
"label": "Practitioner consensus v1",
|
||||
"kuja_houses": (1, 2, 4, 7, 8, 12),
|
||||
"reference_frames": ("lagna", "moon", "venus"),
|
||||
"exact_house_sign_exceptions": {
|
||||
2: {"Gemini", "Virgo"},
|
||||
4: {"Aries", "Scorpio"},
|
||||
7: {"Capricorn", "Cancer"},
|
||||
8: {"Sagittarius", "Pisces"},
|
||||
12: {"Taurus", "Libra"},
|
||||
},
|
||||
"leo_aquarius_exception": "variant_only",
|
||||
"own_or_exalted": "mitigates",
|
||||
"jupiter_contact": "mitigates",
|
||||
"moon_contact": "mitigates",
|
||||
"papasamya_planets": PAPASAMYA_PLANETS,
|
||||
"gender_asymmetry": False,
|
||||
"notes": [
|
||||
"Kuja is Mars-only; Papasamya is a separately reported five-planet pressure layer.",
|
||||
"Lagna, Moon and Venus are shown separately; they are not converted into an opaque weighted score.",
|
||||
"Own/exalted Mars and Jupiter or Moon contact mitigate a signal in this default profile; they do not erase the raw placement.",
|
||||
"Age, caste and sex-asymmetric comparison rules are not used for a user-facing conclusion.",
|
||||
],
|
||||
},
|
||||
"raman_muhurtha_v1": {
|
||||
"label": "B. V. Raman Muhurtha variant",
|
||||
"kuja_houses": (2, 4, 7, 8, 12),
|
||||
"reference_frames": ("lagna", "moon", "venus"),
|
||||
"exact_house_sign_exceptions": {
|
||||
2: {"Gemini", "Virgo"},
|
||||
4: {"Aries", "Scorpio"},
|
||||
7: {"Capricorn", "Cancer"},
|
||||
8: {"Sagittarius", "Pisces"},
|
||||
12: {"Taurus", "Libra"},
|
||||
},
|
||||
"leo_aquarius_exception": "cancels",
|
||||
"own_or_exalted": "mitigates",
|
||||
"jupiter_contact": "mitigates",
|
||||
"moon_contact": "mitigates",
|
||||
"papasamya_planets": PAPASAMYA_PLANETS,
|
||||
"gender_asymmetry": False,
|
||||
"notes": [
|
||||
"Raman-style house set omits the first house. It is a selectable lineage, not the product default.",
|
||||
"The traditional Leo/Aquarius exception is applied only in this variant profile.",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def available_profiles() -> Dict[str, Dict[str, Any]]:
|
||||
"""Return a presentation-safe copy of available rule profiles."""
|
||||
return deepcopy(RULE_PROFILES)
|
||||
|
||||
|
||||
def _profile(profile_id: str) -> Dict[str, Any]:
|
||||
if profile_id not in RULE_PROFILES:
|
||||
raise ValueError("Unknown marriage-match rule profile: %s" % profile_id)
|
||||
return RULE_PROFILES[profile_id]
|
||||
|
||||
|
||||
def _lon(value: Any) -> Optional[float]:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value) % 360.0
|
||||
if isinstance(value, Mapping):
|
||||
for key in ("longitude", "degree_raw", "degree", "lon"):
|
||||
if isinstance(value.get(key), (int, float)):
|
||||
return float(value[key]) % 360.0
|
||||
return None
|
||||
|
||||
|
||||
def _sign_index(value: Any) -> Optional[int]:
|
||||
if isinstance(value, str) and value in SIGNS:
|
||||
return SIGNS.index(value)
|
||||
longitude = _lon(value)
|
||||
return int(longitude // 30) if longitude is not None else None
|
||||
|
||||
|
||||
def _planet_data(chart: Mapping[str, Any], planet: str) -> Mapping[str, Any]:
|
||||
planets = chart.get("planets", {}) if isinstance(chart, Mapping) else {}
|
||||
value = planets.get(planet, {}) if isinstance(planets, Mapping) else {}
|
||||
return value if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _planet_lon(chart: Mapping[str, Any], planet: str) -> Optional[float]:
|
||||
value = _planet_data(chart, planet)
|
||||
longitude = _lon(value)
|
||||
return longitude if longitude is not None else _lon(chart.get(planet + "_lon"))
|
||||
|
||||
|
||||
def _reference_lon(chart: Mapping[str, Any], reference: str) -> Optional[float]:
|
||||
if reference == "lagna":
|
||||
for value in (chart.get("ascendant"), chart.get("lagna"), chart.get("asc_lon")):
|
||||
longitude = _lon(value)
|
||||
if longitude is not None:
|
||||
return longitude
|
||||
return None
|
||||
return _planet_lon(chart, "Moon" if reference == "moon" else "Venus")
|
||||
|
||||
|
||||
def _reference_sign(chart: Mapping[str, Any], reference: str) -> Optional[int]:
|
||||
value = _reference_lon(chart, reference)
|
||||
if value is not None:
|
||||
return int(value // 30)
|
||||
if reference == "lagna":
|
||||
return _sign_index(chart.get("ascendant")) or _sign_index(chart.get("lagna"))
|
||||
return _sign_index(_planet_data(chart, "Moon" if reference == "moon" else "Venus"))
|
||||
|
||||
|
||||
def _planet_sign(chart: Mapping[str, Any], planet: str) -> Optional[int]:
|
||||
lon = _planet_lon(chart, planet)
|
||||
return int(lon // 30) if lon is not None else _sign_index(_planet_data(chart, planet))
|
||||
|
||||
|
||||
def _house_from(reference_sign: Optional[int], planet_sign: Optional[int]) -> Optional[int]:
|
||||
if reference_sign is None or planet_sign is None:
|
||||
return None
|
||||
return ((planet_sign - reference_sign) % 12) + 1
|
||||
|
||||
|
||||
def _same_sign_contact(chart: Mapping[str, Any], planet_a: str, planet_b: str) -> bool:
|
||||
a, b = _planet_sign(chart, planet_a), _planet_sign(chart, planet_b)
|
||||
return a is not None and a == b
|
||||
|
||||
|
||||
def _mars_dignity(chart: Mapping[str, Any]) -> str:
|
||||
sign_idx = _planet_sign(chart, "Mars")
|
||||
if sign_idx is None:
|
||||
return "unknown"
|
||||
sign = SIGNS[sign_idx]
|
||||
if sign == "Capricorn":
|
||||
return "exalted"
|
||||
if sign in {"Aries", "Scorpio"}:
|
||||
return "own"
|
||||
if sign == "Cancer":
|
||||
return "debilitated"
|
||||
return "other"
|
||||
|
||||
|
||||
def _frame_kuja(chart: Mapping[str, Any], reference: str, profile: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
mars_sign_idx = _planet_sign(chart, "Mars")
|
||||
ref_sign_idx = _reference_sign(chart, reference)
|
||||
house = _house_from(ref_sign_idx, mars_sign_idx)
|
||||
frame = {
|
||||
"reference": reference,
|
||||
"reference_sign": SIGNS[ref_sign_idx] if ref_sign_idx is not None else None,
|
||||
"mars_sign": SIGNS[mars_sign_idx] if mars_sign_idx is not None else None,
|
||||
"mars_house": house,
|
||||
"raw_signal": house in set(profile["kuja_houses"]) if house is not None else None,
|
||||
"status": "not_available" if house is None else "not_triggered",
|
||||
"mitigations": [],
|
||||
}
|
||||
if house is None or not frame["raw_signal"]:
|
||||
return frame
|
||||
|
||||
mars_sign = frame["mars_sign"]
|
||||
exact_exceptions = profile["exact_house_sign_exceptions"]
|
||||
if mars_sign in exact_exceptions.get(house, set()):
|
||||
frame["status"] = "cancelled_by_profile_exception"
|
||||
frame["mitigations"].append("house_sign_exception")
|
||||
return frame
|
||||
if mars_sign in {"Leo", "Aquarius"}:
|
||||
mode = profile["leo_aquarius_exception"]
|
||||
if mode == "cancels":
|
||||
frame["status"] = "cancelled_by_profile_exception"
|
||||
frame["mitigations"].append("leo_aquarius_profile_exception")
|
||||
return frame
|
||||
if mode == "variant_only":
|
||||
frame["mitigations"].append("leo_aquarius_exception_disputed")
|
||||
|
||||
frame["status"] = "active"
|
||||
dignity = _mars_dignity(chart)
|
||||
if dignity in {"own", "exalted"}:
|
||||
frame["mitigations"].append("mars_%s" % dignity)
|
||||
if _same_sign_contact(chart, "Mars", "Jupiter"):
|
||||
frame["mitigations"].append("jupiter_conjunction")
|
||||
if _same_sign_contact(chart, "Mars", "Moon"):
|
||||
frame["mitigations"].append("moon_conjunction")
|
||||
return frame
|
||||
|
||||
|
||||
def analyze_kuja(chart: Mapping[str, Any], profile_id: str = DEFAULT_PROFILE) -> Dict[str, Any]:
|
||||
"""Analyze Mars-only Kuja / Mangal Dosha across visible reference frames."""
|
||||
profile = _profile(profile_id)
|
||||
if _planet_sign(chart, "Mars") is None:
|
||||
return {
|
||||
"system": "Kuja / Mangal Dosha",
|
||||
"scope": "Mars only",
|
||||
"profile_id": profile_id,
|
||||
"status": "not_available",
|
||||
"reason": "Mars position is required.",
|
||||
"frames": [],
|
||||
}
|
||||
frames = [_frame_kuja(chart, ref, profile) for ref in profile["reference_frames"]]
|
||||
active = [frame for frame in frames if frame["status"] == "active"]
|
||||
cancelled = [frame for frame in frames if frame["status"] == "cancelled_by_profile_exception"]
|
||||
severity = {0: "none", 1: "mild", 2: "material", 3: "strong"}[len(active)]
|
||||
return {
|
||||
"system": "Kuja / Mangal Dosha",
|
||||
"scope": "Mars only",
|
||||
"profile_id": profile_id,
|
||||
"status": "executed",
|
||||
"active_reference_count": len(active),
|
||||
"cancelled_reference_count": len(cancelled),
|
||||
"qualitative_level": severity,
|
||||
"frames": frames,
|
||||
"boundary": "Reference count is a presentation label, not a probability or a compatibility score.",
|
||||
}
|
||||
|
||||
|
||||
def analyze_papasamya(chart: Mapping[str, Any], profile_id: str = DEFAULT_PROFILE) -> Dict[str, Any]:
|
||||
"""Report five-planet pressure separately from Mars-only Kuja."""
|
||||
profile = _profile(profile_id)
|
||||
rows = []
|
||||
for planet in profile["papasamya_planets"]:
|
||||
planet_sign = _planet_sign(chart, planet)
|
||||
if planet_sign is None:
|
||||
rows.append({"planet": planet, "status": "not_available", "frames": []})
|
||||
continue
|
||||
frames = []
|
||||
for reference in profile["reference_frames"]:
|
||||
house = _house_from(_reference_sign(chart, reference), planet_sign)
|
||||
frames.append({
|
||||
"reference": reference,
|
||||
"house": house,
|
||||
"in_pressure_house": house in set(profile["kuja_houses"]) if house is not None else None,
|
||||
})
|
||||
rows.append({"planet": planet, "status": "executed", "frames": frames})
|
||||
active = [
|
||||
{"planet": row["planet"], "reference": frame["reference"], "house": frame["house"]}
|
||||
for row in rows for frame in row.get("frames", []) if frame.get("in_pressure_house")
|
||||
]
|
||||
return {
|
||||
"system": "Papasamya pressure",
|
||||
"scope": "Mars, Saturn, Sun, Rahu and Ketu; separate from Kuja.",
|
||||
"profile_id": profile_id,
|
||||
"status": "executed" if any(row["status"] == "executed" for row in rows) else "not_available",
|
||||
"planet_rows": rows,
|
||||
"active_placements": active,
|
||||
"boundary": "This is a descriptive pressure layer. It is not renamed as Mangal Dosha and is not reduced to a sex-weighted score.",
|
||||
}
|
||||
|
||||
|
||||
def compare_kuja(person_a: Mapping[str, Any], person_b: Mapping[str, Any], profile_id: str = DEFAULT_PROFILE) -> Dict[str, Any]:
|
||||
"""Compare two Kuja results without sex-specific tolerance or scoring."""
|
||||
a, b = analyze_kuja(person_a, profile_id), analyze_kuja(person_b, profile_id)
|
||||
if a["status"] != "executed" or b["status"] != "executed":
|
||||
outcome = "incomplete"
|
||||
else:
|
||||
difference = abs(a["active_reference_count"] - b["active_reference_count"])
|
||||
outcome = "not_triggered" if not a["active_reference_count"] and not b["active_reference_count"] else (
|
||||
"broadly_balanced" if difference <= 1 else "asymmetric"
|
||||
)
|
||||
return {
|
||||
"comparison_method": "neutral_reference_count",
|
||||
"gender_asymmetry_used": False,
|
||||
"outcome": outcome,
|
||||
"person_a": a,
|
||||
"person_b": b,
|
||||
"boundary": "Both-active is a traditional balancing consideration, not an automatic approval or cancellation.",
|
||||
}
|
||||
|
||||
|
||||
def _moon_lon(chart: Mapping[str, Any]) -> Optional[float]:
|
||||
return _planet_lon(chart, "Moon") or _lon(chart.get("moon_lon"))
|
||||
|
||||
|
||||
def _are_natural_friends(lord_a: str, lord_b: str) -> bool:
|
||||
if lord_a == lord_b:
|
||||
return True
|
||||
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]:
|
||||
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)
|
||||
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:
|
||||
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 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,
|
||||
"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"],
|
||||
"candidate_cancellations": bhakoot_exceptions,
|
||||
"status": "candidate_mitigation" if bhakoot_exceptions else "none",
|
||||
},
|
||||
"nadi": {
|
||||
"raw_score": raw["scores"]["Nadi"],
|
||||
"candidate_cancellations": nadi_exceptions,
|
||||
"status": "candidate_mitigation" if nadi_exceptions else "none",
|
||||
},
|
||||
"yoni": {
|
||||
"raw_score": raw["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.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _structure_status(chart: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
keys = {"d1_relationship", "d9", "upapada", "darakaraka", "relationship_dasha"}
|
||||
supplied = [key for key in keys if chart.get(key) is not None]
|
||||
return {
|
||||
"status": "partial" if supplied else "not_available",
|
||||
"supplied_layers": sorted(supplied),
|
||||
"required_for_complete_reading": ["D1 relationship structure", "D9", "UL", "DK", "relationship timing / Dasha"],
|
||||
"boundary": "36-point matching and Dosha layers do not replace D1/D9/UL/DK or timing review.",
|
||||
}
|
||||
|
||||
|
||||
def build_marriage_match_dossier(
|
||||
person_a: Mapping[str, Any],
|
||||
person_b: Mapping[str, Any],
|
||||
*,
|
||||
relationship_type: str = "romantic_or_marriage",
|
||||
profile_id: str = DEFAULT_PROFILE,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build the stable, user-facing love/marriage compatibility payload.
|
||||
|
||||
Inputs are chart snapshots only. The caller is responsible for calculating
|
||||
D1/D9/timing snapshots; this function never stores birth details.
|
||||
"""
|
||||
if relationship_type not in VALID_RELATIONSHIP_TYPES:
|
||||
raise ValueError("This product supports romantic/marriage matching only.")
|
||||
profile = _profile(profile_id)
|
||||
kuja = compare_kuja(person_a, person_b, profile_id)
|
||||
ashtakoota = _ashtakoota_with_exceptions(person_a, person_b)
|
||||
return {
|
||||
"product": PRODUCT_VERSION,
|
||||
"relationship_type": relationship_type,
|
||||
"rule_profile": {
|
||||
"id": profile_id,
|
||||
"label": profile["label"],
|
||||
"notes": profile["notes"],
|
||||
"gender_asymmetry_used": False,
|
||||
},
|
||||
"ashtakoota_36": ashtakoota,
|
||||
"kuja_mangal_dosha": kuja,
|
||||
"papasamya": {
|
||||
"person_a": analyze_papasamya(person_a, profile_id),
|
||||
"person_b": analyze_papasamya(person_b, profile_id),
|
||||
},
|
||||
"relationship_structure": {
|
||||
"person_a": _structure_status(person_a),
|
||||
"person_b": _structure_status(person_b),
|
||||
},
|
||||
"presentation": {
|
||||
"headline": "Relationship compatibility is presented as parallel traditional layers, not one approval score.",
|
||||
"decision_boundary": "Do not let the 36-point score, Kuja, or Papasamya alone determine a relationship conclusion.",
|
||||
"not_in_scope": ["business partnerships", "family matching", "age-based cancellation", "sex-asymmetric tolerance"],
|
||||
},
|
||||
"audit": {
|
||||
"kuja": "executed: Mars-only, Lagna/Moon/Venus frames where data is supplied",
|
||||
"papasamya": "executed separately: five-planet pressure placement review",
|
||||
"ashtakoota": ashtakoota["status"],
|
||||
"d1_d9_ul_dk_timing": "input-dependent; no missing layer is fabricated",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# Love / Marriage Compatibility Product Contract
|
||||
|
||||
## Scope
|
||||
|
||||
This product is for romantic compatibility and marriage matching only. It does
|
||||
not score business partners, family members, caste, age, or sexual orientation.
|
||||
The two input people are always `person_a` and `person_b`; no result depends on
|
||||
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
|
||||
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,
|
||||
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
|
||||
pressure layer. It is never labelled Kuja Dosha.
|
||||
5. **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.
|
||||
|
||||
## Rule Profiles
|
||||
|
||||
`practitioner_consensus_v1` is the default. It uses the common 1/2/4/7/8/12
|
||||
Kuja house set and displays Lagna, Moon, and Venus separately. It does not use
|
||||
gender-asymmetric tolerance, age cancellation, caste, or a numerical
|
||||
compatibility delta.
|
||||
|
||||
`raman_muhurtha_v1` is a visible alternative lineage: it omits the first house
|
||||
and applies its documented Leo/Aquarius exception. It must be selected
|
||||
explicitly and never replaces the default result silently.
|
||||
|
||||
## Input and API Contract
|
||||
|
||||
The API is `POST /api/marriage-match` and accepts only already calculated chart
|
||||
snapshots; it does not accept or store raw birth records:
|
||||
|
||||
```json
|
||||
{
|
||||
"profile_id": "practitioner_consensus_v1",
|
||||
"relationship_type": "romantic_or_marriage",
|
||||
"person_a": {
|
||||
"ascendant": {"longitude": 0.0},
|
||||
"planets": {
|
||||
"Moon": {"longitude": 30.0},
|
||||
"Venus": {"longitude": 60.0},
|
||||
"Mars": {"longitude": 180.0}
|
||||
}
|
||||
},
|
||||
"person_b": {"ascendant": {"longitude": 90.0}, "planets": {"Moon": {"longitude": 120.0}, "Venus": {"longitude": 150.0}, "Mars": {"longitude": 270.0}}}
|
||||
}
|
||||
```
|
||||
|
||||
The equivalent calculator command is:
|
||||
|
||||
```bash
|
||||
python3 scripts/jyotish_engine.py marriage-match \
|
||||
--a-asc 0 --a-moon 30 --a-venus 60 --a-mars 180 \
|
||||
--b-asc 90 --b-moon 120 --b-venus 150 --b-mars 270
|
||||
```
|
||||
|
||||
Optional five-pressure-planet longitudes can be supplied to both calls. The
|
||||
response includes an `audit` block that distinguishes executed, unavailable,
|
||||
and input-dependent layers.
|
||||
|
||||
## Customer Presentation Rules
|
||||
|
||||
- Show the 36-point total as a compact summary, with the eight 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
|
||||
Papasamya alone.
|
||||
- If a requested D1/D9/UL/DK/timing layer was not supplied, show it as missing
|
||||
instead of suppressing the section.
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
||||
if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
from marriage_match import build_marriage_match_dossier
|
||||
|
||||
|
||||
def chart(asc, moon, venus, mars, *, saturn=210, sun=120, rahu=300, ketu=120):
|
||||
return {
|
||||
"ascendant": {"longitude": asc},
|
||||
"planets": {
|
||||
"Moon": {"longitude": moon}, "Venus": {"longitude": venus},
|
||||
"Mars": {"longitude": mars}, "Saturn": {"longitude": saturn},
|
||||
"Sun": {"longitude": sun}, "Rahu": {"longitude": rahu}, "Ketu": {"longitude": ketu},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_default_profile_separates_mars_kuja_from_papasamya():
|
||||
a = chart(0, 30, 60, 180, saturn=180)
|
||||
b = chart(90, 120, 150, 270, saturn=270)
|
||||
report = build_marriage_match_dossier(a, b)
|
||||
assert report["rule_profile"]["id"] == "practitioner_consensus_v1"
|
||||
assert report["kuja_mangal_dosha"]["person_a"]["scope"] == "Mars only"
|
||||
pressure = report["papasamya"]["person_a"]
|
||||
assert {row["planet"] for row in pressure["planet_rows"]} == {"Mars", "Saturn", "Sun", "Rahu", "Ketu"}
|
||||
assert "score" not in report["kuja_mangal_dosha"]
|
||||
|
||||
|
||||
def test_kuja_comparison_is_gender_neutral_and_not_numeric_tolerance():
|
||||
a = chart(0, 30, 60, 180)
|
||||
b = chart(90, 120, 150, 270)
|
||||
forward = build_marriage_match_dossier(a, b)["kuja_mangal_dosha"]
|
||||
reverse = build_marriage_match_dossier(b, a)["kuja_mangal_dosha"]
|
||||
assert forward["gender_asymmetry_used"] is False
|
||||
assert forward["outcome"] == reverse["outcome"]
|
||||
assert forward["comparison_method"] == "neutral_reference_count"
|
||||
|
||||
|
||||
def test_raman_variant_is_visible_and_first_house_difference_is_explicit():
|
||||
# Mars in the first house from each reference frame: default sees it;
|
||||
# Raman's selected lineage does not.
|
||||
same_sign = chart(0, 0, 0, 0)
|
||||
default = build_marriage_match_dossier(same_sign, same_sign)
|
||||
raman = build_marriage_match_dossier(same_sign, same_sign, profile_id="raman_muhurtha_v1")
|
||||
assert default["kuja_mangal_dosha"]["person_a"]["active_reference_count"] == 3
|
||||
assert raman["kuja_mangal_dosha"]["person_a"]["active_reference_count"] == 0
|
||||
assert raman["rule_profile"]["id"] == "raman_muhurtha_v1"
|
||||
|
||||
|
||||
def test_bhakoot_and_nadi_exceptions_are_exposed_without_rewriting_raw_score():
|
||||
# Moon longitudes both in Ashwini but different padas: same Nadi raw zero,
|
||||
# candidate cancellation stays a separately auditable flag.
|
||||
a = chart(0, 1, 20, 180)
|
||||
b = chart(90, 4, 120, 270)
|
||||
report = build_marriage_match_dossier(a, b)
|
||||
nadi = report["ashtakoota_36"]["exception_review"]["nadi"]
|
||||
assert nadi["raw_score"] == 0
|
||||
assert "same_nakshatra_different_pada" in nadi["candidate_cancellations"]
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def test_api_handler_uses_the_new_product_contract_without_legacy_kuja_score():
|
||||
from jyotish_api_server import JyotishAPIHandler
|
||||
|
||||
payload = {
|
||||
"person_a": chart(0, 1, 20, 180),
|
||||
"person_b": chart(90, 4, 120, 270),
|
||||
}
|
||||
result = JyotishAPIHandler._compute_marriage_match(None, payload)
|
||||
assert result["product"] == "marriage_match_product_v1"
|
||||
assert result["kuja_mangal_dosha"]["gender_asymmetry_used"] is False
|
||||
@@ -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` | — | 婚恋合盘第一期:独立页、任意两人、从业者共识口径(分档并列)、按报告扣点(BUG-1370 起) | 排队(等上限单合入) | `codex/marriage-match-p1-20261017`;动表 |
|
||||
| `TASK-marriage-match-p1-20261017.md` | — | 婚恋合盘第一期:独立页、任意两人、直接搬上游占星师 marriage_match.py(3c01a214)、按报告扣点(BUG-1370 起) | 排队(等上限单合入) | `codex/marriage-match-p1-20261017`;动表 |
|
||||
|
||||
@@ -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` 开分支。
|
||||
- 原型(v4,规则已按共识口径改):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名、分数、宫位全是虚构示例。
|
||||
- 原型(v5,火星冲克按上游模块口径):https://claude.ai/artifact/Tq6vhXFMnGitDZhJdamaHe 。原型里的人名、分数、宫位全是虚构示例。
|
||||
|
||||
## 现状实证(按符号定位)
|
||||
|
||||
@@ -39,40 +39,26 @@
|
||||
| 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` main `3c01a2148e82741dca62547a0ec1bab0b252766e` 的 `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 | 第二期(本单不做、不留占位):两人大运同步「什么时候顺」、就这对关系开对话、导出报告、非婚恋关系 |
|
||||
|
||||
### 规则口径 `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 岁后减弱(多处明说无经典依据) | 写明「有争议」,不参与轻重判断,只列出 |
|
||||
| 单人轻重(文字) | **未见**:三处都没触发,或每处触发都被广泛公认的条件化解;**轻**:触发只被常见条件化解,或只在某一宫位版本下成立;**明显**:至少一处触发没有任何广泛公认或常见的化解 | 页面脚注写明:这是计算层的归纳,不是定论,要结合整盘看 |
|
||||
| 两人比较 | 两人都「明显」或都「轻」→ 相互抵消(广泛公认,前提是程度相当);一方「明显」、另一方「未见」→ 需要留意,并提示看婚姻宫压力是否平衡。**不分男女** | — |
|
||||
| 36 分 | `calculate_ashtakoot(moon_a, moon_b)`,`person_a` 当男方。原始分不改;Bhakoot(同主星、互为朋友)与 Nadi(同星宿不同 pada、同星座不同星宿)只给 `candidate_cancellations` | **按档案性别把男方放 `person_a`**(D5:同性或缺性别按选择顺序)。页面表尾两个合计:原始合计、计入候选例外后的合计 |
|
||||
| 性情 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 的并行 + 跳过外部概览),再组快照调用 |
|
||||
|
||||
**婚姻宫压力(Papasamya)**:多为南印做法。火星、土星、太阳、罗睺落在从上升、月亮、金星数的 1、2、4、7、8、12 宫,逐处列出;计都单列一行,标「部分派别计入」。两人比较只写「大致均衡 / 一方明显偏重」。**不出加权总分**(各派权重不同,经典没有公布分表)。南印习惯里「女方不得多于男方」的比较带性别不对称,不采用,脚注说明。单独一节,不叫火星冲克。
|
||||
**结论措辞**(产品层,不进上游模块):落点三档「助力多于考验」「有几处需要留意,建议结合整盘细看」「考验较多,建议请占星师细看」。规则:火星冲克 `asymmetric`,或任一方「重」→ 至少第二档;同时原始 36 分低于 18 且火星冲克 `asymmetric` → 第三档;其余第一档。36 分只作一层参考。
|
||||
|
||||
**八项配对(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 已报给占星师)**,搬入时在本地修掉,并在 PROGRESS 里记成「与上游差异」,等上游修好后再对齐:
|
||||
1. 结果依赖 A / B 顺序(种姓等有方向的项);我们靠「男方放 `person_a`」规避,**不改上游算法**。
|
||||
2. `_moon_lon`、`_reference_sign` 里的 `or` 把 0 当缺失:月亮在 0° 时 36 分 `not_available`;上升只给星座名「Aries」时上升一处 `not_available`。本地改成显式 `is None` 判断,各补一条测试。
|
||||
|
||||
## 硬红线
|
||||
|
||||
@@ -90,12 +76,12 @@
|
||||
|
||||
| # | 内容 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| 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`(`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) |
|
||||
| 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)选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮;点数来自定价不写死;之前的合盘列表只列「人名 × 人名 · 日期」,点开不扣点);结果页按原型 v4:三句话总述 → 各自的婚姻征象 → 八项配对(含例外绿字 / 另一派灰字、两个合计)→ 火星冲克(每人逐处列出触发与化解,化解条件带分档标签;两人对比)→ 婚姻宫压力 → 脚注(口径来源、分档含义、「计算层归纳不是定论」) | 渲染测试:两个状态(两人火星冲克相当 / 一方明显)的 golden 渲染;20 人时下拉可滚动;手机 390 px 无横向滚动;`next build` 后 `/` 仍 Static,首屏 gzip ±2% |
|
||||
| T6 | **页面**:侧栏「婚恋合盘」;`/marriage-match`(路由名可议,写进 DESIGN)选人页(两个自定义下拉,底行「+ 新增星盘」复用星盘档案新建表;居中按钮;点数来自定价不写死;之前的合盘列表只列「人名 × 人名 · 日期」,点开不扣点);结果页按原型 v5:三句话总述 → 各自的婚姻征象 → 八项配对(含例外绿字 / 另一派灰字、两个合计)→ 火星冲克(每人逐处列出「从哪里数、落第几宫、是否触发」,减轻条件带分档标签并注明「不改变这一处」;Raman 一派不同时并列一句;两人对比)→ 婚姻宫压力 → 脚注(口径来源、分档含义、「计算层归纳不是定论」) | 渲染测试:两个状态(两人火星冲克相当 / 一方明显)的 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` 真人清单(选人、新增星盘、扣点与再次打开不扣、删人连带删除、手机宽) | 文档齐 |
|
||||
@@ -113,6 +99,9 @@ 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/
|
||||
# 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`。
|
||||
|
||||
Reference in New Issue
Block a user