Files
astrololo/services/presentation/app/rozszerzenia.py
T
gitea 62d6f9d4d5
build / build (push) Failing after 2s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
astrololo: generowanie tekstu jako moduł odłączalny (2/5)
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.

DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.

CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).

JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.

TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:

1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
   wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
   deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
   `llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
   w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
   międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
   jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.

DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.

TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.

Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.

SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.

Testy: presentation 362, logic 342, data 37, render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 14:25:56 +00:00

87 lines
2.8 KiB
Python

"""Most do modułów opcjonalnych — CELOWO nic o nich nie wie.
Ten plik jest w każdym obrazie, także tam, gdzie żadnego modułu nie ma. Dlatego
nie wolno w nim wymienić z nazwy ani jednej funkcji, którą moduł wnosi: nazwa
w moście byłaby dokładnie tym śladem, którego wydzielanie ma się pozbyć.
Most odpowiada wyłącznie na pytanie „czy coś jest podpięte i co wnosi". Gdy nie
ma nic, wszystkie odpowiedzi są puste, a aplikacja zachowuje się tak, jakby
takiego pojęcia nigdy nie było — nie jak aplikacja z wyłączoną funkcją.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover - zależy od obrazu
from app import dodatki as _modul
except ImportError: # obraz zbudowany bez tego katalogu
_modul = None # type: ignore[assignment]
DOSTEPNY = _modul is not None
def funkcje() -> tuple[tuple[str, str, str, str], ...]:
"""Pozycje do katalogu funkcji — jako surowe krotki, nie obiekty.
Surowe, bo katalog buduje `features`, a gdyby moduł importował stamtąd klasę,
powstałby cykl: features → most → moduł → features."""
return _modul.FUNKCJA if _modul else ()
def trasy() -> dict[tuple[str, str], str]:
return _modul.TRASY if _modul else {}
def zasoby() -> dict[str, Any]:
return _modul.ZASOBY if _modul else {}
def akcje() -> dict[str, str]:
"""Akcje formularza wnoszone przez moduł: nazwa → wymagane uprawnienie."""
return _modul.AKCJE if _modul else {}
def pola_formularza():
"""Zależność FastAPI deklarująca dodatkowe pola formularza.
Pola muszą być zadeklarowane w SYGNATURZE, żeby FastAPI je odczytało, a
sygnatura wspólnego handlera nie może ich wymieniać. Zależność rozwiązuje
jedno i drugie: moduł deklaruje własne pola u siebie, a handler wie tylko,
że dostaje słownik."""
if _modul:
return _modul.pola
return lambda: {}
def kontekst_szablonu(request) -> dict:
"""Co moduł dokłada do kontekstu szablonu. Bez modułu — nic."""
return _modul.katalog_dla(request) if _modul else {}
def obsluz(akcja: str, **dane) -> dict | None:
"""Wykonanie akcji modułu. None = to nie jest jego akcja."""
return _modul.obsluz(akcja, **dane) if _modul else None
def zamontuj(app, **zaleznosci) -> None:
"""Podpięcie tras modułu i przekazanie mu tego, czego potrzebuje."""
if _modul:
_modul.zamontuj(app, **zaleznosci)
def katalog_szablonow() -> list[str]:
"""Katalogi szablonów modułu — nakładane PRZED wspólnymi."""
if not _modul:
return []
import pathlib
return [str(pathlib.Path(_modul.__file__).parent / "templates")]
def katalog_zasobow() -> list[str]:
if not _modul:
return []
import pathlib
return [str(pathlib.Path(_modul.__file__).parent / "static")]