feat(soft-delete): faza 02 — publikacje (5 modeli, widoki, kaskada, kanarki) - #741
Merged
Conversation
Kanarek katalogowy fazy 01 wywolany na rozszerzonej liscie tabel, bez modyfikowania TABELE_SOFT_DELETE (czyli bez czerwonego testu na starcie galezi). 15 widokow w 5 kategoriach. Ustalenie, ktorego nie bylo w tabeli zadan planu: 6 widokow agregujacych (bpp_nowe_sumy_* x5 + rozbieznosci_dyscyplin) wymaga poprawki -- plan mial dla nich wylacznie ostrzezenie "sprawdz, czy wymagaja". Wynik negatywny: zero winowajcow wsrod 3 tabel *_autor fazy 01. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…kada
Mixin BppPublikacjaSoftDeleteMixin: delete()/restore() per-instancja, jawna
kaskada na wiersze *_Autor pod wspolnym transaction_id, BEZ refleksyjnej
kaskady pakietu (ta zjechalaby po *_Streszczenie i spolce, i to CICHO --
delete() pakietu ma domyslnie strict=False).
Rozpoznanie ksztaltu kaskady przez _meta.related_objects, a NIE przez
type(self).__dict__.get("autorzy_set"), jak proponowal plan. Ten drugi
sposob NIE dziala: property autorzy_set jest zadeklarowana na abstrakcyjnej
bazie Praca_Doktorska_Baza, a dziedziczenie abstrakcyjne w Django kopiuje do
potomka POLA, nie zwykle atrybuty Pythona -- __dict__ klasy konkretnej jest
pusty, wiec test na property dawalby falszywe "to through-model", a zaraz
potem AttributeError na FakeSet.model. Metadane Django znaja wylacznie
PRAWDZIWE relacje, wiec odpowiadaja na pytanie, ktore faktycznie zadajemy.
Przy okazji nazwa pola FK pochodzi z relacji, zamiast byc zaszyta na sztywno
w restore().
deleted_objects to BppDeletedManager (faza 01), nie pakietowy DeletedManager
-- plan cytowal ten drugi, bo powstal przed runda poprawek fazy 01.
save() bumpuje ostatnio_zmieniony przy soft-delete i restore (kontrakt PINNED
fazy 01) -- bez tego nagrobki dla harvestu przyrostowego nie byly by
odpytywalne po ostatnio_zmieniony__gte.
Migracja 0496: 15 pol + 5 indeksow CZESCIOWYCH (WHERE deleted_at IS NOT
NULL), tym samym wzorcem co faza 01 -- pelny btree byl by samym kosztem, bo
deleted_at IS NULL pasuje do ~100% wierszy.
Test zweryfikowany MUTACYJNIE (dwie mutacje, po jednej na asercje): kaskada
wylaczona -> pada asercja o deleted_at; kaskada bez wspolnego txid -> pada
asercja o transaction_id. Przy pierwszej mutacji wyszlo, ze dane fixtury byly
nieprawidlowe (baker nadawal obu autorstwom kolejnosc=0, lamiac
wc_autor_excl_rekord_kolejnosc) i test przechodzil CZESCIOWO z powodu efektu
ubocznego: ograniczenie jest DEFERRABLE i warunkowane deleted_at IS NULL,
wiec soft-delete wyprowadzal oba wiersze poza jego zakres przed COMMIT.
Naprawione jawna, rozna kolejnoscia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…asujaca
Migracja 0497, odpowiednik 0489 z fazy 01 dla drugiej sciezki. Ta sama
wymuszona kolejnosc: widoki -> funkcje refresh -> regeneracja bramki WHEN
(bramka jest wyliczana z pg_depend, wiec musi isc PO widokach).
Wzorca z 0489 NIE dalo sie uzyc wprost. Tamta funkcja dopisuje WHERE na
koncu i asertuje, ze definicja konczy sie golym "FROM <tabela>". Dla
publikacji sa TRZY rozne ksztalty (introspekcja zywego katalogu):
A. wydawnictwo_{ciagle,zwarte}_view, patent_view -- koncza sie
LEFT JOIN ... GROUP BY <t>.id, bez WHERE najwyzszego poziomu.
WHERE po GROUP BY to blad skladni, wiec predykat idzie PRZED GROUP BY.
B. praca_{doktorska,habilitacyjna}_view -- koncza sie FROM <t>; wzorzec
z 0489 dziala wprost.
C. praca_{doktorska,habilitacyjna}_autorzy -- maja WLASNY WHERE (join po
przecinku), wiec predykat idzie do niego.
Rozpoznanie ksztaltu NIE moze isc po samym slowie "WHERE": rodzina A
zawiera count(...) FILTER (WHERE ..._autor.deleted_at IS NULL) z 0494,
a wszystkie maja WHERE w podzapytaniach o django_content_type.
Dyskryminatorem jest wciecie dwoch spacji (klauzula glowna w formacie
pg_get_viewdef(pretty=true)).
W rodzinie A filtrujemy tabele PUBLIKACJI -- lewa, napedzajaca strone
LEFT JOIN-a. Pulapka z handoffu 3.2 dotyczy strony PRAWEJ i jest pokryta
testem test_publikacja_bez_zywych_autorow_ZOSTAJE.
Zdjete xfail(strict=True) z dwoch kanarkow fazy 01 w
test_cache/test_soft_delete_preconditions.py -- razem z symulacja (ALTER
ADD COLUMN + owijka widoku), ktora te testy stosowaly, zeby dalo sie je
w ogole napisac przed faza 02. Teraz mechanizm jest prawdziwy.
Weryfikacja mutacyjna (2 mutacje) ujawnila, ze pierwsza wersja testu na
pulapke agregatu BYLA BEZWARTOSCIOWA: odpytywala bpp_rekord_mat, czyli
cache, ktorego soft-delete autorstwa nie przelicza -- niesniezy wiersz
maskowal zdegenerowany widok. Test odpytuje teraz WIDOK (wyrocznia), a
cache dopiero po wymuszonym przeliczeniu.
Test odwracalnosci 0497 zlapal blad w backward: katalog przechowuje
predykat w postaci ZNORMALIZOWANEJ (dla jednej tabeli w zasiegu Postgres
usuwa kwalifikacje), wiec _bez_filtra szukala tekstu, ktorego tam nie ma.
Przy poprawce kolejnosc kandydatow jest nosna: wariant z "AND" musi byc
sprawdzany pierwszy, bo "\n WHERE <predykat>" pasuje jako PREFIKS do
"WHERE <predykat> AND ..." i usuniecie samego prefiksu zostawiloby WHERE
zaczynajacy sie od AND.
ZNANA REGRESJA, nie naprawiona w tym commicie (do decyzji):
test_migracja_0489_rewers i test_migracje_0490_0493_rewers padaja.
Przyczyna ustalona: django-denorm podpina post_migrate ->
install_triggers(), ktore odbudowuje triggery z AKTUALNYCH modeli. Zjazd
ponizej 0496 usuwa kolumny deleted_at z tabel publikacji, ale modele je
nadal deklaruja, wiec denorm generuje WHEN z OLD."deleted_at" ->
UndefinedColumn. Zweryfikowane: oba testy PRZECHODZA na stanie sprzed
fazy 02. Ma to konsekwencje wdrozeniowe (rollback ponizej 0496), wiec
wymaga decyzji, nie cichej latki w tescie.
Trzeci failure (test_symulacja_fazy_02 w kanarku) jest OCZEKIWANY --
nalezy do zadania "rozszerzenie kanarkow".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…rewersu Naprawia regresje zglosozna w poprzednim commicie. django-denorm podpina post_migrate -> install_triggers(), ktore odbudowuje triggery z AKTUALNYCH modeli. Testy odwracalnosci schodza ponizej 0496, czyli ponizej migracji dodajacej deleted_at na tabelach publikacji -- kolumny w bazie wtedy nie ma, ale modele nadal ja deklaruja, wiec denorm generuje WHEN z OLD."deleted_at" i CREATE TRIGGER pada na UndefinedColumn. KOREKTA wobec poprzedniego commita: to NIE jest nowe zjawisko. Faza 01 udokumentowala dokladnie ten mechanizm w runbooku (sekcja 4 "Rollback: cofac KOD I MIGRACJE razem"), z identycznym komunikatem bledu. Nowe jest tylko to, ze sciezka zjazdu testow fazy 01 przecina teraz 0496, wiec trafiaja one w mechanizm, ktory wczesniej ich nie dotyczyl. To nie jest problem wdrozeniowy: w prawdziwym rollbacku wycofuje sie KOD razem ze schematem, a stare modele nie maja deleted_at. Kombinacja "nowy kod + stary schemat" powstaje wylacznie w tescie, ktory manipuluje samym schematem trzymajac kod na miejscu. Fixtura odpina handler na czas testu i na koncu przebudowuje triggery recznie. W finally doprowadza tez schemat do najnowszej migracji SAMA, zamiast ufac ze test zdazyl -- inaczej przy asercji padajacej w polowie przebudowa wywalilaby sie na brakujacej kolumnie, przykrywajac PRAWDZIWY powod porazki. Runbook fazy 01 rozszerzony o zakres fazy 02 i o informacje, ze obejscie jest wylacznie testowe. Pozostaje 1 oczekiwana porazka: test_symulacja_fazy_02 w kanarku katalogowym -- nalezy do zadania "rozszerzenie kanarkow", bo jego dowod zaklada, ze widoki publikacji SA jeszcze winowajcami. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Task 2d -- zadanie, ktorego NIE BYLO w tabeli kolejnosci planu fazy 02. Plan mial dla tych widokow wylacznie ostrzezenie "sprawdz, czy wymagaja poprawki"; inwentaryzacja kanarka na starcie fazy pokazala, ze wymagaja wszystkie szesc. bpp.0498 -- piec widokow bpp_nowe_sumy_*_view. rozbieznosci_dyscyplin.0023 -- rozbieznoscizrodelview. Oba dopelniaja migracje fazy 01 (0495 i 0022), ktore zamknely w tych widokach wymiar AUTORSTWA. Wymiar PUBLIKACJI byl wtedy otwarty, bo publikacje staly sie soft-delete dopiero w 0496. Zakres szerszy niz w fazie 01: 0495 objela 3 widoki (tylko typy z through-modelem), 0498 obejmuje 5 -- praca_doktorska i praca_habilitacyjna nie maja tabeli *_autor, wiec 0495 nie miala tam czego filtrowac, ale soft-delete samej pracy dotyczy ich tak samo. PULAPKA AGREGATU TU NIE WYSTEPUJE -- sprawdzone introspekcja, nie zalozone. Te widoki nie maja ANI JEDNEGO LEFT JOIN-a (zlaczenia po przecinku albo jawne JOIN) ani GROUP BY -- sumowanie dzieje sie dopiero w modelach Django nad UNION ALL. Semantyka jest zreszta odwrotna niz przy liczba_autorow: skasowana publikacja MA wypasc z rankingu, a nie zostac w nim z zerem. Dlatego wystarczyl wspoldzielony helper widok_dopisz_warunek (ktory zreszta sam odrzuca definicje z GROUP BY/OR/UNION). TESTY -- kazdy wymiar osobno, bo kaskada maskuje. Naiwny test "wc.delete() -> wiersz znika z sum" NIE jest wyrocznia dla tych migracji: delete() kaskaduje na autorstwa, wiec wiersz znika z DWOCH niezaleznych powodow -- przez nowy filtr publikacji ORAZ przez filtr autorstwa z fazy 01. Potwierdzone mutacyjnie: po wylaczeniu OBU migracji tego zadania test end-to-end dalej PRZECHODZI, a padaja tylko testy izolujace wymiar (surowy UPDATE samej publikacji) i testy kontraktu pg_depend. Oba warianty zostaja: izolujacy jako wyrocznia, end-to-end jako pokrycie realnej sciezki operatora wraz z restore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…arkow MIGRACJA 0499 -- siedem widokow bpp_kronika_* jednym cieciem. Faza 01 chciala skasowac trzy z nich i zostala zablokowana: zaleza od nich dwa widoki nadrzedne, wiec goly DROP nie przechodzi, a CASCADE zabralby po cichu takze je. Graf trzeba rozciac w jednym miejscu albo wcale. Dowod martwoty mocniejszy niz grep: pg_depend pokazuje, ze JEDYNE zaleznosci od tej siodemki sa WEWNATRZ rodziny (kronika_view <- all_unsorted_view <- piec lisci). Nic spoza niej na nich nie stoi -- obejmuje to takze widoki i reguly, ktorych nazwa nie zawiera slowa "kronika". Do tego zero trafien w kodzie/szablonach/JSON-ach i zero Meta.db_table. Zywotnosc dwoch z nich (praca_doktorska, praca_habilitacyjna) byla nieznana -- wyszla na jaw dopiero, gdy kanarek dostal do zakresu tabele publikacji. Dokladnie ta klasa odkrycia, dla ktorej kanarek istnieje. Odwracalnosc przez sidecar .sql WYGENEROWANY z pg_get_viewdef(), nie przepisany recznie -- przy 7 KB SQL-a przepisywanie to proszenie sie o cicha literowke. Test odwracalnosci sprawdza kolejnosc CREATE VIEW, ktorej sam generator nie gwarantuje. KANAREK KATALOGOWY -- TABELE_SOFT_DELETE += 5 tabel publikacji, WYJATKI oprozznione (przedmiot zniknal razem z kronika). Test symulacyjny fazy 01 opieral dowod na tym, ze widoki publikacji SA jeszcze winowajcami; faza 02 to uniewaznila, wiec przerobiony na dowod MUTACYJNY: sam wytwarza usterke (zdejmuje filtr) i pokazuje, ze stary rdzen tekstowy ja przepuszcza -- bo w definicji nadal jest slowo deleted_at, tyle ze z FILTER po tabeli *_autor, czyli po DRUGIEJ stronie JOIN-a. Ta wersja zostaje prawdziwa niezaleznie od stanu bazy. KANAREK ORM -- rozdzielony na dwa testy. Relacje autorstwa (faza 01) zostaja zwyklym, ZIELONYM testem. Relacje publikacji dostaja osobny test z xfail(strict=True), bo skan znalazl 10 prawdziwych wyciekow (Count/filter po odwrotnej relacji do publikacji od strony Zrodlo/Charakter_Formalny), a audyt wywolan ORM w imporcie/dedup/PBN plan przypisuje jawnie FAZIE 03. Wrzucenie wszystkiego do jednego xfail-a wylaczyloby takze ochrone wywalczona w fazie 01 -- stad podzial. strict=True jest mechanizmem wymuszajacym: gdy faza 03 to naprawi, test zacznie padac jako XPASS i zmusi do zdjecia markera. Ten sam wzorzec faza 01 zastosowala wobec fazy 02. Triage (10 wyciekow + 4 falszywe trafienia, kazde z uzasadnieniem) w docs/superpowers/reviews/2026-08-07-faza-02-inwentaryzacja-orm.md -- faza 03 nie musi go odtwarzac. Falszywe trafienia biora sie stad, ze nazwy relacji fazy 02 to zwykle slowa (patent, wydawnictwo_ciagle), a nie dystynktywne jak autorzy_set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…snych wnioskow Audyt z poprzedniego commita byl najslabiej zweryfikowana rzecza w tej fazie (triage z podgladu, nie z dowodow). Self-review obalil trzy jego elementy: 1. PREMISA FALSZYWA. Dokument twierdzil, ze zapytania startujace OD publikacji sa bezpieczne, bo objects to BppSoftDeleteManager. Sprawdzone empirycznie: Wydawnictwo_Ciagle i Wydawnictwo_Zwarte maja wciaz menedzera z fazy 01 i WIDZA skasowane rekordy, bo Task 4 (przeplecenie menedzerow) nie jest jeszcze zrobiony. 2. LISTA NIEKOMPLETNA. Audyt uzyl pieciu nazw modeli, pomijajac "rekord" (FK z modeli-dzieci: streszczenia, dodatkowe tytuly, zewnetrzne bazy) i "wydawnictwo_nadrzedne". Po ich dolozeniu skan daje 120 znalezisk zamiast 14, wiec zdanie "lista gotowa, faza 03 nie musi jej odtwarzac" bylo nieuprawnione. 3. ...ale te 120 to w wiekszosci FALSZYWE trafienia -- i to jest wniosek o NARZEDZIU, nie o kodzie. Nazwa "rekord" oznacza raz publikacje, raz widok Rekord (juz przefiltrowany przez 0497), a raz parametr GET albo klucz formularza. Kanarek ORM dopasowuje NAZWY, nie modele: w fazie 01 dzialal swietnie, bo nazwy byly dystynktywne (autorzy_set), w fazie 02 to zwykle slowa i precyzja metody sie zalamuje. Uczciwe narzedzie musialoby rozwiazywac sciezke lookupu wobec _meta -- tak, jak kanarek katalogowy robi to na pg_depend. To osobne narzedzie, nie parametr. Dodatkowo poprawiony blad w tabeli: wpis o usun_zrodla_bez_publikacji mial naglowek "KASUJE ZRODLA", sugerujacy utrate danych, podczas gdy kierunek jest odwrotny i lagodny -- zrodlo z publikacjami w koszu NIE zostanie skasowane (zalegajace smieci, nie utrata danych). Podzial faza 02 = warstwa bazodanowa / faza 03 = wywolania ORM zostaje, ale faza 03 nie powinna traktowac tabelki jako gotowej listy zadan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…woty Wynik self-review audytu martwoty rodziny kronika (na prosbe wlasciciela). BLAD W MIGRACJI. Uzylem golego DROP VIEW bez CASCADE -- swiadomie, zeby dostac glosny blad, gdyby cos spoza rodziny na tym stalo. Ale przegapilem druga, NIEZALEZNA os: brak IF EXISTS. Na bazie, gdzie ktos juz kiedys te pietnastoletnie widoki recznie sprzatnal, migracja twardo padala. IF EXISTS nie oslabia glosnosci -- przy istniejacej zaleznosci DROP dalej rzuca blad -- wiec daje idempotentnosc za darmo. LUKA METODY. Audyt przeszukiwal wylacznie repo aplikacji, a to sa obiekty BAZODANOWE: konsument nie musi tam mieszkac. Sprawdzone repozytoria siostrzane: bpp-mcp i bpp-skills czyste; bpp-deploy odwoluje sie do kroniki w trzech miejscach, ale ZADNE nie jest funkcjonalnym konsumentem (dwa komentarze + jedna kontrolka diagnostyczna). sed w migracji kolacji celuje we wzorzec COLLATE, a nie w nazwy widokow, wiec skrypty nie przestaja dzialac. Odnotowane w docstringu jako nieblokujace TODO dla bpp-deploy: pg-collation-migrate-3-load.sh drukuje po loadzie "kronika views: N" jako sanity-check, wiec po tej migracji wypisze 0 i operator moze odczytac to jako nieudany load. DOMKNIECIE DOWODU. flexible_reports trzyma definicje raportow jako wiersze w bazie produkcyjnej, wiec z repo nie dalo sie ich sprawdzic -- to byl jedyny element dowodu oparty na zalozeniu, nie na fakcie. Potwierdzone przez wlasciciela systemu: zaden raport nie odpytuje tych widokow. Wlasciciel potwierdzil takze, ze kod generujacy kronike zostal skasowany. Dowod stoi teraz na czterech niezaleznych przeslankach: pg_depend (jedyne zaleznosci wewnatrz rodziny -- obejmuje obiekty, ktorych nazwa nie zawiera slowa "kronika", czego grep by nie zobaczyl), grep po repo, przeglad repo siostrzanych, potwierdzenie wlasciciela. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…o-easy-audit TASK 4. Wydawnictwo_Ciagle_Manager i Wydawnictwo_Zwarte_Manager dziedzicza teraz po BppSoftDeleteManager zamiast po golym models.Manager. Do tej pory menedzer zadeklarowany w ciele klasy przeslanial ten wniesiony przez BppPublikacjaSoftDeleteMixin, wiec objects na DWOCH najwazniejszych modelach pokazywalo kosz -- podczas gdy Patent/Praca_Doktorska/Praca_Habilitacyjna mialy filtr "z urodzenia". Asymetria byla niewidoczna, bo wszystkie dotychczasowe testy fazy 02 szly przez widoki albo surowy SQL. Mutacja obalila moje wlasne uzasadnienie: twierdzilem w docstringu, ze KOLEJNOSC baz jest nosna, bo przy odwroceniu get_queryset() wzielby sie z mixinu oplat. Nieprawda -- ManagerModeliZOplataZaPublikacjeMixin NIE jest menedzerem, tylko czystym mixinem z jedna metoda, wiec nie wnosi wlasnego get_queryset i nie ma o co konkurowac w MRO. Odwrocenie kolejnosci nie zmienia zachowania (sprawdzone). Nosna jest DRUGA BAZA. Docstring poprawiony. SHIM NA django-easy-audit. Task 4 odslonil OSMY mechanizm zakladajacy, ze skasowany wiersz znika (handoff wymienial siedem z fazy 01) -- tym razem w pakiecie zewnetrznym. easyaudit pobiera poprzednia wersje wiersza przez sender.objects, wiec na modelu soft-delete restore() leci DoesNotExist (wiersz jest wtedy jeszcze w koszu). BPP ma PROPAGATE_EXCEPTIONS=True, wiec wyjatek wywraca cala operacje. To NIE jest blad wprowadzony przez faze 02. Zgloszenie_Publikacji jest SoftDeleteModel od dawna i figuruje w REGISTERED_CLASSES, wiec na dev NIE DA SIE dzis przywrocic skasowanego zgloszenia -- zweryfikowane. Faza 02 rozszerza zasieg z jednego modelu na szesc. Through-modele *_Autor byly nietkniete, bo nie ma ich w REGISTERED_CLASSES. Poprawne jest _base_manager: Django dokumentuje go jako menedzera zwracajacego WSZYSTKIE obiekty i sam tworzy dla niego zwykly Manager, gdy model nie ustawia base_manager_name. Zweryfikowane -- _base_manager widzi kosz. Przy okazji koryguje moja wczesniejsza teze, ze _base_manager jest przefiltrowany i ze przejscie po FK do skasowanej publikacji tez by padlo: nie padnie, i base_manager_name NIE jest potrzebne. Shim zamiast forka, bo wadliwe wywolanie jest w calym pakiecie DOKLADNIE JEDNO, a sygnaly sa podpinane z dispatch_uid -- mozna czysto podmienic sam handler. Podmiana dziala niezaleznie od kolejnosci ready(): Signal.connect ignoruje duplikat dispatch_uid, wiec gdy nasz ready() biegnie pierwszy (tak jest dzis), pozniejszy connect easyauditu jest pomijany. Upstream zna to jako issue #175 (otwarte od 2021-02). Cztery PR-y w szesc lat (#168, #176, #318, #342), zaden nie scalony, mimo ze projekt jest aktywnie wydawany. Z watku przy #168 wynika dlaczego: maintainer odsyla do obejscia przez CRUD_DIFFERENCE_CALLBACKS, ktore NIE DZIALA, bo wyjatek leci zanim callbacki zostana sprawdzone. Test test_upstream_nadal_ma_blad_czyli_shim_jest_potrzebny to STRAZNIK ODWROTNY: pada, gdy upstream naprawi blad, i kaze skasowac shim zamiast "naprawiac test". Test audytu wymagal zdjecia DWOCH bramek niezwiazanych z shimem (brak requestu z userem -> callback BPP odrzuca zdarzenie; brak settings.TEST -> easyaudit odklada zapis na on_commit, ktory w tescie nigdy nie odpala). Obie dawaly "zero zdarzen", czyli objaw nieodrozgnialny od zepsutego shimu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…udit Do wklejenia jako komentarz do issue #175 (nie nowe issue -- byloby piatym watkiem o tej samej sprawie). Nowa przeslanka wobec czterech poprzednich prob (#168, #176, #318, #342): obejscie sugerowane przez maintainera przez CRUD_DIFFERENCE_CALLBACKS jest NIEOSIAGALNE, bo wyjatek leci w linii 102, a callbacki sa wolane dopiero w 113. Zadne z poprzednich zgloszen tego nie stwierdzilo -- a to prawdopodobnie tlumaczy, dlaczego poprawka od szesciu lat nie wchodzi. Zawiera minimalna reprodukcje, uzasadnienie _base_manager zamiast _default_manager (ten drugi tez moze filtrowac) oraz obserwacje, ze przy braku Meta.base_manager_name Django samo tworzy nieprzefiltrowany Manager, wiec zmiana jest no-opem dla projektow, ktore niczego nie nadpisuja. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…cja fazy (5+6)
TASK 2c -- ZERO ZMIAN W KODZIE, i to jest wynik, nie unik.
Inwentaryzacja: w calym kodzie produkcyjnym sa DOKLADNIE DWIE zaleznosci
@depend_on_related celujace w model publikacji -- obie ("self",
"wydawnictwo_nadrzedne") na Wydawnictwo_Zwarte, obie BEZ only=. Brak only=
znaczy, ze denorm buduje bramke WHEN ze WSZYSTKICH kolumn, wiec deleted_at
wchodzi do niej automatycznie. Potwierdzone niezaleznie i przypadkiem: to
wlasnie ta bramka wywalala testy odwracalnosci migracji na
CREATE TRIGGER ... WHEN (OLD."deleted_at" ...) dla bpp_patent.
Dokladanie denorm_always_only albo list only= byloby wiec martwym kodem.
Zostaje test semantyczny odpowiadajacy na pytanie z planu ("czy soft-delete
ksiazki-matki ma uniewazniac denorm-cache rozdzialow"): NIE, bo soft-delete
nie zmienia tytulu matki -- nie ma czego uniewazniac. Test pilnuje, ze
rozdzial zostaje zywy i jego cache sie NIE zmienia.
TASK 5 -- testy integracyjne: restore podnosi wylacznie autorstwa skasowane
tym samym transaction_id (te skasowane wczesniej, osobna decyzja, zostaja
w koszu), post_soft_delete jest emitowany, kaskada nie rusza *_Streszczenie,
gate na bulk update(deleted_at=) rzuca na wszystkich 5 modelach.
TASK 6 -- weryfikacja fazy ujawnila REGRESJE, ktorej kanarek ORM nie mial
szans zlapac. test_rok_habilitacji_view padal, bo widok robi
`autor.praca_habilitacyjna` -- odwrotne OneToOne. Django rozwiazuje je przez
ReverseOneToOneDescriptor, ktory pyta _base_manager, z definicji
NIEprzefiltrowany. Skasowana habilitacja byla tam nadal osiagalna i widok
zwracal 200 zamiast 404.
To odwrotna strona faktu, ktory przy easyaudicie wygladal na dobra wiadomosc:
nieprzefiltrowany _base_manager ratuje audyt i psuje trawersowanie relacji.
Kanarek ORM skanuje ARGUMENTY wywolan (filter/annotate/Count), a tu nie ma
zadnego wywolania -- jest dostep do atrybutu. Zadne rozszerzanie listy
RELACJE tego nie zmieni; to inna os problemu.
Naprawione punktowo w widoku (jawny check deleted_at) -- centralnie sie nie
da, bo Meta.base_manager_name wskazujacy menedzer filtrujacy jest przez
Django jawnie odradzany. Klasa wycieku dopisana do dokumentu dla fazy 03.
Drugi skutek wyszedl warstwe glebiej: soft-skasowana habilitacja NADAL trzyma
referencje O2O PROTECT do autora, wiec autora nie da sie skasowac
(ProtectedError). To nie jest regresja -- rekord istnieje i nie wolno go
osierocic. Test aktualizowany (hard_delete przed kasowaniem autora), bo
sprawdza sciezki 404 widoku, a nie semantyke kasowania.
WERYFIKACJA: 306 passed / 1 xfailed (swiadomy dlug fazy 03) w suitach
soft-delete + cache + rozbieznosci; 416 passed w regresji publikacji
(-k "wydawnictwo or patent or doktor or habilit"); makemigrations --check
czysty dla bpp i rozbieznosci_dyscyplin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
TASK 3 (slug) -- SWIADOMIE NIEWYKONANY, decyzja wlasciciela. slug zostaje unique=True, bezwarunkowo. Zadanie z planu polegalo na OSLABIENIU ograniczenia (unikalnosc tylko wsrod zywych), a jego przeslanka nie zachodzi: get_slug() sklada slug jako tytul-zrodlo-autorzy-<content_type_pk>-<pk>, wiec zawiera klucz glowny. Dwa rozne wiersze nie moga miec tego samego slugu, a skasowany rekord nie zablokuje nowego -- nowy dostaje nowe pk. ID jest w slugu CELOWO, wlasnie po to, zeby slug byl unikalny. Zaznaczone w trzech miejscach planu (box przy tasku, tabela kolejnosci, Definition of Done), zeby nikt nie "dokonczyl" tego w dobrej wierze -- tresc oryginalna schowana w <details> jako kontekst historyczny. Wyszlo przy pisaniu testow TDD: padly wszystkie dziesiec, w tym ten, ktory powinien przechodzic JESZCZE PRZED zmiana. Denorm dodatkowo nadpisuje recznie ustawiony slug, wiec kolizji nie da sie nawet wywolac sztucznie. Tabela kolejnosci uzupelniona takze o Task 2d, ktorego w planie nie bylo (6 widokow agregujacych -- plan mial dla nich tylko ostrzezenie "sprawdz"). HANDOFF dla fazy 03. Najwazniejsze, co niesie: - lista mechanizmow zakladajacych, ze skasowany wiersz znika, urosla z 7 (faza 01) do 9 -- oba nowe spoza kodu, ktory zmienialismy: django-easy-audit i odwrotne OneToOne przez _base_manager; - OSTRZEZENIE, ze tabelka 10 wyciekow ORM NIE jest gotowa lista zadan. Kanarek dopasowuje NAZWY, nie modele; po dolozeniu "rekord" daje 120 znalezisk, w wiekszosci falszywych. Faza 03 ma zbudowac narzedzie model-aware (pytajace _meta), a nie triazowac 120 pozycji recznie; - klasa wycieku, ktorej kanarek strukturalnie NIE MOZE zlapac: dostep atrybutowy do relacji; - cztery rzeczy obalone mutacyjnie w fazie 02, w tym dwa moje wlasne testy i jeden wiarygodnie brzmiacy docstring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Moje wczesniejsze przebiegi byly PODZBIORAMI (soft-delete + cache + regresja publikacji) i nie dotykaly zadnego z tych miejsc. Pelna suita znalazla 7 failed + 2 errors. Osobno warte odnotowania: `make tests-without-playwright` zwrocilo EXIT 0 mimo tych porazek -- kod wyjscia nie jest tu dowodem, trzeba czytac podsumowanie pytest. 1. ARTEFAKTY DjangoQL (3 testy). Faza 02 dodala deleted_at do 5 modeli publikacji, wiec zapisane artefakty schematu dla LLM sie rozjechaly. Zregenerowane komenda, ktora podaje sam test. 2. pbn_api, DWA e2e testy migracji (2 errors, w TEARDOWNIE). Ta sama przyczyna, co naprawiona wczesniej fixtura: denorm przebudowuje triggery w post_migrate z AKTUALNYCH modeli. Te testy cofaja swoja aplikacje MigrationExecutor-em, a poniewaz migracje bpp zaleza od pbn_api, Django cofa razem z nimi takze 0496 -- i CREATE TRIGGER pada na nieistniejacej kolumnie deleted_at. Fixtura bez_reinstalacji_denorma przeniesiona z bpp/tests/test_soft_delete/conftest.py do GLOBALNEGO src/conftest.py, bo potrzebuja jej dwie rozne rodziny testow w roznych aplikacjach. 3. PBN_Export_Queue.check_if_record_still_exists (1 test). Uzywa ContentType.get_object_for_this_type(), ktore pyta _base_manager -- z definicji NIEfiltrujacy. Soft-skasowana publikacja byla tam nadal znajdowana, wiec kolejka wysylalaby do PBN rekord usuniety przez operatora. To TRZECIE wystapienie tej samej klasy bledu w tej fazie (po easyaudicie i odwrotnym OneToOne) -- _base_manager omija soft-delete wszedzie, gdzie ktos siega po niego wprost. 4. KOMENDY CZYSZCZACE (3 testy): wyczysc_publikacje_importu i cleanup_demo_data. QuerySet.delete() na modelu soft-delete jest MIEKKIE, wiec "wyczyszczone" publikacje zostawaly w bazie razem z dziecmi. Gorzej: trzymaly dalej FK z PROTECT (Wydawnictwo_Zwarte.wydawca), wiec kasowanie slownikow dalej w manifescie wywalalo sie na ProtectedError. Obie komendy dostaly hard_delete() ORAZ global_objects -- komenda czyszczaca musi widziec takze kosz, inaczej zostawia publikacje, ktore operator juz uznal za usuniete, a ktore przy kolejnym imporcie koliduja jako niewidoczne duplikaty. getattr zamiast isinstance, bo listy modeli przychodza z manifestu/konfiguracji i mieszaja soft-delete z reszta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Faza 02 soft-delete: 5 modeli publikacji (
Wydawnictwo_Ciagle,Wydawnictwo_Zwarte,Patent,Praca_Doktorska,Praca_Habilitacyjna).Stackowany na #312 (baza:
feat/soft-delete) — nie scalać przed nim.Zakres
bpp/0496deleted_at/restored_at/transaction_id+ 5 indeksów częściowych (WHERE deleted_at IS NOT NULL)bpp/0497deleted_atw 7 widokach, gałąź kasująca w 5 funkcjach refresh, regeneracja bramkiWHENbpp/0498bpp_nowe_sumy_*przestaje punktować skasowane publikacjebpp/0499bpp_kronika_*(odwracalne, sidecar.sql)rozbieznosci_dyscyplin/0023Do tego: mixin z wąską kaskadą na
*_Autorpod wspólnymtransaction_id(bez refleksyjnej kaskady pakietu, żeby nie ruszyć*_Streszczenie), przeplecenie menedżerówWydawnictwo_*z filtrem soft-delete, rozszerzenie obu kanarków.Rzeczy, na które warto spojrzeć w recenzji
1. Zadanie, którego nie było w planie. Inwentaryzacja kanarka na starcie fazy (19 s) dała 15 widoków w 5 kategoriach — w tym 6 widoków agregujących, dla których plan miał tylko ostrzeżenie „sprawdź, czy wymagają". Wymagały wszystkie. →
0498+rozbieznosci/0023.2. Pułapka agregatu — sprawdzona, nie założona. W
0497filtrujemy tabelę publikacji, czyli lewą stronęLEFT JOIN-a; warunek po prawej zdegenerowałby go doINNER JOINi publikacja bez żywych autorów zniknęłaby z serwisu. Pilnuje tegotest_publikacja_bez_zywych_autorow_ZOSTAJE. W0498pułapka nie występuje — te widoki nie mają ani jednegoLEFT JOIN-a aniGROUP BY.3.
_base_manageromija soft-delete — trzy niezależne wystąpienia.django-easy-audit(sender.objectswpre_save) →restore()leciałDoesNotExist. Błąd zastany:Zgloszenie_Publikacjibyło nieprzywracalne nadevjuż przed tą fazą. Naprawione shimem (src/bpp/easyaudit_shim.py), zgłoszenie upstream przygotowane.OneToOne(autor.praca_habilitacyjna) → widok zwracał 200 zamiast 404.ContentType.get_object_for_this_type()→ kolejka PBN wysyłałaby usunięty rekord.4. Task 3 (
slug) świadomie NIEwykonany.get_slug()wkleja do slugupk, więc kolizja jest konstrukcyjnie niemożliwa; warunkowanie ograniczenia tylko osłabiłoby gwarancję. Udokumentowane w trzech miejscach planu.5. Dług przekazany fazie 03 z mechanizmem wymuszającym. Kanarek ORM rozdzielony na dwa testy: relacje autorstwa zostają zielone, relacje publikacji dostają
xfail(strict=True). Gdy faza 03 naprawi wycieki, test zacznie padać jako XPASS i wymusi zdjęcie markera. Analogicznietest_upstream_nadal_ma_blad_czyli_shim_jest_potrzebnykaże skasować shim, gdy upstream scali poprawkę.rekorddaje 120 znalezisk, w większości fałszywych.Weryfikacja
make tests-without-playwright)makemigrations --checkczysty dlabppirozbieznosci_dyscyplinpre-commitzielony na wszystkich 37 plikach fazy0497i0499mają testy odwracalnościmake tests-without-playwrightzwraca EXIT 0 mimo porażek — pierwszy pełny przebieg miał 7 failed + 2 errors przy zerowym kodzie wyjścia. Czytać podsumowanie pytest, nie kod wyjścia.Dokumenty
docs/superpowers/HANDOFF-soft-delete-faza-03.md— wejście dla następnej fazydocs/superpowers/reviews/2026-08-07-faza-02-inwentaryzacja-widokow.mddocs/superpowers/reviews/2026-08-07-faza-02-inwentaryzacja-orm.md(z self-review)docs/superpowers/reviews/2026-08-07-easyaudit-upstream-zgloszenie.md🤖 Generated with Claude Code
https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh