soft-delete faza 05b — nagrobki dla konsumentów przyrostowych - #767
Open
mpasternak wants to merge 17 commits into
Open
soft-delete faza 05b — nagrobki dla konsumentów przyrostowych#767mpasternak wants to merge 17 commits into
mpasternak wants to merge 17 commits into
Conversation
…wych Faza 05b wyszla z fazy 05 (miala baner zakresu i zero taskow). Spec powstal z brainstormingu; cztery decyzje wlasciciela: D1: nagrobek = DOPELNIENIE EKSPOZYCJI, nie tylko soft-delete. Rekord znika z feedu na cztery sposoby (kosz, nie_eksportuj_przez_api, ukryty status_korekty, odpiety ostatni autor z uczelni) i dla harvestera sa one nierozroznialne. Nagrobki tylko dla kosza czynilyby deklaracje deletedRecord czesciowo nieprawdziwa. D2: deletedRecord = "transient" (dzis "no"). Nie gwarantujemy trwalosci nagrobka: husk moze zniknac przy twardym kasowaniu albo czyszczeniu kosza w fazie 07. D3+D5: REST dostaje osobny endpoint /api/v1/usuniete/ z samym identyfikatorem i znacznikiem czasu — nigdy trescia huska, ktory usunieto czesto wlasnie dlatego, ze byl bledny albo zawieral dane osobowe. REST znaczy wezej niz OAI (kosz, nie dopelnienie), bo nie sklada obietnicy deletedRecord. D4: wszystkie 7 niepustych setow — deklaracje deletedRecord sklada sie dla CALEGO repozytorium. Kluczowe ustalenie architektoniczne: predykat widocznosci sklei dzis PRZYNALEZNOSC do tenanta z EKSPOZYCJA. Dopelniac wolno tylko druga — naiwne "wszystko minus widoczne" wystawiloby w multi-hosted nagrobki dla rekordow innych uczelni (widoczne_jednostki filtruje uczelnia=uczelnia wprost). Stronicowanie: paginujemy nadzbior (przynaleznosc) zamiast dokladac drugi strumien. resumptionToken niesie jeden kursor (datestamp, pk) i zaklada jeden porzadek; dwa strumienie zepsulyby przyrostowosc from/until, czyli dokladnie to, co ta faza naprawia. Ostrzezenie operacyjne zapisane w specu: wylaczenie Uczelnia.eksport_cerif_osoby wystawi nagrobki dla WSZYSTKICH autorow uczelni naraz (widoczni_autorzy zwraca wtedy none()). Zachowanie poprawne, ale jednorazowo bardzo hałaśliwe.
…pecu Plan rozpisuje spec z 2026-08-15 na 10 taskow TDD, 1400 linii, z konkretnymi plikami, komendami i trescia testow. Kolejnosc podyktowana ryzykiem: kontrakt providera (1) → przynaleznosc providerow wlasnych z testem izolacji tenantow (2) → pochodnych (3) → stronicowanie nadzbioru (4, najdelikatniejsze) → emisja status=deleted (5) → GetRecord (6) → Identify transient (7) → cztery drogi znikniecia (8) → REST (9) → weryfikacja (10). Task 4 jako jedyny konczy sie przy CZERWONYM test_oai.py — zmienia ksztalt zwrotki strona() na pary (obiekt, nagrobek), a wolajacych dostosowuje Task 5. Zapisane wprost w komunikacie commita tego kroku, zeby wykonawca nie uznal tego za regresje. Global Constraints zawieraja dwa zakazy specyficzne dla tego repo: nowa grupa GrupaApiV1 jest zabroniona (wymagalaby pola api_v1_<value> na Uczelni plus migracji, pilnuje tego test_kazda_grupa_ma_pole_na_uczelni), a make clean-testcontainers jest zabronione, bo host bywa wspoldzielony. Self-review usunelo trzy placeholdery: tresc nalezacy_grantodawcy, sciezke relacji w nalezacy_autorzy i cztery testy Taska 8 opisane proza. Wszystkie zastapione kodem zweryfikowanym w zrodlach. KOREKTA SPECU: sekcja "Ograniczenia" twierdzila, ze Instytucja_Finansujaca w secie orgunits generuje nagrobki przez widocznosc pochodna od projektow. Nieprawda — widoczni_grantodawcy filtruje finansowanie__projekt__jednostka__ uczelnia wprost, czyli sama atrybucja bez reguly ekspozycji, wiec dopelnienie jest puste. orgunits generuje nagrobki, ale wylacznie przez Jednostka.
…videra Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…a tenantow Helpery naleza_*/nalezace_* obok istniejacych widoczne_*: sama atrybucja tenanta, bez regul ekspozycji. Publikacje, patenty i autorzy ida przez global_objects po OBU stronach (rekord i model autorstwa), zeby rekord w koszu dostal nagrobek zamiast zniknac po cichu. publikacje.py: prefetche wydzielone do dekoruj(), zeby queryset() i przynaleznosc() nie mogly sie rozjechac — strona() paginuje ta druga. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…todawcy) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
UWAGA: po tym commicie CZERWONE sa test_providery.py, test_serializery.py i test_widocznosc.py — zmienil sie ksztalt zwrotki strona(), a one wolaja realnego providera i rozpakowuja gole obiekty. Wolajacych dostosowuje nastepny task. test_oai.py zostaje ZIELONY, bo chodzi po atrapach (FikcyjnyProvider), wiec kontraktu realnej klasy bazowej w ogole nie dotyka — plan spodziewal sie tu czerwieni, ale trafil w niewlasciwy plik. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
_naglowek() dostaje flage usuniety=; _dopisz_rekordy wypuszcza nagrobek PRZED serializacja (nie ma czego serializowac) i liczy zbiory widocznosci wylacznie dla zywych rekordow. Dostosowani wolajacy zmienionego ksztaltu strona(): - czasowniki: _zbierz_strone/_wg_setu/_kursor_dla przenosza pary, - test_oai.py: FikcyjnyProvider zwraca pary (same zywe), - pozostale suity: helper strona_zywych() odtwarza dawna semantyke zwrotki, wiec asercje o widocznosci zostaja bez zmian, - test_integralnosc*: harvest pomija nagrobki — nie maja ladunku, a zaliczanie ich do wydanych oslabialoby test wiszacych referencji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…Exist pojedynczy() szuka w nadzbiorze, wiec rekord tenanta ktory przestal byc wystawiany JEST odnajdywany; _znajdz_rekord dokleja czwarty element (czy_nagrobek), a _get_record wychodzi wczesnie — bez zbiorow widocznosci i kontekstu, ktore dla nagrobka sa czystym kosztem. najstarszy_datestamp tez idzie po nadzbiorze: nagrobek jest rekordem o dacie i moze byc najstarszy. idDoesNotExist zostaje dla identyfikatorow spoza tenanta (4 przypadki w test_oai.py dalej zielone). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Test czterech drog jest mutation-tested: podmiana ktoregokolwiek global_objects na objects w naleza_wydawnictwa lapie sie odpowiednio na przypadku 'kosz' i 'odpiecie_autora'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Sam identyfikator (model + pk) i data usuniecia, nigdy tresc: rekord bywa kasowany wlasnie dlatego, ze byl bledny albo mial dane osobowe. Zakres WEZSZY niz nagrobek OAI (decyzja D5 specu) — tu wychodzi wylacznie kosz, bo REST nie sklada obietnicy deletedRecord. Filtr zakresowy usuniety_od_after/_before dziala na querysecie kazdego modelu (deleted_at__gte/__lte), nie na sklejonej liscie. Data nieparsowalna daje 400, nie ciche pominiecie filtra — inaczej klient przyrostowy dostalby caly kosz i uznal, ze to wszystko zniknelo od jego ostatniego odpytania. permission_classes=[AllowAny]: domyslne DjangoModelPermissionsOrAnonReadOnly wymaga queryset-u, ktorego viewset laczacy szesc modeli nie ma; dla GET tamta klasa i tak nie zada uprawnien, wiec zachowanie sie nie zmienia. Realna bramka to z_bramka_api_v1(grupa=DANE). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…operacyjne Walidacja XSD: plan zakladal istniejacy test walidujacy ODPOWIEDZ, ale testy serializerow walidowaly tylko pojedyncze encje profilu CERIF. status=deleted to konstrukcja koperty OAI-PMH, wiec zaden z nich by go nie zobaczyl. Zvendorowany OAI-PMH.xsd + plik spinajacy oba namespace'y (metadata ma processContents=strict, wiec sam schemat koperty nie wystarcza dla rekordow zywych). Testy sa mutation-tested: podmiana status=deleted na inna wartosc oraz doklejenie pustego <metadata> lapia sie na XSD. Ostrzezenie o eksport_cerif_osoby poszlo do docs/, NIE w help_text pola — zmiana help_text wygenerowalaby migracje AlterField, a ta faza z zalozenia migracji nie dodaje (makemigrations --check potwierdza: zgloszenia tylko dla favicon/flexible_reports/siteblog, zastane). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
trailing-whitespace/end-of-file-fixer/mixed-line-ending przepisywalyby schematy pobrane wprost od wydawcy (profil OpenAIRE CERIF 1.2, koperta OAI-PMH 2.0). Przy podbiciu wersji diff ma pokazywac zmiany SCHEMATU, nie nasze przeformatowanie. Problem byl utajony — istniejace vendorowane pliki tez maja trailing whitespace; nowy OAI-PMH.xsd tylko go ujawnil. Wyjatek nie obejmuje naszego pliku spinajacego oai-pmh-z-profilem.xsd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Samo owiniecie sklejonej listy paginatorem byloby polsrodkiem: kazde zadanie dalej ciagneloby CALY kosz, a klient robilby teraz N zadan zamiast jednego — laczna praca by WZROSLA. Dlatego szesc modeli laczy UNION ALL o wspolnym ksztalcie (etykieta modelu, pk, deleted_at), a ORDER BY i LIMIT/OFFSET wykonuje PostgreSQL. Sortowanie (deleted_at, etykieta, pk): sama data nie wystarcza, bo dwa rekordy skasowane w tej samej mikrosekundzie mialyby niezdeterminowana kolejnosc — a to na granicy strony znaczy zgubiony albo zdublowany wiersz. NULL-e ida na koniec (Postgres, ASC), czyli tam gdzie stawial je poprzedni sort w Pythonie. order_by() czysci Meta.ordering kazdej galezi (Autor.sort, Praca_Doktorska.rok/tytul) — sortowanie po kolumnach spoza SELECT-a, i tak nadpisane przez ORDER BY calosci. test_usuniete_tnie_strone_w_bazie_a_nie_w_pythonie pilnuje, zeby ta optymalizacja nie zdegradowala sie po cichu: sprawdza, ze zlaczone zapytanie faktycznie niesie LIMIT. Przy okazji: fixture'y przestaly przestawiac deleted_at przez .update() — BppSoftDeleteQuerySet slusznie to blokuje (Autor dziedziczy gate, publikacje nie, wiec dotychczasowy test opieral sie na tej asymetrii). Zamiast obchodzic zabezpieczenie polegamy na zegarze i jawnie asertujemy, ze znaczniki wyszly rozne. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…ria gate'u update() Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Swieza sesja nie odtworzy z gita, ze wszystkie cztery PR-y stosu sa otwarte i ze #767 czeka na review. Dopisany tez wybor bazy dla fazy 06 (05 vs 05b) z uzasadnieniem, ze fazy sa rozlaczne. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…ciciela) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Faza 05b stosu soft-delete. Baza:
feat/soft-delete-05.Konsument przyrostowy (harvester OAI-PMH, klient REST) dowiaduje się, że
rekord przestał być wystawiany — zamiast odkrywać to po cichej nieobecności.
Spec:
docs/superpowers/specs/2026-08-15-soft-delete-nagrobki-design.mdPlan:
docs/superpowers/plans/2026-08-16-soft-delete-05b-nagrobki.mdCztery decyzje specu
deletedRecord = "transient", niepersistent— nie gwarantujemy trwałości nagrobka (husk znika przy twardym kasowaniu albo czyszczeniu kosza w fazie 07)./api/v1/usuniete/wystawia wyłącznie kosz, bo REST nie składa obietnicydeletedRecord.Rozszczepienie przynależność / ekspozycja
Dotychczasowy
queryset()sklejał dwa różne pytania: „czy ten rekord jestnasz?" (atrybucja tenanta) i „czy wolno go pokazać?" (reguły ekspozycji).
Nagrobek to różnica tych zbiorów, więc muszą zamieszkać w osobnych metodach:
ProviderEncji.przynaleznosc(uczelnia, model)— sama atrybucja,ProviderEncji.nagrobki(...)—przynaleznosc − queryset.Dlaczego nie dopełnienie całej widoczności:
widoczne_jednostki()filtruje
uczelnia=uczelniawprost, więc naiwne „wszystko minus widoczne"zamieniłoby każdą jednostkę drugiej uczelni w nagrobek pierwszej — wyciek
identyfikatorów w multi-hosted. Pilnuje tego
test_nagrobki_nie_wyciekaja_miedzy_uczelniami.Dlaczego
strona()paginuje nadzbiórresumptionTokenniesie jeden kursor(datestamp, pk)i zakłada jedenporządek. Nagrobki puszczone osobnym przebiegiem po żywych rekordach
wymagałyby drugiego kursora i rozjechałyby okno
from/until— czylizepsułyby dokładnie tę przyrostowość, którą ta faza naprawia.
Skutkiem jest zmiana kształtu zwrotki:
strona()zwraca[(obiekt, czy_nagrobek)]. Wszyscy wołający zostali dostosowani; testy pozatest_nagrobki.pyużywają helperatests/pomocnicze.py::strona_zywych,który odtwarza dawną semantykę, więc ich asercje o widoczności zostały
nietknięte.
Odchylenia od planu (i dlaczego)
naleza_*używająglobal_objectspo OBU stronach, nie tylkow podzapytaniu autorstwa jak w snippecie planu.
Wydawnictwo_Ciagle.objectsto menedżer soft-delete i ukrywa kosz — wersja z planu po cichu gubiłaby
drogę „rekord w koszu". Udowodnione mutacyjnie: podmiana któregokolwiek
managera na
objectswywala odpowiednio przypadekkosziodpiecie_autorawtest_kazda_droga_znikniecia_daje_nagrobek.Takiego nie było — testy serializerów walidują pojedyncze encje profilu
CERIF, a
status="deleted"to konstrukcja koperty OAI-PMH. ZvendorowanyOAI-PMH.xsd+ plik spinający oba namespace'y (<metadata>maprocessContents="strict", więc sam schemat koperty nie wystarcza dlarekordów żywych). Też mutation-tested.
test_providery/serializery/widocznosc,a nie
test_oai.py— ten ostatni chodzi po atrapach, więc kontraktu realnejklasy bazowej nie dotykał.
eksport_cerif_osobyposzło dodocs/, nie whelp_text—zmiana
help_textwygenerowałaby migracjęAlterField, a ta fazaz założenia migracji nie dodaje.
Wyniki przebiegu lokalnego
CI nie biegnie na PR-ach do gałęzi
feat/soft-delete*, więc to jedyny dowód.Poza zakresem (świadomie, sekcja „Ograniczenia" specu)
Zrodlo,Konferencja,Projekt,Jednostka) nie są soft-delete —ich nagrobki biorą się z dopełnienia ekspozycji, więc twarde skasowanie
takiego wiersza znika po cichu.
Stronicowanie
/api/v1/usuniete/(dołożone po pierwszym przebiegu)Endpoint stronicuje
BppLimitOffsetPagination— aleORDER BYiLIMIT/OFFSETwykonuje baza, nie Python.Samo owinięcie sklejonej listy paginatorem byłoby półśrodkiem: każde żądanie
dalej ciągnęłoby cały kosz z sześciu modeli, a klient robiłby teraz N żądań
zamiast jednego — łączna praca by wzrosła. Zamiast tego sześć gałęzi
łączy
UNION ALLo wspólnym kształcie(etykieta modelu, pk, deleted_at);etykieta to stała z
Value, bo po złączeniu klucze główne kolidują międzymodelami.
Sortowanie
(deleted_at, etykieta, pk)— sama data nie wystarcza: dwarekordy skasowane w tej samej mikrosekundzie miałyby niezdeterminowaną
kolejność, a to na granicy strony znaczy zgubiony albo zdublowany wiersz.
NULL-e lądują na końcu (Postgres,ASC), czyli tam, gdzie stawiał jepoprzedni sort w Pythonie.
Przy okazji każda gałąź dostaje
.order_by()czyszcząceMeta.orderingmodelu (
Autor.sort,Praca_Doktorska.rok/tytul_oryginalny) — sortowanie pokolumnach, których nawet nie ma w
SELECT, i tak nadpisane przez zewnętrznyORDER BY.test_usuniete_tnie_strone_w_bazie_a_nie_w_pythoniesprawdza, że złączonezapytanie faktycznie niesie
LIMIT— żeby ta optymalizacja niezdegradowała się po cichu do pesymalizacji.
Dług ujawniony przy okazji (nie naprawiany tutaj)
BppSoftDeleteQuerySetblokuje.update(deleted_at=...)(omijałobypost_save, kaskadę*_Autor,SoftDeleteLogi reversion), ale gatedziedziczy tylko
AutorQuerySet—BppDeletedQuerySetpublikacji nie.Czyli
Autor.deleted_objects...update(deleted_at=...)rzucaRuntimeError,a to samo na
Wydawnictwo_Ciagleprzechodzi. Wygląda na przeoczenie, nie nadecyzję. Testy tej fazy na tej asymetrii nie polegają (opierają się na
zegarze i jawnie asertują, że znaczniki wyszły różne), więc domknięcie
gate'u ich nie zepsuje. Zapisane w handoffie fazy 06.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK