fix(web): keep unrectified birth data usable without inventing a minute
Rectification stays optional. Reported minutes can consult and generate reports; date-plus-period uses a declared window instead of a midpoint or 00:00. Updates BUG-341. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -337,3 +337,220 @@ def compute_sade_sati(
|
||||
)
|
||||
result["result_hash"] = _canonical_hash(result)
|
||||
return result
|
||||
|
||||
|
||||
_CLOCK_PLANETS = (
|
||||
"Sun",
|
||||
"Moon",
|
||||
"Mars",
|
||||
"Mercury",
|
||||
"Jupiter",
|
||||
"Venus",
|
||||
"Saturn",
|
||||
"Rahu",
|
||||
"Ketu",
|
||||
)
|
||||
_MINUTES_PER_DAY = 24 * 60
|
||||
|
||||
|
||||
def _require_hhmm(value: Any, *, field: str) -> str:
|
||||
clock = str(value or "").strip()
|
||||
if len(clock) != 5 or clock[2] != ":":
|
||||
raise CalculationError(f"{field} must be HH:MM")
|
||||
try:
|
||||
hour = int(clock[:2])
|
||||
minute = int(clock[3:])
|
||||
except ValueError as exc:
|
||||
raise CalculationError(f"{field} must be HH:MM") from exc
|
||||
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
||||
raise CalculationError(f"{field} must be HH:MM")
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
def _clock_minutes(clock: str) -> int:
|
||||
return int(clock[:2]) * 60 + int(clock[3:])
|
||||
|
||||
|
||||
def _minutes_to_clock(total: int) -> str:
|
||||
normalized = total % _MINUTES_PER_DAY
|
||||
if normalized < 0:
|
||||
normalized += _MINUTES_PER_DAY
|
||||
return f"{normalized // 60:02d}:{normalized % 60:02d}"
|
||||
|
||||
|
||||
def declared_window_probe_clocks(range_start: str, range_end: str) -> list[str]:
|
||||
start = _clock_minutes(_require_hhmm(range_start, field="range_start"))
|
||||
end = _clock_minutes(_require_hhmm(range_end, field="range_end"))
|
||||
span = end + _MINUTES_PER_DAY - start if end < start else end - start
|
||||
if span < 1:
|
||||
raise CalculationError("declared window must span at least one minute")
|
||||
clocks: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for numerator in (0, 1, 2, 3):
|
||||
offset = int((span * numerator) / 3 + 0.5)
|
||||
clock = _minutes_to_clock(start + offset)
|
||||
if clock not in seen:
|
||||
seen.add(clock)
|
||||
clocks.append(clock)
|
||||
if len(clocks) < 2:
|
||||
raise CalculationError("declared window must yield at least two distinct probes")
|
||||
return clocks
|
||||
|
||||
|
||||
def _probe_role(index: int, count: int) -> str:
|
||||
if index == 0:
|
||||
return "range_start"
|
||||
if index == count - 1:
|
||||
return "range_end"
|
||||
return "interior"
|
||||
|
||||
|
||||
def _window_layer_snapshot(chart: dict[str, Any]) -> dict[str, Any]:
|
||||
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
||||
planet_signs: dict[str, str] = {}
|
||||
for name in _CLOCK_PLANETS:
|
||||
planet = planets.get(name)
|
||||
sign = planet.get("sign") if isinstance(planet, dict) else None
|
||||
if isinstance(sign, str) and sign:
|
||||
planet_signs[name] = sign
|
||||
moon = planets.get("Moon") if isinstance(planets.get("Moon"), dict) else {}
|
||||
moon_nakshatra = moon.get("nakshatra") if isinstance(moon.get("nakshatra"), str) else None
|
||||
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
|
||||
ascendant_sign = ascendant.get("sign") if isinstance(ascendant.get("sign"), str) else None
|
||||
houses_raw = chart.get("houses") if isinstance(chart.get("houses"), dict) else {}
|
||||
house_cusp_signs: dict[str, str] = {}
|
||||
for index in range(1, 13):
|
||||
house = houses_raw.get(f"house_{index}")
|
||||
sign = house.get("cusp_sign") if isinstance(house, dict) else None
|
||||
if isinstance(sign, str) and sign:
|
||||
house_cusp_signs[str(index)] = sign
|
||||
return {
|
||||
"planet_signs": planet_signs,
|
||||
"moon_nakshatra": moon_nakshatra,
|
||||
"ascendant_sign": ascendant_sign,
|
||||
"house_cusp_signs": house_cusp_signs,
|
||||
}
|
||||
|
||||
|
||||
def _unique_in_order(values: list[Any]) -> list[Any]:
|
||||
ordered: list[Any] = []
|
||||
for value in values:
|
||||
if value not in ordered:
|
||||
ordered.append(value)
|
||||
return ordered
|
||||
|
||||
|
||||
def compute_declared_window_chart(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if "hour" in payload or "minute" in payload or "second" in payload:
|
||||
raise CalculationError("declared window must not include a single birth minute")
|
||||
range_start = _require_hhmm(payload.get("range_start", payload.get("rangeStart")), field="range_start")
|
||||
range_end = _require_hhmm(payload.get("range_end", payload.get("rangeEnd")), field="range_end")
|
||||
clocks = declared_window_probe_clocks(range_start, range_end)
|
||||
snapshots: list[dict[str, Any]] = []
|
||||
probes: list[dict[str, str]] = []
|
||||
for index, clock in enumerate(clocks):
|
||||
hour = int(clock[:2])
|
||||
minute = int(clock[3:])
|
||||
chart = compute_chart({
|
||||
"year": payload["year"],
|
||||
"month": payload["month"],
|
||||
"day": payload["day"],
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"second": 0,
|
||||
"lat": payload["lat"],
|
||||
"lon": payload["lon"],
|
||||
"tz": payload["tz"],
|
||||
"timezone_id": payload.get("timezone_id", payload.get("timezoneId")),
|
||||
"ayanamsa": payload.get("ayanamsa"),
|
||||
"node_mode": payload.get("node_mode", payload.get("nodeMode", "mean")),
|
||||
})
|
||||
snapshots.append(_window_layer_snapshot(chart))
|
||||
probes.append({"clock": clock, "role": _probe_role(index, len(clocks))})
|
||||
|
||||
stable_planet_signs: dict[str, str] = {}
|
||||
varying_planet_signs: dict[str, list[str]] = {}
|
||||
for name in _CLOCK_PLANETS:
|
||||
signs = _unique_in_order([
|
||||
snapshot["planet_signs"][name]
|
||||
for snapshot in snapshots
|
||||
if name in snapshot["planet_signs"]
|
||||
])
|
||||
if len(signs) == 1:
|
||||
stable_planet_signs[name] = signs[0]
|
||||
elif len(signs) > 1:
|
||||
varying_planet_signs[name] = signs
|
||||
|
||||
moon_nakshatras = _unique_in_order([
|
||||
snapshot["moon_nakshatra"]
|
||||
for snapshot in snapshots
|
||||
if snapshot["moon_nakshatra"]
|
||||
])
|
||||
ascendant_signs = _unique_in_order([
|
||||
snapshot["ascendant_sign"]
|
||||
for snapshot in snapshots
|
||||
if snapshot["ascendant_sign"]
|
||||
])
|
||||
house_variation: dict[str, list[str]] = {}
|
||||
stable_houses: dict[str, str] = {}
|
||||
for house in (str(index) for index in range(1, 13)):
|
||||
signs = _unique_in_order([
|
||||
snapshot["house_cusp_signs"][house]
|
||||
for snapshot in snapshots
|
||||
if house in snapshot["house_cusp_signs"]
|
||||
])
|
||||
if len(signs) == 1:
|
||||
stable_houses[house] = signs[0]
|
||||
elif len(signs) > 1:
|
||||
house_variation[house] = signs
|
||||
|
||||
stable_layers: dict[str, Any] = {"planet_signs": stable_planet_signs}
|
||||
if len(moon_nakshatras) == 1:
|
||||
stable_layers["moon_nakshatra"] = moon_nakshatras[0]
|
||||
if len(ascendant_signs) == 1:
|
||||
stable_layers["ascendant_sign"] = ascendant_signs[0]
|
||||
if stable_houses:
|
||||
stable_layers["house_cusp_signs"] = stable_houses
|
||||
|
||||
varying_layers: dict[str, Any] = {}
|
||||
if varying_planet_signs:
|
||||
varying_layers["planet_signs"] = varying_planet_signs
|
||||
if len(moon_nakshatras) > 1:
|
||||
varying_layers["moon_nakshatra"] = moon_nakshatras
|
||||
if len(ascendant_signs) > 1:
|
||||
varying_layers["ascendant_signs"] = ascendant_signs
|
||||
if house_variation:
|
||||
varying_layers["house_cusp_signs"] = house_variation
|
||||
|
||||
wraps_midnight = _clock_minutes(range_end) < _clock_minutes(range_start)
|
||||
packet = {
|
||||
"declared_range": {
|
||||
"start": range_start,
|
||||
"end": range_end,
|
||||
"wraps_midnight": wraps_midnight,
|
||||
},
|
||||
"probe_count": len(probes),
|
||||
"probes": probes,
|
||||
"stable_layers": stable_layers,
|
||||
"varying_layers": varying_layers,
|
||||
"blocked_layers": [
|
||||
"vimshottari_boundaries",
|
||||
"narayana_boundaries",
|
||||
"vargas",
|
||||
"personal_transits",
|
||||
*(["lagna", "houses"] if len(ascendant_signs) > 1 else []),
|
||||
],
|
||||
"answer_policy": {
|
||||
"can_answer_direction": bool(
|
||||
stable_planet_signs
|
||||
or stable_layers.get("moon_nakshatra")
|
||||
or stable_layers.get("ascendant_sign")
|
||||
),
|
||||
"can_answer_precise_timing": False,
|
||||
"birth_time_confidence": "declared_window",
|
||||
"candidate_is_confirmed": False,
|
||||
"should_lead_with_limitations": True,
|
||||
},
|
||||
}
|
||||
packet["result_hash"] = _canonical_hash(packet)
|
||||
return packet
|
||||
|
||||
Reference in New Issue
Block a user