kundali-chart-mcp 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +67 -0
  2. package/azure-function/function_app.py +93 -0
  3. package/azure-function/host.json +15 -0
  4. package/azure-function/kundali_bridge.py +952 -0
  5. package/azure-function/python/kundali_lib/__init__.py +1 -0
  6. package/azure-function/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/azure-function/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  8. package/azure-function/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  9. package/azure-function/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  10. package/azure-function/python/kundali_lib/ephemeris.py +30 -0
  11. package/azure-function/python/kundali_lib/geocoder.py +82 -0
  12. package/azure-function/python/kundali_lib/vedic/__init__.py +1 -0
  13. package/azure-function/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/azure-function/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  15. package/azure-function/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  16. package/azure-function/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  17. package/azure-function/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  18. package/azure-function/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  19. package/azure-function/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  20. package/azure-function/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  21. package/azure-function/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  22. package/azure-function/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  23. package/azure-function/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  24. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  25. package/azure-function/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  26. package/azure-function/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  27. package/azure-function/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  28. package/azure-function/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  29. package/azure-function/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  30. package/azure-function/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  31. package/azure-function/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  32. package/azure-function/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  33. package/azure-function/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  34. package/azure-function/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  35. package/azure-function/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  36. package/azure-function/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  37. package/azure-function/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  38. package/azure-function/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  39. package/azure-function/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  40. package/azure-function/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  41. package/azure-function/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  42. package/azure-function/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  43. package/azure-function/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  44. package/azure-function/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  45. package/azure-function/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  46. package/azure-function/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  47. package/azure-function/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  48. package/azure-function/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  49. package/azure-function/python/kundali_lib/vedic/arishta.py +465 -0
  50. package/azure-function/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  51. package/azure-function/python/kundali_lib/vedic/avasthas.py +292 -0
  52. package/azure-function/python/kundali_lib/vedic/ayanamsa.py +106 -0
  53. package/azure-function/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  54. package/azure-function/python/kundali_lib/vedic/char_dasha.py +308 -0
  55. package/azure-function/python/kundali_lib/vedic/chart.py +126 -0
  56. package/azure-function/python/kundali_lib/vedic/chart_types.py +338 -0
  57. package/azure-function/python/kundali_lib/vedic/compatibility.py +705 -0
  58. package/azure-function/python/kundali_lib/vedic/constants.py +108 -0
  59. package/azure-function/python/kundali_lib/vedic/dasha_extended.py +262 -0
  60. package/azure-function/python/kundali_lib/vedic/dasha_systems.py +439 -0
  61. package/azure-function/python/kundali_lib/vedic/doshas.py +453 -0
  62. package/azure-function/python/kundali_lib/vedic/gandanta.py +213 -0
  63. package/azure-function/python/kundali_lib/vedic/gochara.py +277 -0
  64. package/azure-function/python/kundali_lib/vedic/hora.py +263 -0
  65. package/azure-function/python/kundali_lib/vedic/houses.py +30 -0
  66. package/azure-function/python/kundali_lib/vedic/jaimini.py +361 -0
  67. package/azure-function/python/kundali_lib/vedic/kalachakra.py +226 -0
  68. package/azure-function/python/kundali_lib/vedic/kartari.py +243 -0
  69. package/azure-function/python/kundali_lib/vedic/kurmachakra.py +383 -0
  70. package/azure-function/python/kundali_lib/vedic/lunar_return.py +402 -0
  71. package/azure-function/python/kundali_lib/vedic/muhurta.py +414 -0
  72. package/azure-function/python/kundali_lib/vedic/nabhasha.py +349 -0
  73. package/azure-function/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  74. package/azure-function/python/kundali_lib/vedic/panchanga.py +297 -0
  75. package/azure-function/python/kundali_lib/vedic/planets.py +55 -0
  76. package/azure-function/python/kundali_lib/vedic/shadbala.py +500 -0
  77. package/azure-function/python/kundali_lib/vedic/special_conditions.py +319 -0
  78. package/azure-function/python/kundali_lib/vedic/sudarshana.py +232 -0
  79. package/azure-function/python/kundali_lib/vedic/tajaka.py +482 -0
  80. package/azure-function/python/kundali_lib/vedic/upagrahas.py +229 -0
  81. package/azure-function/python/kundali_lib/vedic/varshaphal.py +185 -0
  82. package/azure-function/python/kundali_lib/vedic/yogas.py +935 -0
  83. package/azure-function/python/kundali_lib/vedic/zodiac.py +42 -0
  84. package/azure-function/python/kundali_lib/vedicastro_bridge.py +198 -0
  85. package/azure-function/requirements.txt +9 -0
  86. package/index.js +747 -0
  87. package/kundali-chart-mcp.js +159 -0
  88. package/kundali_bridge.py +952 -0
  89. package/package.json +41 -0
  90. package/python/kundali_lib/__init__.py +1 -0
  91. package/python/kundali_lib/__pycache__/__init__.cpython-313.pyc +0 -0
  92. package/python/kundali_lib/__pycache__/ephemeris.cpython-313.pyc +0 -0
  93. package/python/kundali_lib/__pycache__/geocoder.cpython-313.pyc +0 -0
  94. package/python/kundali_lib/__pycache__/vedicastro_bridge.cpython-313.pyc +0 -0
  95. package/python/kundali_lib/ephemeris.py +30 -0
  96. package/python/kundali_lib/geocoder.py +82 -0
  97. package/python/kundali_lib/vedic/__init__.py +1 -0
  98. package/python/kundali_lib/vedic/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/python/kundali_lib/vedic/__pycache__/arishta.cpython-313.pyc +0 -0
  100. package/python/kundali_lib/vedic/__pycache__/ashtakavarga.cpython-313.pyc +0 -0
  101. package/python/kundali_lib/vedic/__pycache__/avasthas.cpython-313.pyc +0 -0
  102. package/python/kundali_lib/vedic/__pycache__/ayanamsa.cpython-313.pyc +0 -0
  103. package/python/kundali_lib/vedic/__pycache__/bhava_chalit.cpython-313.pyc +0 -0
  104. package/python/kundali_lib/vedic/__pycache__/char_dasha.cpython-313.pyc +0 -0
  105. package/python/kundali_lib/vedic/__pycache__/chart.cpython-313.pyc +0 -0
  106. package/python/kundali_lib/vedic/__pycache__/chart_types.cpython-313.pyc +0 -0
  107. package/python/kundali_lib/vedic/__pycache__/compatibility.cpython-313.pyc +0 -0
  108. package/python/kundali_lib/vedic/__pycache__/constants.cpython-313.pyc +0 -0
  109. package/python/kundali_lib/vedic/__pycache__/dasha_extended.cpython-313.pyc +0 -0
  110. package/python/kundali_lib/vedic/__pycache__/dasha_systems.cpython-313.pyc +0 -0
  111. package/python/kundali_lib/vedic/__pycache__/doshas.cpython-313.pyc +0 -0
  112. package/python/kundali_lib/vedic/__pycache__/gandanta.cpython-313.pyc +0 -0
  113. package/python/kundali_lib/vedic/__pycache__/gochara.cpython-313.pyc +0 -0
  114. package/python/kundali_lib/vedic/__pycache__/hora.cpython-313.pyc +0 -0
  115. package/python/kundali_lib/vedic/__pycache__/houses.cpython-313.pyc +0 -0
  116. package/python/kundali_lib/vedic/__pycache__/jaimini.cpython-313.pyc +0 -0
  117. package/python/kundali_lib/vedic/__pycache__/kalachakra.cpython-313.pyc +0 -0
  118. package/python/kundali_lib/vedic/__pycache__/kartari.cpython-313.pyc +0 -0
  119. package/python/kundali_lib/vedic/__pycache__/kurmachakra.cpython-313.pyc +0 -0
  120. package/python/kundali_lib/vedic/__pycache__/lunar_return.cpython-313.pyc +0 -0
  121. package/python/kundali_lib/vedic/__pycache__/muhurta.cpython-313.pyc +0 -0
  122. package/python/kundali_lib/vedic/__pycache__/nabhasha.cpython-313.pyc +0 -0
  123. package/python/kundali_lib/vedic/__pycache__/nakshatra_details.cpython-313.pyc +0 -0
  124. package/python/kundali_lib/vedic/__pycache__/panchanga.cpython-313.pyc +0 -0
  125. package/python/kundali_lib/vedic/__pycache__/planets.cpython-313.pyc +0 -0
  126. package/python/kundali_lib/vedic/__pycache__/shadbala.cpython-313.pyc +0 -0
  127. package/python/kundali_lib/vedic/__pycache__/special_conditions.cpython-313.pyc +0 -0
  128. package/python/kundali_lib/vedic/__pycache__/sudarshana.cpython-313.pyc +0 -0
  129. package/python/kundali_lib/vedic/__pycache__/tajaka.cpython-313.pyc +0 -0
  130. package/python/kundali_lib/vedic/__pycache__/upagrahas.cpython-313.pyc +0 -0
  131. package/python/kundali_lib/vedic/__pycache__/varshaphal.cpython-313.pyc +0 -0
  132. package/python/kundali_lib/vedic/__pycache__/yogas.cpython-313.pyc +0 -0
  133. package/python/kundali_lib/vedic/__pycache__/zodiac.cpython-313.pyc +0 -0
  134. package/python/kundali_lib/vedic/arishta.py +465 -0
  135. package/python/kundali_lib/vedic/ashtakavarga.py +213 -0
  136. package/python/kundali_lib/vedic/avasthas.py +292 -0
  137. package/python/kundali_lib/vedic/ayanamsa.py +106 -0
  138. package/python/kundali_lib/vedic/bhava_chalit.py +137 -0
  139. package/python/kundali_lib/vedic/char_dasha.py +308 -0
  140. package/python/kundali_lib/vedic/chart.py +126 -0
  141. package/python/kundali_lib/vedic/chart_types.py +338 -0
  142. package/python/kundali_lib/vedic/compatibility.py +705 -0
  143. package/python/kundali_lib/vedic/constants.py +108 -0
  144. package/python/kundali_lib/vedic/dasha_extended.py +262 -0
  145. package/python/kundali_lib/vedic/dasha_systems.py +439 -0
  146. package/python/kundali_lib/vedic/doshas.py +453 -0
  147. package/python/kundali_lib/vedic/gandanta.py +213 -0
  148. package/python/kundali_lib/vedic/gochara.py +277 -0
  149. package/python/kundali_lib/vedic/hora.py +263 -0
  150. package/python/kundali_lib/vedic/houses.py +30 -0
  151. package/python/kundali_lib/vedic/jaimini.py +361 -0
  152. package/python/kundali_lib/vedic/kalachakra.py +226 -0
  153. package/python/kundali_lib/vedic/kartari.py +243 -0
  154. package/python/kundali_lib/vedic/kurmachakra.py +383 -0
  155. package/python/kundali_lib/vedic/lunar_return.py +402 -0
  156. package/python/kundali_lib/vedic/muhurta.py +414 -0
  157. package/python/kundali_lib/vedic/nabhasha.py +349 -0
  158. package/python/kundali_lib/vedic/nakshatra_details.py +945 -0
  159. package/python/kundali_lib/vedic/panchanga.py +297 -0
  160. package/python/kundali_lib/vedic/planets.py +55 -0
  161. package/python/kundali_lib/vedic/shadbala.py +500 -0
  162. package/python/kundali_lib/vedic/special_conditions.py +319 -0
  163. package/python/kundali_lib/vedic/sudarshana.py +232 -0
  164. package/python/kundali_lib/vedic/tajaka.py +482 -0
  165. package/python/kundali_lib/vedic/upagrahas.py +229 -0
  166. package/python/kundali_lib/vedic/varshaphal.py +185 -0
  167. package/python/kundali_lib/vedic/yogas.py +935 -0
  168. package/python/kundali_lib/vedic/zodiac.py +42 -0
  169. package/python/kundali_lib/vedicastro_bridge.py +198 -0
  170. package/remote-server.js +590 -0
  171. package/requirements.txt +8 -0
  172. package/setup.sh +218 -0
