feat(soft-delete): faza 01 — soft-delete autorstw + spójność cache - #312
Draft
mpasternak wants to merge 79 commits into
Draft
feat(soft-delete): faza 01 — soft-delete autorstw + spójność cache#312mpasternak wants to merge 79 commits into
mpasternak wants to merge 79 commits into
Conversation
…ji (ODŁOŻONE) Analiza wprowadzenia soft-delete dla Wydawnictwo_Ciagle/Zwarte, Praca_Doktorska, Praca_Habilitacyjna, Patent. Status: świadomie odłożone — to spec/rozpoznanie, nie zlecenie implementacji. Kluczowe ustalenia: - choke-point w triggerze bpp_refresh_cache(): "deleted_at IS NOT NULL" traktowany jak DELETE → wszystko czytające przez Rekord/Cache_* czyści się jednym ruchem, - django-soft-delete już w repo (pyproject.toml), precedens w zglos_publikacje; domyślny manager ukrywa usunięte → kat. A czysta za darmo, kat. B (import/dedup/PBN) musi przejść na global_objects, - kaskada/auto-undelete pakietu zweryfikowana w kodzie: strict=True wymaga by dzieci były SoftDeleteModel → rekomendacja Projekt A (override delete(), dzieci nietknięte, cache/trigger robi resztę), - slug unique → warunkowy UniqueConstraint(deleted_at__isnull=True), - szacunek ~2-3 tygodnie; otwarte decyzje spisane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zatwierdzony design (brainstorming 2026-06-04) rozszerzający feasibility-spec o: soft-delete autora (PROTECT z pracami / soft-delete husku bez prac), wycofanie oświadczeń z PBN przez rozszerzenie pbn_export_queue (operacja WYCOFANIE, async+retry), dedykowany SoftDeleteLog zasilany sygnałami pakietu, oraz admin superuser-only (kosz/filtr/przywróć/usuń-trwale). Kluczowe decyzje: - asymetria: pełny SoftDeleteModel dla 5 publikacji (Projekt A, trigger jako choke-point), ale autor soft-delete TYLKO bez prac → through-modele/doktorat /habilitacja NIE stają się soft-delete (mały blast radius), - flip FK autor CASCADE→PROTECT + guard w soft delete() (PROTECT nie łapie UPDATE-owego soft-delete), - synergia z deduplikator_autorow: husk po merge staje się odwracalny, - PBN: wycofanie oświadczeń instytucji (delete_all_publication_statements), obiektu publikacji nie kasujemy; restore → re-WYSYLKA, - retencja: brak auto-czyszczenia, tylko ręczny hard-delete. Stary 2026-06-03-soft-delete-publikacje.md oznaczony jako ZASTĄPIONY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… trigger Aktualizacja designu po decyzji użytkownika: - 3 through-modele (Wydawnictwo_Ciagle/Zwarte_Autor, Patent_Autor) stają się SoftDeleteModel jako cel WĄSKIEJ kaskady z soft-delete publikacji (wspólny transaction_id), nie pełnego refleksyjnego Projektu B — pozostałe dzieci (*_Streszczenie itd.) nietknięte, kaskada niewirusowa, - powód: 90 bezpośrednich zapytań *_Autor.objects (głównie ewaluacja_ optymalizacja) staje się poprawnych z domyślnego menedżera — eliminuje ryzyko silent-leak skasowanej pracy do ewaluacji, - trigger UPROSZCZONY: wszystkie 8 tabel pod triggerem mają własne deleted_at → reguła "deleted_at IS NOT NULL → DELETE" jednolita, BEZ JOIN do rodzica; widoki źródłowe filtrują po własnej kolumnie, - guard autora MUSI liczyć przez global_objects (kaskadowo-skasowane autorstwa są ukryte przed default objects) — inaczej autor z pracami tylko-w-koszu przeszedłby przez guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-review wykrył 3 luki materialne + drobiazgi: - A2: filtr deleted_at w widokach źródłowych ustawiony jako mechanizm #1 (pokrywa odczyt z bpp_rekord, verify_cache, re-insert); trigger-skip zdegradowany do opcjonalnej optymalizacji — sam nie wystarcza, - A3: doprecyzowana semantyka SentData przy WYCOFANIE (submitted_successfully =False + znacznik, bez kasowania wiersza), - A1/§2.6: dodana pominięta self-referencja Wydawnictwo_Zwarte (rozdziały → książka-matka) z proponowanym defaultem (brak kaskady + ostrzeżenie) do potwierdzenia; + nota o GenericForeignKey (soft-delete bezpieczniejszy), - B1: doprecyzowany Cel (powiązania *_Autor soft-deletowane, nie "aktywne"), - B2: kolejność migracji w fazie 1 (deleted_at column przed trigger/widok), - §10: dopisane decyzje #9/#10 + sekcja "oczekuje potwierdzenia". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…autora) Decyzja A1: soft-delete książki-matki zablokowany, jeśli ma rozdziały. Dwuwarstwowy wzorzec identyczny z guardem autora: flip FK wydawnictwo_nadrzedne CASCADE→PROTECT + guard w soft delete() liczący rozdziały przez global_objects. Eliminuje problem "rozdziały wskazujące na skasowaną książkę" u źródła. Zaktualizowane §2.6, §8 (faza 4 = guardy PROTECT), §10 (decyzja #11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan-indeks (00) z wpiętymi wspólnymi kontraktami + 8 planów fazowych w formacie superpowers:writing-plans (bite-sized TDD, bez placeholderów): 01 *_Autor SoftDeleteModel + widoki/trigger + spójność cache 02 publikacje SoftDeleteModel + wąska kaskada na *_Autor + slug + menedżery 03 audyt kategorii B (global_objects, hard_delete w pbn_import) 04 guardy PROTECT (autor + książka-matka) + flip FK 05 PBN wycofanie przez pbn_export_queue (operacja WYCOFANIE) + restore WYSYLKA 06 SoftDeleteLog + receivery sygnałów + atrybucja usera (thread-local) 07 admin superuser-only (kosz/filtr/przywróć/usuń-trwale/powód) 08 testy regresji E2E Plany rozpisane przez równoległych agentów z wglądem w realny kod. Wykryte rozbieżności spec↔kod do naniesienia osobnym commitem. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Weryfikacja w realnym kodzie (przez agentów rozpisujących fazy) ujawniła: - C1: aktualny trigger to 0399_fix_refresh_cache_upsert.sql (upsert ON CONFLICT + advisory locks, DELETE-przed-upsertem bezwarunkowy), nie baseline 0001; + nota o utajonym bugu (string in lista-krotek) do poprawy w fazie 01, - C2: verify_cache.py to martwy stub (NotImplementedError + hardcoded host) — spójność weryfikujemy przez Rekord.objects.full_refresh(), nie verify_cache, - C3: slug to pole @denormalized (django-denorm-iplweb), nie proste unique=True — zdjąć unique z kwargs denorm + UniqueConstraint w Meced konkretnych klas, - C5: PBN_Export_Queue.zamowil jest NOT NULL → konto techniczne dla zakolejkowań systemowych (nie nullable), - reconcyliacja API thread-local: kanoniczne soft_delete_context(user,reason) + get_soft_delete_user/reason (faza 06 tworzy, 07 używa). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Funkcja bpp_refresh_cache optymalizowana w osobnej gałęzi (prace użytkownika). Faza 01 = BLOKER do czasu wskazania tej gałęzi i aktualizacji feat/soft-delete. Ortogonalność: faza 01 dotyka widoków źródłowych (filtr deleted_at), optymalizacja dotyka funkcji triggera. Zapisany inwariant do weryfikacji: bezwarunkowy DELETE przed re-insertem/upsertem (na nim wisi wystarczalność filtra widoku). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decyzja użytkownika: bpp_refresh_cache() optymalizowany osobno; faza 01 zmienia WYŁĄCZNIE widoki źródłowe (filtr deleted_at IS NULL). Trigger-skip i fix utajonego buga z krotkami WYCIĘTE z fazy 01. Box AKTUALIZACJA ZAKRESU na górze planu 01 + jednoznaczna nota w indeksie. Inwariant do weryfikacji po wskazaniu gałęzi optymalizacji: bezwarunkowy DELETE przed re-insertem/upsertem. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec soft-delete opieral sie na dwoch zalozeniach o warstwie cache, ktore przestaly obowiazywac po PR #363 (migracje 0432/0433): 1. ze UPDATE ustawiajacy deleted_at doleci do triggera cache -- NIE doleci, bo bramka WHEN z 0433 zna tylko kolumny zasilajace widok (z pg_depend), a django-soft-delete zapisuje przez save(update_fields=['deleted_at', 'restored_at', 'transaction_id']); 2. ze filtr "deleted_at IS NULL" w widoku zrodlowym WYSTARCZY, bo trigger robi bezwarunkowy DELETE przed upsertem ("inwariant delete-first") -- NIE robi: funkcje refresh z 0432 to czysty INSERT ... SELECT FROM widok ... ON CONFLICT DO UPDATE, wiec odfiltrowanie wiersza ze zrodla daje no-op, a stary wiersz przezywa w _mat. Oba testy sa ZIELONE na obecnym kodzie -- przypinaja stan "soft-delete by nie zadzialal". Faza 01 odwroci ich asercje razem z wprowadzeniem galezi kasujacej w funkcjach refresh i regeneracja bramki WHEN. Kolumne deleted_at i filtr widoku symulujemy DDL-em wewnatrz transakcji testowej (DDL w Postgresie jest transakcyjny). Cale DDL musi isc PRZED utworzeniem rekordu -- ALTER TABLE odmawia, gdy tabela ma zakolejkowane zdarzenia wyzwalaczy z INSERT-a w tej samej transakcji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Spec powstal przed portem bpp_refresh_cache PL/Python -> PL/pgSQL. Weryfikacja na aktualnym dev obalila dwa nosne zalozenia warstwy cache i ujawnila 5 luk. Cache (§2.1 przepisana od zera, decyzja #9 uniewazniona): - funkcja bpp_refresh_cache() NIE ISTNIEJE (DROP w 0432); zastapilo ja 16 funkcji per-tabela + bramka WHEN na UPDATE (0433), - inwariant "bezwarunkowy DELETE przed upsertem", na ktorym wisiala wystarczalnosc filtra widoku, NIE ISTNIEJE -- funkcja refresh to czysty upsert, wiec odfiltrowanie wiersza ze zrodla jest no-opem, - UPDATE ustawiajacy deleted_at nie przechodzi przez bramke WHEN, bo django-soft-delete zapisuje przez save(update_fields=[...]). Zamiast jednego mechanizmu potrzebne sa TRZY, wszystkie obowiazkowe: filtr w widoku + galaz kasujaca w funkcji refresh + regeneracja bramki WHEN. Domkniete decyzje (#12-#16): - #12 full_refresh() to denorm.rebuildall, NIE re-projekcja _mat -- nie nadaje sie do weryfikacji spojnosci (test przechodzilby z falszywych powodow), - #13 unique_together na *_Autor -> warunkowy UniqueConstraint (jak slug), - #14 import trafiajacy w kosz: POMIN + ZARAPORTUJ (nie update, nie restore), - #15 Cache_Punktacja_* kasowane przy soft-delete, przeliczane przy restore -- luka nieobjeta kaskada *_Autor (brak FK do publikacji), - #16 PBN: jeden prymityw, dwa wejscia (kolejka + synchroniczne), bo rekord bywa wysylany bez kolejki. Plany: BLOKER fazy 01 zdjety; nowy Task 3 (widoki+funkcje+bramka) zamiast kopii 0399; nowy Task 2b w fazie 02 (ten sam DDL dla 5 tabel publikacji -- faza 01 dotyka wylacznie sciezki autorstwa); Task 1b w 03, 05.0/05.9 w 05, 6b w 06; przenumerowane migracje (0488+, liste 0487); 90 -> 128 miejsc *_Autor.objects; korekta strict (delete=False, restore=True). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
mpasternak
force-pushed
the
feat/soft-delete
branch
from
August 6, 2026 07:32
33c3707 to
7119a76
Compare
Pre-flight scan SDD wykryl plan-mandated defekt: test_managery_sa_wlasciwych_klas mial asercje prawdziwe zawsze (issubclass(X.__mro__[0], object), isinstance(__qualname__, str)) -- nie weryfikowal niczego. Zastapiony testem sprawdzajacym rzecz, na ktorej naprawde zalezy: czy oba managery zwracaja BppSoftDeleteQuerySet (inaczej gate na update() nie dziala, bo pakietowy QuerySet go nie ma). Plus nota, ze na tym etapie nie wolno iterowac po queryset-cie -- kolumny deleted_at jeszcze nie ma (Task 2). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Adwersaryjny self-review (Fable 5) znalazl 4 blokery; wszystkie zweryfikowane
w kodzie przed naprawa.
B1 (blad wprowadzony przy rewizji 2026-08-06): filtr widokow bpp_*_autorzy
uzywal object_id_raw, ktory w TYCH widokach jest id PUBLIKACJI
(0421_cache_trigger_pk_filter.sql:336), a nie pk wiersza through. Porownywal
wiec id publikacji z id autorstwa. Dwa realne skutki: skasowane autorstwo
zostaje w widoku, a przy zbieznosci numerow wycinane sa autorstwa CUDZEJ
publikacji. Poprawione na NOT EXISTS po (_orig.id)[2].
Dodany test SEMANTYCZNY -- planowane testy (substring "deleted_at" w viewdef,
bramka zna deleted_at) przechodza takze dla blednego klucza, bo pg_depend
widzi kolumne przez podzapytanie niezaleznie od sensu porownania.
B3: BazaModeluOdpowiedzialnosciAutorow ma CZWARTEGO potomka --
Zgloszenie_Publikacji_Autor (zglos_publikacje/models.py:315). Wpiecie
SoftDeleteModel w abstrakt objeloby aplikacje zgloszen (nieplanowana migracja,
podmieniony objects, dryf makemigrations --check). Zamiast tego mixin
BppAutorstwoSoftDeleteMixin wpinany w 3 KONKRETNE modele + test negatywny.
B2 (faza 02): Praca_Doktorska_Baza.autorzy_set to property zwracajaca FakeSet
(podklasa list) z atrapami bez .delete() (praca_doktorska.py:30-70). Kaskada
mixinu wywalilaby sie AttributeError na doktoracie i habilitacji. Dodane
_model_through()/_autorstwa_do_kaskady().
P8: content_type_id nie istnieje na modelach publikacji -- to property na
RekordBase (rekord.py:288). Testy faz 01/02/06/08 padalyby AttributeError
z falszywego powodu. Zamienione na ContentType.objects.get_for_model.
P12: widoki bpp_praca_{doktorska,habilitacyjna}_autorzy dopisane do fazy 02.
D3: "trigger dziala tylko z prawdziwym commitem" to falsz -- kanarki chodza
pod django_db; transactional_db usuniety jako zbedny.
D4: sprzecznosc DROP CASCADE vs CREATE OR REPLACE.
Dlugi dla faz 03-06 (nieaktualny stan pbn_export_queue, rozjechany kontrakt
zakolejkuj_*, konto techniczne zamowil, MetrykaAutora, AutorManager, i in.)
spisane w ledgerze SDD do splaty przed odpowiednimi fazami.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Task 1 fazy 01 wdrożenia soft-delete. Tworzy współdzielony fundament menedżerów dla całego wdrożenia (8 faz): - BppSoftDeleteQuerySet — gate blokujący bulk .update(deleted_at=...) / .update(restored_at=...), bo omijałoby post_save, kaskadę *_Autor, SoftDeleteLog i reversion. - BppSoftDeleteManager — domyślny manager, filtruje deleted_at__isnull=True. - BppGlobalManager — widzi wszystkie obiekty (usunięte i nie). Guard zależności (raise_if_has_protected_children) NIE jest częścią tego taska — dopisze go faza 04. Test test_managery_zwracaja_bpp_queryset użył Element_Repozytorium zamiast Wydawnictwo_Ciagle_Autor z brief-u — patrz task-1-report.md po uzasadnienie (Django resoluje pola przy .filter(), nie dopiero przy SQL). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
… Element_Repozytorium Wykryte przy wykonaniu Task 1 fazy 01. Plan twierdzil, ze budowa queryset-u jest leniwa, wiec isinstance przejdzie na modelu bez kolumny deleted_at. Nieprawda: Django resolwuje nazwy pol juz w Query.build_filter(), przy wywolaniu .filter() -- odroczone jest tylko WYKONANIE zapytania, nie jego walidacja. Test na Wydawnictwo_Ciagle_Autor padalby FieldError, i to nie z powodu wadliwej implementacji. Test korzysta teraz z Element_Repozytorium (repozytorium.py:18) -- trzeciego precedensu SoftDeleteModel w repo, o ktorym spec nie wiedzial. Dopisany do sekcji precedensow razem z ostrzezeniem o Zgloszenie_Publikacji_Autor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…at + indeks Wpiecie BppAutorstwoSoftDeleteMixin (SoftDeleteModel + nasze managery z Task 1) w 3 KONKRETNE through-modele: Wydawnictwo_Ciagle_Autor, Wydawnictwo_Zwarte_Autor, Patent_Autor. Abstrakt BazaModeluOdpowiedzialnosciAutorow celowo NIE ruszony -- ma czwartego potomka (Zgloszenie_Publikacji_Autor) poza zakresem soft-delete. Migracja 0488 dodaje deleted_at/restored_at/transaction_id (9 AddField) + indeks na deleted_at (3 AddIndex) dla 3 tabel. Indeksy zadeklarowane tez w Meta.indexes kazdego konkretnego modelu (nie w abstrakcyjnym mixinie, zeby uniknac kolizji nazw) -- inaczej makemigrations --check chcialby je co chwile usuwac. Dodatkowo: nadpisany restore() w mixinie z domyslnym strict=False. django-softdelete ma asymetryczne defaulty -- delete() domyslnie strict=False, restore() strict=True -- a strict=True wywala SoftDeleteException dla kazdej relacji (rowniez zwyklego forward FK) do modelu spoza SoftDeleteModel. *_Autor ma FK do Autor/Jednostka/ rekordu, ktore nie sa soft-delete w tej fazie, wiec bez tej poprawki kazde .restore() by padalo. Znany, oczekiwany efekt przejsciowy: 4 testy w test_cache/ (delete() na *_Autor nie czysci juz bpp_autorzy_mat/bpp_rekord_mat, bo DB-owe triggery/widoki jeszcze nie znaja deleted_at) -- to domena Task 3 (widoki + galaz kasujaca w funkcjach refresh + bramka WHEN), potwierdzona przez test_cache/test_soft_delete_preconditions.py i task-3-brief.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…instancja filter_rekord() oczekuje obiektu Rekord (pk = krotka (content_type_id, object_id) obslugiwana przez TupleField), nie Wydawnictwo_Ciagle_Autor (pk = zwykly int autoinkrementowany). Test podawal 'aca' (through-row) zamiast juz dostepnego 'r' (Rekord). Przed Taskiem 2 .delete() na *_Autor bylo hard-delete, wiec Django zerowalo aca.pk -> filter(rekord_id=None) -> "IS NULL" -> 0 wierszy -> test przechodzil PRZYPADKIEM, mimo zlego wywolania. Po wpieciu SoftDeleteModel .delete() jest miekkie, aca.pk zostaje liczba -> porownanie integer[] = integer w Postgresie -> UndefinedFunction, zanim tresc bpp_autorzy_mat zaczela miec jakiekolwiek znaczenie. Po poprawce test pada na wlasciwej asercji o liczbie wierszy w cache (2 == 0) -- oczekiwane do czasu Taska 3 (galaz kasujaca w funkcjach refresh + widoki znajace deleted_at), nie na bledzie SQL. Zgloszone przez coordinatora w rundzie poprawek 1/5 do Taska 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Wykryte przy wykonaniu fazy 01 Task 2. SoftDeleteModel.restore() ma domyslnie strict=True i sprawdza KAZDE pole z related_model -- takze zwykle FK w przod -- zanim rozroznii typ relacji. Poniewaz *_Autor ma FK do Autor i Jednostka (nie-soft), gole .restore() rzuca SoftDeleteException i nie da sie przywrocic niczego. Kontrakt: kazdy model soft-delete w BPP nadpisuje restore() ze strict=False. Ostrzezenie dopisane takze do fazy 04 -- Autor ma FK do Tytul itd., wiec trafilby na dokladnie ten sam blad przy przywracaniu husku, i test padlby nie z powodu wadliwego guardu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Recenzja Taska 2 fazy 01 wykryla Critical: BPP ma DWA niezalezne systemy triggerow na tych samych tabelach, oba z bramka po liscie kolumn. 1. nasz cache _mat: <tabela>_cache_upd, bramka z pg_depend (0433) -- Task 3; 2. django-denorm: d_aft_row_upd_on_*, bramka z list only= w depend_on_related (denorm/db/triggers.py:102-118) -- NIE byla objeta zadnym taskiem. Zadna z 15 zaleznosci celujacych w *_Autor nie wymienia deleted_at, wiec UPDATE soft-delete nie odpala triggera denorm, a stary AFTER DELETE nie ma juz czego lapac. Skutek: opis_bibliograficzny_cache, slug i cached_punkty_dyscyplin zostaja nieswieze NA STALE -- publiczna strona pokazuje usunietego autora, a punkty dyscyplin dalej go licza. Task 3b (faza 01) dodaje denorm_always_only na 3 modelach *_Autor, z obowiazkowa weryfikacja wstepna: laczenie listy only z denorm_always_only, w polaczeniu z galezia "brak only -> obserwuj wszystkie pola", ZAWEZILOBY zaleznosc pozbawiona only do samego deleted_at. Zweryfikowane: wszystkie 15 ma jawne only, wiec dodanie jest addytywne -- ale krok kaze to powtorzyc. Task 2c (faza 02) to samo dla 5 modeli publikacji, z pytaniem o self-FK wydawnictwo_nadrzedne (byc moze nieosiagalne przez guard PROTECT z fazy 04). Dodana tez jawna tabela kolejnosci wykonania fazy 02 -- taski dopisywane po rewizjach wyladowaly poza numeracja dokumentu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…/global_objects) Runda poprawek 2/5 do Taska 2 (recenzja koordynatora, Important). PROBLEM: BppAutorstwoSoftDeleteMixin.restore() mial strict=False jako DOMYSLNY parametr, ale django_softdelete wola go z JAWNYM strict=True z poziomu querysetow: - DeletedQuerySet.restore() (deleted_objects) -> obj.restore(strict=True) - GlobalQuerySet.restore() -- global_objects w BPP w ogole nie mial tej metody (BppGlobalManager zwraca BppSoftDeleteQuerySet, ktory dziedziczy z SoftDeleteQuerySet bez restore()) -> AttributeError Efekt: Wydawnictwo_Ciagle_Autor.deleted_objects.restore() i .global_objects.restore() padaly, mimo poprawki na poziomie instancji. ROZWIAZANIE (wariant b z dwoch zaproponowanych przez koordynatora -- czysciejszy architektonicznie): rozszerzenie PINNED kontraktu o BppDeletedQuerySet/BppDeletedManager (nowa czwarta/piata klasa, jawnie udokumentowana w docstringu modulu jako czesc kontraktu) + restore() dopisane do juz-PINNED BppSoftDeleteQuerySet (ta sama klasa, ktora BppGlobalManager juz zwracal dla gate'u update() -- test Taska 1 test_managery_zwracaja_bpp_queryset sprawdza dokladnie to i pozostaje zielony bez zmian). Oba nowe restore() maja strict=False domyslnie, zgodnie z inwariantem zapisanym teraz w docstringu modulu. Skorygowana tez sygnatura instancyjnego restore(): odwzorowuje rodzica (strict, transaction_id, *args, **kwargs) i przekazuje pozycyjnie do super() -- poprzednia wersja (super().restore(*args, strict=strict, **kwargs)) dawala TypeError: got multiple values for argument 'strict' przy wywolaniu pozycyjnym typu obj.restore(False, txid). Dodane testy (8, wszystkie PASS) pokrywajace: - restore() z kazdej z 3 sciezek nie rzuca SoftDeleteException, - wc.autorzy_set.all() faktycznie ukrywa soft-deletowane (reverse FK manager dziedziczy z _default_manager), - deleted_objects jest DeletedManager i zwraca tylko skasowane, - gate .update(deleted_at=...) rzuca RuntimeError na realnym queryset-cie konkretnego modelu (Task 1 testowal to tylko w izolacji klasy). Dodany tez newsfragment (brakowal w commicie feature -- wymog CLAUDE.md). Weryfikacja: uv run pytest src/bpp/tests/test_soft_delete/ src/bpp/tests/test_cache/ -q -> 24 passed w test_soft_delete/ (bylo 16), 97 passed / 4 failed (te same znane, zakres Task 3) / 1 skipped w sumie obu katalogow. uv run ruff check src/bpp/models/soft_delete.py src/bpp/tests/test_soft_delete/test_autor_softdelete_model.py -> czyste. Szerszy sweep (test_models/test_wydawnictwo_autor.py, test_models_legacy.py, test_admin.py) -> 209 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Trzy zmiany w jednej migracji (0489), w wymuszonej kolejnosci: filtr deleted_at w widokach bpp_*_autorzy -> galaz kasujaca w funkcjach bpp_refresh_autor_* -> regeneracja bramki WHEN (pg_depend zna juz deleted_at). Odwrocenie 1<->3 dalo by bramke bez deleted_at, czyli cichy staleness. Zadna z nich nie wystarcza sama: filtr widoku nie sprzata _mat (upsert po 0432 nie kasuje, wiec wypadniecie wiersza ze zrodla jest no-opem), a bez deleted_at w bramce UPDATE soft-delete (save(update_fields=['deleted_at','restored_at','transaction_id'])) w ogole nie dochodzi do funkcji triggera. Wariant filtra: WHERE wewnetrzny (`... FROM <tabela> WHERE <tabela>.deleted_at IS NULL`), nie owijka `SELECT * FROM (orig) _orig WHERE NOT EXISTS (...)` z planu. Plan kazal ZMIERZYC, czy owijka zachowuje plan wykonania -- nie zachowuje: hot path triggera schodzi z Index Scan (7.35) na Nested Loop Anti Join (12.21), a pelny skan z 11.50 na 26.84. Filtr wewnetrzny daje plan identyczny z oryginalem (7.35), a pelny skan przyspiesza do 2.36 (indeks na deleted_at z 0488). Dodatkowo jest odporny z konstrukcji na pulapke klucza opisana w planie: nie porownuje zadnych kluczy, wiec pomylka object_id_raw (id PUBLIKACJI) vs (id)[2] (pk wiersza through) jest tu niemozliwa. Definicje widokow i funkcji generowane z introspekcji (reuzycie generatorow z 0432/0433 przez importlib), nie kopiowane do .sql. backward() odtwarza widoki bez filtra z tekstu 0421 (ostatnia migracja, ktora je definiowala), funkcje przez _p0432._create_through_function, bramke przez ponowne przeliczenie pg_depend. Testy: src/bpp/tests/test_soft_delete/test_views_sql.py -- 3 kontrakty DDL (viewdef / functiondef / triggerdef) plus test SEMANTYCZNY klucza filtra (scenariusz z pk wiersza through rownym id publikacji-pulapki, zeby zly klucz wywalil sie na obu asercjach). test_migracja_0489_rewers.py pilnuje odwracalnosci. Zazielenilo test_cache_pk_filter::test_dodanie_i_usuniecie_autora_odswieza_ autorzy_mat oraz test_cache_trigger_v3::test_usuniecie_jednej_z_dwoch_rol_ autora_zostawia_druga. test_opis_bibliograficzny_dependent i test_wca_delete_cache pozostaja czerwone -- to Task 3b (bramka django-denorm, drugi, niezalezny system triggerow). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Blad kolejnosci w planie, wykryty przy wykonaniu Taska 3 fazy 01. Decyzja #13 (unique_together -> warunkowy UniqueConstraint z condition deleted_at IS NULL) byla zaplanowana w fazie 02, razem ze slugiem. Ale *_Autor staje sie soft-delete juz w fazie 01 (Task 2), wiec unique_together blokuje re-insert od TAMTEJ fazy, nie od nastepnej. Regresja zweryfikowana na realnym kodzie: import_sqlite/tests/test_patent_apply.py::test_apply_idempotent_update -> UniqueViolation na bpp_patent_autor_rekord_id_autor_id_kolejnosc_uniq Zrodlo: import_sqlite/handlers/patent.py:192 robi autorzy_set.all().delete() i wstawia od nowa; po Tasku 2 delete() jest miekki, wiec stary wiersz fizycznie istnieje i constraint go widzi. Wzorzec jest ogolny, nie dotyczy jednego importera -- kazdy przeplyw "skasuj autorstwa i wstaw od nowa" (re-import, admin inline, korekta kolejnosci) uderzy w to samo, a skasowany wiersz jest przy tym niewidoczny dla operatora. Nowy Task 3c w fazie 01 zawiera test odtwarzajacy regresje oraz obowiazkowa weryfikacje adminu: validate_unique() honoruje unique_together, ale POMIJA UniqueConstraint z condition, wiec inline autorstwa moze zaczac zwracac IntegrityError zamiast czytelnego bledu formularza. W fazie 02 odpowiedni task oznaczony jako przeniesiony (tresc zwinieta jako kontekst historyczny). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Task 3b (faza 01): BPP ma dwa niezależne systemy triggerów na tabelach *_Autor. Task 3 naprawił bramkę WHEN naszego cache _mat. Bramka django-denorm (budowana z list only= w @depend_on_related) zostaje ślepa na deleted_at, więc soft-delete autorstwa nie odświeża pól denormalizowanych rodzica (opis_bibliograficzny_cache i pochodne). Dodaje test end-to-end weryfikujący cały mechanizm (nie samą obecność stringu w DDL): soft-delete autorstwa + denorms.flush() musi usunąć nazwisko autora z opis_bibliograficzny_cache rodzica. Test na razie czerwony — implementacja w kolejnym commicie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
BPP ma dwa niezależne systemy triggerów na tabelach *_Autor:
1. cache _mat (nasz) — bramka WHEN naprawiona w migracji 0489 (Task 3).
2. django-denorm — bramka WHEN budowana z list only= w
@depend_on_related. Bez deleted_at w tej liście UPDATE, który tylko
soft-kasuje wiersz *_Autor, nie odpalał triggera denorm — pola
denormalizowane rodzica (opis_bibliograficzny_cache,
opis_bibliograficzny_autorzy_cache,
opis_bibliograficzny_zapisani_autorzy_cache, slug,
cached_punkty_dyscyplin) zostawały nieświeże na stałe.
Weryfikacja pułapki z brief-a: `only = (only or ()) + denorm_always_only`
w połączeniu z `if only: ... else: only = <wszystkie pola>` w
denorm/db/base.py oznacza, że dodanie denorm_always_only do modelu,
którego zależność nie ma only=, zawęziłoby ją z "wszystkie kolumny" do
"tylko deleted_at" — cicha regresja. Sprawdzono ręcznie wszystkie 15
zależności @depend_on_related celujących w *_Autor (5 na każdy z 3
modeli, we wszystkich 3 plikach) — każda ma jawne only=. Dodanie
denorm_always_only jest więc czysto addytywne.
Dodano do Wydawnictwo_Ciagle_Autor, Wydawnictwo_Zwarte_Autor i
Patent_Autor:
denorm_always_only = ("deleted_at",)
Triggery denorm są instalowane automatycznie przez post_migrate
(denorm/apps.py -> denorms.install_triggers(), DROP TRIGGER IF EXISTS
+ CREATE TRIGGER, idempotentne) — żadna nowa migracja Django nie jest
potrzebna. Zweryfikowano na żywej bazie testowej: bramki
d_aft_row_upd_on_bpp_wydawnictwo_ciagle_autor_* zawierają teraz
"deleted_at IS DISTINCT FROM" w klauzuli WHEN.
Zazieleniło dwa ostatnie czerwone testy w test_cache/:
test_opis_bibliograficzny_dependent i test_wca_delete_cache. Cały
src/bpp/tests/test_cache/ oraz src/bpp/tests/test_soft_delete/ zielone
(113 passed, 1 skipped).
makemigrations --check --dry-run: brak zmian w bpp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
Task 3c: od Taska 2 modele *_Autor są soft-delete, wiec .delete() zostawia wiersz fizycznie w bazie. unique_together w Meta wciąż go widzi, przez co wzorzec "skasuj autorstwa i wstaw od nowa" (re-import, korekta kolejności, edycja inline) wywala się na IntegrityError. Regresja jest realna i zweryfikowana: src/import_sqlite/tests/test_patent_apply.py:: test_apply_idempotent_update pada z tym samym błędem. Nowy plik src/bpp/tests/test_soft_delete/test_autor_unique.py odtwarza regresję na Patent_Autor, Wydawnictwo_Ciagle_Autor i Wydawnictwo_Zwarte_Autor oraz dodaje test kontrolny potwierdzający, że kolizja dwóch ŻYWYCH wierszy nadal jest blokowana (nie chodzi o zniesienie unikalności, tylko o uwzględnienie soft-delete). Stan: RED — 4 testy failują (3 nowe + realny test_apply_idempotent_update), naprawa w kolejnym commicie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…_Autor Task 3c. Zamienia unique_together (widzi soft-deleted wiersze) na warunkowe constrainty ograniczone do żywych wierszy (condition=Q(deleted_at__isnull)) w trzech modelach Wydawnictwo_Ciagle_Autor, Wydawnictwo_Zwarte_Autor, Patent_Autor. Migracja 0490. Po drodze wykryto DRUGĄ, niezależną przyczynę tej samej regresji: legacy migracja 0132 dokłada surowym SQL-em dodatkowy UNIQUE (rekord_id, kolejnosc) DEFERRABLE INITIALLY DEFERRED (poza Django ORM, więc niewidoczny dla makemigrations) - też blokuje re-insert po soft-delete, tylko odroczony do commitu/zwolnienia savepointu. UniqueConstraint nie może łączyć condition z deferrable (Django to blokuje), a deferrable jest tu wymagany przez drag&drop reorder autorów w adminie (adminsortable2). Zastąpiony ExclusionConstraint (GiST + btree_gist, rozszerzenie już włączone w bazie od migracji 0056) - jedyny typ ograniczenia w Postgresie łączący WHERE z DEFERRABLE. Migracja 0490 usuwa legacy constraint i dodaje odpowiednik przez ORM. Weryfikacja admina (obowiązkowa wg brief-u): Model.validate_unique() w ogóle nie sprawdza Meta.constraints - to osobny krok Django >=4.1, validate_constraints(), wołany automatycznie przez ModelForm._post_clean(). Ten krok DZIAŁA dla warunkowych constraintów, ale tylko gdy pole użyte w condition (deleted_at) nie jest wykluczone z walidacji - a pole spoza Meta.fields formularza Django automatycznie wyklucza. Skutek bez naprawy: UniqueConstraint cicho pomija walidację (łapie FieldError, kolizja przechodzi formularz), ExclusionConstraint w ogóle nie łapie FieldError (is_valid() wywala się wyjątkiem, HTTP 500, przy KAŻDYM zapisie - nie tylko przy kolizji). Naprawa: deleted_at jest teraz jawnym, ukrytym/wyłączonym polem w generuj_formularz_dla_autorow (zawsze None - formularz operuje tylko na żywych wierszach). Druga, niezależna luka: formset inline (generuj_inline_dla_autorow) nie łapał kolizji NOWEGO wiersza z ISTNIEJĄCYM widocznym w tym samym formsecie - dla nowego wiersza rekord nie jest jeszcze ustawiony w momencie walidacji (przypisuje go dopiero save_new(), po walidacji), więc DB-owy UniqueConstraint.validate() pomija sprawdzenie (NULL != NULL), a formsetowe validate_unique() Django sprawdza pary formularzy tylko dla constraintów bezwarunkowych. Naprawa: ręczne porównanie par formularzy w formset.clean() (_waliduj_kolizje_autorstwa_w_formsecie), bez zapytań do bazy. Testy: - src/bpp/tests/test_admin/test_autor_inline_unique.py - standalone Wydawnictwo_Ciagle_Autor_Admin i inline formset, obie pułapki wyżej. - src/bpp/tests/test_soft_delete/test_migracja_0490_rewers.py - odwracalność migracji (forward/backward/forward), w tym legacy constraintu. Wynik: src/import_sqlite/tests/test_patent_apply.py:: test_apply_idempotent_update - zielony. test_soft_delete/, test_cache/, test_admin/ - bez regresji (1300+ passed). makemigrations --check --dry-run (bpp) - brak zmian. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…+ 4 Minor) Important 1 — utrata pokrycia indeksem na FK rekord. Warunkowe UniqueConstraint/ExclusionConstraint z pierwszej rundy sa CZESCIOWE (WHERE deleted_at IS NULL), wiec db_index=False na FK rekord (uzasadnione, dopoki unique_together dawalo PELNY indeks) przestalo byc uzasadnione - RI-check Postgresa przy DELETE rodzica, kolektor kaskady Django i global_objects/deleted_objects.filter(rekord=...) robilyby seq scan. Usunieto db_index=False z 3 modeli, zregenerowano migracje 0490 (dodane AlterField przywracajace indeks). Nowy test test_soft_delete/test_autor_rekord_index.py sprawdza wprost w pg_indexes. Important 2 — falszywy pozytyw walidatora formsetu. Uzycie formset.deleted_forms w _waliduj_kolizje_autorstwa_w_formsecie bylo bledne: ta property sama zaczyna od "if not self.is_valid(): return []", wiec gdy INNY wiersz formsetu mial blad walidacji pola, wiersze zaznaczone do usuniecia wracaly jako pusta lista i dostawaly falszywy komunikat o kolizji z wierszem, ktory je zastepuje. Naprawa: formset._should_delete_form (f) zamiast f not in formset.deleted_forms - patrzy tylko na dane TEGO wiersza. Nowy test test_inline_falszywy_duplikat_gdy_inny_wiersz_ma_blad, zweryfikowany jako faktycznie lapiacy regresje (failuje na starym kodzie). Minor 3 — walidator formsetu nie pokrywal ExclusionConstraint (rekord, kolejnosc) miedzy ROZNYMI autorami. Uproszczone: gola kolejnosc (bez autora) jest scislejszym nadzbiorem "ten sam autor, ta sama kolejnosc", wiec zastapila oba klucze jednym. Nowy test test_inline_dwaj_rozni_autorzy_ta_sama_kolejnosc. Minor 4 — komentarze w Meta 3 modeli powielaly blad briefu (twierdzily, ze validate_unique() ignoruje UniqueConstraint z condition; w rzeczywistosci validate_unique() w ogole nie patrzy na Meta.constraints, mechanizmem jest osobny validate_constraints(), Django >=4.1). Poprawione na zgodne z tym, co raport Taska 3c juz ustalil. Minor 5 — test re-insertu inline konczyl sie na is_valid(), nie dowodzil naprawy regresji IntegrityError PRZY ZAPISIE. Dodano formset.save() i asercje na stan bazy (1 zywy wiersz, inny pk, stary wiersz w global_objects z deleted_at ustawionym). Minor 6 — RunSQL DROP CONSTRAINT bez IF EXISTS wywalilby migracje na instancji, gdzie constraint z 2018 zdjeto juz kiedys recznie. Dodano IF EXISTS do wszystkich 3 DROP. Udokumentowano w komentarzu na gorze migracji, ze reverse_sql nie przejdzie, jesli w tabeli sa juz soft-deletowane wiersze dublujace (rekord_id, kolejnosc) z zywymi - rollback przestaje dzialac dokladnie wtedy, gdy funkcja soft-delete byla juz uzywana. Migracja 0490 zamendowana in-place (nie wypchnieta do origin, brief-owa zasada "nigdy nie modyfikuj migracji" dotyczy migracji, na ktorych ktos inny moglby juz polegac). Weryfikacja: uv run pytest src/bpp/tests/test_soft_delete/ src/bpp/tests/test_admin/ src/import_sqlite/ -q -> 714 passed. uv run ruff check src/bpp -> 36 bledow, wszystkie przedistniejace w plikach spoza zakresu tego taska (zero w plikach dotknietych). makemigrations --check --dry-run (bpp) -> brak zmian. pre-commit run -> wszystkie hooki Passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…les" PR #312 dotknął export_bibtex.py (filtr deleted_at, e80d165), przez co check "Lint changed files" objął cały plik i ujawnił dwa wcześniejsze, niezwiązane z tą gałęzią długi: - C901: handle() miał złożoność 12 (próg 10). Wydzieliłem dwie metody o jednej odpowiedzialności każda: - _zbierz_publikacje(options) — budowa Q() dla ciagle/zwarte, filtry rok/autor/id/typ/limit, CommandError gdy brak wyników. - _zapisz_wynik(options, bibtex_content) — zapis do pliku (--output) albo na stdout. handle() teraz tylko woła obie metody + generuje BibTeX. Czysto strukturalny refaktor — logika filtra deleted_at (autorzy_set) i cała reszta zachowania bez zmian. - B904: `raise CommandError(...)` w except OSError dostał `from e`, żeby zachować łańcuch wyjątków. Dopisałem też smoke test dla samej komendy (test_export_bibtex_command.py) — wcześniej istniały tylko testy funkcji z bpp.export.bibtex, handle() management commandu nie miał żadnego pokrycia. Zweryfikowano: ruff check/format czyste, 4 nowe testy + 44 istniejące w src/bpp/tests/test_export/ przechodzą. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Uzupelnienie dc45d1c. Prefiksy z jawnym deleted_at byly liczone raz na zycie instancji schematu (flaga _ast_zbadany). Dla apply_search() to bez znaczenia (nowa instancja na kazde zapytanie), ale instancja bywa DLUGOWIECZNA: bpp/multiseek_registry/djangoql_export.py trzyma BppQLSchemaOgraniczony(Rekord) jako modulowy singleton bramki walidacyjnej. Z flaga pierwsze zapytanie po starcie procesu zamrazaloby swoja odpowiedz na pytanie "czy uzytkownik prosil o kosz" dla wszystkich kolejnych. Zamiast flagi licznik glebokosci — validate() rekuruje po sobie, wiec analize robimy na wezle najwyzszym, ale KAZDEGO wywolania. Test regresji: test_dlugowieczna_instancja_schematu_nie_pamieta_prosby_o_kosz uruchamia dwa zapytania na TEJ SAMEJ instancji schematu (przez build_filter, tak jak robi to djangoql.breakdown). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Faza 01 zamknieta (41 commitow, 9 migracji, PR #312 CI 23/23). Dokument przekazania, zeby nastepna sesja nie odtwarzala historii z gita. Zawiera rzeczy, ktorych nie widac w diffie, a ktore w fazie 01 kosztowaly rundy poprawek: - siedem mechanizmow, ktore zakladaly ze skasowany wiersz znika (bramka WHEN cache, funkcje refresh, bramka denorm, unique_together, legacy raw-SQL constraint z 2018, restore(strict=True), widoki pochodne) -- odkrywane pojedynczo, przez awarie; - instrukcje, zeby rozszerzyc kanarek katalogowy NA STARCIE fazy 02, nie na koncu: w tym cala jego wartosc, a faza 01 zrobila to odwrotnie; - pulapke agregatow: warunek na prawej stronie LEFT JOIN degeneruje go do INNER JOIN i publikacja bez zywych autorow ZNIKA z bpp_rekord_mat; poprawnie jest count(...) FILTER (WHERE ...); - 11 faktow o kodzie, ktore byly zrodlem bledow (m.in. .filter() resolwuje pola natychmiast, content_type_id nie istnieje na publikacjach, dwie fixtury to ten sam obiekt, _upsert_sql mapuje kolumny POZYCYJNIE, get_default() usuniete i pilnowane guardem); - czego NIE powtarzac procesowo: rownolegle agenty w jednym worktree (trzy kolizje w tej sesji), REUSE kontenerow dajacy falszywe alarmy, oraz zasade "pusty wynik nie jest potwierdzeniem"; - otwarte decyzje, w tym rekomendacje strategii wydania (scalac fazami, wydac dopiero po fazie 04 -- faza 01 sama nie daje wartosci uzytkownikowi, a faza 03 jest obowiazkowa razem z 02, bo inaczej re-import tworzy duplikaty). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FbCGt7UfRrzUZVdCuU5Xy4
…anarki) (#741) * docs(soft-delete): inwentaryzacja widokow na starcie fazy 02 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 * feat(soft-delete): 5 modeli publikacji -> SoftDeleteModel + waska kaskada 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 * feat(soft-delete): filtr deleted_at w 7 widokach publikacji + galaz kasujaca 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 * test(soft-delete): fixtura odpinajaca reinstalacje denorma w testach 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 * feat(soft-delete): 6 widokow agregujacych pomija skasowane publikacje 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 * feat(soft-delete): kasacja martwej rodziny kronika + rozszerzenie kanarkow 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 * docs(soft-delete): self-review audytu ORM -- trzy zastrzezenia do wlasnych 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 * fix(soft-delete): DROP VIEW IF EXISTS w 0499 + domkniecie dowodu martwoty 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 * feat(soft-delete): przeplecenie menedzerow publikacji + shim na django-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 * docs(soft-delete): gotowy tekst zgloszenia do upstreamu django-easy-audit 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 * feat(soft-delete): bramka denorm (2c) + testy integracyjne i weryfikacja 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 * docs(soft-delete): decyzja o Tasku 3 + handoff przed faza 03 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 * fix(soft-delete): 7 regresji wykrytych dopiero przez PELNA suite 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 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(failing) Test bazowy fazy 03. Rekord_w_bpp matchuje przez Rekord, czyli widok bpp_rekord_mat przefiltrowany po deleted_at juz w fazie 01 -- skasowana publikacja z niego znika, matching zwraca None, a importer tworzy DUPLIKAT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
get_bpp_publication i rekord_w_bpp matchowaly przez Rekord, czyli widok bpp_rekord_mat przefiltrowany po deleted_at juz w fazie 01. Soft-skasowana publikacja z niego znika, wiec matching zwracal None, importer zakladal brak rekordu i tworzyl DUPLIKAT. Teraz odpytujemy modele zrodlowe przez global_objects. Patent NIE ma pola pbn_uid -- sprawdzone na modelach, nie zalozone. Wpisanie go do listy wywalilo by FieldError przy pierwszym .filter(), bo Django resolwuje nazwy pol natychmiast, a nie przy iteracji. Zachowana historyczna ROZNICA miedzy tymi dwiema metodami: przy wielu trafieniach get_bpp_publication zwraca None, a rekord_w_bpp sklejone tytuly; przy braku trafien rekord_w_bpp spada do fuzzy matchingu. Nie ujednolicam tego przy okazji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…sk 1b) ZMIANA DECYZJI #14 planu fazy 03, rozstrzygnieta przez wlasciciela systemu. Plan zakladal "POMIN + ZARAPORTUJ", argumentujac, ze auto-restore pozwolilby importowi wskrzeszac rzeczy skasowane celowo. Wlasciciel rozstrzygnal inaczej: PBN jest zrodlem prawdy dla tych publikacji, wiec skoro rekord tam jest, ma wrocic takze do BPP. Ryzyko wskazane w tamtej decyzji nie znika -- zmienia postac z "nie robmy tego" na "robmy, ale zostawmy slad". Tym sladem jest nowy model RekordPrzywroconyPrzezImport (pbn_integrator, migracja 0001): rekord (generic FK, bo import dotyka kilku modeli), data, publikacja PBN i sciezka importu. Operator, ktory znajdzie w bazie publikacje skasowana przez siebie tydzien wczesniej, ma gdzie sprawdzic, ze wrocila z importu, kiedy i skad. Wpis powstaje WYLACZNIE przy realnym wskrzeszeniu -- zwykly re-import zywego rekordu nie zostawia nic. Inaczej rejestr zapelnilby sie szumem i przestal cokolwiek znaczyc; przypina to osobny test. Helper przyjmuje takze wejscie NIE-modelowe bez wyjatku: rekord_w_bpp zwraca STRING ze sklejonymi tytulami przy wielu trafieniach po pbn_uid (zachowanie historyczne, utrzymane w Tasku 2) albo None. Helper stoi na sciezce importu, wiec wywrocenie przebiegu z tego powodu byloby regresja niezwiazana z koszem. Wpiete w trzy wejscia importera: articles, books, chapters. Zweryfikowane mutacyjnie: wylaczenie warunku na deleted_at wywala test o wskrzeszeniu i rejestrze (pierwsza czerwien byla ImportError, wiec nie dowodzila, ze asercje cokolwiek pilnuja). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…zji #14 w planie matchuj_publikacje i helpery (_try_match_pub_by_doi/zrodlo/isbn/uri/title) odpytywaly klass.objects, wiec fuzzy matching nie widzial soft-skasowanych rekordow -- re-import uznawal je za nieistniejace i tworzyl DUPLIKAT, ktorego operator nie zauwazy, bo oryginal siedzi w koszu. Wprowadzony helper widzacy_manager(klass) z getattr-fallbackiem, bo klass bywa takze Rekord: to widok (managed=False), nie SoftDeleteModel, wiec nie ma global_objects -- za to jest odfiltrowany po deleted_at juz na poziomie SQL-a (faza 01), czyli "menedzer widzacy" dla niego nie istnieje. ODSTEPSTWO OD PLANU, swiadome: plan wymienial linie 87/108/181/235/249, pomijajac _build_isbn_query. Zmienilem tam rowniez baze zapytania, bo uzasadnienie jest identyczne -- re-import ksiazki matchowanej po ISBN tworzylby duplikat tak samo. Na objects zostaja WYLACZNIE dwa wywolania wydawnictwa_nadrzedne_dla_innych(): to metoda menedzera domenowego, ktorej GlobalManager pakietu nie ma, a plan swiadomie akceptuje liczenie nadrzednych sposrod nieusunietych. Test po DOI mial blad PO MOJEJ STRONIE: podawal celowo inny tytul, a _try_match_pub_by_doi mimo zawezenia po DOI przepuszcza kandydata przez prog podobienstwa tytulu (0.80). Test padal wiec z powodu reguly matchingu, nie widocznosci kosza. Poprawiony, zeby mierzyl to, co mial mierzyc. Plan fazy 03: decyzja #14 oznaczona jako ZMIENIONA (PRZYWROC + ODNOTUJ), z zachowaniem pierwotnego rozumowania -- bo to ono uzasadnia istnienie rejestru. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
… 5, 6) TASK 5. Matching ksiazki-matki rozdzialu (chapters.py) szukal przez Wydawnictwo_Zwarte.objects, wiec skasowanej ksiazki nie widzial: importer uznawal ja za nieistniejaca i importowal PONOWNIE. Powstawal duplikat ksiazki, a rozdzialy odpinaly sie od oryginalu. Lookup wydzielony do znajdz_ksiazke_nadrzedna() -- glownie po to, zeby dalo sie go przetestowac bez budowania calego JSON-a z PBN. Trafienie w kosz oznacza wskrzeszenie, zgodnie z decyzja z Taska 1b; wpis w rejestrze dostaje osobne zrodlo "chapters:ksiazka-nadrzedna", zeby dalo sie odroznic wskrzeszenie ksiazki od wskrzeszenia samego rozdzialu. Kontrakt zachowany: brak trafienia to nadal None (a nie wyjatek), bo wolajacy ma wtedy zaimportowac ksiazke z PBN. Przypiete testem. TASK 6. _delete_existing_publications w pbn_import mial dac czysty stan przed pelnym re-importem, ale po fazie 02 QuerySet.delete() jest MIEKKIE. Zmienione na hard_delete() ORAZ global_objects -- oba potrzebne z roznych powodow: - hard_delete(), bo inaczej publikacje zostaja w bazie i "czyszczenie" nie czysci; - global_objects, bo objects nie widzi kosza, wiec publikacje PBN skasowane wczesniej przez operatora przezylyby czyszczenie, a potem re-import by je wskrzesil. Operator zobaczylby z powrotem rekordy, ktore usunal, i to bez sladu, ze przezyly czyszczenie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
ruff format na calym katalogu pbn_import zagarnal fix_import_dat_oswiadczen_pbn.py, ktorego faza 03 nie dotyka. Ta sama lekcja co w fazie 02: formatowac TYLKO wlasne pliki, inaczej PR robi sie nieczytelny. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
… decyzji (Task 7, 8) TASK 7. Jedyna realna zmiana w tym tasku: transfer w deduplikator_autorow/utils/merge.py. Cytowane w planie linie byly NIEAKTUALNE (plik zrefaktoryzowany -- ostrzegal o tym handoff fazy 02), wiec lookupy odnalezione na nowo: dwa miejsca, jedno dla publikacji (Praca_*), jedno dla through-modeli. Bez widzenia kosza autorstwa i prace soft-skasowane zostawaly przy autorze-duplikacie, ktory po scaleniu ma zniknac -- powstawaly SIEROTY. W fazie 04 zablokowalyby dodatkowo guard PROTECT, wiec problem ujawnilby sie dopiero tam, w miejscu niezwiazanym z przyczyna. Reszta Taska 7 to decyzje "ZOSTAW objects", zapisane jako rejestr w pliku testowym (ewaluacja, snapshot odpiec, komparator PBN, REST API). Sprawdzone przy okazji: wszystkie .update() w tych miejscach dotycza przypieta / dyscyplina_naukowa / afiliuje, a NIE deleted_at -- gate BppSoftDeleteQuerySet.update() z fazy 01 sie tam nie odpala. TASK 8. Skanowanie do dedupu publikacji ZOSTAJE na objects i dostaje test. Rekord w koszu nie jest duplikatem do rozstrzygniecia; podsuwanie go operatorowi kazaloby mu scalac rzecz, ktora sam usunal. To decyzja ODWROTNA niz przy matchingu importu -- i wlasnie dlatego jest przypieta testem, zamiast zostac "oczywista". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Najwazniejsze, co niesie: zmiana decyzji #14 (PRZYWROC + ODNOTUJ zamiast POMIN), szesc faktow o kodzie, ktore w fazie 03 kosztowaly runde poprawek (m.in. Patent bez pbn_uid, rekord_w_bpp zwracajacy STRING, prog podobienstwa tytulu przy matchingu po DOI), oraz lista tego, co czeka faze 04 -- w tym odwrotne OneToOne omijajace soft-delete, na ktore faza 04 trafi ponownie, bo dotyka tej samej relacji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Handoff byl skupiony wylacznie na fazie 04. Dopisana pelna mapa 04-08 wraz z zaleznosciami (kolejnosc nie jest dowolna: 06 konsumuje kolejke z 05, 07 konsumuje log z 06) oraz lista tego, co fazy 01-03 juz pod nie podlozyly -- sygnatury delete/restore z user/reason, emitowane sygnaly i kontrakt ostatnio_zmieniony. Odnotowane osobno: NAGROBKI na zewnatrz (OAI-PMH status=deleted, CERIF, API) sa nadal NIEZROBIONE, mimo ze handoff fazy 02 traktowal je jako bramke wydania. Fundament gotowy, brakuje ekspozycji; nie naleza do zadnej z faz 04-08, wiec bez tego wpisu przepadlyby miedzy fazami. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
… fazy 05 DWIE DECYZJE WLASCICIELA, 2026-08-08. 1. BRAMKA WYDANIA PRZESUNIETA Z FAZY 04 NA 07. Rekomendacja faz 01-03 brzmiala "wydac po 04" i byla uzasadniona bezpieczenstwem -- guardy zapobiegaja osieroceniu. Ale waskim gardlem nie jest bezpieczenstwo, tylko spojnosc dla operatora. Sprawdzone na kodzie: src/bpp/admin/ nie ma DZIS zadnego kosza. Trafienia deleted_at to hydraulika -- ukryte pole formularza dla walidacji ograniczen i filtry agregatow -- a nie filtr "pokaz skasowane" ani akcja "Przywroc". Jednoczesnie SoftDeleteQuerySet.delete jest juz miekki, a delete_selected Django wola wlasnie queryset. Wydanie po fazie 04 daloby wiec operatorowi: "Usun" przestaje znaczyc "zniknelo", rekordu nie da sie przywrocic ani obejrzec bez programisty, i nie da sie go usunac naprawde. Traci jedna zdolnosc i nie dostaje zadnej w zamian, bo kosz przychodzi dopiero w fazie 07. To regresja UX, nie funkcja. Fazy 01-06 scalamy dalej, ale nie wydajemy. Swiadomie odrzucone: wydanie 01+02 z tymczasowym "Usun = twardo" w adminie -- oznaczaloby dwie semantyki kasowania rownolegle i wyrzucenie tego kodu w fazie 07. 2. NAGROBKI WCIAGNIETE DO FAZY 05: OAI-PMH status=deleted, CERIF, sposob odkrycia usunietych w API. To ten sam motyw co wycofanie oswiadczen z PBN -- systemy zewnetrzne dowiaduja sie, ze cos zniknelo. Trzymane osobno przepadlyby miedzy fazami, bo zaden plan ich nie obejmowal, a handoff fazy 02 traktowal je jako bramke wydania. Fundament gotowy: soft-delete bumpuje ostatnio_zmieniony, wiec lista nagrobkow to deleted_objects.filter po tym polu. Brakuje samej ekspozycji. Termin: przed faza 07, nie przed 04. Faza 05 przemianowana na "propagacja usuniecia na zewnatrz". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
soynatan/django-easy-audit#348, z forka mpasternak/django-easy-audit. Dopisany komentarz do ich issue #175 z jedyna nowa przeslanka wobec czterech poprzednich prob: obejscie sugerowane przez maintainera przez CRUD_DIFFERENCE_CALLBACKS jest NIEOSIAGALNE, bo wyjatek leci w linii 102, a callbacki sa wolane dopiero w 113. W handoffie zapisane, co zrobic po scaleniu: skasowac easyaudit_shim.py, wywolanie zainstaluj() w BppConfig.ready() i test_easyaudit_shim.py. Przypomni o tym test_upstream_nadal_ma_blad_czyli_shim_jest_potrzebny, ktory wtedy zacznie padac jako XPASS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…a admina DECYZJA WLASCICIELA 2026-08-08. Soft-delete znaczy, ze rekordu NIE MA. Akcesor, ktory zwraca rzecz skasowana, lamie kontrakt "Rekord albo None", na ktorym stoja wszyscy jego konsumenci. COFNIETE W CALOSCI: - pbn_api/models/publication.py -- rekord_w_bpp i get_bpp_publication wracaja do pytania Rekord. Moja wersja zwracala model KONKRETNY, przez co admin PBN wywalal sie AttributeError na CALEJ changeliscie: admin/publication.py:183 wola .original, a to cached_property istniejaca wylacznie na Rekord. Ten sam blad cicho psul szablon change_form.html (Django polyka brak atrybutu w szablonie -> pusty href, objaw gorszy niz wyjatek). - import_common/core/publikacja.py -- caly widzacy_manager. Fuzzy matching tez nie ma widziec kosza. Przy okazji znika blad, ktorego nie zauwazylem: drugim wolaczem matchuj_publikacje jest deduplikator_publikacji (tasks.py:187), ktory przekazuje modele KONKRETNE -- wiec dedup zaczal podsuwac operatorowi rekordy z kosza, wbrew decyzji z Taska 8. Moj wlasny test tego nie zlapal, bo pokrywal tylko polowe skanujaca (_get_publications_to_scan), a nie matchujaca. Zaglada do kosza IMPORTER, nie akcesor -- to jego decyzja i ma siedziec w sciezce importu. TESTY: dwa testy matchingu skasowane (sprawdzaly juz-bledne zachowanie), test akcesora ODWROCONY na kontrakt "skasowane = nieznalezione". DOLOZONY test_admin_rekord_w_bpp.py -- ta metoda admina nie miala ZADNEGO pokrycia, dlatego bloker wyszedl dopiero w recenzji. Zweryfikowany mutacyjnie: przywrocenie zepsutej wersji wywala oba testy na AttributeError 'Wydawnictwo_Ciagle' object has no attribute 'original'. Znika przy okazji ustalenie #8 z recenzji: nowy przypadek "wiele trafien" byl skutkiem iterowania po czterech modelach zamiast pytania Rekord. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Sesja przerwana w polowie naprawiania. PR #742 NIE nadaje sie do scalenia. Blokery 1 i 2 (padniety admin PBN, cichy pusty href w szablonie) naprawione commitem eb04689. Zostaje 5 pozycji, opisanych z file:line i kolejnoscia, plus 5 ustalen recenzji o nizszym priorytecie. Handoff zawiera dwie decyzje wlasciciela, ktore ustawiaja reszte pracy: soft-delete znaczy ze rekordu NIE MA -- akcesor zwraca Rekord albo None, a zagladanie do kosza jest decyzja IMPORTERA; oraz wariant A dla trafienia w kosz (PRZYWROC + ODNOTUJ). Osobna sekcja z pulapkami, ktore juz kosztowaly -- w tym najwazniejsza: lista wolaczy to nie to samo co lista zalozen wolaczy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…one (3.1) Zajrzenie do kosza siedzialo w galezi ``ret is not None``, gdzie po cofnieciu akcesorow (commit eb04689) bylo z definicji no-opem: ``rekord_w_bpp`` zwraca tam ``Rekord``, a ``Rekord`` nie ma pola ``deleted_at``. Realny problem jest w galezi przeciwnej. Kosz jest jednoczesnie: - NIEWIDOCZNY dla matchingu -- ``Rekord`` to widok odfiltrowany po ``deleted_at``, wiec ``rekord_w_bpp`` zwraca ``None``, - WIDOCZNY dla bazy -- ``pbn_uid`` to ``OneToOneField(unique=True)`` BEZ warunku partial, wiec rekord w koszu nadal to pole trzyma. Import wchodzil wiec w galaz "utworz nowy" i wywalal sie IntegrityError-em na ``bpp_wydawnictwo_{ciagle,zwarte}_pbn_uid_id_*_uniq``. To nie cichy duplikat, tylko wywrocenie calego przebiegu. Nowe ``znajdz_lub_wskrzes_rekord()`` (preambula wspolna dla trzech importerow) najpierw pyta akcesor o rekord zywy, a gdy go nie ma -- jawnie szuka w koszu po ``pbn_uid`` i wskrzesza (wariant A: PBN jest zrodlem prawdy, slad w rejestrze ``RekordPrzywroconyPrzezImport``). Model jest parametrem, a nie petla po wszystkich publikacjach, bo ``Patent`` nie ma ``pbn_uid`` i ``.filter()`` na nim wywala ``FieldError``. Testy: 5 nowych, na poziomie realnych funkcji importera. Czerwien przed zmiana to IntegrityError z bazy. Zweryfikowane mutacyjnie -- kazda z czterech mutacji (deleted_objects->objects, restore bez rejestru, znajdz bez restore, zle zrodlo_importu) wywraca dokladnie te asercje, ktore ma wywracac. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
BLOKER 3 z self-review. ``Praca_Habilitacyjna.autor`` bylo ``OneToOneField``,
czyli twarde ``UNIQUE (autor_id)``, ktore kosza NIE zna. Scalanie autorow
przenosi wiersze RAZEM Z KOSZEM (zeby nie zostawiac sierot), wiec duplikat
z habilitacja soft-skasowana + glowny autor z zywa wywracal CALE scalanie:
bpp_praca_habilitacyjna_autor_id_b83969d3_uniq
Zamiana na ``ForeignKey`` + ``UniqueConstraint(fields=["autor"],
condition=Q(deleted_at__isnull=True))`` -- ten sam wzorzec, co faza 01
zastosowala do ``*_Autor``. Django wymusza ``unique=True`` w
``OneToOneField.__init__``, wiec zdjecie bezwarunkowego UNIQUE wymaga zmiany
typu pola.
Konsekwencje zmiany typu (sprawdzone, nie tylko "kto wola", ale "co robi
z wynikiem"):
- Akcesor odwrotny to teraz ``autor.praca_habilitacyjna_set`` (manager).
Sciezka FILTROWANIA w ORM/DjangoQL sie NIE zmienia -- ``related_query_name``
domyslnie i tak jest nazwa modelu.
- ``RokHabilitacjiView`` pyta teraz jawnie ``Praca_Habilitacyjna.objects``.
Przy okazji znika reczny warunek na ``deleted_at``: odwrotne OneToOne szlo
przez ``_base_manager`` i pokazywalo rekordy z kosza, manager relacji nie.
- ``browse/autor.html`` idzie petla po ``praca_habilitacyjna_set``, jak juz
robil to doktorat (ktory od zawsze jest FK). Bez tego sekcja "Stopnie
naukowe" zniknelaby CICHO -- szablony Django polykaja brak atrybutu.
Oba te miejsca mialy juz testy w repo i oba zaswiecily sie na czerwono.
Prawdziwy konflikt (obie habilitacje zywe) konczy sie teraz
``KonfliktScalania`` z komunikatem "Nie mozna scalic autorow: obaj maja prace
habilitacyjna." zamiast IntegrityError. Nowy wyjatek jest lapany osobno --
sprzeczne dane to nie awaria, wiec nie ida do Rollbara.
Walidacja w adminie: ``validate_unique()`` juz tego nie pilnuje, a
``validate_constraints()`` CICHO POMIJA constraint, ktorego pole warunku
(``deleted_at``) jest wykluczone z walidacji -- a admin wyklucza wszystko spoza
``fieldsets``. Sztuczka z ukrytym polem (``bpp/admin/core.py``) dziala dla
inline'ow; tutaj wyprodukowalaby widoczny pusty wiersz "Deleted at", wiec jest
jawne ``clean_autor()``. Constraint zostaje ostateczna gwarancja w bazie.
Testy: 6 nowych (3 scalanie, 3 admin). Kazda z czterech mutacji wywraca
dokladnie ten test, ktory ma wywracac; bezwarunkowy unique zostal
udokumentowany przez oryginalna czerwien.
Refs #742
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…go do PBN (3.3, 3.4) 3.3 -- detekcja kolizji autorstw szla po `model.objects`, czyli po menedzerze ZYWYCH. Gdy publikacja jest w koszu, oba autorstwa (glownego i duplikatu) tez tam sa, wiec kolizji nie bylo widac: transfer przechodzil i w koszu ladowaly DWA wiersze `(rekord, glowny, typ)`. Warunkowy `wc_autor_uniq_rekord_autor_typ` obowiazuje wsrod zywych, wiec kolizja wybuchala dopiero przy `publikacja.restore()` -- kosz stawal sie drzwiami jednokierunkowymi. Detekcja idzie teraz po `global_objects` (ten sam `getattr`-fallback, co `wiersze_do_transferu`, bo funkcja obsluguje tez modele bez soft-delete). Uwaga na przyszlosc, ujawniona przy weryfikacji mutacyjnej: wiersz duplikatu znika dzis dlatego, ze `autor_duplikat.delete()` kaskaduje TWARDO po FK. Pierwsza wersja poprawki odpinala go dodatkowo od grupy restore'u wlasnym `transaction_id` -- mutacja pokazala, ze to martwy kod, wiec zostal usuniety. Gdy faza 04 zmieni kasowanie autora na miekkie, ta galaz bedzie musiala wrocic; zapisane w handoffie fazy 04. 3.4 -- scalanie kolejkowalo do `PBN_Export_Queue` wszystko, co przenioslo, takze rekordy w koszu. To sprzeczne z kierunkiem fazy 05: soft-delete ma oswiadczenia z PBN WYCOFYWAC, a nie wysylac tam rzeczy, ktorych w BPP "nie ma". Guard `_w_koszu()` w obu miejscach kolejkujacych (autorstwa + proste publikacje). Testy: 5 nowych. Kazda z trzech mutacji (detekcja po `objects`, zdjecie guardu PBN w kazdym z dwoch miejsc) wywraca dokladnie swoj test. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
- §1: baseline jest niesswiezy (bpp/0487 vs 0500) i byl taki juz przed faza 03; odswiezenie przy scalaniu, nie w feature-branchu - §2a (nowe): druga zmiana kierunku -- akcesory NIE zagladaja do kosza - §3: tabela zmian przepisana na stan faktyczny (poprzednia opisywala implementacje cofnieta w eb04689) + sekcja "Czego faza 03 NIE naprawia" - §4: dwie pulapki o wolaczach akcesorow (`.original` w adminie, `matchuj_publikacje` z dwoma wolaczami o sprzecznych potrzebach) - §5: ostrzezenie dla fazy 04 -- `scal_autora` polega dzis na TWARDEJ kaskadzie `autor_duplikat.delete()`; po zmianie na miekkie kasowanie autora wiersz przetrwa jako sierota i zepsuje `restore()` - §6: cztery pozycje dlugu z handoffu domkniecia (admin rejestru, force, Oswiadczenie_Instytucji, post_hard_delete) - §7: trzy nowe wnioski procesowe -- mutacja ktora PRZESZLA to informacja; dane referencyjne z baseline nie sa w testach gwarantowane; `git checkout` kasuje niezacommitowana implementacje przy testowaniu mutacyjnym Handoff domkniecia oznaczony jako historyczny, z mapowaniem pozycji na commity. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…iki suity Workflow `Tests` ma `pull_request: branches: [dev]`, wiec PR #742 (`feat/soft-delete-03` -> `feat/soft-delete`) dostaje wylacznie GitGuardiana. Zielony check na takim PR-ze NIE jest dowodem, ze testy przeszly -- dotyczy to tak samo faz 04-07, dopoki sa stackowane na `feat/soft-delete`. Dopisany wynik lokalnego `make tests` dla fazy 03: 9522 + 157 + 81 passed, 0 failed. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…kupu (#9, #13) #9 -- rejestr osiagalny wylacznie przez `manage.py shell` nie spelnial swojej roli. To jego DOSTEPNOSC dla operatora byla calym uzasadnieniem zmiany decyzji #14 planu ("nie robmy tego" -> "robmy, ale zostaw slad"). Slad, do ktorego nie da sie zajrzec, sladem nie jest. - `RekordPrzywroconyPrzezImportAdmin`, TYLKO DO ODCZYTU: trzy `has_*_permission` zwracaja False, bo rejestr, w ktorym operator moze dopisac albo skasowac wpis, przestaje byc dowodem. Samo `readonly_fields` by nie wystarczylo -- chowa pola, ale wciaz pozwala dodac i skasowac wiersz. - Indeks `pbnint_przywr_ct_objid_idx` na `(content_type, object_id)` (migracja `pbn_integrator/0002`). Istniejacy `(przywrocono DESC, content_type)` obsluguje "co wrocilo ostatnio", ale NIE "czy TEN rekord wrocil?" -- a to jedyne pytanie zadawane wprost o konkretna publikacje. #13 -- `Oswiadczenie_Instytucji.get_bpp_publication` (INNY model niz `Publication.get_bpp_publication`, ta sama nazwa metody) nie byl przy audycie rozstrzygniety w ogole. Rozstrzygniecie: ZOSTAJE na `objects`, bo to akcesor, a zagladanie do kosza jest decyzja importera. Nie jest to martwy przepis -- lancuch wolaczy realizuje wariant A w calosci: akcesor zwraca None -> `statements.py:404` wola `importuj_publikacje_instytucji` -> `znajdz_lub_wskrzes_rekord` wskrzesza i wpisuje do rejestru. Wolacz przyjmuje oba ksztalty wyniku (`Rekord` przez `.original`, model konkretny wprost). Gdyby akcesor sam zagladal do kosza, importer nigdy by sie nie odpalil. Testy: 5 nowych (4 rejestr, 1 rejestr decyzji). Test rejestru decyzji przechodzi od razu -- to przypiecie istniejacego zachowania -- wiec zweryfikowany mutacyjnie (`objects` -> `global_objects`). Zlapana wlasna slaba asercja: `"articles" in tresc` na changeliscie bylo falszywie zielone, bo ten lancuch pojawia sie tez w bocznym `list_filter`. Przechodzilo nawet po wyrzuceniu kolumny z `list_display`. Asercje ida teraz po `class="field-<nazwa>"`, czyli po komorkach tabeli. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…ort i scalanie a kosz) feat(soft-delete): faza 03 — audyt kategorii B (import i scalanie a kosz)
Plan fazy 04 powstal przed fazami 02 i 03 i cytuje stan, ktorego juz nie ma.
Nowa sekcja §4c handoffu -- pieciopunktowa lista rozjazdow, kazdy sprawdzony
w kodzie, nie zgadniety:
- R1 (BLOKER): Zadanie 3 (miekki `Autor.delete()`) zepsuje scalanie autorow
w przypadku "kolizja autorstw w koszu", bo znika twarda kaskada, na ktorej
ten przypadek dzis stoi. Zadanie 6 tego NIE wykryje -- symuluje wylacznie
czysty transfer. Miejsce poprawki i uzasadnienie opisane; pisac RAZEM
z Zadaniem 3, nie po nim.
- R2: `Praca_Habilitacyjna.autor` to juz nie `OneToOneField` (faza 03).
Konkluzja planu ("nie ruszamy") zostaje sluszna, ale reverse i licznik
relacji juz nie -- autor moze miec wiele habilitacji (zywa + kosz).
- R3: Zadanie 5 wskazuje nieistniejacy override `delete()` w
`wydawnictwo_zwarte.py`. Faza 02 umiescila go we WSPOLDZIELONYM mixinie,
wiec guard na rozdzialy trafilby tam do wszystkich pieciu modeli publikacji.
- R4: `AutorManager` NIE jest przepleciony filtrem `deleted_at`. Plan traktuje
to jako przypis ("warunek wstepny"), a to osobna robota (MRO + FTS)
w zakresie Zadania 3 -- inaczej husk autora bedzie widoczny wszedzie.
- R5: wszystkie numery linii nieaktualne, z tabela poprawnych.
Plus lista tego, co w planie NADAL jest prawdziwe -- zeby nastepna sesja nie
zweryfikowala wszystkiego od zera.
Nowy `PROMPT-start-fazy-04.md`: gotowy prompt do wklejenia w nowa sesje,
z zalozeniem worktree, kolejnoscia czytania dokumentow i zasadami, ktore
sprawdzily sie w fazie 03 (mutacje, kopia pliku zamiast `git checkout`,
fixture zamiast baseline, brak CI na tym PR-ze).
§1 zaktualizowany o scalenie fazy 03 (774f1a7).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
docs(soft-delete): rozjazdy plan-vs-kod dla fazy 04 + prompt startowy
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.
Co to jest
Faza 01 soft-delete: autorstwa (
*_Autor→SoftDeleteModel) wraz z pełnym projektem wdrożeniowym i planami TDD dla pozostałych 7 faz.Refs #303. Zastępuje feasibility-spec z #304.
Stan: implementacja fazy 01 GOTOWA
26 commitów, 9 migracji, 9384 testy zielone / 0 failed.
*_Autor→SoftDeleteModel0488bpp_*_autorzy+ gałąź kasująca w funkcjach triggera + bramkaWHEN0489UniqueConstraint,ExclusionConstraint0490–0493liczba_autorowprzezcount(...) FILTER0494bpp_nowe_sumy_*)0495rozbieznosci_dyscyplin/0022django-denorm(denorm_always_only)pg_depend)Dlaczego to więcej niż „dodanie kolumny"
Soft-delete zmienia kontrakt
delete()z „wiersz przestaje istnieć" na „wiersz istnieje, ale się nie liczy". Każdy mechanizm, który polegał na pierwszym znaczeniu, trzeba było znaleźć i przekonfigurować. W BPP było ich siedem:WHENtriggerów cache (0433) —save(update_fields=[...])nie ruszał żadnej bramkowanej kolumny, więc trigger się nie odpalał0432) — czysty upsert bezDELETE; odfiltrowanie wiersza z widoku było no-opemdjango-denorm— drugi, niezależny system triggerów z własną bramką z listonly=; bez niegoopis_bibliograficzny_cache,slugicached_punkty_dyscyplinzostawały nieświeże na stałeunique_together— blokował wzorzec „skasuj autorstwa i wstaw od nowa" (UniqueViolationw re-imporcie)UNIQUE ... DEFERRABLEz migracji0132(2018) — niewidoczny dla ORM, więcmakemigrationsgo nie widział; wybuchał dopiero przyCOMMITrestore(strict=True)— pakiet sprawdzastrictdla każdej relacji, także zwykłych FK; gołe.restore()rzucałoSoftDeleteExceptionliczba_autorow, ranking i raport rozbieżności czytały surową tabelęKanarek katalogowy — żeby faza 02 nie odkrywała tego od nowa
Konsumentów odkrywaliśmy pojedynczo, przez awarie. Nowy test (
test_kanarek_katalogowy.py) pytapg_dependo zależność na poziomie kolumny konkretnej tabeli i pada, gdy jakikolwiek widok czyta tabelę objętą soft-delete bez filtra po jejdeleted_at.Pierwsza wersja używała
"deleted_at" in definicja— recenzja wykazała symulacją, że w fazie 02 przepuściłaby 3 z 4 widoków, bo odziedziczyły filtr po*_autorz fazy 01. Poprawione; trwały test symulujący fazę 02 asertuje dwustronnie, że stara wersja przepuszczała, a nowa łapie.Uwaga wdrożeniowa
0492wymaga okna serwisowego —ADD CONSTRAINT ... EXCLUDE USING GISTbierzeACCESS EXCLUSIVE(blokuje też odczyty) i nie ma wariantu współbieżnego. Szczegóły, procedura po przerwanej migracji i zasady rollbacku:docs/deweloper/runbook-soft-delete-faza-01.md.Dokumentacja
docs/superpowers/specs/2026-06-04-soft-delete-publikacje-i-autorzy-design.md— spec (zrewidowany po PR perf(cache): port bpp_refresh_cache PL/Python → PL/pgSQL + bramka WHEN #363)docs/superpowers/plans/2026-06-04-soft-delete-0{0..8}-*.md— plany TDD faz 01–08docs/deweloper/runbook-soft-delete-faza-01.md— runbook wdrożeniowy🤖 Generated with Claude Code