# Review — Omurga Chunk 4 (EGT-49) — r3 — onay sonrası teyit turu — 2026-07-26

> **Review type:** impl-review (dar kapsam — onay sonrası üç maddenin teyidi; onaylı gövde yeniden taranmadı)
> **Previous review:** `review-omurga-chunk4-impl-2026-07-26-r2.md` (heyet 138 — ✅ APPROVED)
> **Upstream document(s) read:** `docs/plans/omurga-plan_chunk4.md` §4.4/§4.8, `docs/plans/omurga-plan_chunk5.md` §5.9 D-6/D-7, `docs/plans/omurga-plan_chunk9.md` §9.5 S-16/S-17 + kriter 7
> **Stage locks in effect:** Spec v1.10 + Plan v2.5 locked (pin değişmedi, doğrulandı)

**Bağımsız kanıt (bu makinede):** 278/278 test (851 assertion) — CI #730 iddiası yeniden üretildi. Ciphertext uzunluğu tinker ile ölçüldü; taşma canlı DB'de INSERT denemesiyle gösterildi (aşağıda).

---

## Üç maddenin teyidi

| Madde | Durum | Kanıt |
|---|---|---|
| **1. Kural + tarama (açık soru 1)** | ✓ Doğru yerlerde | chunk5 D-6'ya kural cümlesi + S-16 atfı + "yazıldı ama tutulmuyor" uyarısı bizzat metinde (`omurga-plan_chunk5.md:167`); chunk9 §9.5 **S-16** kesin tarifli (`:95` — `config('crypto.encrypted_columns')`, argüman taraması); §9.5 meta-kuralı S-17'yi de sayıyor (`:98`); kriter 7 "on yedi kural" (`:151`) |
| **2. `original_filename` şifreleme (açık soru 2)** | ❌ **Mekanizma kırık — F-r3-1** | Cast bağlanmış (`DocumentVersion.php:59`, gerekçe yorumu :50-55), test ham kolonu doğruluyor — ama kolon `varchar(255)` kaldı ve şifreli değer sığmıyor (aşağıda) |
| **3. D-2a değişmezi (Sets notu)** | ✓ Doğru yerlerde | Plan §4.4 D-2a (`omurga-plan_chunk4.md:69` — tek uç, parametre, "bilinecek bir yol yoktur"); chunk9 **S-17** (`:96` — iki kolon adı + `Storage::url(`/`temporaryUrl(` global, `app/Support/Files/**` dahil olduğu açıkça yazılmış); arch testi `builds no direct URL to a stored document` mevcut ve yeşil (`tests/Arch/SensitiveDataTest.php:157-179`) |

Ayrıca r2'nin iki notu kapanmış: **N-1** — plan §4.4 "Silme koruması — iki trigger" olarak güncellenmiş (`:73`); **N-2** — `claim()`'e gerekçe yorumu yazılmış (`SecureFileStore.php:172-177`).

## ⚠️ Findings & Risks (in-scope for this review type)

**F-r3-1 — `original_filename` şifreli değeri `varchar(255)` kolonuna sığmıyor; 40+ karakterlik dosya adı yüklemede patlıyor.** *(BLOCKER)*
Kolon `$table->string('original_filename')` olarak kaldı (migration:227) — 255 karakter. Laravel `Encrypter` çıktısı base64(JSON{iv, value, mac}):

- Bu makinede ölçüldü: 15 karakter ad → **200**, 40 karakter → **256**, 100 karakter → **372** karakter ciphertext.
- Canlı kanıt: 44 karakterlik adın ciphertext'i (256 karakter) `document_versions`'a INSERT edildi → **`ERROR: value too long for type character varying(255)`**.

Yani bugünkü kodda `SGK_hizmet_dokumu_[ad]_[TCKN]_2026.pdf` gibi tamamen sıradan bir dosya adı `store()` anında `QueryException` ile düşer. 278 testin tamamı geçiyor çünkü test adları kısa (`12345678901.pdf` — 15 karakter); uzun-ad senaryosunun testi yok. Düzeltme kolonu `text` yapmaktır; testine 100+ karakterlik ad eklenmelidir.

**F-r3-1b (aynı kökten kural eksiği):** chunk5 D-6/D-7'de "şifreli kolonun tipi `text`'tir" kuralı yok — `national_id` için `string()` yazan bir chunk5 implementasyonu aynı tuzağa düşer (TCKN ciphertext'i ~200 karakterle bugün sığar, IBAN/adres sınırı aşar; sığması tesadüf, kural değil). Bir cümle: *"Şifreli kolon her zaman `text`; `key_version` yanında"* — D-7'nin doğal evi.

