Skip to content

refactor(render): run_render 97 → 1 (faza 3b)#69

Merged
mpasternak merged 11 commits into
mainfrom
cleanup/faza-3b-render
Jul 23, 2026
Merged

refactor(render): run_render 97 → 1 (faza 3b)#69
mpasternak merged 11 commits into
mainfrom
cleanup/faza-3b-render

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Ostatnie trzy naruszenia complexity w repozytorium.

funkcja CC
run_render 97 → 1
_render_step 49 → 2
_mux_tracks_for_timeline 19 → 9

_run.py 941 → 85. Po tym PR-ze: zero naruszeń C901 i zero plików kodu ponad 600 linii w całym repo.

Najwyższe CC czegokolwiek nowego: 9. Każda nowa funkcja ≤ 8.

Trzy obiekty stanu o różnych czasach życia

_RenderPlan (frozen) — wszystko ustalone zanim istnieje przeglądarka · _Stage — co jest teraz na ekranie · _Clock — oś nagrania

run_render to teraz dziewięć instrukcji: przygotuj → otwórz scenę → wystartuj zegar → przejdź kroki → opublikuj film.

Nowe moduły: stage.py 456, loop.py 506, plan.py 303, post.py 227, clock.py 134.

Niezmienniki kolejności — teraz strukturalne

Rejestracja skryptów init: cała w jednej funkcji, której ciało jest kolejnością, a warstwy podlegające role-gatingowi powstają ze słownika sprawdzanego wobec _ROLE_GATED_LAYERS na starcie renderu. Czwarta nakładka dodana bez zadeklarowania podnosi RenderError w pierwszej linii renderu, zamiast zamontować duplikat kursora wewnątrz ramki — defekt widoczny wyłącznie w gotowym filmie.

Kompozycja przed edycją czasu: ogon przeniesiony do post.py jako trzy nazwane etapy (_RecordedFilm_ComposedFilm_VirtualFilm), więc kolejność jest przepływem danych, a nie dwoma sąsiadującymi akapitami.

Sonda nieobecności przed narracją: teraz granica funkcji — „czy ten krok w ogóle się wydarzy" jest oddzielone od „wykonaj go".

Monotoniczność last_freeze_frame: _Clock jest jej jedynym czytelnikiem i jedynym pisarzem; on_sfx to metoda związana, więc odczytuje self w momencie wywołania — dzisiejsza semantyka z konstrukcji.

Znalezisko: jeden z testów z Fazy 0 jest pusty

test_sfx_after_a_freeze_never_lands_inside_the_hold nie dyskryminuje na tej maszynie i nigdy nie dyskryminował. Wykonawca nie zdołał go wywalić — ani przez migawkę wartości, ani przez całkowite usunięcie klamry (not_before=0). Sprawdził to samo usunięcie na stanie sprzed refaktoru (527e9f4): też zielone.

Wyścig, który ten test opisuje, nie materializuje się na tym sprzęcie. Problem istniał wcześniej i nie został wywołany tą pracą — ale oznacza, że projekt oparty na metodzie związanej jest po stronie SFX chroniony rozumowaniem, nie testem. Strona narracji jest pokryta naprawdę (dowód mutacyjny (a)).

Korekty do mojej analizy

Utajony brak else w dyspozytorze jest nieosiągalny, nie tylko nieprzetestowany. CachedAction.action to ActionKindLiteral z dokładnie sześcioma wartościami na modelu pydantica; sidecar nazywający cokolwiek innego jest odrzucany przy wczytaniu. Zachowanie nie zostało zmienione; zamiast tego doszedł strażnik AST przypinający kompletność dyspozycji wobec ActionKind, więc siódmej akcji nie da się już dodać po cichu.

Scalenie card_active/active_card daje 0 punktów CC, nie ~3. Liczba if-ów się nie zmienia; znika assert (0 CC) i klasa błędów desynchronizacji. Warto było, ale nie dla metryki.

_Stage nie może użyć slots=True. stage.observe trafia do BrowserContext.on("page", …) jako metoda związana, a Playwright memoizuje swój wrapper przez setattr na __self__ — instancja ze slotami rzuca AttributeError z wnętrza dyspozytora zdarzeń.

Dowody mutacyjne

mutacja wynik
last_freeze_frame przekazany wartością 2 testy przeplatania narracji czerwone, zapadnięte odstępy
zamiana kompozycji i edycji czasu test z Fazy 0 czerwony: got ['edit', 'compose']
chrome.js przed desktop.js test kolejności czerwony na indeksie 2
usunięcie gałęzi waitFor z dyspozycji nowy strażnik AST czerwony, po nazwie

_run.py nie zawiera już żadnego szwu — 18 miejsc podmiany przecelowanych, a test_render_seams.py wyłapał cztery, które wykonawca przeoczył za pierwszym podejściem. Dokładnie po to powstał.

