feat(logic): aspekty pozazodiakalne — antyscja i paralele deklinacji (LOG-07)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m38s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 20s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m37s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 32s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 20s

Aspekty głowne (LOG-06) mierzą kąt wzdłuż ekliptyki. LOG-07 dokłada dwa rodzaje
powiązań, które klasyczna astrologia liczy naprawdę, nie na oko:

- PARALELA/KONTRPARALELA DEKLINACJI — dwa ciała na tej samej („parallel") lub
  przeciwnej („contraparallel") deklinacji działają jak koniunkcja/opozycja,
  mimo że wzdłuż ekliptyki mogą stać gdziekolwiek. Deklinacja liczona PEŁNYM
  wzorem (z szerokością ekliptyczną — istotne dla Księżyca i planet schodzących
  z ekliptyki), przez to_equatorial z LOG-04. Orb konfigurowalny (domyślnie 1°,
  ciasno — to kontakty punktowe).
- ANTYSCJA/KONTRANTYSCJA — odbicie długości względem osi przesileń (0° Raka –
  0° Koziorożca) albo równonocy (0° Barana – 0° Wagi).

Liczone na współrzędnych TROPIKALNYCH of-date, bo deklinacja jest fizyczna
(równikowa, niezależna od zodiaku), a antyscja z definicji tropikalna — jej oś to
kardynalne punkty zodiaku tropikalnego. Dlatego PRZED przesunięciem na zodiak
syderyczny/draconiczny.

Dodatkowo: flaga „out of bounds" (|deklinacja| > nachylenie ekliptyki — ciało
poza zakresem Słońca) i kolumna deklinacji przy każdej pozycji.

Integracja z bazą: baza interpretacyjna ZNA paralele pod frazą „P. Dec.", więc
generujemy fasetkę paraleli (trafia też do promptu LLM). PUŁAPKA: samo „Dec." w
bazie to często DEKANAT („3rd Dec. of [Gem"), więc szukamy dokładnie „P. Dec." —
inaczej sypnęłoby fałszywymi trafieniami. Kontrparaleli i antyscji baza nie
opisuje osobnym znacznikiem, więc zostają obliczeniem display-only.

Pary z RIGID_PAIRS (węzły) odsiane: SN = NN+180° na ekliptyce → deklinacja
ZAWSZE przeciwna, czyli definicyjna kontrparalela bez informacji.

Walidacja wobec faktów NIEZALEŻNYCH od kodu:
- deklinacja Słońca 30.04.1984 = +14.88° wobec ~+14.9° z almanachu,
- antyscja to arytmetyka odbicia: inwolucja i pary znaków (Rak↔Bliźnięta,
  Baran↔Panna) sprawdzone na piechotę,
- węzły faktycznie mają przeciwną deklinację i są odsiane,
- deklinacja niezależna od zodiaku (tropikalny == syderyczny co do 1e-6°),
- realna baza: mechanizm paraleli znajduje wpisy „P. Dec." i odsiewa dekanaty
  (z 33 wpisów „P. Dec." 5 ma niepusty efekt — głównie „[conj or P. Dec.").

UI: kolumna deklinacji (+znacznik OOB), tabela paraleli/kontrparaleli i tabela
antyscji na ekranie Horoskop.

Testy: 15 nowych (out_of_zodiac) + 2 (fasetka paraleli vs pułapka dekanatu).
Całość: logika 251 passed / 1 skipped, prezentacja 25 passed. Render szablonu
sprawdzony osobno (bez błędu Jinja, wszystkie sekcje obecne).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 20:20:02 +02:00
parent 56fdf01d7d
commit 012e4f39f6
8 changed files with 425 additions and 5 deletions
+29 -3
View File
@@ -114,17 +114,24 @@ def build_report(
positions: list[dict],
data: DataSource,
aspects: list[dict] | None = None,
parallels: list[dict] | None = None,
per_object_limit: int = 5000,
group: bool = False,
) -> dict:
"""positions: pozycje z build_chart (name, sign, direction, house).
Dla każdego obiektu fasety: „w znaku", „w domu" oraz „w aspekcie" (dla każdego
aspektu głównego z listy `aspects`, jeśli w bazie są dopasowania). Duplikaty
(ten sam sygnifikator i opis) są odsiewane wewnątrz każdej fasety.
Dla każdego obiektu fasety: „w znaku", „w domu", „w aspekcie" (dla każdego
aspektu głównego z listy `aspects`) oraz „paralela deklinacji" (dla paraleli
z listy `parallels` — LOG-07). Duplikaty (ten sam sygnifikator i opis) są
odsiewane wewnątrz każdej fasety.
"""
from app.engine.aspects import DB_TOKEN as ASP_TOKEN, PL_NAME as ASP_NAME
# W bazie paralela deklinacji zapisana jest jako fraza „P. Dec." (nie token [).
# UWAGA: samo „Dec." w bazie to CZĘSTO dekanat („3rd Dec. of [Gem"), więc
# szukamy dokładnie „P. Dec.", inaczej sypnęłoby fałszywymi trafieniami.
PARALLEL_MARK = "P. Dec."
items: list[dict] = []
provider = None
for p in positions:
@@ -180,6 +187,25 @@ def build_report(
"count": len(asp_samples), "samples": asp_samples,
})
# paralele deklinacji (LOG-07) — baza zna je jako „P. Dec.". Tylko `parallel`:
# kontrparaleli i antyscji baza nie opisuje osobnym znacznikiem.
for par in (parallels or []):
if par.get("type") != "parallel" or name not in (par.get("obj1"), par.get("obj2")):
continue
other = par["obj2"] if par["obj1"] == name else par["obj1"]
if other not in PLANET_ABBR:
continue
other_tok = "[" + PLANET_ABBR[other]
par_samples = _facet_samples(rows, [PARALLEL_MARK, other_tok])
if not par_samples: # pokazujemy tylko z trafieniami
continue
facets.append({
"type": "parallel", "label": f"paralela deklinacji z {other}",
"token": f"{PARALLEL_MARK} + {other_tok}",
"orb": par.get("orb"), "allowed": par.get("allowed"),
"count": len(par_samples), "samples": par_samples,
})
# punktacja siły (LOG-21), opcjonalne grupowanie po opisie, ranking faset
for f in facets:
f["score"] = score_facet(f)