Skip to content

feat(soft-delete): faza 02 — publikacje (5 modeli, widoki, kaskada, kanarki) - #741

Merged
mpasternak merged 13 commits into
feat/soft-deletefrom
feat/soft-delete-02
Aug 7, 2026
Merged

feat(soft-delete): faza 02 — publikacje (5 modeli, widoki, kaskada, kanarki)#741
mpasternak merged 13 commits into
feat/soft-deletefrom
feat/soft-delete-02

Conversation

@mpasternak

Copy link
Copy Markdown
Member

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

Migracja Co robi
bpp/0496 pola deleted_at/restored_at/transaction_id + 5 indeksów częściowych (WHERE deleted_at IS NOT NULL)
bpp/0497 filtr deleted_at w 7 widokach, gałąź kasująca w 5 funkcjach refresh, regeneracja bramki WHEN
bpp/0498 5 widoków bpp_nowe_sumy_* przestaje punktować skasowane publikacje
bpp/0499 kasuje martwą rodzinę 7 widoków bpp_kronika_* (odwracalne, sidecar .sql)
rozbieznosci_dyscyplin/0023 raport rozbieżności pomija skasowane publikacje

Do tego: mixin z wąską kaskadą na *_Autor pod wspólnym transaction_id (bez refleksyjnej kaskady pakietu, żeby nie ruszyć *_Streszczenie), przeplecenie menedżerów Wydawnictwo_* 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 0497 filtrujemy tabelę publikacji, czyli lewą stronę LEFT JOIN-a; warunek po prawej zdegenerowałby go do INNER JOIN i publikacja bez żywych autorów zniknęłaby z serwisu. Pilnuje tego test_publikacja_bez_zywych_autorow_ZOSTAJE. W 0498 pułapka nie występuje — te widoki nie mają ani jednego LEFT JOIN-a ani GROUP BY.

3. _base_manager omija soft-delete — trzy niezależne wystąpienia.

  • django-easy-audit (sender.objects w pre_save) → restore() leciał DoesNotExist. Błąd zastany: Zgloszenie_Publikacji było nieprzywracalne na dev już przed tą fazą. Naprawione shimem (src/bpp/easyaudit_shim.py), zgłoszenie upstream przygotowane.
  • odwrotne 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 slugu pk, 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. Analogicznie test_upstream_nadal_ma_blad_czyli_shim_jest_potrzebny każe skasować shim, gdy upstream scali poprawkę.

⚠️ Tabelka 10 wycieków ORM w dokumencie inwentaryzacji NIE jest gotową listą zadań — patrz self-review w tym samym pliku. Kanarek dopasowuje nazwy, nie modele; po dołożeniu rekord daje 120 znalezisk, w większości fałszywych.

Weryfikacja

  • 9496 passed, 0 failed, 0 errors, 2 xfailed (make tests-without-playwright)
  • makemigrations --check czysty dla bpp i rozbieznosci_dyscyplin
  • pre-commit zielony na wszystkich 37 plikach fazy
  • migracje 0497 i 0499 mają testy odwracalności

⚠️ make tests-without-playwright zwraca 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 fazy
  • docs/superpowers/reviews/2026-08-07-faza-02-inwentaryzacja-widokow.md
  • docs/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

mpasternak and others added 13 commits August 7, 2026 22:59
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
@mpasternak
mpasternak merged commit 2e3b386 into feat/soft-delete Aug 7, 2026
1 check passed
@mpasternak
mpasternak deleted the feat/soft-delete-02 branch August 7, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant