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>
astrololo
Aplikacja w modelu trójwarstwowym, w pełni modułowa: trzy niezależne usługi, każda komunikuje się wyłącznie z sąsiadem (nigdy „przez głowę”).
┌──────────────────────┐ formularz (w dół) ┌──────────────────────┐ zapytanie (w dół) ┌──────────────────────┐
│ PREZENTACJA (:8000) │ ───────────────────▶ │ LOGICZNA (:8001) │ ───────────────────▶ │ BAZODANOWA (:8002) │
│ strona WWW + form │ ◀─────────────────── │ reguły biznesowe │ ◀─────────────────── │ wyszukiwanie danych │
└──────────────────────┘ wyniki (w górę) └──────────────────────┘ dane (w górę) └──────────────────────┘
HTML/UI pośrednik + logika Excel(+cache) ▸ SQL
Każda warstwa to osobny katalog, osobny requirements.txt, osobny Dockerfile
i osobne README. Komunikacja przez HTTP/JSON. Warstwa zna tylko adres warstwy
bezpośrednio pod nią — nic o jej wnętrzu.
| Warstwa | Katalog | Zna w dół | Zadanie |
|---|---|---|---|
| Prezentacji | services/presentation |
LOGIC_URL |
serwuje stronę, przekazuje formularz, renderuje wyniki |
| Logiczna | services/logic |
DATA_URL |
reguły biznesowe, tłumaczenie zapytań, opracowanie wyników |
| Bazodanowa | services/data |
pliki Excela / SQL | tylko wyszukiwanie danych i podanie ich w górę |
Szybki start (Docker)
make sample # przykładowe pliki .xlsx do warstwy bazodanowej
make up # zbuduj i uruchom 3 warstwy
# otwórz http://localhost:8000
Szybki start (lokalnie, bez Dockera — zalecane)
python -m venv .env && source .env/bin/activate # venv (jednorazowo)
make install # zależności WSZYSTKICH warstw
make sample # opcjonalnie: dane przykładowe
Potem w 3 osobnych terminalach (w każdym source .env/bin/activate):
make dev-data # terminal 1 -> :8002
make dev-logic # terminal 2 -> :8001
make dev-presentation # terminal 3 -> :8000 -> http://localhost:8000
make installinstaluje zależności wszystkich trzech warstw do aktywnego venv. Testy silnika:make test. Wyczyszczenie cache:make clean-cache.
Modułowość — dowód
- Wymień prezentację (np. na SPA/React) → reszta bez zmian, kontrakt
/api/querystały. - Wymień bazę (Excel → SQL) → prezentacja i logika bez zmian (patrz niżej).
- Każdą warstwę da się uruchomić, testować i wdrażać osobno.
Wydajność warstwy Excela — cache 4-poziomowy
Dziś dane to setki dużych .xlsx, przeszukiwanych po wykrytym nagłówku i
układzie kolumn. To kosztowne, więc warstwa bazodanowa ma cache (szczegóły:
services/data/README.md):
- Schemat (L1, SQLite) — wykryty nagłówek + mapowanie kolumn zapisane raz na wersję pliku.
- Dane (L2, Parquet) — znormalizowany arkusz; kolejne odczyty 10–100× szybsze niż
.xlsx. - Zapytania (L3, in-memory TTL/LRU) — powtarzalne wyszukiwania natychmiast (łatwo podmienić na Redis).
- Odwrócony indeks (L4, SQLite) —
wartość → plik; otwieramy tylko trafione pliki zamiast skanu setek.
Unieważnianie automatyczne: klucz cache = odcisk pliku (mtime+rozmiar,
opcjonalnie sha256). Zmiana pliku → przebudowa tylko jego wpisów.
Droga na przyszłość — migracja do SQL
Warstwa bazodanowa ukrywa źródło za interfejsem DataProvider (wzorzec
Repository). Migracja:
make migrate # ETL: tym samym loaderem Excel -> tabela 'records' + indeksy
export DATA_PROVIDER=sql # przełącz całą warstwę
SqlDataProvider realizuje ten sam kontrakt /search, więc warstwa logiczna i
prezentacji nie zmieniają ani jednej linii. Odwrócony indeks z L4 (SQLite) jest
już pomostem — rozbudowa o wszystkie kolumny = docelowa baza.