Source code for stellium.presentation.sections.rectification

"""Report sections for sect rectification (the compare-hypothesis workbench)."""

from __future__ import annotations

from collections.abc import Iterable
from typing import Any

from stellium.core.models import CalculatedChart
from stellium.rectification.analysis import SectAnalysis, analyze_sect
from stellium.rectification.matrix import ConvergenceMatrix, convergence_matrix

_SECT_LIGHT_ABBR = {"day": "Sun", "night": "Moon"}
_MALEFIC_ABBR = {"day": "Mars", "night": "Saturn"}

_MALEFIC_FLAVOR = {
    "day": "hot, sharp, sudden — accidents, violence, fever, conflict",
    "night": "cold, slow, chronic — illness, poverty, confinement, decline",
}
_BENEFIC_FLAVOR = {
    "day": "Jupiter-toned — honour, wealth, law, faith, expansion",
    "night": "Venus-toned — love, art, beauty, pleasure, music",
}


def _cond(pc) -> str:
    return f"{pc.planet} in {pc.sign} ({pc.dignity})"


def _hypothesis_table(a: SectAnalysis) -> dict[str, Any]:
    """A single side-by-side comparison table (renders in every format)."""
    d, n = a.day, a.night
    return {
        "type": "table",
        "headers": ["", "IF DAY (above horizon)", "IF NIGHT (below horizon)"],
        "rows": [
            ["Sect light", _cond(d.sect_light), _cond(n.sect_light)],
            [
                "Out-of-sect malefic",
                _cond(d.out_of_sect_malefic),
                _cond(n.out_of_sect_malefic),
            ],
            ["  → hardship reads", _MALEFIC_FLAVOR["day"], _MALEFIC_FLAVOR["night"]],
            ["In-sect benefic", _cond(d.in_sect_benefic), _cond(n.in_sect_benefic)],
            ["  → fortune reads", _BENEFIC_FLAVOR["day"], _BENEFIC_FLAVOR["night"]],
        ],
    }


def _lean_word(day_v: float, night_v: float) -> str:
    if abs(day_v - night_v) < 1e-9:
        return "even"
    return "DAY" if day_v > night_v else "NIGHT"


