From 4c0f5f2b26dad2fa340eb33004a514df1c408457 Mon Sep 17 00:00:00 2001 From: migatu Date: Mon, 20 Jul 2026 20:02:02 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Lots=20/=20punkty=20arabskie=20=E2=80=94=20?= =?UTF-8?q?7=20Lots=20hermetycznych=20(LOG-08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - engine/lots.py: formuła Lot = C + A − B z odwracaniem w horoskopach nocnych (zamiana A<->B); 7 Lots hermetycznych (Fortuna, Duch, Eros, Konieczność, Odwaga, Zwycięstwo, Nemezis) — Fortuna i Duch liczone pierwsze, bo pozostałe się do nich odwołują. Dwa warianty: degree (domyślny) i sign (całe znaki). - build_chart: liczy sektę (reużyta is_day_birth z Firdarii) i zwraca lots ze znakiem, pozycją i domem; parametr lots_method. - wyszukiwarka: token [PF (tak Fortuna występuje w realnej bazie) + rozwinięcie skrótu do "Part of Fortune". - widok Horoskop: tabela Lots z formułami i sektą. Walidacja (dwie niezależne wyrocznie z notes3): - Fortuna = Can 12°35'27" vs astro-seek Can 12°35'24" (3 sekundy różnicy), dom 1; - Duch potwierdzony przez swoją antyscję (Taurus 28°14' z tabeli antyscji); - odwracanie nocne: Fortuna nocna == Duch dzienny i odwrotnie; - Lots pochodne faktycznie używają Fortuny/Ducha; wariant sign trafia w 0° znaku. 85 testów przechodzi (nowy test_lots). Odblokowuje Zodiacal Releasing (LOG-11), które startuje z Fortuny/Ducha. Co-Authored-By: Claude Opus 4.8 --- services/logic/README.md | 2 +- services/logic/app/abbreviations.py | 2 + services/logic/app/engine/chart.py | 19 ++++- services/logic/app/engine/lots.py | 60 ++++++++++++++++ services/logic/app/significators.py | 2 + services/logic/tests/test_lots.py | 71 +++++++++++++++++++ .../presentation/app/templates/chart.html | 12 ++++ 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 services/logic/app/engine/lots.py create mode 100644 services/logic/tests/test_lots.py diff --git a/services/logic/README.md b/services/logic/README.md index 30c97a6..ab6bad8 100644 --- a/services/logic/README.md +++ b/services/logic/README.md @@ -6,7 +6,7 @@ i nie w bazie. ## API - `POST /api/query` → `QueryRequest` → `QueryResponse` -- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`. +- `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Zwraca też sektę i 7 Lots hermetycznych z domami (LOG-08). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`. - `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów) - `POST /chart/profections` → `{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10) - `POST /chart/return` → `{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12) diff --git a/services/logic/app/abbreviations.py b/services/logic/app/abbreviations.py index e08b109..557286e 100644 --- a/services/logic/app/abbreviations.py +++ b/services/logic/app/abbreviations.py @@ -24,6 +24,8 @@ ABBREVIATIONS: dict[str, str] = { "NN": "North Node", "SN": "South Node", "Lilith": "Lilith", "Chiron": "Chiron", # osie "Asc": "Ascendant", "Dsc": "Descendant", "MC": "Midheaven", "IC": "Imum Coeli", + # Lots (punkty arabskie) + "PF": "Part of Fortune", "Fortune": "Part of Fortune", "Spirit": "Lot of Spirit", # aspekty "conj": "conjunction", "sex": "sextile", "sq": "square", "tri": "trine", "opp": "opposition", "semisex": "semisextile", "semisq": "semisquare", diff --git a/services/logic/app/engine/chart.py b/services/logic/app/engine/chart.py index f13c3b6..2c2f2c9 100644 --- a/services/logic/app/engine/chart.py +++ b/services/logic/app/engine/chart.py @@ -21,7 +21,8 @@ def _fmt(name: str, lon: float) -> dict: } -def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN) -> dict: +def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN, + lots_method: str = "degree") -> dict: from app.engine.aspects import find_aspects positions = engine.positions(moment) @@ -50,4 +51,20 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str ] for pdict, obj in zip(result["positions"], positions): pdict["house"] = H.assign_house(obj.longitude, cusp_list) + + # Lots (LOG-08) — wymagają Asc i sekty (dzień/noc) + from app.engine.firdaria import is_day_birth + from app.engine.lots import compute_lots + + pts = {p.name: p.longitude for p in positions} + pts["Asc"] = asc + day = is_day_birth(pts["Sun"], asc, mc) if "Sun" in pts else True + result["sect"] = "day" if day else "night" + result["lots"] = [ + {**lot, + "sign": SIGNS[sign_index(lot["longitude"])], + "in_sign": in_sign(lot["longitude"]), + "house": H.assign_house(lot["longitude"], cusp_list)} + for lot in compute_lots(pts, day, lots_method) + ] return result diff --git a/services/logic/app/engine/lots.py b/services/logic/app/engine/lots.py new file mode 100644 index 0000000..40939f9 --- /dev/null +++ b/services/logic/app/engine/lots.py @@ -0,0 +1,60 @@ +"""Lots / punkty arabskie (LOG-08) — 7 Lots hermetycznych. + +Formuła: Lot = C + A − B (od punktu C odmierzamy odległość między A i B). +Większość Lots **odwraca się w horoskopach nocnych** (zamiana A↔B) — np. +Fortuna: dzień Asc + Mo − Su, noc Asc + Su − Mo. + +Dwa warianty liczenia (notes3): +- `degree` (domyślny) — dokładny stopień, +- `sign` — liczone całymi znakami (Lot wypada na 0° wyliczonego znaku). + +Kolejność ma znaczenie: Fortuna i Duch liczone są pierwsze, bo pozostałe Lots +odwołują się do nich. +""" +from __future__ import annotations + +from app.engine.formats import norm360, sign_index + +# (nazwa, C, A, B, odwracalny w nocy) +LOT_DEFS: list[tuple[str, str, str, str, bool]] = [ + ("Fortune", "Asc", "Moon", "Sun", True), + ("Spirit", "Asc", "Sun", "Moon", True), + ("Eros", "Asc", "Venus", "Spirit", True), + ("Necessity", "Asc", "Fortune", "Mercury", True), + ("Courage", "Asc", "Fortune", "Mars", True), + ("Victory", "Asc", "Jupiter", "Spirit", True), + ("Nemesis", "Asc", "Fortune", "Saturn", True), +] + +METHODS = ("degree", "sign") + + +def compute_lots( + points: dict[str, float], is_day: bool, method: str = "degree" +) -> list[dict]: + """points: nazwa → długość natalna (wymagane Asc + planety formuł). + + Zwraca listę {name, longitude, formula} w kolejności definicji. + """ + if method not in METHODS: + raise ValueError(f"nieznana metoda liczenia Lots: {method}") + + vals = dict(points) + out: list[dict] = [] + for name, c, a, b, reversible in LOT_DEFS: + first, second = (a, b) if (is_day or not reversible) else (b, a) + if any(k not in vals for k in (c, first, second)): + continue # brak składnika — pomijamy + if method == "sign": + idx = (sign_index(vals[c]) + sign_index(vals[first]) + - sign_index(vals[second])) % 12 + lon = idx * 30.0 + else: + lon = norm360(vals[c] + vals[first] - vals[second]) + vals[name] = lon # dostępny dla kolejnych Lots + out.append({ + "name": name, + "longitude": lon, + "formula": f"{c} + {first} − {second}", + }) + return out diff --git a/services/logic/app/significators.py b/services/logic/app/significators.py index 01c008e..a029807 100644 --- a/services/logic/app/significators.py +++ b/services/logic/app/significators.py @@ -22,6 +22,8 @@ PLANET_ABBR = { "Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl", # punkty wirtualne — tokeny wg SIGNIFICATORS KEY ([NN, [SN, [Lilith) "North Node": "NN", "South Node": "SN", "Lilith": "Lilith", + # Lots — w bazie Fortuna występuje jako [PF (Part of Fortune) + "Fortune": "PF", "Spirit": "Spirit", } SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR)) diff --git a/services/logic/tests/test_lots.py b/services/logic/tests/test_lots.py new file mode 100644 index 0000000..1a3cf79 --- /dev/null +++ b/services/logic/tests/test_lots.py @@ -0,0 +1,71 @@ +"""Lots / punkty arabskie (LOG-08). + +Wyrocznie z notes3 (astro-seek, horoskop referencyjny 30.04.1984, urodzenie DZIENNE): +- Fortuna wprost w tabeli obiektów: Cancer 12°35'24" = 102.5900° +- Ducha (Spirit) potwierdza jego antyscja: Taurus 28°14' -> Spirit = 180 − 58.2333 +""" +import pytest + +from app.engine.formats import sign_index +from app.engine.lots import LOT_DEFS, compute_lots + +# natalne długości horoskopu referencyjnego +NATAL = {"Asc": 112.1786, "Sun": 40.1392, "Moon": 30.5514, "Mercury": 27.3839, + "Venus": 27.6769, "Mars": 234.5411, "Jupiter": 282.9617, "Saturn": 223.3108} + +FORTUNE_REF = 102.5900 # Cancer 12°35'24" +SPIRIT_REF = 180.0 - 58.2333 # z antyscji Taurus 28°14' + + +def _by_name(lots): + return {lot["name"]: lot["longitude"] for lot in lots} + + +def _arcmin(a, b): + return abs(((a - b + 180.0) % 360.0) - 180.0) * 60.0 + + +def test_fortune_matches_astroseek(): + lots = _by_name(compute_lots(NATAL, is_day=True)) + assert _arcmin(lots["Fortune"], FORTUNE_REF) < 2.0 + assert sign_index(lots["Fortune"]) == 3 # Cancer + + +def test_spirit_matches_antiscia_reference(): + lots = _by_name(compute_lots(NATAL, is_day=True)) + assert _arcmin(lots["Spirit"], SPIRIT_REF) < 2.0 + + +def test_fortune_and_spirit_reverse_at_night(): + day = _by_name(compute_lots(NATAL, is_day=True)) + night = _by_name(compute_lots(NATAL, is_day=False)) + # nocą formuła się odwraca: Fortuna nocna = Duch dzienny (i odwrotnie) + assert _arcmin(night["Fortune"], day["Spirit"]) < 0.01 + assert _arcmin(night["Spirit"], day["Fortune"]) < 0.01 + + +def test_all_seven_hermetic_lots_present(): + lots = compute_lots(NATAL, is_day=True) + assert [lot["name"] for lot in lots] == [d[0] for d in LOT_DEFS] + assert len(lots) == 7 + + +def test_derived_lots_use_fortune_and_spirit(): + lots = _by_name(compute_lots(NATAL, is_day=True)) + # Necessity = Asc + Fortune − Mercury + expected = (NATAL["Asc"] + lots["Fortune"] - NATAL["Mercury"]) % 360.0 + assert _arcmin(lots["Necessity"], expected) < 0.01 + # Eros = Asc + Venus − Spirit + expected_eros = (NATAL["Asc"] + NATAL["Venus"] - lots["Spirit"]) % 360.0 + assert _arcmin(lots["Eros"], expected_eros) < 0.01 + + +def test_by_sign_method_lands_on_sign_start(): + lots = compute_lots(NATAL, is_day=True, method="sign") + for lot in lots: + assert lot["longitude"] % 30.0 == 0.0 # 0° wyliczonego znaku + + +def test_unknown_method_rejected(): + with pytest.raises(ValueError): + compute_lots(NATAL, is_day=True, method="bzdura") diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index 7d1ae20..0367c65 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -88,6 +88,18 @@ + {% if result.lots %} +
Lots hermetyczne ({{ result.lots | length }}) · sekta: {{ result.sect }}
+ + + + {% for l in result.lots %} + + {% endfor %} + +
LotZnakW znakuDomFormuła
{{ l.name }}{{ l.sign }}{{ l.in_sign }}{{ l.house }}{{ l.formula }}
+ {% endif %} + {% set with_stations = result.positions | selectattr('stations', 'defined') | list %} {% if with_stations %}
Stacje planet (poprzednia / następna; blisko = mniej niż 7 dni)
-- 2.52.0 From 9f9d5d4e21e184ac1f0732310fdd1edb03a124d8 Mon Sep 17 00:00:00 2001 From: migatu Date: Mon, 20 Jul 2026 20:10:08 +0200 Subject: [PATCH 2/2] fix workflows --- {.github => .gitea}/workflows/tests.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github => .gitea}/workflows/tests.yml (100%) diff --git a/.github/workflows/tests.yml b/.gitea/workflows/tests.yml similarity index 100% rename from .github/workflows/tests.yml rename to .gitea/workflows/tests.yml -- 2.52.0