Spięcie osi czasu z bazą interpretacji i widokiem (LOG-14, 1B->2B)

Realizuje przepływ 1B->2B z notes2: predykcyjne sygnifikatory (z datami)
dopasowane do interpretacji z bazy.

Logika:
- timeline.py: zdarzenia niosą strukturę (directed/aspect/target dla dyrekcji,
  lord/sign dla profekcji) do budowy tokenów.
- significators.interpret_events + _event_tokens: z każdego zdarzenia buduje
  tokeny bazy (dyrekcja: [planeta][aspekt][cel]; profekcja: [władca][znak]) i
  dopina interpretacje reużywając _facet_samples (AND tokenów, dedup, rozwinięcie).
- /chart/timeline: flaga interpret=true.

Prezentacja:
- nowa strona /timeline "Kalendarz": formularz (urodzenie + zakres dat) -> oś
  czasu z technikami, datami i interpretacjami; nawigacja + wspólne now.js.

Walidacja E2E na realnym main_base.xlsx (2025-2026):
- dyr. Saturn kwadratura MC -> 50 interpret. ("...5th house" -> "abortion/miscarriage")
- Władca Roku Saturn (wiek 42) -> 63; strona renderuje badge dat/technik.
71 testów przechodzi (nowy test_timeline_interpret).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 12:31:50 +02:00
parent 97ad21d2e4
commit c4a181b810
9 changed files with 247 additions and 9 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ i nie w bazie.
- `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)
- `POST /chart/timeline``{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end) (LOG-14)
- `POST /chart/timeline``{when_utc, lat, lon, from_date, to_date, techniques?}` → zbiorcza oś czasu: profekcje + Solar Return + dyrekcje solar-arc, posortowane (technique | significator | start | exact | end); z interpret=true dopina interpretacje z bazy do dat (LOG-14, 1B->2B)
- `POST /chart/compare` → jak wyżej → raport różnic dwóch silników (LOG-26; wymaga silnika B)
- `GET /health` (sprawdza też warstwę bazodanową)
+10 -5
View File
@@ -58,13 +58,15 @@ def solar_arc_directions(
if not (lo_age <= age <= hi_age) or (p == q and arc < 1e-6):
continue
exact = _add_years(birth, age)
out.append(_row(
row = _row(
"solar_arc",
f"dyr. {p} {PL_NAME[asp]} {q}",
_add_years(birth, age - orb_years),
exact,
_add_years(birth, age + orb_years),
))
)
row.update(directed=p, aspect=asp, target=q) # do budowy tokenów (1B->2B)
out.append(row)
return out
@@ -83,10 +85,13 @@ def profection_events(natal_asc: float, birth: datetime, lo: datetime, hi: datet
if end < lo or start > hi:
continue
sign = profected_sign(natal_asc, age)
out.append(_row(
"profection", f"Władca Roku: {DOMICILE_RULERS[sign]} (Asc {sign}, wiek {age})",
lord = DOMICILE_RULERS[sign]
row = _row(
"profection", f"Władca Roku: {lord} (Asc {sign}, wiek {age})",
start, start, end,
))
)
row.update(lord=lord, sign=sign) # do budowy tokenów (1B->2B)
out.append(row)
return out
+15 -3
View File
@@ -180,11 +180,15 @@ class TimelineRequest(BaseModel):
from_date: str # zakres: YYYY-MM-DD
to_date: str
techniques: list[str] | None = None # profection | solar_return | solar_arc
interpret: bool = False # dopnij interpretacje z bazy (1B->2B)
@app.post("/chart/timeline")
def chart_timeline(req: TimelineRequest) -> dict:
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end."""
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end.
Z interpret=true dopina do zdarzeń interpretacje z warstwy danych (LOG-19, 1B->2B).
"""
from app.engine import houses as H
from app.engine.models import ChartMoment
from app.engine.timeline import build_timeline
@@ -196,8 +200,16 @@ def chart_timeline(req: TimelineRequest) -> dict:
for p in engine.positions(natal):
points[p.name] = p.longitude
events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques)
return {"engine": engine.name, "from": req.from_date, "to": req.to_date,
"count": len(events), "events": events}
out = {"engine": engine.name, "from": req.from_date, "to": req.to_date}
if req.interpret:
from app.significators import interpret_events
try:
interpret_events(events, DataClient())
except httpx.HTTPError as e:
out["data_error"] = f"Warstwa danych niedostępna: {e}"
out.update(count=len(events), events=events)
return out
@app.get("/health")
+49
View File
@@ -195,3 +195,52 @@ def build_report(
"facets": facets,
})
return {"provider": provider, "objects": items}
def _event_tokens(event: dict) -> list[str]:
"""Tokeny bazy dla zdarzenia osi czasu (spięcie 1B→2B). Pierwszy = planeta."""
from app.engine.aspects import DB_TOKEN
if event.get("technique") == "solar_arc":
p = PLANET_ABBR.get(event.get("directed"))
a = DB_TOKEN.get(event.get("aspect"))
q = PLANET_ABBR.get(event.get("target")) # None dla Asc/MC (nie ma tokenu)
toks = []
if p:
toks.append("[" + p)
if a:
toks.append(a)
if q:
toks.append("[" + q)
return toks if p else []
if event.get("technique") == "profection":
lord = PLANET_ABBR.get(event.get("lord"))
sign = SIGN_TO_ABBR.get(event.get("sign"))
toks = []
if lord:
toks.append("[" + lord)
if sign:
toks.append("[" + sign)
return toks if lord else []
return []
def interpret_events(events: list[dict], data: DataSource, limit: int = 4,
per_object_limit: int = 5000) -> list[dict]:
"""Dopina interpretacje z bazy do zdarzeń osi czasu (predykcyjne 1B → 2B).
Wyszukuje po tokenie planety zdarzenia i zawęża do wszystkich tokenów
(aspekt/druga planeta lub znak), z odsiewaniem szumu i duplikatów.
"""
for ev in events:
tokens = _event_tokens(ev)
if not tokens:
continue
raw = data.search(
key="significator", value=tokens[0], exact=False, limit=per_object_limit,
fields=["significator", "actioneffect", "topicresult", "bodypart"],
)
samples = _facet_samples(raw.get("rows", []), tokens)
ev["interpretations"] = samples[:limit]
ev["interpretations_count"] = len(samples)
return events
@@ -0,0 +1,46 @@
"""Spięcie osi czasu z bazą interpretacji (LOG-14 → 1B→2B)."""
from app.significators import _event_tokens, interpret_events
class FakeData:
def __init__(self, rows_by_value):
self.rows_by_value = rows_by_value
def search(self, key, value, exact, limit, fields=None):
rows = self.rows_by_value.get(value, [])
return {"provider": "fake", "total": len(rows), "rows": rows}
def test_event_tokens_solar_arc():
ev = {"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"}
assert _event_tokens(ev) == ["[Ve", "[conj", "[NN"]
def test_event_tokens_solar_arc_to_angle_has_no_target_token():
ev = {"technique": "solar_arc", "directed": "Sun", "aspect": "square", "target": "MC"}
assert _event_tokens(ev) == ["[Su", "[sq"] # MC nie ma tokenu planety
def test_event_tokens_profection():
ev = {"technique": "profection", "lord": "Saturn", "sign": "Capricorn"}
assert _event_tokens(ev) == ["[Sa", "[Cap"]
def test_solar_return_has_no_tokens():
assert _event_tokens({"technique": "solar_return"}) == []
def test_interpret_attaches_matches_with_all_tokens():
events = [
{"technique": "solar_arc", "directed": "Venus", "aspect": "conjunction", "target": "North Node"},
{"technique": "solar_return"},
]
data = FakeData({"[Ve": [
{"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # wszystkie tokeny
{"significator": "[Ve [conj [Mo", "actioneffect": "inny"}, # brak [NN
{"significator": "[Ve [conj [NN", "actioneffect": "spotkanie losowe"}, # duplikat
]})
interpret_events(events, data)
assert events[0]["interpretations_count"] == 1 # duplikat odsiany, tylko z [NN
assert events[0]["interpretations"][0]["expanded"] == "Venus conjunction North Node"
assert "interpretations" not in events[1] # solar_return pominięty