[docs] class SectRectificationSection: """Compare-hypothesis sect analysis — day vs night, anchored by the validated daylight × malefic classifier (~70% LOO). An *indicator, not an oracle*. Events/temperament are auto-looked-up for notables (by chart name); pass them explicitly for anyone else, or pass ``events=()`` for a geometry-only analysis. """ def __init__( self, *, events: Iterable | None = None, temperament: Iterable | None = None, ) -> None: self._events = events self._temperament = temperament @property def section_name(self) -> str: return "Sect Rectification"
[docs] def generate_data(self, chart: CalculatedChart) -> dict[str, Any]: a = analyze_sect(chart, events=self._events, temperament=self._temperament) anchor = { "Chart sect (as given)": (a.chart_sect or "undetermined").capitalize(), "Daylight prior P(day)": f"{a.daylight_fraction:.2f} (geometry, the base rate)", "Calibrated P(day)": ( f"{a.p_day:.2f} → leans {a.leans.upper()} ({a.confidence:.0%} conf)" + ("" if a.has_events else " [geometry only — no events]") ), "Reading": "indicator, not oracle — adjudicate with real knowledge", } if a.moon_band: anchor["⚠ Moon band"] = a.moon_band sections: list[tuple[str, dict[str, Any]]] = [ ("Anchor", {"type": "key_value", "data": anchor}), ("Day vs Night hypotheses", _hypothesis_table(a)), ] if a.has_events or a.has_temperament: sections.append( ( "Evidence & convergence", {"type": "text", "text": self._evidence_text(a)}, ) ) return {"type": "compound", "sections": sections}
def _evidence_text(self, a: SectAnalysis) -> str: lines: list[str] = [] if a.has_events: lines.append("Evidence alignment — does the life match each hypothesis?") if a.hardship: lines.append( f" hardship flavour (malefic-of-sect, VALIDATED +0.35): " f"leans {_lean_word(*a.hardship)} " f"({a.hardship[0]:.1f} day / {a.hardship[1]:.1f} night)" ) if a.fortune: lines.append( f" fortune flavour (benefic-of-sect): " f"leans {_lean_word(*a.fortune)} " f"({a.fortune[0]:.1f} day / {a.fortune[1]:.1f} night)" ) if a.firdaria: lines.append( f" firdaria time-lord fit: day-hits {a.firdaria.day_hits} / " f"night-hits {a.firdaria.night_hits} → favours {a.firdaria.favors}" ) if a.has_temperament: lines.append("") lines.append( "Soft signals (⚠ ~null on strangers — high value only with " "real first-hand knowledge):" ) mt = a.malefic_temper or 0.0 st = a.sect_light_temper or 0.0 lines.append( f" malefic-of-temperament (Mars-hot − Saturn-cold) = {mt:+.0f} → " f"{'DAY' if mt > 0 else 'NIGHT' if mt < 0 else '—'}" ) lines.append( f" sect-light temperament (Solar − Lunar) = {st:+.0f} → " f"{'DAY' if st > 0 else 'NIGHT' if st < 0 else '—'}" ) votes = a.technique_votes() dv = sum(1 for _, v in votes if v == "day") nv = len(votes) - dv lines.append("") lines.append( f"Convergence (counted, never summed): DAY {dv} · NIGHT {nv} " f"(of {len(votes)} techniques)" ) lines.append( "→ SECT: trust this anchor — it is the only cross-validated signal (~70%). " "The soft/timing rows routinely point the wrong way; when they fight the " "anchor on sect, the anchor wins. TIME-within-sect is where your own " "knowledge decides." ) return "\n".join(lines)
def _hhmm(minute: int) -> str: return f"{minute // 60:02d}:{minute % 60:02d}"
[docs] class SectConvergenceMatrixSection: """Two-lens Tebbs convergence matrix (exploratory, ~10s per render). Lens A lays out the distinct charts across the 24h scored by solar-arc / transits / profection; Lens B histograms which times the events themselves nominate. The timing signals are whisper-level (never summed) — the matrix is a *display* for a human, and defers to the validated sect anchor on sect. """ def __init__(self, *, events: Iterable | None = None) -> None: self._events = events @property def section_name(self) -> str: return "Sect Convergence Matrix"
[docs] def generate_data(self, chart: CalculatedChart) -> dict[str, Any]: m = convergence_matrix(chart, events=self._events) if not m.columns: return { "type": "text", "text": ( "No life events available for a convergence matrix. Supply " "events=[...] or use a notable with a biographical timeline." ), } return { "type": "compound", "sections": [ ("Lens A — structural band", self._lens_a_table(m)), ( "Lens B — event hooks", {"type": "text", "text": self._lens_b_text(m)}, ), ("Synthesis", {"type": "text", "text": self._synthesis_text(m)}), ], }
def _lens_a_table(self, m: ConvergenceMatrix) -> dict[str, Any]: cols = m.columns heads = [""] + [f"{c.sect[0].upper()}{_hhmm(c.start_minute)}" for c in cols] def fit_cells(key: str) -> list[str]: peak = m.method_peak_index.get(key) return [ (f"*{getattr(c, key):+.2f}" if i == peak else f"{getattr(c, key):+.2f}") for i, c in enumerate(cols) ] rows = [ ["Ascendant", *[c.asc_sign[:3] for c in cols]], ["Asc-ruler", *[c.asc_ruler[:3] for c in cols]], ["sect light", *[_SECT_LIGHT_ABBR[c.sect][:3] for c in cols]], ["out-of-sect malefic", *[_MALEFIC_ABBR[c.sect][:3] for c in cols]], ["solar-arc → angles", *fit_cells("dir")], ["transits → angles", *fit_cells("trans")], ["profection", *fit_cells("prof")], [ "firdaria (by sect)", *[ f"{(m.firdaria_day if c.sect == 'day' else m.firdaria_night):+.2f}" for c in cols ], ], ["progressed Moon", *[f"{m.progressed_moon:+.2f}" for _ in cols]], ["convergence", *[("●" * n or "·") for n in m.convergence_counts]], ] return {"type": "table", "headers": heads, "rows": rows} def _lens_b_text(self, m: ConvergenceMatrix) -> str: lines = [ "distinct events with an apt directed/transiting hit, across the 24h:", "", ] for h in range(24): n = m.hooks_by_hour[h] lines.append(f" {h:02d}:00 {'█' * n}{'' if n else '·'} {n or ''}") if m.hooks_spread: lines.append("") lines.append( "⚠ hooks spread across most of the day — the events do NOT localise " "time well (the expected ill-posed-time signature). Read peaks as weak." ) if m.hook_peaks: lines.append("") lines.append("top event-nominated times:") for p in m.hook_peaks: lines.append( f" {_hhmm(p.minute)} ({p.sect}, Asc {p.asc_sign[:3]}): " f"{p.count} events — " + ", ".join(p.detail) ) return "\n".join(lines) def _synthesis_text(self, m: ConvergenceMatrix) -> str: anchor = m.anchor_sect.upper() lines = [ f"validated sect anchor : {anchor} (P(day)={m.p_day:.2f}) " "← the only cross-validated signal (~70%)" ] if m.a_convergent and m.a_winner_index is not None: w = m.columns[m.a_winner_index] lines.append( f"Lens A (structure) : {w.sect}, Asc {w.asc_sign} @ " f"~{_hhmm(w.start_minute)} ({max(m.convergence_counts)}/3 methods peak)" ) else: lines.append( "Lens A (structure) : no structural convergence — the " "timing methods peak in different columns (whisper-level)." ) if m.hook_peaks: b = m.hook_peaks[0] lines.append( f"Lens B (event hooks) : {b.sect}, Asc {b.asc_sign} @ {_hhmm(b.minute)} " f"({b.count} events nominate it — weakly)" ) lines.append("") lines.append( "HOW TO READ: for SECT, trust the anchor — the timing lenses are " "whisper-level and routinely point the wrong way; when they fight the " "anchor on sect, the anchor wins. The lenses are for exploring " f"TIME-WITHIN-SECT ({anchor}), then deciding with first-hand knowledge. " "The matrix never collapses that step — by design." ) return "\n".join(lines)