Testy

1529 unit + 51 integration (po scaleniu z Fazą 3a), 1 skipped
ruff check i preview blank-lines czysto · C901: zero naruszeń w repo

🤖 Generated with Claude Code

https://claude.ai/code/session_0134rSCftVxsMVL8sngceT7A

`card_active` i `active_card` były jedną informacją zapisaną w dwóch
miejscach: `card_active == (active_card is not None)` w każdym z ~10 miejsc
zapisu, a kod jawnie to asertował. Para nie mogła wyrazić niczego poza
rozjazdem, więc znika na rzecz `card: Card | None`.

Zero zmian zachowania: każdy warunek `card_active` staje się
`card is not None`, każda para przypisań — jednym przypisaniem.
…zeglądarką

Pierwszy z trzech czasów życia, które `run_render` przeplatał. `plan.py`
zbiera preflight: wczytanie scenariusza, nadpisania z CLI, walidację sidecara,
ikony pulpitu i pre-syntezę narracji. Trzy domknięcia (`step_message`,
`note_skip`, `persist_resolved`) stają się metodami — to były punkty CC
rodzica przy zerowej zmianie zachowania.

Kolejność w `_prepare_render` jest kontraktem: każde odrzucenie sidecara musi
paść PRZED pre-syntezą, która potrafi spędzić minuty w providerze TTS. Dlatego
banner (`plik:linia` + fragment YAML) jest osobnym, małym obiektem `_Banner`:
walidacja go potrzebuje, zanim plan może istnieć.

run_render: CC 97 -> 82. Zero zmian zachowania.
…ptów init

Drugi z trzech czasów życia. `stage.py` trzyma wszystko, co istnieje tylko
razem z kontekstem przeglądarki: strony, wstrzyknięte warstwy, popup, kartę
slajdu. Sześć domknięć `run_render` (`_chrome_hide`, `_chrome_show`,
`_assert_card_alive`, `_ensure_card`, `observe_page`, `mark_closed`,
`_reveal_shell`) staje się metodami albo funkcją modułową — czyli punktami CC
rodzica oddanymi przy zerowej zmianie zachowania.

Niezmiennik kolejności przestaje być komentarzem: cała rejestracja skryptów
init to jedna funkcja `_install_page_scripts`, której CIAŁEM jest kolejność,
plus sprawdzenie w czasie wykonania — czwarta warstwa dopisana bez ogłoszenia
w `_ROLE_GATED_LAYERS` (albo wsunięta po chrome.js) zatrzymuje render na
pierwszej linii zamiast po cichu zamontować drugi kursor w iframie strony.

`_Stage` świadomie NIE jest `slots=True`: `observe` trafia do
`context.on("page", ...)` jako metoda związana, a Playwright zapamiętuje swój
wrapper przypisując atrybut na `__self__`.

Szwy `Overlay` i `SlideOverlay` wędrują z konstruktorami do `stage` — 10
miejsc podmiany w `test_render.py` przecelowanych w tym samym commicie.

run_render: CC 60. _run.py: 592 linie.
Trzeci z trzech czasów życia. `clock.py` zbiera zamrożenia, umiejscowienia
SFX i narracji — wszystko w KLATKACH osi nagrania, nie w sekundach.

Sedno: `last_freeze_frame` zostaje POLEM, a callback `on_sfx` — METODĄ
ZWIĄZANĄ. `on_sfx` odpala się o jedną ramkę wywołania niżej, w `_render_step`,
i musi czytać `last_freeze_frame` W TAMTEJ CHWILI (zamrożenie z narracji tego
samego kroku jest właśnie tym, przed czym ma się odsunąć). Przekazanie
wartością zrywa to PO CICHU: każde sprawdzenie długości dalej przechodzi
(`probe_frame_count == virtual_frames`, tolerancja muxa, kontrola przekroczenia
ścieżki), bo zapadnięte umiejscowienie nie zmienia długości filmu. Metoda
związana JEST tym domknięciem — czyta `self` w chwili wywołania, więc nie ma
wartości do przekazania i nie ma czego zestarzeć.

Dowód mutacyjny: `stamp()` bez klamry (`not_before=0`) wywala
`test_hold_frame_narrations_never_overlap` oraz
`test_hold_frame_narrations_inside_taken_branch_never_overlap`.

run_render: CC 51. _run.py: 550 linii.
…to szew

`loop.py` rozbija ciało pętli na fazy, każda z jednym argumentem `_StepCtx`
(plan + stage + clock + krok), nie na osiem pozycyjnych.

