docs(tasks): marriage match follows upstream bde5d615 (35-point core, order-independent)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bafa296829
commit
c4c983cf2a
+66
-10
@@ -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.",
|
||||
},
|
||||
+17
-8
@@ -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.
|
||||
+63
@@ -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")
|
||||
Reference in New Issue
Block a user