@@ -0,0 +1,482 @@
1
+ """Tajaka Yogas: special yogas in annual charts (Varshaphal) based on Persian/Tajika system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kundali_lib.vedic.constants import RASHI_LORDS, RASHIS
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Reference data
9
+ # ---------------------------------------------------------------------------
10
+
11
+ RASHI_LORDS_MAP: dict[str, str] = {r: RASHI_LORDS[i] for i, r in enumerate(RASHIS)}
12
+
13
+ # Approximate daily mean motion (degrees/day) used to determine faster vs slower planet.
14
+ MEAN_DAILY_MOTION: dict[str, float] = {
15
+ "Moon": 13.2,
16
+ "Mercury": 1.38,
17
+ "Venus": 1.2,
18
+ "Sun": 0.985,
19
+ "Mars": 0.524,
20
+ "Jupiter": 0.083,
21
+ "Saturn": 0.034,
22
+ "Rahu": -0.053,
23
+ "Ketu": -0.053,
24
+ }
25
+
26
+ DUSTHANA_HOUSES = {6, 8, 12}
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Internal helpers
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def _get_planet(positions: list[dict], name: str) -> dict | None:
35
+ for p in positions:
36
+ if p["name"] == name:
37
+ return p
38
+ return None
39
+
40
+
41
+ def _angular_diff(a: float, b: float) -> float:
42
+ """Signed difference (a − b) normalised to [−180, 180]."""
43
+ diff = (a - b) % 360.0
44
+ if diff > 180.0:
45
+ diff -= 360.0
46
+ return diff
47
+
48
+
49
+ def _planet_speed(planet: dict) -> float:
50
+ """Return absolute daily motion; prefer actual speed field if present."""
51
+ return abs(
52
+ planet.get("speed_longitude", MEAN_DAILY_MOTION.get(planet["name"], 0.5))
53
+ )
54
+
55
+
56
+ def _house_of_sign(sign: str, asc_sign: str) -> int:
57
+ """Return the house number that a given sign falls in, relative to the ascendant sign."""
58
+ asc_idx = RASHIS.index(asc_sign)
59
+ sign_idx = RASHIS.index(sign)
60
+ return (sign_idx - asc_idx) % 12 + 1
61
+
62
+
63
+ def _sign_of_house(house: int, asc_sign: str) -> str:
64
+ asc_idx = RASHIS.index(asc_sign)
65
+ return RASHIS[(asc_idx + house - 1) % 12]
66
+
67
+
68
+ def _planet_strength(planet: dict, varshaphal_chart: dict) -> str:
69
+ """Rough strength assessment for a planet in the Varshaphal chart."""
70
+ name = planet["name"]
71
+ rashi = planet.get("rashi", "")
72
+ house = planet.get("house", 0)
73
+
74
+ from kundali_lib.vedic.yogas import ( # local import to avoid cycles
75
+ DEBILITATION,
76
+ EXALTATION,
77
+ )
78
+
79
+ if rashi == EXALTATION.get(name):
80
+ return "Exalted (Strong)"
81
+ if rashi == DEBILITATION.get(name):
82
+ return "Debilitated (Weak)"
83
+ if RASHI_LORDS_MAP.get(rashi) == name:
84
+ return "Own sign (Strong)"
85
+ if house in {1, 4, 7, 10}:
86
+ return "Angular (Moderate-Strong)"
87
+ if house in {6, 8, 12}:
88
+ return "Dusthana (Weak)"
89
+ return "Moderate"
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Yoga checkers
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ def _check_ithasala(varshaphal_positions: list[dict], asc_sign: str) -> list[dict]:
98
+ """Ithasala (Approach) yoga: faster planet approaching slower one within 13°."""
99
+ results = []
100
+ planets = [p for p in varshaphal_positions if p["name"] not in ("Rahu", "Ketu")]
101
+
102
+ checked: set[frozenset] = set()
103
+ for i, p1 in enumerate(planets):
104
+ for p2 in planets[i + 1 :]:
105
+ pair = frozenset([p1["name"], p2["name"]])
106
+ if pair in checked:
107
+ continue
108
+ checked.add(pair)
109
+
110
+ speed1 = _planet_speed(p1)
111
+ speed2 = _planet_speed(p2)
112
+
113
+ faster, slower = (p1, p2) if speed1 >= speed2 else (p2, p1)
114
+ lon_f = faster["longitude"]
115
+ lon_s = slower["longitude"]
116
+ diff = _angular_diff(lon_s, lon_f) # positive → slower is ahead of faster
117
+
118
+ if 0 < diff <= 13:
119
+ yoga_type = "Poorna Ithasala" if diff <= 1 else "Ithasala"
120
+ results.append(
121
+ {
122
+ "name": yoga_type,
123
+ "present": True,
124
+ "planets": [faster["name"], slower["name"]],
125
+ "orb": round(diff, 2),
126
+ "description": (
127
+ f"{faster['name']} is approaching {slower['name']} "
128
+ f"(separation {round(diff, 2)}°)."
129
+ ),
130
+ "effect": "The matter will come to pass — good results",
131
+ }
132
+ )
133
+ return results
134
+
135
+
136
+ def _check_ishrafa(varshaphal_positions: list[dict]) -> list[dict]:
137
+ """Ishrafa (Separation) yoga: faster planet has just passed the slower one within 13°."""
138
+ results = []
139
+ planets = [p for p in varshaphal_positions if p["name"] not in ("Rahu", "Ketu")]
140
+
141
+ checked: set[frozenset] = set()
142
+ for i, p1 in enumerate(planets):
143
+ for p2 in planets[i + 1 :]:
144
+ pair = frozenset([p1["name"], p2["name"]])
145
+ if pair in checked:
146
+ continue
147
+ checked.add(pair)
148
+
149
+ speed1 = _planet_speed(p1)
150
+ speed2 = _planet_speed(p2)
151
+ faster, slower = (p1, p2) if speed1 >= speed2 else (p2, p1)
152
+ lon_f = faster["longitude"]
153
+ lon_s = slower["longitude"]
154
+ diff = _angular_diff(
155
+ lon_f, lon_s
156
+ ) # positive → faster has moved past slower
157
+
158
+ if 0 < diff <= 13:
159
+ results.append(
160
+ {
161
+ "name": "Ishrafa",
162
+ "present": True,
163
+ "planets": [faster["name"], slower["name"]],
164
+ "orb": round(diff, 2),
165
+ "description": (
166
+ f"{faster['name']} has already passed {slower['name']} "
167
+ f"(separation {round(diff, 2)}°)."
168
+ ),
169
+ "effect": "Matter will not succeed, past opportunity missed",
170
+ }
171
+ )
172
+ return results
173
+
174
+
175
+ def _check_mutthasila(varshaphal_positions: list[dict], asc_sign: str) -> list[dict]:
176
+ """Mutthasila yoga: a third planet creates connection between Lagna lord and another."""
177
+ results = []
178
+ lagna_lord_name = RASHI_LORDS_MAP.get(asc_sign)
179
+ if not lagna_lord_name:
180
+ return results
181
+
182
+ lagna_lord = _get_planet(varshaphal_positions, lagna_lord_name)
183
+ if not lagna_lord:
184
+ return results
185
+
186
+ for connector in varshaphal_positions:
187
+ if connector["name"] in (lagna_lord_name, "Rahu", "Ketu"):
188
+ continue
189
+ for target in varshaphal_positions:
190
+ if target["name"] in (lagna_lord_name, connector["name"], "Rahu", "Ketu"):
191
+ continue
192
+ diff_lc = abs(
193
+ _angular_diff(connector["longitude"], lagna_lord["longitude"])
194
+ )
195
+ diff_ct = abs(_angular_diff(target["longitude"], connector["longitude"]))
196
+ if diff_lc <= 13 and diff_ct <= 13:
197
+ results.append(
198
+ {
199
+ "name": "Mutthasila",
200
+ "present": True,
201
+ "planets": [lagna_lord_name, connector["name"], target["name"]],
202
+ "orb": round(max(diff_lc, diff_ct), 2),
203
+ "description": (
204
+ f"{connector['name']} connects {lagna_lord_name} "
205
+ f"and {target['name']} indirectly."
206
+ ),
207
+ "effect": "Results come through an intermediary",
208
+ }
209
+ )
210
+ return results
211
+
212
+
213
+ def _check_nakta(varshaphal_positions: list[dict]) -> list[dict]:
214
+ """Nakta yoga: Moon collects light between two planets."""
215
+ results = []
216
+ moon = _get_planet(varshaphal_positions, "Moon")
217
+ if not moon:
218
+ return results
219
+
220
+ moon_lon = moon["longitude"]
221
+ others = [
222
+ p for p in varshaphal_positions if p["name"] not in ("Moon", "Rahu", "Ketu")
223
+ ]
224
+
225
+ for i, p1 in enumerate(others):
226
+ for p2 in others[i + 1 :]:
227
+ lon1, lon2 = p1["longitude"], p2["longitude"]
228
+ lo, hi = (lon1, lon2) if lon1 <= lon2 else (lon2, lon1)
229
+ if lo <= moon_lon <= hi or (hi - lo > 180 and not (lo <= moon_lon <= hi)):
230
+ diff = abs(hi - lo)
231
+ if diff <= 30: # Moon collects within a reasonable arc
232
+ results.append(
233
+ {
234
+ "name": "Nakta",
235
+ "present": True,
236
+ "planets": [p1["name"], "Moon", p2["name"]],
237
+ "description": (
238
+ f"Moon is between {p1['name']} and {p2['name']}, "
239
+ f"collecting light."
240
+ ),
241
+ "effect": "Results come slowly through intermediary help",
242
+ }
243
+ )
244
+ return results
245
+
246
+
247
+ def _check_yamaya(varshaphal_positions: list[dict], birth_chart: dict) -> list[dict]:
248
+ """Yamaya yoga: planet within 1° of its natal position in Varshaphal."""
249
+ results = []
250
+ natal_positions = birth_chart.get("planetary_positions", [])
251
+ for vp in varshaphal_positions:
252
+ for np_ in natal_positions:
253
+ if vp["name"] == np_["name"]:
254
+ diff = abs(_angular_diff(vp["longitude"], np_["longitude"]))
255
+ if diff <= 1.0:
256
+ results.append(
257
+ {
258
+ "name": "Yamaya",
259
+ "present": True,
260
+ "planets": [vp["name"]],
261
+ "orb": round(diff, 2),
262
+ "description": (
263
+ f"{vp['name']} is within 1° of its natal position "
264
+ f"in the Varshaphal chart."
265
+ ),
266
+ "effect": f"Strong year for matters ruled by {vp['name']}",
267
+ }
268
+ )
269
+ return results
270
+
271
+
272
+ def _check_duphali_kuttha(
273
+ varshaphal_positions: list[dict], asc_sign: str
274
+ ) -> list[dict]:
275
+ """Duphali Kuttha: Varsha Lagna lord in dusthana and weak."""
276
+ results = []
277
+ lagna_lord_name = RASHI_LORDS_MAP.get(asc_sign)
278
+ if not lagna_lord_name:
279
+ return results
280
+ ll = _get_planet(varshaphal_positions, lagna_lord_name)
281
+ if not ll:
282
+ return results
283
+ house = ll.get("house", 0)
284
+ strength = _planet_strength(ll, {})
285
+ if house in DUSTHANA_HOUSES and "Weak" in strength:
286
+ results.append(
287
+ {
288
+ "name": "Duphali Kuttha",
289
+ "present": True,
290
+ "planets": [lagna_lord_name],
291
+ "description": (
292
+ f"Varsha Lagna lord {lagna_lord_name} is in house {house} "
293
+ f"(dusthana) and {strength}."
294
+ ),
295
+ "effect": "Difficult year",
296
+ }
297
+ )
298
+ return results
299
+
300
+
301
+ def _check_tambira(muntha_house: int) -> list[dict]:
302
+ """Tambira yoga: Muntha in dusthana (6, 8, 12)."""
303
+ if muntha_house in DUSTHANA_HOUSES:
304
+ return [
305
+ {
306
+ "name": "Tambira",
307
+ "present": True,
308
+ "planets": [],
309
+ "description": f"Muntha is in house {muntha_house} (dusthana).",
310
+ "effect": "Health challenges and obstacles during the year",
311
+ }
312
+ ]
313
+ return []
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Year-quality assessment
318
+ # ---------------------------------------------------------------------------
319
+
320
+
321
+ def _assess_year_quality(
322
+ yogas: list[dict], muntha_house: int, lagna_lord_strong: bool
323
+ ) -> str:
324
+ names = [y["name"] for y in yogas]
325
+ score = 0
326
+ if "Poorna Ithasala" in names:
327
+ score += 3
328
+ if "Ithasala" in names:
329
+ score += 2
330
+ if "Yamaya" in names:
331
+ score += 2
332
+ if "Mutthasila" in names:
333
+ score += 1
334
+ if "Nakta" in names:
335
+ score += 1
336
+ if "Ishrafa" in names:
337
+ score -= 2
338
+ if "Duphali Kuttha" in names:
339
+ score -= 3
340
+ if "Tambira" in names:
341
+ score -= 2
342
+ if muntha_house in {1, 4, 7, 10}:
343
+ score += 1
344
+ if muntha_house in DUSTHANA_HOUSES:
345
+ score -= 1
346
+ if lagna_lord_strong:
347
+ score += 1
348
+
349
+ if score >= 4:
350
+ return "Excellent"
351
+ if score >= 2:
352
+ return "Good"
353
+ if score >= -1:
354
+ return "Average"
355
+ return "Difficult"
356
+
357
+
358
+ # ---------------------------------------------------------------------------
359
+ # Public API
360
+ # ---------------------------------------------------------------------------
361
+
362
+
363
+ def get_tajaka_analysis(
364
+ birth_chart: dict,
365
+ varshaphal_chart: dict,
366
+ year_of_life: int,
367
+ ) -> dict:
368
+ """Analyse Tajaka yogas for a Varshaphal (Solar Return) chart.
369
+
370
+ Args:
371
+ birth_chart: Output of ``build_chart()`` for the native's birth.
372
+ varshaphal_chart: Output of ``get_varshaphal()``; the ``varshaphal_chart``
373
+ sub-dict (containing ``planetary_positions``, ``houses``,
374
+ ``ascendant``, …).
375
+ year_of_life: Solar return number (e.g. 26 for the 26th year).
376
+
377
+ Returns:
378
+ dict with Muntha details, Munthesh, all detected Tajaka yogas, year
379
+ quality assessment, and a brief summary.
380
+ """
381
+ # ── Accept either the top-level get_varshaphal() output or the inner chart ─
382
+ if "varshaphal_chart" in varshaphal_chart:
383
+ vp = varshaphal_chart["varshaphal_chart"]
384
+ else:
385
+ vp = varshaphal_chart
386
+
387
+ vp_positions: list[dict] = vp.get("planetary_positions", [])
388
+ asc_info: dict = vp.get("ascendant", {})
389
+ asc_sign: str = asc_info.get("rashi", RASHIS[0])
390
+
391
+ # ── Muntha ────────────────────────────────────────────────────────────────
392
+ birth_asc_sign: str = birth_chart.get("ascendant", {}).get("rashi", RASHIS[0])
393
+ birth_asc_idx = RASHIS.index(birth_asc_sign)
394
+ muntha_idx = (birth_asc_idx + year_of_life) % 12
395
+ muntha_sign = RASHIS[muntha_idx]
396
+ muntha_house = _house_of_sign(muntha_sign, asc_sign)
397
+ muntha_lord_name = RASHI_LORDS_MAP[muntha_sign]
398
+
399
+ muntha_significance_map = {
400
+ 1: "Self, health, vitality — very prominent year",
401
+ 2: "Finances, family, speech",
402
+ 3: "Courage, siblings, short travel",
403
+ 4: "Home, mother, property",
404
+ 5: "Children, creativity, intellect",
405
+ 6: "Enemies, disease, service",
406
+ 7: "Partnerships, marriage, business",
407
+ 8: "Obstacles, transformation, longevity",
408
+ 9: "Luck, dharma, father, long travel",
409
+ 10: "Career, reputation, authority — prominent year",
410
+ 11: "Gains, social network, fulfilment",
411
+ 12: "Losses, expenditure, spirituality",
412
+ }
413
+ muntha_significance = muntha_significance_map.get(muntha_house, "")
414
+
415
+ muntha_lord_planet = _get_planet(vp_positions, muntha_lord_name)
416
+ muntha_lord_house = muntha_lord_planet.get("house", 0) if muntha_lord_planet else 0
417
+ muntha_lord_strength = (
418
+ _planet_strength(muntha_lord_planet, vp) if muntha_lord_planet else "Unknown"
419
+ )
420
+
421
+ # ── Lagna lord for Duphali Kuttha check ──────────────────────────────────
422
+ lagna_lord_name = RASHI_LORDS_MAP.get(asc_sign, "")
423
+ lagna_lord_planet = _get_planet(vp_positions, lagna_lord_name)
424
+ lagna_lord_strong = (
425
+ "Strong" in _planet_strength(lagna_lord_planet, vp)
426
+ if lagna_lord_planet
427
+ else False
428
+ )
429
+
430
+ # ── Yoga detection ────────────────────────────────────────────────────────
431
+ yogas: list[dict] = []
432
+ yogas.extend(_check_ithasala(vp_positions, asc_sign))
433
+ yogas.extend(_check_ishrafa(vp_positions))
434
+ yogas.extend(_check_mutthasila(vp_positions, asc_sign))
435
+ yogas.extend(_check_nakta(vp_positions))
436
+ yogas.extend(_check_yamaya(vp_positions, birth_chart))
437
+ yogas.extend(_check_duphali_kuttha(vp_positions, asc_sign))
438
+ yogas.extend(_check_tambira(muntha_house))
439
+
440
+ # Normalise yoga list to match the requested output schema
441
+ normalised_yogas = []
442
+ for y in yogas:
443
+ normalised_yogas.append(
444
+ {
445
+ "name": y["name"],
446
+ "present": y["present"],
447
+ "description": y["description"],
448
+ "effect": y["effect"],
449
+ }
450
+ )
451
+
452
+ # ── Year quality ──────────────────────────────────────────────────────────
453
+ year_quality = _assess_year_quality(yogas, muntha_house, lagna_lord_strong)
454
+
455
+ # ── Summary ───────────────────────────────────────────────────────────────
456
+ yoga_names = [y["name"] for y in yogas]
457
+ summary_parts = [
458
+ f"Year {year_of_life} Varshaphal analysis.",
459
+ f"Muntha is in {muntha_sign} (house {muntha_house}): {muntha_significance}.",
460
+ f"Munthesh ({muntha_lord_name}) is in house {muntha_lord_house} — {muntha_lord_strength}.",
461
+ ]
462
+ if yoga_names:
463
+ summary_parts.append(f"Active Tajaka yogas: {', '.join(yoga_names)}.")
464
+ summary_parts.append(f"Overall year quality: {year_quality}.")
465
+ summary = " ".join(summary_parts)
466
+
467
+ return {
468
+ "muntha": {
469
+ "sign": muntha_sign,
470
+ "house_in_varshaphal": muntha_house,
471
+ "lord": muntha_lord_name,
472
+ "significance": muntha_significance,
473
+ },
474
+ "munthesh": {
475
+ "planet": muntha_lord_name,
476
+ "varshaphal_house": muntha_lord_house,
477
+ "strength": muntha_lord_strength,
478
+ },
479
+ "yogas": normalised_yogas,
480
+ "year_quality": year_quality,
481
+ "summary": summary,
482
+ }