Niezmiennik „sonda nieobecności PRZED narracją" przestaje być komentarzem:
`_probe_absence` („czy ten krok w ogóle się dzieje") jest osobną funkcją od
`_narrate`/`_perform` („zrób to"), a szwem jest sama granica między nimi.
Zamrożona bramka to jedyny przypadek nierozstrzygalny przed akcją — jej
`waitFor` JEST akcją — więc wraca z `_render_step` jako `_OptionalAbsent`;
obie nieobecności lądują w `_note_absent`.

Przy okazji dwa identyczne bloki `except Exception` (redakcja, komunikat,
`pause_on_error`) stają się jedną funkcją `_fail_step` — używaną dwukrotnie,
więc to nie jest jednolinijkowy pomocnik na jedno wywołanie.

Szew `Recorder` wędruje z konstruktorem do `loop` (5 miejsc podmiany).

run_render: CC 97 -> 6. _run.py: 231 linii (było 941).
`post.py`: nagranie -> skomponowane -> wirtualne -> zmasterowane. Kompozycja
popupu MUSI iść przed edycją czasu, a nazwy stadiów (`_RecordedFilm`,
`_ComposedFilm`, `_VirtualFilm`) sprawiają, że czyta się to jak kolejność, a
nie jak dwa sąsiadujące akapity. Repo nie ma type-checkera, więc te typy są
czytelnością, nie wymuszeniem — realną ochroną zostaje test z fazy 0
(`test_popup_is_composed_before_time_editing_and_feeds_it`).

Szwy `compose_popup_video` i `probe_frame_count` wędrują z wywołaniami do
`post` (3 miejsca podmiany).

_run.py: 84 linie i ZERO szwów — cała reszta przeniosła się do modułu, który
faktycznie konstruuje albo woła daną nazwę.
`_render_step` to dyspozycja na `kind` scenariusza ORAZ na `cached.action`
sidecara, w relacji wiele-do-wielu (`teach` zamraża się do dowolnej akcji,
`wait` rozjeżdża się między obie na `requires_target()`). Pojedynczy rejestr
na `kind` byłby strukturalnie błędny — każdy handler musiałby powtórzyć
strażników zamrożonej akcji, którzy siedzą MIĘDZY dyspozycjami.

Kształt: dwa krótkie łańcuchy `if` z jednolinijkowymi delegacjami i kontekstem
`_Replay` jako JEDNYM argumentem, nie ośmioma pozycyjnymi. `say`, które nie
potrzebuje niczego, zostaje dwoma liniami `return`, a nie pustą implementacją
jednolitego protokołu.

Druga dyspozycja NADAL nie ma `else` — nieznana akcja sidecara nic nie robi i
nic nie zgłasza. To utajony błąd z backlogu specu; naprawa jest zmianą
zachowania i należy do osobnego commita z własnym testem. Tutaj przenoszony
bajt w bajt, z jawnym komentarzem w docstringu modułu.

Domknięcie `mark_click_started` (z `nonlocal`) staje się `_ClickWatch` —
znowu metoda związana, czytająca żywy zbiór stron w chwili kliknięcia.

_render_step: CC 49 -> 2. _step.py: 537 linii.
…bajt

8 z 19 punktów tej funkcji to dwa domknięcia (`build_track`, `build_bounded`)
i zostają na miejscu: jedno z nich domyka cztery zmienne i jako funkcja
modułowa wymagałoby przewleczenia sześciu parametrów przez `asyncio.to_thread`.

Wychodzą dwie rzeczy: strażnik przekroczenia narracji (`_assert_narration_fits`)
i zbiórka z drenażem po anulowaniu (`_gather_tracks`). Ta druga to poprawny
względem anulowania asyncio z ręcznym shield/drain, od którego zależy czas
życia `TemporaryDirectory` — przeniesiona bez jednej zmiany, z komentarzem
„nie sprzątać".

_mux_tracks_for_timeline: CC 19 -> 9.
…Kind

Druga dyspozycja `_render_step` nie ma `else`: nieznana akcja nic nie robi i
nic nie zgłasza. To utajony błąd, starszy niż faza 3, i spec trzyma go w
backlogu, bo naprawa jest ZMIANĄ ZACHOWANIA. Ten commit jej nie robi.

Robi coś, co daje tę samą ochronę bez zmiany zachowania: przypina totalność
dyspozycji. Dziś gałąź `else` jest nieosiągalna, bo `CachedAction.action` to
`ActionKind` — `Literal` z sześcioma wartościami na modelu pydantica, więc
sidecar z czymkolwiek innym leci przy wczytaniu. Realnym ryzykiem jest SIÓDMA
akcja dopisana do `ActionKind`, której dyspozytor po cichu zignoruje — krok
zagrałby jako pusta pauza w gotowym filmie.

Strażnik czyta AST `_replay_action` i porównuje zbiory. Zweryfikowany
mutacyjnie: usunięcie gałęzi `waitFor` wywala go z nazwą osieroconej akcji.
@mpasternak
mpasternak merged commit 4ac2487 into main Jul 23, 2026
3 checks passed
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