diff --git a/services/logic/README.md b/services/logic/README.md index 5cad6da..8dec6aa 100644 --- a/services/logic/README.md +++ b/services/logic/README.md @@ -7,7 +7,7 @@ i nie w bazie. ## API - `POST /api/query` → `QueryRequest` → `QueryResponse` - `POST /chart/positions` → `{when_utc, lat, lon, objects?}` → pozycje obiektów (LOG-01) -- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: z pozycji generuje sygnifikatory (planeta w znaku) i zwraca pasujące interpretacje z warstwy danych (zalążek LOG-16/18/19) +- `POST /chart/report` → `{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: z pozycji + domów generuje sygnifikatory (fasety: planeta w znaku i w domu) i zwraca pasujące interpretacje z warstwy danych (zalążek LOG-16/18/19) - `POST /chart/positions` → `{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`. - `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B) - `GET /health` (sprawdza też warstwę bazodanową) diff --git a/services/logic/app/main.py b/services/logic/app/main.py index aa46937..6cc6756 100644 --- a/services/logic/app/main.py +++ b/services/logic/app/main.py @@ -87,16 +87,17 @@ class ReportRequest(BaseModel): @app.post("/chart/report") def chart_report(req: ReportRequest) -> dict: - """Wynik obliczeń szukany w bazie: z pozycji generuje sygnifikatory i pyta - warstwę danych o pasujące interpretacje (pierwsza wersja przepływu).""" + """Wynik obliczeń szukany w bazie: z pozycji + domów generuje sygnifikatory + (fasety znak/dom) i pyta warstwę danych o pasujące interpretacje.""" + from app.engine.chart import build_chart from app.engine.models import ChartMoment from app.significators import build_report engine = get_engine() moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon) - positions = engine.positions(moment) + chart = build_chart(engine, moment) # pozycje z numerami domów try: - report = build_report(positions, DataClient(), per_object_limit=req.limit) + report = build_report(chart["positions"], DataClient(), per_object_limit=req.limit) except httpx.HTTPError as e: return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"} return {"engine": engine.name, **report} diff --git a/services/logic/app/significators.py b/services/logic/app/significators.py index 8fe607a..68a3821 100644 --- a/services/logic/app/significators.py +++ b/services/logic/app/significators.py @@ -1,24 +1,25 @@ """Most: policzony horoskop → tokeny sygnifikatorów → wyszukiwanie w bazie. -Pierwsza wersja (LOG-16/18/19 w zalążku): z pozycji obiektów generujemy tokeny w -składni bazy (np. Słońce w Byku → planeta `[Su`, znak `[Tau`), pytamy warstwę -danych o rekordy zawierające token planety, a następnie zawężamy do tych, które -wspominają też jej znak („planeta w swoim znaku"). To realizuje przepływ „wynik -obliczeń szukany w pliku". +Zalążek LOG-15/16: z pozycji obiektów (wraz z domami) generujemy tokeny w składni +bazy i wyszukujemy pasujące rekordy. Dla każdego obiektu tworzymy kilka **faset**: +- „w znaku" — planeta + token znaku (`[Su` + `[Tau`) +- „w domu" — planeta + token domu (`[Su` + `11th H.`) +Aspekty (`[conj`,`[sq`,`[opp`) wymagają policzenia aspektów (LOG-06) — na później. -Format skrótów odczytany z realnej bazy (Encyclopaedia of Medical Astrology): -planety `[Su`,`[Mo`,…; znaki `[Ari`,`[Tau`,…; np. `[Su affl. in [Vir`. +Format skrótów odczytany z realnej bazy: planety `[Su`,`[Mo`,…; znaki +`[Ari`,`[Tau`,…; domy `12th H.`; np. `[Sa [conj [Su in 6th H.`. """ from __future__ import annotations from typing import Any, Protocol -from app.engine.formats import SIGN_ABBR, sign_index +from app.engine.formats import SIGN_ABBR, SIGNS PLANET_ABBR = { "Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma", "Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl", } +SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR)) class DataSource(Protocol): @@ -45,16 +46,42 @@ def _is_noise(sig: str, effect: str) -> bool: ) -def build_report(positions, data: DataSource, per_object_limit: int = 60) -> dict: - """Dla każdego obiektu: wyszukaj sygnifikatory planety i zawęź do jej znaku.""" +def _ordinal(n: int) -> str: + if 10 <= n % 100 <= 20: + suffix = "th" + else: + suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th") + return f"{n}{suffix}" + + +def _facet_samples(rows: list[dict], token: str, limit: int = 4) -> list[dict]: + """Rekordy, których sygnifikator zawiera token — bez szumu.""" + out: list[dict] = [] + tok = token.lower() + for r in rows: + sig = str(r.get("significator") or "") + if tok not in sig.lower(): + continue + eff = _effect(r) + if _is_noise(sig, eff): + continue + out.append({"significator": sig.strip(), "effect": eff}) + return out + + +def build_report(positions: list[dict], data: DataSource, per_object_limit: int = 5000) -> dict: + """positions: pozycje z build_chart (name, sign, direction, house). + + Dla każdego obiektu: jedno zapytanie o token planety, potem faseta „w znaku" + i „w domu" (jeśli dom policzony). + """ items: list[dict] = [] provider = None for p in positions: - if p.name not in PLANET_ABBR: + name = p.get("name") + if name not in PLANET_ABBR: continue - planet_tok = "[" + PLANET_ABBR[p.name] - sign_tok = "[" + SIGN_ABBR[sign_index(p.longitude)] - + planet_tok = "[" + PLANET_ABBR[name] raw = data.search( key="significator", value=planet_tok, @@ -65,24 +92,31 @@ def build_report(positions, data: DataSource, per_object_limit: int = 60) -> dic provider = raw.get("provider", provider) rows = raw.get("rows", []) - samples: list[dict] = [] - for r in rows: - sig = str(r.get("significator") or "") - if sign_tok.lower() not in sig.lower(): - continue - eff = _effect(r) - if _is_noise(sig, eff): - continue - samples.append({"significator": sig.strip(), "effect": eff}) + facets: list[dict] = [] + sign = p.get("sign") + sign_tok = "[" + SIGN_TO_ABBR.get(sign, "") + sign_samples = _facet_samples(rows, sign_tok) + facets.append({ + "type": "sign", "label": f"w znaku {sign}", "token": sign_tok, + "count": len(sign_samples), "samples": sign_samples, + }) + + house = p.get("house") + if house: + ordn = _ordinal(int(house)) + house_samples = _facet_samples(rows, f"{ordn} h") # matcuje '12th H.' + facets.append({ + "type": "house", "label": f"w {ordn} domu", "token": f"{ordn} H.", + "count": len(house_samples), "samples": house_samples, + }) items.append({ - "object": p.name, - "sign": p.sign, - "direction": p.direction, + "object": name, + "sign": sign, + "house": house, + "direction": p.get("direction"), "planet_token": planet_tok, - "sign_token": sign_tok, "planet_total": raw.get("total", 0), - "in_sign_count": len(samples), - "samples": samples[:5], + "facets": facets, }) return {"provider": provider, "objects": items} diff --git a/services/logic/tests/test_significators.py b/services/logic/tests/test_significators.py index a71c6dd..aca4716 100644 --- a/services/logic/tests/test_significators.py +++ b/services/logic/tests/test_significators.py @@ -1,6 +1,6 @@ """Testy mostu obliczenia → sygnifikatory → wyszukiwanie (bez efemeryd/HTTP).""" -from app.engine.models import DEFAULT_OBJECTS, ObjectPosition -from app.significators import PLANET_ABBR, build_report +from app.engine.models import DEFAULT_OBJECTS +from app.significators import PLANET_ABBR, _ordinal, build_report class FakeData: @@ -12,31 +12,34 @@ class FakeData: return {"provider": "fake", "total": len(rows), "rows": rows} -def test_build_report_filters_to_sign_and_drops_noise(): - positions = [ObjectPosition("Sun", 40.0, 0.0, 0.95, False)] # Taurus 10° -> [Su + [Tau +def test_sign_and_house_facets(): + positions = [{"name": "Sun", "sign": "Taurus", "direction": "D", "house": 11}] data = FakeData({"[Su": [ - {"significator": "[Su in [Tau", "actioneffect": "efekt A"}, # trafienie - {"significator": "[Su in [Vir", "actioneffect": "inny znak"}, # zły znak - {"significator": "[Su in [Tau", "actioneffect": "nan"}, # szum (pusty efekt) - {"significator": "SIGNIFICATOR nagłówek", "actioneffect": "x"}, # szum (nagłówek) + {"significator": "[Su in [Tau", "actioneffect": "efekt znak"}, # faseta znak + {"significator": "[Su in 11th H.", "actioneffect": "efekt dom"}, # faseta dom + {"significator": "[Su in [Vir", "actioneffect": "inny"}, # żadna + {"significator": "[Su in [Tau", "actioneffect": "nan"}, # szum ]}) - item = build_report(positions, data, per_object_limit=100)["objects"][0] - assert item["object"] == "Sun" and item["sign"] == "Taurus" - assert item["planet_token"] == "[Su" and item["sign_token"] == "[Tau" - assert item["planet_total"] == 4 - assert item["in_sign_count"] == 1 - assert item["samples"][0]["effect"] == "efekt A" + item = build_report(positions, data)["objects"][0] + assert item["object"] == "Sun" and item["house"] == 11 + facets = {f["type"]: f for f in item["facets"]} + assert facets["sign"]["count"] == 1 + assert facets["sign"]["samples"][0]["effect"] == "efekt znak" + assert facets["house"]["count"] == 1 + assert facets["house"]["token"] == "11th H." + assert facets["house"]["samples"][0]["effect"] == "efekt dom" -def test_effect_falls_back_to_topic_or_bodypart(): - positions = [ObjectPosition("Mars", 220.0, 0.0, -0.2, True)] # Scorpio -> [Ma + [Sco - data = FakeData({"[Ma": [ - {"significator": "[Ma in [Sco", "actioneffect": "", "topicresult": "temat X"}, - {"significator": "[Ma in [Sco", "bodypart": "część ciała"}, - ]}) - samples = build_report(positions, data)["objects"][0]["samples"] - assert samples[0]["effect"] == "temat X" - assert samples[1]["effect"] == "część ciała" +def test_no_house_facet_when_house_missing(): + positions = [{"name": "Mars", "sign": "Scorpio", "direction": "Rx"}] + data = FakeData({"[Ma": [{"significator": "[Ma in [Sco", "actioneffect": "eff"}]}) + types = [f["type"] for f in build_report(positions, data)["objects"][0]["facets"]] + assert "sign" in types and "house" not in types + + +def test_ordinal(): + assert _ordinal(1) == "1st" and _ordinal(2) == "2nd" and _ordinal(3) == "3rd" + assert _ordinal(4) == "4th" and _ordinal(11) == "11th" and _ordinal(12) == "12th" def test_planet_abbr_covers_all_default_objects(): diff --git a/services/presentation/app/static/styles.css b/services/presentation/app/static/styles.css index 0943574..346db84 100644 --- a/services/presentation/app/static/styles.css +++ b/services/presentation/app/static/styles.css @@ -59,3 +59,5 @@ tr:last-child td { border-bottom: none; } .samples { margin-top: .3rem; } .samples td { font-size: .92rem; vertical-align: top; } .samples td.nowrap { color: var(--accent); padding-right: 1rem; } +.facet { margin: .4rem 0 .6rem 1rem; } +.facet-head { color: var(--ink); font-size: .9rem; } diff --git a/services/presentation/app/templates/interpret.html b/services/presentation/app/templates/interpret.html index aa5f4f4..ee21c32 100644 --- a/services/presentation/app/templates/interpret.html +++ b/services/presentation/app/templates/interpret.html @@ -44,22 +44,25 @@ {% for o in result.objects %}
- {{ o.object }} w {{ o.sign }} + {{ o.object }} w {{ o.sign }}{% if o.house %}, {{ o.house }}. dom{% endif %} {{ o.direction }} - — szukano {{ o.planet_token }} + {{ o.sign_token }}; - w znaku: {{ o.in_sign_count }} (planeta ogółem: {{ o.planet_total }}) + (planeta {{ o.planet_token }}: {{ o.planet_total }} rekordów)
- {% if o.samples %} - - - {% for s in o.samples %} - - {% endfor %} - -
{{ s.significator }}{{ s.effect }}
- {% else %} -
brak dopasowań „w znaku" w bazie
- {% endif %} + {% for f in o.facets %} +
+
{{ f.label }} — {{ f.count }} dopasowań + [{{ f.token }}]
+ {% if f.samples %} + + + {% for s in f.samples %} + + {% endfor %} + +
{{ s.significator }}{{ s.effect }}
+ {% endif %} +
+ {% endfor %}
{% endfor %} {% endif %}