# DEEP SYSTEM AUDIT — VILMAL ## Głęboki audyt architektoniczny i kontrola integracji (tryb READ-ONLY) **Data audytu:** 2026-10-01 **Zakres:** `server/src`, `web/src`, `services/recolor`, `scripts`, `docs` **Metoda:** wyłącznie inspekcja kodu źródłowego, schematu i konfiguracji. **Nie wykonano:** testów, builda, eksportów, wywołań BaseLinkera, inspekcji wizualnej ani zapytań do `vilmal.db`. Wszystkie stwierdzenia o „działaniu" opierają się na analizie ścieżek kodu, nie na uruchomieniach. --- ## 0. Streszczenie wykonawcze System VILMAL **nie jest** prowizorką jednorazową — ma prawdziwe warstwy domenowe (fakty, warianty, outbox, kolejka zadań, media review, provenance), transakcyjność SQLite z savepointami, i architekturę „człowiek zatwierdza, AI doradza". Jednocześnie **nie jest gotowy na dowolny mebel z fabryki**: istnieją sztywne wartości zaszyte pod OSKAR/TILIA/FUJI (seed przy każdym pustym starcie, domyślna liczba poduszek `5`, fallback SKU `VIL-OSK-TIL`, profile per-slug), pozycyjna heurystyka ról galerii vendora, brak twardych ograniczeń negatywnych w promptach AI, oraz maski segmentacji wypiekane automatycznie bez jakiejkolwiek akceptacji człowieka. Najpoważniejsze pojedyncze defekty: 1. **Weryfikacja eksportu sprawdza tylko SKU + nazwę** — pominięte zdjęcia (>2 MB lub błąd dekodowania), błędny EAN, złe `features` czy brak `parent_id` nigdy nie zostaną wykryte odczytem zwrotnym (Filar 1a/1c). 2. **Brak kanału negatywnego promptu** — `ImageEditRequest` nie ma pola negative; wszystkie ograniczenia są klauzulami tekstowymi w jednym prompcie, bez `no recliners / no blankets / stationary sofa` (Filar 2b). 3. **`cushions.loose_back_count ?? 5`** — produkt bez potwierdzonego faktu dostanie w prompcie „dokładnie 5 poduszek" (wartość OSKAR-a) (Filar 2b/5a). 4. **Maska sidecar wypieka się automatycznie przy pierwszym recolorze, bez przeglądu człowieka**, a pliki `.mask.png` i cache `recolor-v7` nie są śledzone w DB — zostają osierocone na dysku po skasowaniu produktu/mastera (Filar 3a). 5. **Pojedynczy recolor działa synchronicznie w żądaniu HTTP** — na ścieżce bez sidecara to minuty pracy CPU/GPU wewnątrz requestu (Filar 3d). 6. **EAN zwolniony do puli może zostać przypisany innemu produktowi**, podczas gdy stary wariant nadal żyje w BaseLinkerze — brak stanu „burned/tombstone" (Filar 4b). 7. **`seedIfEmpty` wkłada całego OSKAR-a do każdej pustej bazy** — świeża instalacja produkcyjna startuje z cudzym produktem, faktami „confirmed", tkaniną TILIA i matrycą wariantów (Filar 5a). --- ## FILAR 1: INTEGRACJA Z BASELINKEREM I TRANSAKCYJNY OUTBOX ### 1a. Idempotentność SKU — update czy kolejne add? - **[`exportOutbox.dispatchExportBatch` → pętla wysyłkowa]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** `server/src/domain/exportOutbox.ts:539-580` - 539-548: rekonsyliacja `unconfirmed` — odczyt `getProductData(external_id)` i `findProductIdBySku`. - 556-565: przy `external_id == null` wyszukiwanie istniejącego produktu po SKU (`findProductIdBySku`) — jeśli znaleziono, `external_id` jest ustawiane i pozycja przechodzi do `verified` **bez ponownego wysyłania** (update pól pominięty). - 577-580: `wire.product_id = item.external_id`, potem `adapter.addInventoryProduct(...)`. - `server/src/integration/baseAdapter.ts:30-35` — interfejs `BaseAdapter` ma **tylko** `addInventoryProduct`, `getInventoryProductsData`, `findProductIdBySku`, `getInventories`. **Nie istnieje `updateInventoryProduct`.** - `docs/BASE-KONTRAKT.md:113` — semantyka API: `product_id` w requeście = update, bez = create. Czyli update jest realizowany przez `addInventoryProduct` z `product_id` — mechanizm działa, ale **jawna metoda update nie istnieje**. - **OPIS PROBLEMU I RYZYKO:** - Ponowny eksport **nie tworzy duplikatów**: najpierw rekonsyliacja po `external_id`/SKU, potem add-z-`product_id` (update). To dobrze. - **Ale:** wiersz 565: jeśli produkt istnieje w Base (znaleziony po SKU), pozycja dostaje `verified` **bez wysłania jakichkolwiek danych** — zdjęcia, cena, EAN, features ze starego rekordu zostają nietknięte, a UI raportuje sukces. Dla nowego mebla, który przypadkiem dostanie SKU kolidujące z czymś w magazynie, system „zweryfikuje" cudzy rekord jako własny. - `verifyReadBack` (`exportOutbox.ts:453-466`) porównuje **wyłącznie `sku` i `name`** — nie weryfikuje liczby zdjęć, EAN, `features`, `parent_id`, cen. Eksport „zweryfikowany" może mieć w Base pustą galerię. - `worker.ts:919` — fallback SKU do odczytu: `VIL-OSK-TIL` wpisany na sztywno jako wartość domyślna. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** 1. Dodać do `BaseAdapter` jawną metodę `updateInventoryProduct(productId, params)` — nawet jeśli wewnętrznie wywołuje `addInventoryProduct` — żeby intencja była czytelna i testowalna. 2. Rozbić `verifyReadBack` na checksum pól krytycznych: `sku`, `ean`, `parent_id`, liczba niepustych slotów `images`, klucze `features`. Rozbieżność → `unconfirmed` z listą pól. 3. Gdy produkt znaleziony po SKU (gałąź 565) — wysłać pełny update zamiast cichego `verified`, albo wymusić decyzję operatora („znaleziono istniejący rekord — nadpisać?"). 4. Usunąć fallback `VIL-OSK-TIL` z `worker.ts` — błąd zamiast domyślnego SKU cudzego produktu. ### 1b. Słownik parametru „Kolor" i nazwa handlowa odcienia - **[`baseColor.mapBaseColor` + `offerContent` features]** - **STATUS: PRODUKCYJNY (z zastrzeżeniem)** - **DOWÓD W KODZIE:** `server/src/domain/baseColor.ts:14-62`; `server/src/domain/offerContent.ts` (features `Tkanina`, `Kod odcienia`, `Odcień tkaniny`, `Kolor`; fabricLabel w nazwie wariantu) - `BASE_COLOR_VALUES` (14-29): dokładnie 14 wartości słownika — Antracyt, Beżowy, Biały, Brązowy, Czarny, Inny kolor, Kremowy, Natura, Niebieski, Różowy, Srebrny, Szary, Wielokolorowy, Zielony. - `NAME_RULES` (37-46): regexy na nazwie/kodzie odcienia (np. `szar|grey|graphite` → Szary, `silver|srebr` → Srebrny). - `colorFromHex` (49-62): fallback HSL z `color_hex` — potrafi zwrócić ~11 wartości; **„Natura" i „Wielokolorowy" są nieosiągalne** (żadna reguła ich nie produkuje — trafiają w „Inny kolor"). To akceptowalne (słownik to kategoria, nie ścisłość), ale warto znać. - Nazwa handlowa (`LUMO 30`, `TILIA 86`) trafia do: (a) **tytułu wariantu** — `fabricLabel = "${fabric.name} ${shade_code}${shadeName}"` w name/text_fields.name; (b) **dedykowanych cech** `Odcień tkaniny` i `Kod odcienia` w `features` (→ parametry Allegro przez reguły mapowania BL). Wartość słownikowa idzie wyłącznie do `Kolor`. - **OPIS PROBLEMU I RYZYKO:** Mapowanie jest sensowne i dwustopniowe. Ryzyko: `color_hex` to „przybliżony hex chipów UI, nie pomiar barwy" (komentarz migracji v2 w `db.ts:34-37`) — jeśli hex jest przypadkowy, kategoria `Kolor` też. Dla odcieni dwukolorowych/wzorzystych zawsze wyjdzie „Inny kolor". Pełne pokrycie 14 wartości formalnie nie jest osiągalne dla „Natura"/„Wielokolorowy". - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Zmapować „Natura" dla beżowo-drewnopodobnych heksów lub dodać regułę nazwy (`natura|natural|wood`); „Wielokolorowy" jawnie dla tkanin melange/wzór — np. flaga na `fabric_shades`. (2) Traktować `color_hex` jako dane wymagające potwierdzenia przy imporcie odcienia (już częściowo jest — `importShadeHex`), a nie automat. ### 1c. Hosting zdjęć — data URI, limity, brak CDN - **[`materializeMedia` + brak publicznego hosta]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** `server/src/domain/exportOutbox.ts:185-221`; `server/src/integration/baseAdapter.ts` (timeout 30 s, mapowanie błędów sieci → `ExportUncertain`); `docs/BASE-KONTRAKT.md:66-87` - `MAX_IMAGE_BYTES = 2*1024*1024 - 4096`, `MAX_VIDEO_BYTES = 15 MB`. - `materializeMedia` (195-221): zdjęcia/wideo trafiają do payloadu jako `data:` (zgodnie z kontraktem BL — bez prefiksu MIME). Format `url:` **nie jest nigdzie obsługiwany**. - Nie istnieje żadna konfiguracja publicznego hosta/CDN assetów (`settings.ts` nie zna kluczy host/url; `config.ts` nie ma `publicAssetHost`). BASE-KONTRAKT.md:79 uzasadnia: panel lokalny, brak publicznych URL. - Przekroczenie limitu → ostrzeżenie „pominięto zbyt duże zdjęcie" i **slot pomijany po cichu** — item dalej może dostać `verified`, bo verifyReadBack nie liczy zdjęć. - Rodzic (`baseContract.translateParentParams`) nie dostaje **żadnych** zdjęć — galerie są tylko na wariantach. - **OPIS PROBLEMU I RYZYKO:** 8 rendycji JPEG (≤1500 px, q80) w base64 → typowo kilka MB w jednym requestcie. Przy wolnym łączu 30 s timeoutu wystarczy na `ExportUncertain` i retry całości (brak resume mediów — każda próba przesyła wszystkie sloty). Główne ryzyko to **cichy ubytek zdjęć** (limit sieci/API → ostrzeżenie w item.warnings, nie błąd), którego odczyt zwrotny nie wykryje. Brak CDN to decyzja architektoniczna, nie bug — ale oznacza, że masowe aktualizacje galerii zawsze pchają megabajty. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Preflight payloadu — policzyć rozmiar po base64 i odrzucić wsad z listą przekroczonych slotów zamiast cichego pomijania. (2) Rozszerzyć `verifyReadBack` o liczbę slotów `images`. (3) Dodać opcjonalny `publicAssetBaseUrl` (konfig) z transportem `url:` — rendycje są deterministyczne po SHA, idealne do cache'owania; zachować `data:` jako fallback. ### 1d. Relacja Parent-Child - **[`outbox` ordering + `parent_id` injection]** - **STATUS: PRODUKCYJNY** - **DOWÓD W KODZIE:** `server/src/domain/exportOutbox.ts` (outbox: parent position 0; dispatch: 591 `parameters.parent_id = parentExternalId`; 600-606 błąd rodzica przerywa wsad); `server/src/domain/baseContract.ts` (wariant `parent_id: null` w podglądzie — placeholder `{{BASE_PARENT_PRODUCT_ID}}`); `docs/BASE-KONTRAKT.md:53-62` - Rodzic wymaga potwierdzonego `base.parent_sku` (walidacja w budowie wsadu). - Wysyłka idzie w kolejności `position` — rodzic pierwszy; po `verified` jego `external_id` wstrzykiwane jest wariantom jako `parent_id`. - Awaria rodzica → `break` — warianty nie zostają sierotami. - Praktyka potwierdzona dokumentem audytowym: rodzic `690927524` + 34 warianty powiązane `parent_id` (`docs/RAPORT_AUDYTOWY_SYSTEMU.md:160`) — zapis live wykonany wcześniej, poza tym audytem. - **OPIS PROBLEMU I RYZYKO:** Mechanizm jest poprawny i odporny na retry (rekonsyliacja + read-back). Ryzyko resztkowe: rodzic bez galerii i bez ceny/stanu — zależnie od konfiguracji panelu BL oferta wielowariantowa może pokazywać pustą kartę rodzica ([TEST] w kontrakcie). Wariant `unconfirmed` po częściowej awarii może wisieć z `parent_id` już ustawionym — rekonsyliacja to ogarnia. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** Dodać do rodzica przynajmniej 1 reprezentatywne zdjęcie (hero mastera) i zweryfikować na koncie docelowym, jak BL traktuje rodzica przy wystawianiu oferty (checklista §6.6 kontraktu — nadal otwarta). --- ## FILAR 2: POTOK GRAFICZNY AI (RUNCOMFY, RECIPES, 8 RÓL UJĘĆ) ### 2a. Warunkowość nóżek (`furniture_leg_id` vs `null`) - **[`recipes.buildRecipe` + worker resolve legs]** - **STATUS: PRODUKCYJNY** - **DOWÓD W KODZIE:** `server/src/worker.ts:479-489` (fakt `legs.furniture_leg_id` → rekord `furniture_legs` → `ai_prompt` + `image_asset_id`); `server/src/worker.ts:532-545` (`legsNote` domyślne + dołączenie obrazu nóżki jako 3. referencji); `server/src/ai/recipes.ts:125-133` (klauzula w prompcie) - `legs.furniture_leg_id != null` → do promptu trafia `legs.ai_prompt` z katalogu Mir-Tex24 (sparametryzowane per nóżka) i ewentualnie zdjęcie nóżki jako osobna referencja. - `== null` → `legsNote` puste → generyczna klauzula „nie doklejaj nóg niewidocznych w referencji" — oryginalne nogi z mebla zostają. - Trasy CRUD katalogu i przypisania do produktu istnieją (`routes.ts`, `FurnitureLegsPage.tsx`). - **OPIS PROBLEMU I RYZYKO:** To jest poprawnie sparametryzowane — **nie** hardcode. Ryzyko resztkowe: dla produktu z nogami własnymi, których nie widać wyraźnie na referencji, model może je „domknąć" błędnie (klauzula mówi tylko „nie dodawaj niewidocznych"); oraz kolejność referencji (produkt → próbka → nóżka → reszta) jest stała i nieadnotowana semantycznie — model sam musi wywnioskować, że trzeci obraz to nóżka. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** Dodać do promptu jawny segment „referencja N: nóżka montażowa X" (etykietowanie ról referencji w tekście) i fakt `legs.original_visible` dla mebli z własnymi nogami, żeby klauzula brzmiała „zachowaj nogi z referencji" zamiast „nie dodawaj nóg". ### 2b. Negatywne prompty i twarde ograniczenia — skąd halucynacje - **[Brak kanału negative + miękkie klauzule]** - **STATUS: BŁĄD ARCHITEKTURY** - **DOWÓD W KODZIE:** - `server/src/ai/provider.ts:7-14` — `ImageEditRequest` ma wyłącznie `prompt`, `images`, `aspectRatio`, `quality`. **Nie istnieje pole `negative_prompt`** — żaden adapter nie może wysłać negatywu osobnym kanałem. - `server/src/ai/runcomfy.ts` — payload do providera nie zawiera negative. - `server/src/ai/recipes.ts:152-172` — ograniczenia jako klauzule w jednym prompcie: „do not invent hidden parts", „do not add pillows, text or props", zachowanie geometri/podłokietników/szezlonga/mechanizmu. **Brak fraz `stationary sofa`, `uniform upholstery`, `no recliners`, `no blankets`.** - `recipes.ts:152-155` — świadoma decyzja: fakty mechanizmu NIE są wstrzykiwane do promptu; stan ma wynikać wyłącznie z referencji. - `server/src/worker.ts:545` — `Number(factVal("cushions.loose_back_count") ?? 5)` — **domyślne 5 poduszek** (wartość OSKAR-a) trafi w prompt każdemu produktowi bez potwierdzonego faktu. - **OPIS PROBLEMU I RYZYKO:** Mechanizm nożycowy i narzuta na siedzisku mogły powstać, bo: (1) stan mechanizmu zależy w 100% od interpretacji referencji — przy niejednoznacznym ujęciu model „domyśla" typowy dla klasy mechanizm rozkładania; (2) lista zakazów nie nazywa typowych artefaktów tej kategorii (narzuty/pledy, mechanizmy relax/nożycowe, dodatkowe poduszki dekoracyjne); (3) `gpt-image`-klasa modeli bez osobnego negative channel słabo egzekwuje zakazy wplecione w długi prompt; (4) `?? 5` potrafi wymusić błędną liczbę poduszek. Nowy mebel innej klasy (sofa bez funkcji spania, fotel) dostanie prompt zbudowany dla narożnika OSKAR. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** 1. Rozszerzyć `ImageEditRequest` o `negativePrompt?: string`; tam gdzie provider go nie wspiera — dołączyć jawny blok `NEGATIVE:` na końcu promptu. 2. Wprowadzić per-rolę listę twardych zakazów z faktów produktu: np. `stationary sofa` gdy `mechanism.sleep = none`; `no recliner mechanism`, `no blankets/throws`, `uniform upholstery`. 3. `cushions.loose_back_count`: brak faktu → pominąć klauzulę liczby poduszek zamiast defaultu 5; default jest bezwzględnym bugiem produktowym. 4. Rozważyć wstrzyknięcie faktów mechanizmu jako warunku walidacji *po* generacji (Maciuś regułami), nie tylko w referencji. ### 2c. Heurystyka ról galerii vendora (Bobochic) - **[`productImport` pozycyjne przypisanie + `shotRefs` manual override]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** `server/src/domain/productImport.ts:230-253` — `IMPORT_SHOT_ROLES[i]` przypisuje role wg **pozycji w tablicy zdjęć** vendora, z notatką „Auto z importu vendora (poz. N) — zweryfikuj dopasowanie". `server/src/domain/shotRefs.ts` — jawne przypisania `primary`/`supporting` (domena poprawna, manualna). `server/src/routes.ts:1140-1154` — generacja wymaga primary (brak = 422, dobry fail-fast). - **OPIS PROBLEMU I RYZYKO:** Kolejność galerii Bobochic jest traktowana jako semantyka. Zmiana układu strony (np. zdjęcie lifestyle przed packshotem, kolejność kolorów) poprzekłada role — i nikt tego nie wykryje automatycznie, bo heurystyka nie waliduje treści obrazu. Dla innego vendora niż Bobochic mapowanie w ogóle nie istnieje. Operator musi ręcznie poprawiać `shot_refs` dla każdego produktu — w praktyce działa to na zaufaniu do kolejności HTML. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Klasyfikator ról na wejściu (vision-LLM / Maciuś batch) — zdjęcie → propozycja roli z confidence, operator potwierdza; pozycja jako hint, nie wyrok. (2) Minimalny zestaw reguł: packshot = białe/jasne tło + cały mebel; storage/sleep = wykryty mechanizm/komora — już teraz silnik recolor ma detektory DINO, które można użyć do pre-labelu. (3) Raport „nieprzypisane role" przed generacją, nie po. --- ## FILAR 3: SILNIK RECOLOR V7 (PYTHON / SERVICES / MASKI / LAB) ### 3a. Maska sidecar i cache — fizyczna lokalizacja, inwalidacja - **[`service.py` sidecar + `recolor.ts` cacheKey]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** - `services/recolor/fabric_recolor/service.py:210-218` — maska segmentacji zapisywana jako **plik sidecar `.mask.png` obok mastera** w `media/assets//`; wypiekana automatycznie przy pierwszym recolorze danej ramy (weryfikowano istnienie plików `*.mask.png` w `data/media/assets/`). - `server/src/domain/recolor.ts:692-700, 830-843` — `maskKey` = SHA sidecara wchodzi w klucz cache wyniku (`derived/recolor-v7/.jpg`), razem z SHA mastera, kolorami, wersją silnika. - Odrzucenie mastera → `approvedAssetForRole` wskazuje inny asset → inny SHA → inny sidecar → automatyczny re-bake przy kolejnym recolorze. Inwalidacja logiczna **działa przez adresowanie treścią**. - `server/src/domain/product.ts:109-114` — kasowanie produktu usuwa tylko `assets.path`/`thumb_path` i `channel_renditions`; **pliki `.mask.png` i katalog `derived/recolor-v7` nie są w żadnej tabeli → zostają na dysku jako osierocone**. - Komentarz w `service.py` mówi o „zatwierdzonej masce", ale **w kodzie nie istnieje żaden krok akceptacji maski przez człowieka** — jedyne straże to progi coverage w `service.py` i `--force`/`--bake-mask` w CLI (skryptowym). - **OPIS PROBLEMU I RYZYKO:** Architektura content-addressed jest poprawna i daje darmową inwalidację. Ale: (1) błędna maska wypieka się i wielokrotnie serwuje bez możliwości przeglądu w UI — operator widzi tylko wynik; (2) regeneracja wadliwej maski wymaga ręcznego usunięcia pliku lub CLI `--force`; (3) kasowanie produktów zostawia sierocy sidecar i cache — dysk rośnie w nieskończoność; (4) maska nie ma provenance (nie wiadomo, który backend/parametry ją zrobiły poza notes w PNG). - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Tabela `segmentation_masks` (asset_id, sha, backend, notes, status candidate/approved) — maska jako obiekt domenowy z przeglądem w MediaReview i decyzją człowieka przed batch. (2) Dołączyć sidecar/cache do kasowania produktu i do backupu. (3) Dodać endpoint „odśwież maskę" (re-bake + podgląd overlay) w UI zamiast CLI. ### 3b. Auto-protect — co jest chronione i dlaczego poduszka wypadła - **[`prompts.py` CAVITY/APPENDAGE + `subtract_protected`]** - **STATUS: PRODUKCYJNY (z ryzykiem regresji per-produkt)** - **DOWÓD W KODZIE:** - `services/recolor/fabric_recolor/segmentation/prompts.py:28-38` — `CAVITY_PROMPTS` (wnętrze komory: „storage compartment", „wooden board", „furniture base"…) i `APPENDAGE_PROMPTS` (wyrostki na obrysie: nogi, okucia). - `birefnet.py:186-219` — maska = matte BiRefNet całego mebla, od niej odejmowane instancje negatywów: kieszeń komory = kotwica DINO + deficyt głębi DAv2 (`cut_anchored_pockets`); reszta negatywów przez `subtract_protected` — tnij, gdy komponent dotyka brzegu obiektu **lub** różni się o ΔE>12 od mediany wnętrza tapicerki. - `masks.py` — `recessed_pocket_labels`, `cavity_anchors`, `subtract_protected` (ΔE guard). - `birefnet.py:198-200` — komentarz: „studnia siedziska bez kotwicy komory zostaje". - **OPIS PROBLEMU I RYZYKO:** Poduszka siedziska mogła zostać wycięta, gdy: (a) fałszywa detekcja negatywu (np. „furniture base") położyła na niej kotwicę komory → kieszeń głębi wycięła studnię siedziska; **lub** (b) instancja wewnętrzna przekroczyła ΔE>12 względem mediany — siedzisko w innym świetle/z fałdami materiału odbiega jasnością od reszty i system uznał je za „nietapicerowane". Nie istnieje **pozytywna** ochrona klasy „siedzisko/poduszka" — protect działa wyłącznie od strony negatywów. Na innym meblu (jasna tapicerka + ciemne wcięcie) ta sama reguła wycięłaby inny element. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Dodać `KEEP_PROMPTS` (seat cushion, backrest, armrest) — detekcje pozytywne nadpisują negatywy w obszarze kolizji. (2) Podnieść ΔE dla instancji w pełni wewnętrznych lub wymagać potwierdzenia kotwicą zamiast samego ΔE. (3) Raportować w UI listę wyciętych instancji z klasą frazy (notes już istnieją — pokazać je operatorowi przed akceptacją). ### 3c. Matematyka Lab — dlaczego Tilia 62 wypaliła do kremu - **[`transfer.build_delta` + `transform_l` — brak ochrony chromy]** - **STATUS: BŁĄD ARCHITEKTURY (znana, nierozwiązana regresja kolorystyczna)** - **DOWÓD W KODZIE:** `services/recolor/fabric_recolor/transfer.py:33-72` - `build_delta`: `d_l = target_L - anchor_L` ze statystyk próbek (średnie Lab). Dla `d_l > 0` mapa liniowa L: `k = (100 - map_t)/(100 - map_a)`, gdzie `map_a` = średnia L wnętrza mastera, `map_t = clip(anchor_L + d_l, 1, 99)`. - `transform_l`: przekształcenie liniowe bez „kolana" (brak asymptoty/flooru jak w starszych wariantach — `floorP` dotyczy tylko podłogi w innej gałęzi); wszystko powyżej średniej skalowane w stronę 100. - Składowe a/b dostają wyłącznie **jednolitą deltę** (średnia a/b kotwicy → celu); chroma nie jest skalowana proporcjonalnie do zmiany L. - `swatch_stats` liczy średnią z próbki z crop ~8%; `anchor_mode: "swatch"` wymaga faktu `fabric.photographed_shade` (`recolor.ts:723-727`). - **OPIS PROBLEMU I RYZYKO:** Brudny róż → krem to klasyczny podpis dwóch mechanizmów naraz: (1) jeśli ΔL celu względem kotwicy jest duże (Tilia 86 ciemniejsza → Tilia 62 jaśniejsza), `map_t` ląduje wysoko — liniowa kompresja odległości od bieli „przykleja" światła przy ~100, a tekstura traci kontrast; (2) przy wysokim L nawet umiarkowane (a,b) różu wypada poza gamut sRGB → clip do prawie bieli, bo chroma **nie jest** skalowana wraz z L. Do tego `anchor_mode: swatch` uzależnia całość od jakości oficjalnej próbki — jeśli próbka Tilia 62 jest zbyt jasna/prześwietlona (lub `crop_frac` łapie obramowanie), cel jest fałszywy od wejścia. Brak sanity-bound na |ΔL| i brak progu chromy — system nie umie powiedzieć „ta delta jest nieprawdopodobna". - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Skalować chromę proporcjonalnie (C* ratio cel/kotwica), nie jednolitą deltą a/b. (2) Wprowadzić kolano/asymptotę w mapie L (jak w legacy `kneeT`) albo twardy sufit L efektywnego < ~92 dla mebli. (3) Sanity check ΔL/ΔE między próbką kotwicy a celem z ostrzeżeniem/blokadą (np. |ΔL|>25 → flaga QA zamiast cichego transferu). (4) Walidacja próbki (wariancja po cropie, margines od bieli) zanim stanie się celem. ### 3d. Kolejka i obciążenie sprzętu - **[`jobs`/`worker` sequential + subprocess per wariant; pojedynczy recolor synchroniczny w HTTP]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** - `server/src/routes.ts:1432-1483` — `POST /variants/recolor-batch` → `enqueueJob("recolor_batch")` (max 600 zadań, deduplikacja par variant+role). - `server/src/domain/jobs.ts:81-103` — `claimNextJob` atomowo, **jeden worker = jeden job naraz** → concurrency recolorów = 1, sekwencyjnie. - `server/src/worker.ts` — `runRecolorBatch` grupuje po roli (master+maska dekodowane raz na rolę), ale każdy wariant to osobny spawn `python -m fabric_recolor.cli` (`runFabricRecolorCli`, `RECOLOR_TIMEOUT_MS` ~900 s). - `birefnet.py:236` — `torch.cuda.empty_cache()` po każdej segmentacji (higiena VRAM na 4 GB). - **`server/src/routes.ts:1408-1426` — `POST /variants/:variantId/recolor` (pojedynczy) wywołuje `buildRecolor` wprost w żądaniu HTTP** — przy zimnej masce to może być minuty pracy wewnątrz requestu (CPU path znacznie wolniejszy niż 74 s GPU). - **OPIS PROBLEMU I RYZYKO:** Wsad jest poprawnie w tle i nie zarzyna maszyny równoległością (concurrency 1, cache po SHA). Ale: (a) każdy spawn CLI ładuje modele na nowo, chyba że istnieje sidecar — koszt stały per wariant, amortyzowany tylko przez cache maski, nie przez rezydencję modeli; (b) synchroniczny endpoint pojedynczego recoloru może zawiesić HTTP na minuty i zablokować... — worker jest osobny, więc nie blokuje kolejki, ale wisi request i UI; (c) brak limitu kolejki — 600-taskowy batch + inne joby czeka w jednej kolejce. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Usunąć synchroniczny recolor z HTTP — wszystko przez `recolor_batch` (nawet n=1). (2) Opcjonalny tryb daemon/keep-alive procesu Pythona (stdin/stdout JSON) zamiast spawn-per-wariant — wagi BiRefNet/DINO/SAM/DAv2 rezydentne między wywołaniami. (3) Priorytetyzacja jobów (recolor vs generate vs export) albo osobne kanały workerów. --- ## FILAR 4: KATALOG TKANIN, MATRYCA WARIANTÓW I PULA EAN ### 4a. `shadeCodes` — oficjalna ścieżka czy skrypt - **[`ensureVariantMatrix` + API + UI]** - **STATUS: PRODUKCYJNY** - **DOWÓD W KODZIE:** `server/src/domain/fabric.ts:223-237` (filtr `shadeCodes` w `ensureVariantMatrix`); `server/src/routes.ts` ~401-430 (`POST /api/products/:slug/fabrics` przyjmuje `shadeCodes` i `prune`); `web/src/pages/ProductPage.tsx` (BindFabricForm: `selShades` → `api.bindFabric` z `shadeCodes`, blokada przy 0 zaznaczonych, „Zaznacz wszystkie"); `web/src/api.ts` (typ `bindFabric`) - Operator **w UI może wybrać dowolny podzbiór** (np. 4 z 20) — pełna ścieżka domain→API→UI. - Zastrzeżenie: `orchestrator.ts:355` wywołuje `ensureVariantMatrix` **bez** `shadeCodes` — ścieżka orkiestratora zawsze bierze wszystkie odcienie. - Nieistniejące kody w `shadeCodes` są po cichu filtrowane (`valid`) — brak błędu „odcień X nie istnieje w kolekcji". - **OPIS PROBLEMU I RYZYKO:** Główna ścieżka jest kompletna. Ryzyko: ciche odfiltrowanie literówki w kodzie odcienia (wariant nie powstanie, nikt nie zgłosi); orkiestrator ignoruje selekcję. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Walidacja `shadeCodes ⊆ shades(fabric)` z 422 i listą nieznanych. (2) Przekazać `shadeCodes` przez payload operacji orkiestratora. ### 4b. Pula EAN-13 — transakcyjność rezerwacji/zwalniania - **[`eanPool` + savepointy + release]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** `server/src/domain/eanPool.ts` (`addEans`, `claimNextEan`, `backfillVariantEans`); `server/src/db.ts:493-503` (`tx` = SAVEPOINT — zagnieżdżenia bezpieczne); `server/src/domain/fabric.ts` (cała pętla `ensureVariantMatrix` w jednej `tx` → awaria w połowie wsadu = rollback → claimy EAN cofają się z wariantami); `fabric.ts` ~333 (`releaseEansForVariants` przy prune); `server/src/domain/product.ts:134-137` (zwolnienie EAN przy kasowaniu produktu — „kod jest zasobem") - Rezerwacja i zwrot są atomowe: awaria w środku `ensureVariantMatrix` **nie zostawia spalonych EAN-ów** — transakcja obejmuje INSERT wariantów i claim razem. - **OPIS PROBLEMU I RYZYKO:** Lokalnie poprawne. Realne ryzyko jest **zewnętrzne**: `releaseEansForVariants` zwraca kod do puli FIFO → ten sam EAN może zostać przydzielony wariantowi **innego produktu**, podczas gdy stary wariant nadal istnieje w BaseLinkerze (kasowanie lokalne nie kasuje rekordu zdalnego). Kolizja EAN w magazynie BL to błąd na produkcji. Brak stanu „burned/exported" i brak śladu „ten EAN był już eksportowany pod SKU X". - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Rozdzielić `available`/`retired` — EAN wariantu, który kiedykolwiek trafił do `export_items` ze statusem ≥sending, nigdy nie wraca do puli (tombstone z `last_sku`, `last_product_id`). (2) Zwrot do puli tylko dla wariantów nigdy niewysłanych. ### 4c. Parser Lech Fabrics — fabryka czy jednorazówka - **[`fabricParser` registry + allowlist + testy]** - **STATUS: PRODUKCYJNY** - **DOWÓD W KODZIE:** `server/src/import/fabricParser.ts` (`detectFabricVendor` rozpoznaje `lechfabrics.com`; `parseFabricCollectionPage` dispatchuje do `parseLechCollectionPage`); `server/src/import/fabricLech.ts` (implementacja); `server/src/media/sourceFetch.ts` (host w allowlist fabric); `server/test/fabricLech.test.ts` + fixture `lech-collection.html`; `server/src/domain/fabricImport.ts` (import orkiestruje fetch→parse→DB) - **OPIS PROBLEMU I RYZYKO:** Zarejestrowany w fabryce i otestowany fixturem — to nie jest jednorazowy importer. Ryzyko resztkowe: parser jest dopasowany do obecnej struktury HTML Lech (jeden fixture); inny layout strony kolekcji lub tabela PDF innego producenta wywróci parser bez alarmu (typowy problem scrapingu); `fabricImport.ts:4` docstring wciąż mówi „SIC lub Davis" — nieaktualny opis. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Dodać walidację wyniku parsera (min. liczba odcieni, format kodów) z błędem zamiast pustej kolekcji. (2) Fixture testy dla 2-3 wariantów layoutu. (3) Poprawić docstring. --- ## FILAR 5: DŁUG TECHNICZNY, ATRAPY I GOTOWOŚĆ UI ### 5a. Hardcodes produktowe (`oskar`, `lucien`, `tilia`, `lumo`, `fuji`) - **[Seed + profile + migracja + defaulty]** - **STATUS: CZĘŚCIOWY (system działa generycznie, ale produkcja jest zanieczyszczona danymi demo i domyślnymi wartościami OSKAR-a)** - **DOWÓD W KODZIE:** - **`server/src/app.ts:15,29` + `server/src/seed/oskar.ts`** — `seedIfEmpty` przy każdym pustym starcie wkłada: produkt OSKAR, tkaninę TILIA z ~17 odcieniami, fakty ze statusem `confirmed` (wymiary, `fabric.photographed_shade="86"`, `base.parent_sku`, legs „bez nóżek"), matrycę wariantów, nogi, decyzje. Świeża produkcyjna baza startuje **z cudzym produktem oznaczonym jako potwierdzony**. - **`server/src/domain/productProfile.ts:12-100`** — `PROFILES` mapowane po slug: `oskar`, `oskar-lumo`, `oskar-tilia`, `fuji` (model, fabricName, copyVersion). Fallback generyczny istnieje, ale nowy produkt dostaje `fabricName=""` → tytuł rodzica bez nazwy tkaniny. - **`server/src/worker.ts:545`** — `?? 5` dla `cushions.loose_back_count` (patrz 2b). - **`server/src/worker.ts:919`** — fallback SKU `VIL-OSK-TIL` przy read-back. - **`server/src/db.ts:247-266`** — migracja wpisuje na sztywno wielokąty storage dla konkretnych `source_asset` id OSKAR-a (kompatybilność danych, ale produktowe ID w kodzie migracyjnym). - **`server/src/ai/recipes.ts:99`** — komentarz o OSKARZE bez nóg (nie kod, ale dokumentuje, że klauzula wyrósła z jednego produktu). - **`server/src/import/bobochic.ts`** — statyczna lista nazw modeli (FUJI, LUCIEN…) — to słownik normalizacji parsera vendora, nie logika produktu; klasyfikacja: katalog, nie defect. - Komentarze/przykłady `TILIA 86` w `blueprint.ts`, `productCard.ts` — dokumentacyjne, nie funkcjonalne. - **OPIS PROBLEMU I RYZYKO:** Logika rdzenia (fakty, warianty, outbox, recolor) jest generyczna — szkodliwość hardcodes siedzi w **danych początkowych i defaultach**, nie w gałęziach `if oskar`. Realne konsekwencje dla nowego mebla: (1) świeży deploy ma OSKAR-a z faktami `confirmed`, które nie są faktami użytkownika; (2) produkt bez faktu poduszek dostaje w prompt „5"; (3) profile copy dla nieznanego sluga są puste — spada jakość opisów; (4) migracja z OSKAR-id jest martwa poza bazą demo. - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) `seedIfEmpty` → tylko za flagą `VILMAL_SEED_DEMO=1` lub w `env==='development'`; produkcja startuje pusta. (2) Przenieść `PROFILES` do tabeli (`product_profiles`) zarządzanej z UI albo do faktów `offer.*` potwierdzanych jak reszta. (3) Usunąć `?? 5` i `VIL-OSK-TIL` (patrz 1a/2b). (4) Wydzielić OSKAR-polygons z `db.ts` do danych seed/migracji danych, nie kodu. ### 5b. UI vs CLI — co jest klikalne - **[Mapowanie tras ↔ strony ↔ skrypty]** - **STATUS: CZĘŚCIOWY** - **DOWÓD W KODZIE:** `server/src/routes.ts` (pełna lista tras); `web/src/pages/*` (`ProductPage.tsx`, `ProductsPage.tsx`, `FabricCatalogPage.tsx`, `FurnitureLegsPage.tsx`, `MediaReviewPanel.tsx`, `OfferPackagePanel.tsx`, `ExportPanel.tsx`, `JobsPage.tsx`, `SettingsPage.tsx`); `scripts/*.mts` (79 plików wg `RAPORT_AUDYTOWY_SYSTEMU.md:62`) **W pełni klikalne z UI (istnieje trasa + komponent):** - tworzenie produktu ręcznie i import z URL Bobochic (`ProductsPage`), - import tkaniny, przegląd katalogu, powiązanie tkaniny **z wyborem podzbioru odcieni** (`FabricCatalogPage`, `ProductPage` — 4a), - CRUD nóg i przypisanie do produktu (`FurnitureLegsPage`), - upload źródeł z auto-przypisaniem jako primary, przypisania `shot-refs` (`routes.ts:840-906, 942+`), - generowanie ujęć AI (`shots/generate` + primary wymagane — `ProductPage`), - recolor pojedynczy i wsadowy z selekcją odcieni/stron/ról (`ProductPage`, `MediaReviewPanel`), - przegląd mediów, contact-sheet, prescreen Maciusia, decyzje approve/reject (`MediaReviewPanel` + `routes.ts:745-790`), - fakty produktu (potwierdzanie), treść oferty, snapshoty, paczka ofertowa, - pula EAN i token BaseLinker (`SettingsPage`), - wysyłka do BaseLinkera jednym przyciskiem ze statusem per pozycja (`ExportPanel`), - blueprint generate (trasa `blueprint/generate` + odwołania w `ProductPage.tsx`/`api.ts`), - kasowanie produktu (zwalnia EAN — `product.ts:136`). **Dostępne wyłącznie przez CLI/tsx (`scripts/*.mts`), bez odpowiednika w panelu:** - bake/re-bake i podgląd maski segmentacji (`--bake-mask`, `preview-storage-mask`, `debug-mask`), - raporty preview recoloru / edge-compare / measure-master-drift (narzędzia QA), - operacje masowe ostatnich dni: `deploy-oskar-facts`, `confirm-*`, `fix-*`, `approve-*`, `reject-stale-storage`, `restore-storage-base`, `regen-storage-variants`, `enqueue-*`, `backfill-shade-colors`, `import-tilia-swatches`, `import-fuji`, `verify-shades`, `seed-v14-data`, `export-base-oskar.mts`, - czyszczenie stanów pośrednich (częściowe odpowiedniki istnieją: decyzje assetów w UI, kasowanie produktu w UI — ale nie np. masowe unapprove/prune per rola). - **OPIS PROBLEMU I RYZYKO:** Rdzeń codziennej pracy jest w UI — to duży postęp względem etapu skryptowego. Jednak operacje, które faktycznie ratowały pipeline w ostatnich iteracjach (re-bake maski, masowe decyzje, naprawa faktów, regeneracja storage), nadal wymagają konsoli — czyli osoba bez znajomości tsx nie obsłuży incydentów, choć obsłuży happy path. To dokładnie luka względem celu „pracownik fabryki bez skryptów". - **REKOMENDOWANA NAPRAWA INŻYNIERSKA:** (1) Panel „Operacje naprawcze" per produkt: re-bake maski z podglądem, masowe approve/reject per rola, prune wariantów (prune już jest w API bindFabric — wystawić w UI), regen ujęcia z innego źródła. (2) Skrypty `deploy-facts`/`fix-*` zamienić na edytor faktów wsadowy w UI (bulk confirm z source_ref). (3) Skrypty eksperymentalne oznaczyć w repo jako narzędzia dev, nie ścieżka operacyjna. --- ## MATRYCA GOTOWOŚCI PRODUKCYJNEJ | Filar | Gotowość | Uzasadnienie skrótowe | |---|---|---| | **1. BaseLinker + outbox** | **60%** | Outbox transakcyjny, rekonsyliacja i parent-child działają (potwierdzone architekturalnie i historycznym zapisem). Minusy: brak jawnego update, verifyReadBack tylko SKU+name, ciche pomijanie zdjęć >2 MB, rodzic bez galerii, fallback `VIL-OSK-TIL`, brak CDN/`url:`, otwarte pozycje checklisty (inventory/category/manufacturer/warehouse/price group/allegro account). | | **2. Potok AI** | **45%** | Nóżki i referencje sparametryzowane, wymóg primary = dobry fail-fast. Minusy: brak kanału negative prompt i twardych zakazów (obserwowane halucynacje), `?? 5` poduszek, pozycyjna heurystyka ról vendora, mechanizm zależny wyłącznie od interpretacji referencji. | | **3. Recolor v7** | **55%** | Silnik dojrzały inżynieryjnie (matte+negatywy+głębia, sidecar, cache po treści, kolejka wsadowa). Minusy: maska bez akceptacji człowieka i poza DB, osierocone pliki po kasowaniu, nierozwiązana matematyka wypalania jasnych odcieni (brak skalowania chromy + kompresja do bieli), synchroniczny recolor w HTTP, spawn-per-wariant. | | **4. Tkaniny / warianty / EAN** | **70%** | `shadeCodes` pełna ścieżka domain→API→UI, EAN transakcyjny z savepointami i zwrotem przy prune/delete, Lech w fabryce z testami. Minusy: EAN może wrócić do puli i skolidować z rekordem żyjącym w BL, orkiestrator pomija `shadeCodes`, ciche filtrowanie złych kodów. | | **5. Dług / hardcodes / UI** | **50%** | UI pokrywa happy path niemal w całości. Minusy: seed OSKAR-a w każdej pustej bazie, profile per-slug, OSKAR-id w migracji, defaulty produktowe, a kluczowe operacje naprawcze (maski, masowe decyzje, fakty wsadowe) nadal tylko w `scripts/*.mts`. | | **RAZEM** | **~56%** | System ma solidny szkielet domenowy i prawdziwe integracje, ale „dowolny mebel bez znajomości promptów i skryptów" wymaga domknięcia: (a) hardcoded defaults/seed, (b) akceptacji maski i ochrony pozytywnej w recolorze, (c) twardych ograniczeń AI, (d) głębszej weryfikacji eksportu, (e) UI dla operacji naprawczych. | ### Priorytety naprawcze (kolejność sugerowana) 1. **Usunąć produktowe defaulty z kodu** (`?? 5`, `VIL-OSK-TIL`, seed pod flagą) — to najtańsze naprawy z najwyższym ryzykiem cichej korupcji danych nowego produktu. 2. **Maska segmentacji jako obiekt domenowy z akceptacją** — bez tego każdy nowy mebel może dostać zły recolor z automatu, bez widocznej przyczyny. 3. **Twarde ograniczenia AI z faktów + negative channel** — zamyka klasę halucynacji (mechanizmy, narzuty, liczba poduszek). 4. **Weryfikacja eksportu po pełnym zestawie pól + preflight payloadu** — kończy „zweryfikowane" bez galerii/złym EAN. 5. **Tombstone EAN** — chroni przed kolizją zdalną przy ponownym użyciu kodów. 6. **UI operacji naprawczych** — przenosi system z „inżynier z konsolą" na „pracownika fabryki". --- *Audyt wykonany w trybie read-only. Żadna linia kodu produkcyjnego, rekord bazy ani migracja nie została zmieniona. Nie uruchamiano testów, dispatcha ani żadnych wywołań zewnętrznych.*