**F-r3-2 — anon maskesi şifreli kolona düz metin yazıyor; maskeli kopyada her okuma `DecryptException` fırlatır.**
`anon-rules.sql:71-72`: `MASKED WITH VALUE 'masked.bin'`. Kolon artık ciphertext taşıyor; maskeleme bu değeri düz `'masked.bin'` ile değiştirince, maskeli dump'tan beslenen ortamda `original_filename` okuyan her model çözme hatasıyla düşer — maskeli kopyanın varlık amacı çalışan prod-dışı ortamdır (K-2). Değer **`NULL`** olmalı (kolon nullable, cast `null`'ı güvenle geçirir). Katmanın kendisi kalmalı — aşağıda soru 2.

## Soruların cevapları

**1. Yerler ve zorlama yeterli mi; S-16 "yazıldı ama tutulmuyor" sınıfına girer mi?** Yerler doğru (yukarıdaki tablo). Boş liste endişesi pratikte oluşmaz: bugün koşan bir S-16 yok — `rule-scan.sh` chunk9 teslimatıdır ve chunk sırası gereği liste (chunk5) taramadan (chunk9) önce dolar; tarama hiç boş listeyle koşmayacak. `config/crypto.php` iskeletini şimdi koymak tüketicisi olmayan sembol üretir — chunk3/chunk4 tutanağındaki ilkeyle çelişir; **koyulmasın**. Asıl boşluk başka yerde: S-16 listeye güveniyor ama **listenin gerçeği yansıttığını hiçbir şey doğrulamıyor** — chunk7'de birinin `TenantEncrypted` kolonu ekleyip listeye eklememesi sessiz kalır. Öneri (plan değişikliği, karar Sets'in): S-16'ya senkron bacağı — cast'i kullanan kolonlar koddan türetilir ve config listesiyle karşılaştırılır; eksik kolon build'i kırar. `config/audit.php` getTable() emsaliyle aynı desen (desync önleme).

**2. Maskeleme satırı gereksiz mi?** Hayır — **iki katman doğru, değer yanlış** (F-r3-2). Katmanlar farklı tehditlere karşı: şifreleme, anahtarı olmayandan korur; maskeleme, dump ile birlikte `TENANT_ROOT_KEY`'in de taşındığı/sızdığı durumda ayakta kalan bağımsız katmandır ve `anon-rules.sql`'in kendi ilkesi ("kopyaları korur, yerine geçmez") tam bunu söylüyor. Kalsın, ama `MASKED WITH VALUE NULL` olarak.

**3. D-2a taramasının kapsamı doğru mu?** Doğru ve bilinçli iki genişlik var: URL **üreten çağrılar** (`Storage::url(`, `->temporaryUrl(`) globaldir — `app/Support/Files/**` dahil, çünkü D-2a'nın özü URL'nin hiçbir katmanda üretilmemesidir; **kolon adları** ise HTTP/route/js katmanlarıyla sınırlıdır — `SecureFileStore`'un `storage_path`/`original_filename`'e dokunması meşrudur, yazan odur. Gereksiz dar değil. Bir kaçak var (not): `Storage::disk('x')->url($path)` biçimi `Storage::url(` dizgesine uymaz ve ne arch testi ne S-17 tarifi yakalar (`->temporaryUrl(` disk nesnesini yakalıyor, `->url(` tek başına taranamayacak kadar genel). S-17'ye ve arch teste `disk(...)->url(` benzeri dar bir kalıp eklenmeli — chunk9'a not.

**4. N-2 yorumu yeterli mi?** Evet — "sızmış bir yeteneğin doğru cevabı onu öldürmektir" tam doğru çerçeve ve bedelin ucuzluğu da yazılmış (`SecureFileStore.php:172-177`). İsteğe bağlı tek ek: meşru sahibin yeniden bağlantı istemesi ikinci bir erişim logu üretir, yani olay aynı zamanda iz bırakır. Zorunlu değil.

## Verdict: ❌ REJECTED (dar kapsam — yalnız madde 2)

Madde 1 ve 3 doğru yerlere, doğru zorlamalarla yazılmış ve teyit edildi. Madde 2'nin kararı doğru ama implementasyonu bugün kırık: F-r3-1 canlı kanıtla gösterilmiş bir runtime patlaması (kolon `text` olmalı + uzun-ad testi), F-r3-2 maskeli kopyayı bozan maske değeri (`NULL` olmalı), F-r3-1b chunk5'i aynı tuzaktan koruyacak tek cümlelik kural. Üçü de küçük ve mekanik; kapanışta yalnız bu üç kalemin teyidi yeterlidir, tam tur gerekmez. Son karar Sets'indir.
