astrololo: generowanie tekstu jako moduł odłączalny (2/5)
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
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>
This commit is contained in:
@@ -21,6 +21,8 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app import rozszerzenia
|
||||
|
||||
ADMIN = "admin" # zakładanie kont i nadawanie uprawnień — nie do nadania z UI
|
||||
|
||||
|
||||
@@ -55,9 +57,6 @@ EXTRAS: tuple[Feature, ...] = (
|
||||
Feature("advanced_calc", "Obliczenia zaawansowane",
|
||||
"Stacje planet, tabele żywiołów i faz, aspekty poboczne, zodiaki syderyczne.",
|
||||
"Rozszerzenia"),
|
||||
Feature("ai", "Generowanie tekstu przez model",
|
||||
"Horoskopy pisane przez model językowy. UWAGA: każde użycie kosztuje.",
|
||||
"Rozszerzenia"),
|
||||
Feature("files_input", "Wgrywanie i archiwizacja baz",
|
||||
"Dodawanie nowych plików baz i wycofywanie ich z użytku (plik zostaje, "
|
||||
"zamrożony, ze znacznikiem czasu). Kasować może wyłącznie administrator.",
|
||||
@@ -66,7 +65,12 @@ EXTRAS: tuple[Feature, ...] = (
|
||||
"Pobieranie raportu jako PDF i wyników jako Excel.", "Rozszerzenia"),
|
||||
)
|
||||
|
||||
ALL: tuple[Feature, ...] = SCREENS + EXTRAS
|
||||
# Funkcje wnoszone przez moduły opcjonalne. Katalog nie wymienia ich z nazwy —
|
||||
# gdyby wymieniał, nazwa funkcji siedziałaby w kodzie także w obrazie, który tego
|
||||
# modułu nie zawiera, i `grep` po kontenerze pokazałby coś, czego tam nie ma.
|
||||
DOKLADANE: tuple[Feature, ...] = tuple(Feature(*k) for k in rozszerzenia.funkcje())
|
||||
|
||||
ALL: tuple[Feature, ...] = SCREENS + EXTRAS + DOKLADANE
|
||||
BY_KEY: dict[str, Feature] = {f.key: f for f in ALL}
|
||||
GRANTABLE: frozenset[str] = frozenset(BY_KEY) # ADMIN celowo poza tym zbiorem
|
||||
|
||||
@@ -100,7 +104,6 @@ ROUTES: dict[tuple[str, str], str | None] = {
|
||||
("POST", "/files/restore"): ADMIN,
|
||||
("POST", "/files/delete"): ADMIN,
|
||||
("POST", "/files/rules"): ADMIN,
|
||||
("POST", "/horoscope/stream"): "ai",
|
||||
("GET", "/accounts"): ADMIN,
|
||||
("POST", "/accounts/create"): ADMIN,
|
||||
("POST", "/accounts/update"): ADMIN,
|
||||
@@ -116,12 +119,15 @@ ROUTES: dict[tuple[str, str], str | None] = {
|
||||
("POST", "/wyloguj"): None,
|
||||
("POST", "/accounts/sesje"): ADMIN,
|
||||
}
|
||||
# Trasy modułów opcjonalnych. Dokładane po utworzeniu mapy, żeby wpis o module
|
||||
# nie musiał być wpisany w tym pliku z nazwy.
|
||||
ROUTES.update(rozszerzenia.trasy())
|
||||
|
||||
|
||||
# ── zasoby statyczne ─────────────────────────────────────────────────────
|
||||
# Skrypt i arkusz stylów są CZĘŚCIĄ funkcji, nie jej dekoracją. Nazwa pliku jest
|
||||
# zgadywalna, a treść opowiada o funkcji dokładniej niż przycisk: `models.js`
|
||||
# wymienia dostawców modeli, `compile.js` — składanie raportu. Dlatego każdy
|
||||
# zgadywalna, a treść opowiada o funkcji dokładniej niż przycisk: `compile.js`
|
||||
# zdradza składanie raportu, a `wheelzoom.js` — istnienie kosmogramu. Dlatego każdy
|
||||
# zasób przechodzi przez tę samą bramkę co ekrany.
|
||||
#
|
||||
# Wartość to ZBIÓR uprawnień, z których wystarczy JEDNO: wyszukiwarka lokalizacji
|
||||
@@ -142,7 +148,6 @@ STATIC: dict[str, frozenset[str] | str | None] = {
|
||||
"s-files.css": "files",
|
||||
"s-settings.css": "settings",
|
||||
"s-accounts.css": ADMIN,
|
||||
"x-ai.css": "ai",
|
||||
# skrypty
|
||||
"formsync.js": _FORMULARZE,
|
||||
"now.js": _LOKALIZACJA,
|
||||
@@ -153,16 +158,16 @@ STATIC: dict[str, frozenset[str] | str | None] = {
|
||||
"wheeltip.js": _KOLO,
|
||||
"copy.js": frozenset({"interpret", "timeline"}),
|
||||
"compile.js": "compile",
|
||||
"models.js": "ai",
|
||||
"progress.js": "ai",
|
||||
"natal.js": "ai",
|
||||
"predictions.js": "ai",
|
||||
# base.css celowo NIE MA tu wpisu: potrzebuje go ekran logowania, więc jest
|
||||
# publiczny (PUBLIC_PATHS w security.py). Dlatego nie wolno w nim trzymać
|
||||
# niczego, co nazywa jakąkolwiek funkcję.
|
||||
}
|
||||
|
||||
|
||||
# Zasoby modułów opcjonalnych — tak samo jak trasy.
|
||||
STATIC.update(rozszerzenia.zasoby())
|
||||
|
||||
|
||||
def static_required(name: str) -> frozenset[str] | str | None:
|
||||
"""Uprawnienie dla zasobu. Nieznany zasób → admin, jak nieznana trasa."""
|
||||
return STATIC.get(name, ADMIN)
|
||||
|
||||
Reference in New Issue
Block a user