feat(logic): glify astrologiczne — konwersja tekst↔symbol (LOG-22)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m56s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m43s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 30s
Testy / Kontrola składni wszystkich warstw (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m55s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m43s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 37s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 21s

Warunek wstępny pod kosmogram (PRE-12): planety i znaki na kole rysujemy
symbolami. Nowy moduł engine/glyphs.py:

- forward: nazwa → glif (planety, punkty, Loty, znaki, aspekty, retro ℞),
- reverse: glif → nazwa (znosi obecność/brak selektora wariantu),
- glyphify(): tokeny [XX z bazy → glify; przyklad z wymagan
  „[Sa [Pis 26°08' [conj [PF" → „♄  26°08' ☌ ⊗" (stopnie nietkniete).

DAN-18 „tylko tekst, nigdy emoji" potraktowane serio, TRZEMA warstwami — bo
sam selektor NIE wystarcza (potwierdzone wizualnie w przegladarce):
1. w danych: selektor wariantu tekstowego U+FE0E na znakach zodiaku oraz ♀/♂
   (maja wariant emoji); ☉ i reszta go nie dostaja (zbedny),
2. CSS font-variant-emoji:text,
3. CSS font-family celujacy w MONOCHROMATYCZNE fonty symboli PRZED emoji.
   Bez pkt. 3 macOS/Chromium i tak renderowal znaki  jako kolorowe kafelki
   Apple Color Emoji mimo VS15 — planety wychodzily tekstem, znaki nie. Stack
   „Apple Symbols / Segoe UI Symbol / Noto Sans Symbols2" naprawia render.
   NIGDZIE nie emitujemy U+FE0F (emoji) — pilnuje tego test.

Wpiete w build_chart: kazda pozycja dostaje glyph (planeta) + sign_glyph (znak,
zalezny od zodiaku, wiec po przesunieciu), osie i Loty tak samo (Fortuna ⊗,
reszta Lotow bez standardowego symbolu → None), aspekty dostaja glif,
result[„sign_glyphs"] to pierscien 12 znakow pod kolo.

UI: kolumna „Sym." w tabeli pozycji (planeta + znak) i symbol przy aspekcie.

Testy: 14 (kompletnosc — kazdy obiekt/znak/aspekt ma glif; dwukierunkowosc;
DAN-18 brak FE0F, znaki maja VS15, ☉ nie; przyklad glyphify z wymagan).
Calosc: logika 265 passed / 1 skipped, prezentacja 25. Render znakow
potwierdzony wizualnie (czarno-biale symbole, nie emoji).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 20:46:38 +02:00
parent f1956a08ff
commit ace76183e5
6 changed files with 325 additions and 3 deletions
+114
View File
@@ -0,0 +1,114 @@
"""Konwersja tekst ↔ symbol astrologiczny (LOG-22).
Sedno: glify muszą być KOMPLETNE (każdy obiekt/znak/aspekt, który liczymy, ma
symbol), DWUKIERUNKOWE (symbol → tekst z powrotem) i TEKSTOWE, nigdy emoji
(DAN-18) — inaczej na kosmogramie wyjdą kolorowe kwadraty zamiast czarno-białych
symboli.
"""
import pytest
from app.engine import glyphs as G
from app.engine.aspects import MAJOR
from app.engine.formats import SIGNS
from app.engine.models import DEFAULT_OBJECTS
VS15, VS16 = "", ""
def _all_glyphs():
return (list(G.PLANET.values()) + list(G.POINT.values()) + list(G.LOT.values())
+ list(G.SIGN.values()) + list(G.ASPECT.values()) + [G.RETROGRADE])
# ------------------------------------------------------------- kompletność
def test_every_default_object_has_a_glyph():
"""Każdy obiekt z kanonicznego zestawu musi mieć symbol — inaczej na kole
zostanie dziura."""
missing = [name for name in DEFAULT_OBJECTS if G.glyph_for(name) is None]
assert not missing, f"obiekty bez glifu: {missing}"
def test_every_sign_has_a_glyph():
missing = [s for s in SIGNS if G.sign_glyph(s) is None]
assert not missing, f"znaki bez glifu: {missing}"
def test_every_major_aspect_has_a_glyph():
missing = [a for a in MAJOR if G.aspect_glyph(a) is None]
assert not missing, f"aspekty główne bez glifu: {missing}"
def test_fortune_and_retrograde_from_requirement():
"""Wymóg wprost wymienia ⊗ (Fortuna) i ℞ (retrogradacja)."""
assert G.glyph_for("Fortune") == ""
assert G.RETROGRADE == ""
# ------------------------------------------------------ DAN-18: tekst, nie emoji
def test_no_glyph_uses_emoji_variation_selector():
"""VS16 (U+FE0F) wymusiłby prezentację emoji — nie może go być NIGDZIE."""
offenders = [g for g in _all_glyphs() if VS16 in g]
assert not offenders, f"glify z wariantem emoji (FE0F): {offenders!r}"
def test_signs_force_text_presentation():
"""Znaki zodiaku mają domyślnie wariant emoji → MUSZĄ nieść VS15."""
for s in SIGNS:
assert VS15 in G.sign_glyph(s), f"{s} bez wymuszenia tekstu (VS15)"
def test_venus_mars_force_text_but_sun_does_not():
"""♀/♂ mają wariant emoji (VS15 konieczny); ☉ nie ma (VS15 zbędny)."""
assert VS15 in G.glyph_for("Venus")
assert VS15 in G.glyph_for("Mars")
assert VS15 not in G.glyph_for("Sun") # bez zaśmiecania niepotrzebnym selektorem
# ---------------------------------------------------------- dwukierunkowość
def test_object_round_trip():
for name in DEFAULT_OBJECTS:
assert G.name_for_glyph(G.glyph_for(name)) == name
def test_sign_round_trip():
for s in SIGNS:
assert G.sign_for_glyph(G.sign_glyph(s)) == s
def test_aspect_round_trip():
for a in MAJOR:
assert G.aspect_for_glyph(G.aspect_glyph(a)) == a
def test_reverse_tolerates_missing_variant_selector():
"""Symbol wklejony bez VS15 też musi się rozpoznać — bo z zewnątrz przyjdzie
dowolna forma (DAN-17: łańcuch znosi każdy Unicode)."""
assert G.sign_for_glyph("") == "Pisces" # ♓ bez VS15
assert G.sign_for_glyph("" + VS15) == "Pisces" # ♓ z VS15
assert G.name_for_glyph("") == "Venus" # ♀ bez VS15
# --------------------------------------------------------- glifikacja tekstu
def test_glyphify_matches_requirement_example():
"""Przykład z wymagań: [Sa [Pis 26°08' [conj [PF -> ♄ ♓ 26°08' ☌ ⊗."""
out = G.glyphify("[Sa [Pis 26°08' [conj [PF")
assert G.glyph_for("Saturn") in out
assert G.sign_glyph("Pisces") in out
assert G.aspect_glyph("conjunction") in out
assert G.glyph_for("Fortune") in out
assert "26°08'" in out # stopnie nietknięte
def test_glyphify_handles_retrograde_and_leaves_unknown():
out = G.glyphify("[Ma Rx w [Xyz")
assert G.glyph_for("Mars") in out
assert G.RETROGRADE in out
assert "[Xyz" in out # nieznany token bez zmian
def test_glyphify_empty_is_safe():
assert G.glyphify("") == ""