Merge pull request #10 from migatu/feat/richer-significators

Bogatsze sygnifikatory: faseta w domu obok w znaku (LOG-15/16)
This commit is contained in:
2026-07-01 15:58:08 +02:00
committed by GitHub
6 changed files with 114 additions and 71 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ i nie w bazie.
## API ## API
- `POST /api/query``QueryRequest``QueryResponse` - `POST /api/query``QueryRequest``QueryResponse`
- `POST /chart/positions``{when_utc, lat, lon, objects?}` → pozycje obiektów (LOG-01) - `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/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) - `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
- `GET /health` (sprawdza też warstwę bazodanową) - `GET /health` (sprawdza też warstwę bazodanową)
+5 -4
View File
@@ -87,16 +87,17 @@ class ReportRequest(BaseModel):
@app.post("/chart/report") @app.post("/chart/report")
def chart_report(req: ReportRequest) -> dict: def chart_report(req: ReportRequest) -> dict:
"""Wynik obliczeń szukany w bazie: z pozycji generuje sygnifikatory i pyta """Wynik obliczeń szukany w bazie: z pozycji + domów generuje sygnifikatory
warstwę danych o pasujące interpretacje (pierwsza wersja przepływu).""" (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.engine.models import ChartMoment
from app.significators import build_report from app.significators import build_report
engine = get_engine() engine = get_engine()
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon) 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: 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: except httpx.HTTPError as e:
return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"} return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"}
return {"engine": engine.name, **report} return {"engine": engine.name, **report}
+63 -29
View File
@@ -1,24 +1,25 @@
"""Most: policzony horoskop → tokeny sygnifikatorów → wyszukiwanie w bazie. """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 Zalążek LOG-15/16: z pozycji obiektów (wraz z domami) generujemy tokeny w składni
składni bazy (np. Słońce w Byku → planeta `[Su`, znak `[Tau`), pytamy warstwę bazy i wyszukujemy pasujące rekordy. Dla każdego obiektu tworzymy kilka **faset**:
danych o rekordy zawierające token planety, a następnie zawężamy do tych, które - „w znaku" — planeta + token znaku (`[Su` + `[Tau`)
wspominają też jej znak („planeta w swoim znaku"). To realizuje przepływ „wynik - „w domu" — planeta + token domu (`[Su` + `11th H.`)
obliczeń szukany w pliku". 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): Format skrótów odczytany z realnej bazy: planety `[Su`,`[Mo`,…; znaki
planety `[Su`,`[Mo`,…; znaki `[Ari`,`[Tau`,…; np. `[Su affl. in [Vir`. `[Ari`,`[Tau`,…; domy `12th H.`; np. `[Sa [conj [Su in 6th H.`.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Protocol from typing import Any, Protocol
from app.engine.formats import SIGN_ABBR, sign_index from app.engine.formats import SIGN_ABBR, SIGNS
PLANET_ABBR = { PLANET_ABBR = {
"Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma", "Sun": "Su", "Moon": "Mo", "Mercury": "Me", "Venus": "Ve", "Mars": "Ma",
"Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl", "Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl",
} }
SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR))
class DataSource(Protocol): 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: def _ordinal(n: int) -> str:
"""Dla każdego obiektu: wyszukaj sygnifikatory planety i zawęź do jej znaku.""" 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] = [] items: list[dict] = []
provider = None provider = None
for p in positions: for p in positions:
if p.name not in PLANET_ABBR: name = p.get("name")
if name not in PLANET_ABBR:
continue continue
planet_tok = "[" + PLANET_ABBR[p.name] planet_tok = "[" + PLANET_ABBR[name]
sign_tok = "[" + SIGN_ABBR[sign_index(p.longitude)]
raw = data.search( raw = data.search(
key="significator", key="significator",
value=planet_tok, value=planet_tok,
@@ -65,24 +92,31 @@ def build_report(positions, data: DataSource, per_object_limit: int = 60) -> dic
provider = raw.get("provider", provider) provider = raw.get("provider", provider)
rows = raw.get("rows", []) rows = raw.get("rows", [])
samples: list[dict] = [] facets: list[dict] = []
for r in rows: sign = p.get("sign")
sig = str(r.get("significator") or "") sign_tok = "[" + SIGN_TO_ABBR.get(sign, "")
if sign_tok.lower() not in sig.lower(): sign_samples = _facet_samples(rows, sign_tok)
continue facets.append({
eff = _effect(r) "type": "sign", "label": f"w znaku {sign}", "token": sign_tok,
if _is_noise(sig, eff): "count": len(sign_samples), "samples": sign_samples,
continue })
samples.append({"significator": sig.strip(), "effect": eff})
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({ items.append({
"object": p.name, "object": name,
"sign": p.sign, "sign": sign,
"direction": p.direction, "house": house,
"direction": p.get("direction"),
"planet_token": planet_tok, "planet_token": planet_tok,
"sign_token": sign_tok,
"planet_total": raw.get("total", 0), "planet_total": raw.get("total", 0),
"in_sign_count": len(samples), "facets": facets,
"samples": samples[:5],
}) })
return {"provider": provider, "objects": items} return {"provider": provider, "objects": items}
+26 -23
View File
@@ -1,6 +1,6 @@
"""Testy mostu obliczenia → sygnifikatory → wyszukiwanie (bez efemeryd/HTTP).""" """Testy mostu obliczenia → sygnifikatory → wyszukiwanie (bez efemeryd/HTTP)."""
from app.engine.models import DEFAULT_OBJECTS, ObjectPosition from app.engine.models import DEFAULT_OBJECTS
from app.significators import PLANET_ABBR, build_report from app.significators import PLANET_ABBR, _ordinal, build_report
class FakeData: class FakeData:
@@ -12,31 +12,34 @@ class FakeData:
return {"provider": "fake", "total": len(rows), "rows": rows} return {"provider": "fake", "total": len(rows), "rows": rows}
def test_build_report_filters_to_sign_and_drops_noise(): def test_sign_and_house_facets():
positions = [ObjectPosition("Sun", 40.0, 0.0, 0.95, False)] # Taurus 10° -> [Su + [Tau positions = [{"name": "Sun", "sign": "Taurus", "direction": "D", "house": 11}]
data = FakeData({"[Su": [ data = FakeData({"[Su": [
{"significator": "[Su in [Tau", "actioneffect": "efekt A"}, # trafienie {"significator": "[Su in [Tau", "actioneffect": "efekt znak"}, # faseta znak
{"significator": "[Su in [Vir", "actioneffect": "inny znak"}, # zły znak {"significator": "[Su in 11th H.", "actioneffect": "efekt dom"}, # faseta dom
{"significator": "[Su in [Tau", "actioneffect": "nan"}, # szum (pusty efekt) {"significator": "[Su in [Vir", "actioneffect": "inny"}, # żadna
{"significator": "SIGNIFICATOR nagłówek", "actioneffect": "x"}, # szum (nagłówek) {"significator": "[Su in [Tau", "actioneffect": "nan"}, # szum
]}) ]})
item = build_report(positions, data, per_object_limit=100)["objects"][0] item = build_report(positions, data)["objects"][0]
assert item["object"] == "Sun" and item["sign"] == "Taurus" assert item["object"] == "Sun" and item["house"] == 11
assert item["planet_token"] == "[Su" and item["sign_token"] == "[Tau" facets = {f["type"]: f for f in item["facets"]}
assert item["planet_total"] == 4 assert facets["sign"]["count"] == 1
assert item["in_sign_count"] == 1 assert facets["sign"]["samples"][0]["effect"] == "efekt znak"
assert item["samples"][0]["effect"] == "efekt A" 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(): def test_no_house_facet_when_house_missing():
positions = [ObjectPosition("Mars", 220.0, 0.0, -0.2, True)] # Scorpio -> [Ma + [Sco positions = [{"name": "Mars", "sign": "Scorpio", "direction": "Rx"}]
data = FakeData({"[Ma": [ data = FakeData({"[Ma": [{"significator": "[Ma in [Sco", "actioneffect": "eff"}]})
{"significator": "[Ma in [Sco", "actioneffect": "", "topicresult": "temat X"}, types = [f["type"] for f in build_report(positions, data)["objects"][0]["facets"]]
{"significator": "[Ma in [Sco", "bodypart": "część ciała"}, assert "sign" in types and "house" not in types
]})
samples = build_report(positions, data)["objects"][0]["samples"]
assert samples[0]["effect"] == "temat X" def test_ordinal():
assert samples[1]["effect"] == "część ciała" 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(): def test_planet_abbr_covers_all_default_objects():
@@ -59,3 +59,5 @@ tr:last-child td { border-bottom: none; }
.samples { margin-top: .3rem; } .samples { margin-top: .3rem; }
.samples td { font-size: .92rem; vertical-align: top; } .samples td { font-size: .92rem; vertical-align: top; }
.samples td.nowrap { color: var(--accent); padding-right: 1rem; } .samples td.nowrap { color: var(--accent); padding-right: 1rem; }
.facet { margin: .4rem 0 .6rem 1rem; }
.facet-head { color: var(--ink); font-size: .9rem; }
@@ -44,24 +44,27 @@
{% for o in result.objects %} {% for o in result.objects %}
<div class="sig-item"> <div class="sig-item">
<div class="sig-head"> <div class="sig-head">
<strong>{{ o.object }}</strong> w <strong>{{ o.sign }}</strong> <strong>{{ o.object }}</strong> w <strong>{{ o.sign }}</strong>{% if o.house %}, {{ o.house }}. dom{% endif %}
<span class="{{ 'retro' if o.direction == 'Rx' else '' }}">{{ o.direction }}</span> <span class="{{ 'retro' if o.direction == 'Rx' else '' }}">{{ o.direction }}</span>
<span class="muted">— szukano {{ o.planet_token }} + {{ o.sign_token }}; <span class="muted small">(planeta {{ o.planet_token }}: {{ o.planet_total }} rekordów)</span>
w znaku: <strong>{{ o.in_sign_count }}</strong> (planeta ogółem: {{ o.planet_total }})</span>
</div> </div>
{% if o.samples %} {% for f in o.facets %}
<div class="facet">
<div class="facet-head">{{ f.label }} — <strong>{{ f.count }}</strong> dopasowań
<span class="muted small">[{{ f.token }}]</span></div>
{% if f.samples %}
<table class="samples"> <table class="samples">
<tbody> <tbody>
{% for s in o.samples %} {% for s in f.samples %}
<tr><td class="mono nowrap">{{ s.significator }}</td><td>{{ s.effect }}</td></tr> <tr><td class="mono nowrap">{{ s.significator }}</td><td>{{ s.effect }}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %}
<div class="muted small">brak dopasowań „w znaku" w bazie</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endfor %}
{% endif %} {% endif %}
<script> <script>