Files
Jyotisha/scripts/affliction_evidence.py
T
ClaudeandClaude Opus 5.5 8ed5b5f3c9 feat(engine): port upstream skill calculation fixes into the engine (BUG-1699)
- affliction_evidence / career_semantic_profile / career_analysis (functional
  roles, affliction signals, user-industry gate)
- sade_sati.build_saturn_cycle_timeline with bisected ingress moments
  (FLG_SIDEREAL frame, BUG-1275)
- Chesta profile candidates and PL9 AV cell profile as sidecars
  (shadbala.py / ashtakavarga.py stay frozen, ERR-110)
- Raman Sahams in the annual pack; book rule packets + admission gate
- consultation_native_layers: affliction_evidence, career_analysis,
  sade_sati_timeline, shadbala_chesta_profiles, book_rules,
  annual_tajika.raman_sahams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-07 14:42:54 +08:00

84 lines
3.8 KiB
Python

"""Evidence-only Parashari affliction signals.
This module deliberately does not assign a universal weight or verdict. It
records which signals were observed so readers can keep pressure/distance
language conditional and apply an explicit domain profile when needed.
"""
from __future__ import annotations
from typing import Any
MALefic_NATURAL = {'Saturn', 'Mars', 'Sun', 'Rahu', 'Ketu'}
AFFLICTION_SIGNAL_TYPES = (
'conjunction', 'graha_aspect', 'functional_malefic', 'debilitated',
'combust', 'dusthana_placement', 'varga_pressure',
)
def validate_affliction_weight_profile(weights: dict[str, Any] | None = None) -> dict[str, Any]:
"""Validate one shared signal-weight profile for every reading domain."""
supplied = weights or {}
missing = [name for name in AFFLICTION_SIGNAL_TYPES if name not in supplied]
invalid = [name for name, value in supplied.items() if name in AFFLICTION_SIGNAL_TYPES and (not isinstance(value, (int, float)) or value < 0)]
return {
'status': 'ready' if not missing and not invalid else 'parameter_sensitive',
'weights': {name: supplied.get(name) for name in AFFLICTION_SIGNAL_TYPES},
'missing': missing,
'invalid': invalid,
'domains': ['career', 'parents', 'marriage', 'health', 'education'],
'claim_boundary': 'A shared profile is required before comparing pressure across domains; signal count alone is not a verdict.',
}
def _house_distance(source: int, target: int) -> int:
return (target - source) % 12 + 1
def collect_affliction_evidence(
*,
target_house: int,
target_lord: str | None,
planets: dict[str, dict[str, Any]],
functional_malefics: set[str] | list[str] | None = None,
varga_pressure: bool = False,
) -> dict[str, Any]:
"""Collect raw signals for a house/lord/karaka line.
``planets`` uses 1-based houses and optional ``debilitated``/``combust``
flags. Missing data is retained as a limitation rather than inferred.
"""
functional_malefics = set(functional_malefics or ())
target = planets.get(target_lord or '', {})
target_house_no = target.get('house')
signals: list[dict[str, Any]] = []
if target_lord and target_house_no is not None:
for name in ('Rahu', 'Ketu', 'Saturn', 'Mars'):
if name in planets and planets[name].get('house') == target_house_no:
signals.append({'type': 'conjunction', 'planet': name, 'basis': 'same_house'})
for name in ('Saturn', 'Mars'):
source_house = planets.get(name, {}).get('house')
if source_house is not None and _house_distance(source_house, target_house_no) in ({'Saturn': {3, 7, 10}, 'Mars': {4, 7, 8}}[name]):
signals.append({'type': 'graha_aspect', 'planet': name, 'basis': 'classical_special_aspect'})
if target_lord and target_lord in functional_malefics:
signals.append({'type': 'functional_malefic', 'planet': target_lord, 'basis': 'functional_profile'})
for flag in ('debilitated', 'combust'):
if target.get(flag) is True:
signals.append({'type': flag, 'planet': target_lord, 'basis': 'planet_state'})
if target_house_no is not None and _house_distance(target_house, target_house_no) in (6, 8, 12):
signals.append({'type': 'dusthana_placement', 'planet': target_lord, 'basis': 'house_distance'})
if varga_pressure:
signals.append({'type': 'varga_pressure', 'planet': target_lord, 'basis': 'explicit_varga_input'})
return {
'status': 'parameter_sensitive',
'target_house': target_house,
'target_lord': target_lord,
'signals': signals,
'signal_count': len(signals),
'pressure_language_allowed': bool(signals),
'claim_boundary': 'Signals indicate possible pressure or distance only; no universal weight, threshold, presence/absence, diagnosis, or event certainty is inferred.',
}