Skip to content

feat(soft-delete): faza 01 — soft-delete autorstw + spójność cache - #312

Draft
mpasternak wants to merge 79 commits into
devfrom
feat/soft-delete
Draft

feat(soft-delete): faza 01 — soft-delete autorstw + spójność cache#312
mpasternak wants to merge 79 commits into
devfrom
feat/soft-delete

Conversation

@mpasternak

@mpasternak mpasternak commented Jun 4, 2026

Copy link
Copy Markdown
Member

Co to jest

Faza 01 soft-delete: autorstwa (*_AutorSoftDeleteModel) 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.

Element Migracja
3 modele *_AutorSoftDeleteModel 0488
Widoki bpp_*_autorzy + gałąź kasująca w funkcjach triggera + bramka WHEN 0489
Indeks FK, warunkowe UniqueConstraint, ExclusionConstraint 04900493
liczba_autorow przez count(...) FILTER 0494
Ranking autorów (bpp_nowe_sumy_*) 0495
Raport rozbieżności dyscyplin rozbieznosci_dyscyplin/0022
Bramka django-denorm (denorm_always_only)
Kanarek katalogowy (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:

  1. bramka WHEN triggerów cache (0433) — save(update_fields=[...]) nie ruszał żadnej bramkowanej kolumny, więc trigger się nie odpalał
  2. funkcje refresh (0432) — czysty upsert bez DELETE; odfiltrowanie wiersza z widoku było no-opem
  3. bramka django-denorm — drugi, niezależny system triggerów z własną bramką z list only=; bez niego opis_bibliograficzny_cache, slug i cached_punkty_dyscyplin zostawały nieświeże na stałe
  4. unique_together — blokował wzorzec „skasuj autorstwa i wstaw od nowa" (UniqueViolation w re-imporcie)
  5. legacy raw-SQL UNIQUE ... DEFERRABLE z migracji 0132 (2018) — niewidoczny dla ORM, więc makemigrations go nie widział; wybuchał dopiero przy COMMIT
  6. restore(strict=True) — pakiet sprawdza strict dla każdej relacji, także zwykłych FK; gołe .restore() rzucało SoftDeleteException
  7. widoki pochodneliczba_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) pyta pg_depend o zależność na poziomie kolumny konkretnej tabeli i pada, gdy jakikolwiek widok czyta tabelę objętą soft-delete bez filtra po jej deleted_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 *_autor z fazy 01. Poprawione; trwały test symulujący fazę 02 asertuje dwustronnie, że stara wersja przepuszczała, a nowa łapie.

Uwaga wdrożeniowa

0492 wymaga okna serwisowegoADD CONSTRAINT ... EXCLUDE USING GIST bierze ACCESS 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

🤖 Generated with Claude Code

mpasternak and others added 11 commits August 6, 2026 08:51
…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 and others added 16 commits August 6, 2026 14:05
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
mpasternak and others added 5 commits August 7, 2026 13:22
…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
mpasternak and others added 24 commits August 7, 2026 23:50
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant