# Review — Plan: Platform Omurgası v2.0 (EGT-21) — 25.07.2026

> **Review type:** plan-review (index + 9 chunk, bütün olarak; v1.0 REJECTED sonrası yeniden yazım)
> **Reviewed:** `docs/plans/omurga-plan.md` + `omurga-plan_chunk1..9.md` (tamamı okundu)
> **Upstream (kilitli):** `docs/specs/omurga-spec.md` **v1.7** — APPROVED (`review-omurga-spec-2026-07-25-r8.md`) · AD-01…AD-28
> **Previous review:** `review-omurga-plan-2026-07-25.md` (v1.0 — REJECTED, P-1…P-9)
> **Stage locks in effect:** spec v1.7 tartışılmaz · ⛔ sahip kararı: otomatik silme/anonimleştirme yok (planın S-1 taramasıyla doğru uyguladığı teyit edildi, bulgu edilmedi)

---

## P-1…P-9 kapanış doğrulaması

| Bulgu | Doğrulama | Durum |
|---|---|---|
| P-1 ofis çok-tenant erişimi | chunk2 §2.3-2.4 kapsam **listesi** (`app.tenant_ids bigint[]`) + §2.6 dar SECURITY DEFINER agregat sözleşmesi + R-6 testi. Model doğru yönde — kalan boşluklar F1 (DELETE) ve F2 (liste kaynağı) | ✅ büyük ölçüde (kalıntı: F1, F2) |
| P-2 auth bootstrap | chunk2 §2.7 `authenticate_lookup` — spec B-1…B-6'nın birebir karşılığı; A-1…A-6 kuralları ve R-14 testi doğru | ✅ (kalıntı: F4 — bootstrap'ın kapsamadığı auth-öncesi tablolar) |
| P-3 kanca yaşam döngüsü | chunk2 §2.5: kanca **yalnız bekçi** (transaction açmaz, bağlam set etmez, sorgu çalıştırmaz) → reentrancy yapısal olarak yok; transaction sınırı `TenantContext::run()`'da açık | ✅ (kalıntı: F5 — iç içe semantik) |
| P-4 ileri kriter | MFA zorlaması chunk6 §6.4 + kriter 7'de; U-5/A-3 bildirimleri chunk7 kriter 16-17'de; chunk5 yalnız olay yayınını sınıyor | ✅ (yeni benzer kalıntı: F6) |
| P-5 users RLS istisnası | Yapısal olarak yok — `users` tenant tablosu değil (chunk5 §5.1) | ✅ |
| P-6 sessiz RULE | chunk3 §3.2 trigger + `RAISE EXCEPTION`; `audit_activity`'ye konmama gerekçesi (retention chunk-drop) doğru | ✅ |
| P-7 ScopeResolver prod kısıtı | chunk6 §6.5 kısıt + chunk9 §9.6 ön koşul 4 | ✅ |
| P-8 arama + takvim | chunk1 §1.9 `app.immutable_unaccent` (IMMUTABLE sarmalayıcı ihtiyacının fark edilmesi isabetli) + chunk7 §7.7 `reminder_basis=calendar_days` açık varsayım, gerekçeli | ✅ |
| P-9 bağımlılık + audit RLS | chunk8 bağımlılığı chunk3'ü içeriyor; audit tabloları §3.6'da tablolu | ✅ (yeni bulgu: F3 — §3.6 ile §3.4 çelişiyor) |

## Upstream Coverage Matrix (spec v1.7 → plan, özet)

| Spec § | Plan | Durum |
|---|---|---|
| §3, §3.0 (U-1…U-5), §3.1 | chunk5 §5.1-5.2 | ✓ (kriter sınanabilirliği: F6) |
| §3.2 (I-1…I-4) | chunk2 §2.9, chunk4 §4.1 (tenant-anahtarlı hash), S-3 | ✓ |
| §4.1 + B-1…B-6 | chunk2 §2.4-2.7 | ⚠ F1, F2, F4, F5 |
| §4.2 T-1…T-7 | chunk2, chunk6 | ⚠ T-6 sözleşmesi ✓ ama F1/F2; diğerleri ✓ |
| §4.4 V-1…V-5 | chunk6 §6.6 + 404/403 ayrımı | ✓ (404/403 ayrım paragrafı özellikle iyi) |
| §5 (matris, sertleşme, MFA, magic link, A-1…A-6) | chunk5, chunk6 §6.4 | ✓ (F4: magic link token lookup'ının bağlam durumu) |
| §6 Y-1…Y-6 | chunk6 §6.3 | ✓ |
| §7 üç log sınıfı | chunk3 | ⚠ F3 (ofis olaylarının tenant durumu) |
| §8.1-8.4 (D, K serileri) | chunk4 | ✓ (`consents.locale NOT NULL` kriter 29'un doğru DB karşılığı) |
| §8.5 analitik | chunk3 §3.9 + chunk8 §8.6 | ✓ |
| §9 R-1…R-10 | chunk6 §6.7 | ✓ |
| §10 | chunk7 | ✓ (webhook bağlam muafiyeti: F8 terminolojisi) |
| §11 RT-1…RT-6 | chunk8 | ✓ (iki kanal ailesi kimlik ayrımıyla tutarlı) |
| §12 (dil, saat dilimi, arama, takvim) | chunk1 §1.4-1.5, §1.9, chunk7 §7.5/7.7 | ✓ |
| §14 (30 kriter) | chunk9 §9.2 `SPEC_CRITERIA.md` eşleme zorunluluğu | ✓ |

## ✅ What works

- **v1.0'a göre sınıf atlamış bir plan.** Dokuz eski bulgunun dokuzunun da kapanışı gerçek (beyan değil, ilgili bölümler tek tek doğrulandı); özellikle S-9 (`SECURITY DEFINER` envanter taraması — "RLS'i bypass eden tek meşru mekanizmanın envanteri gözden kaçarak büyüyemez") ve iki ayrı izin dünyasının (ofis spatie / müşteri tenant-kapsamlı kendi tabloları) spec §3'ün "yetki tabloları paylaşılmaz" kuralını birebir taşıması üst düzey kararlar.
- **Soru 2 (bekçi tasarımı):** P-3'ü gerçekten kapatıyor. Kancanın hiç sorgu çalıştırmaması ve hiç transaction açmaması reentrancy sınıfını *tanım gereği* yok ediyor; transaction sahipliğinin `Action = iş birimi = transaction sınırı` kuralıyla koda bağlanması doğru. Kalan tek yarım nokta iç içe çağrı semantiği (F5).
- **Soru 3 (çoklu-tenant e-posta girişi):** güvenli. "Şifre eşleşen tek kayıt, aksi halde red + audit, tenant bilgisi verilmez" kurgusu bilgi sızdırmıyor — davranış farkını yalnız doğru şifreyi bilen gözlemleyebilir, o da hesapların sahibidir. `authenticate_lookup`'ın dört alan sınırı R-14 ile test ediliyor.
- **Soru 5 (mali rol ayrımı):** uygulanabilir ve PgBouncer ile çakışmaz — "SET LOCAL ROLE değil ayrı bağlantı/kullanıcı" kararı tam doğru gerekçeye dayanıyor (havuz kullanıcı adına göre ayrışır). Eksikler mekanik: F7.
- **Soru 6'nın büyük kısmı:** kesin muaf tablo listesi (§2.8, "örneğin" yasağı), `reminder_basis` gibi açık varsayımlar, 404/403 ayrımı, Discord PII kararının bilinçli olarak kayda geçmesi, kriter→test eşleme dosyası — kanun standardına uygun netlikler.
- Migration güvenliği yeniden doğrulandı: tüm chunk'lar yalnız CREATE/kolon ekleme; silme/rename/tip değişikliği yok.

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

**F1 (MAJÖR) — `WITH CHECK` DELETE'e uygulanmaz; "çok-tenant kapsamla yazma engellenir" garantisi silme için yok.** (Soru 1'in cevabı.) PostgreSQL'de `WITH CHECK` yalnız INSERT/UPDATE'in *yeni* satırını sınar; **DELETE yalnız `USING`'e tabidir**. §2.4 deseninde DELETE, liste politikasından geçer: ofis kimliği geniş okuma kapsamındayken (`forWrite`'a daraltmadan) kapsamındaki herhangi bir tenant'ın satırını DB katmanında silebilir. UPDATE için garanti çalışıyor (WITH CHECK yeni satırı `write_tenant_id()`'den geçirir) ama silme için ikinci savunma hattı yok — kritik kayıtlar trigger'la korunsa da genel tenant tabloları açık. Çözüm mekanik: tek `FOR ALL` politikası yerine komut bazlı politikalar — `FOR SELECT USING (liste)`, `FOR INSERT/UPDATE` mevcut desen, **`FOR DELETE USING (tenant_id = app.write_tenant_id())`**. `TenantPolicy::apply()` bunu üretmeli; R-serisine "çok-tenant kapsamla DELETE → exception" testi eklenmeli.

**F2 (MAJÖR) — Ofis kapsam listesinin kaynağı tavuk-yumurta; §2.8'in `customers` satırı kendi içinde çelişkili.** Satır hem "tenant listesi **bağlam belirlemeden önce** okunur" diyor hem tabloya `id = ANY(app.tenant_ids())` politikası veriyor — bağlamsız okuma bu politikayla exception üretir. `SimpleScopeResolver` ofis kimliğine "tüm tenant'lar"ı verecekse o listeyi `customers`'tan okumak zorundadır; bunun hangi mekanizmayla (dar bir `SECURITY DEFINER` liste fonksiyonu mu, `customers`'ın gerçekten muaf olması mı) yapılacağı tanımsız. Daha genel hali: **ofis isteklerinin TenantContext'inin her istekte nasıl kurulduğu** (kapsam listesi üretimi → `run()`) hiçbir chunk'ta akış olarak yazılmıyor. "Plan kanundur" standardında implementasyon burada durur.

**F3 (MAJÖR) — Ofis-alanı olaylarının `audit_writes`'taki tenant durumu tanımsız; §3.4 ile §3.6 çelişiyor.** chunk3 §3.4 bu chunk'ta "kimlik doğrulama olayları ve kullanıcı/yetki yönetimi"nin audit'leneceğini söylüyor — bunların önemli kısmı **ofis alanı** olaylarıdır (ofis girişi, `users` yönetimi, `UserPermissionsChanged`) ve hiçbir tenant'a ait değildir. Ama §3.6 tablosunda NULL istisnası **yalnız `audit_activity`'ye** tanınmış; `audit_writes` düz tenant-kapsamlı. Bu haliyle ofis girişinin audit'i ya yazılamaz (bağlam/NULL yok) ya da şema beyanı eksik. `audit_writes`/`audit_access` için de tenant kolonunun nullable olduğu, NULL'un yalnız ofis-alanı olaylarında üretildiği ve okumasının nasıl kısıtlandığı §3.6'daki `audit_activity` disipliniyle aynı açıklıkta yazılmalı.

**F4 (MAJÖR) — Auth-öncesi ve istek-yaşam-döngüsü bağlam kurulumu eksik: `magic_links` ve `customer_user_sessions` bootstrap istisnasının dışında kalıyor.** Bootstrap kapısı yalnız `authenticate_lookup`(e-posta) — doğru ve dar. Ama: (a) **magic link doğrulaması** token'ı bulmak için `magic_links` tablosunu okumak zorunda; tablo tenant-kapsamlı görünüyor (kolonlarında `tenant_id` var, §2.8 muaf listesinde yok) → bağlamsız token lookup bekçiye ve RLS'e takılır. Spec B-6 "tenant bağlantıdan bilinir" der ama *kaydı okumadan* tenant bilinemez — tablonun muaf listesine alınması ya da dar bir ikinci lookup fonksiyonu tanımlanması gerekir; hangisi olduğu yazılmamış. (b) **Login sonrası her istekte** bağlamın nasıl yeniden kurulduğu (oturumdan `tenant_id` okunup `TenantContext` açılması; guard'ın `customer_users` kaydını hangi sırayla yüklediği; `customer_user_sessions`'ın bu akıştaki yeri) hiçbir chunk'ta tanımlı değil. Middleware sırası kanun netliğinde yazılmalı.

**F5 (minör) — İç içe `TenantContext` semantiği yarım: daraltma ve geri yükleme tanımsız.** §2.5 "iç çağrı yeni transaction açmaz, **mevcut bağlamı kullanır**; kapsamı alt küme olmalı" diyor. "Mevcut bağlamı kullanır"sa `forWrite(5)` dış `run([1..10])` içinde çalışmaz — `write_tenant_id()` liste >1 diye exception verir; bu ise ofisin ana deseni ("okurken geniş, yazarken tek müşteriye daralt") demektir. İç çağrının `SET LOCAL` ile **daralttığı** ve `$fn` bitince **eski kapsamı geri yüklediği** açıkça yazılmalı (aynı transaction'da `SET LOCAL` yeniden set edilebilir); R-11'in "alt küme kabul" ifadesi bu semantiği sınayacak şekilde netleşmeli.

**F6 (minör) — chunk5'in 5, 7 ve 11 numaralı kriterleri "panel yetkisi" kavramına dayanıyor; izin altyapısı chunk6'da.** P-4'ün hafif bir tekrarı: U-1 testi ("panel yetkisi olmayan kişi URL ile reddediliyor") ve step-up testi ("panel yetkisi gerektiren rotada şifre/2FA") panel yetkisinin tanımlı olmasını gerektirir — `customer_permissions` tabloları chunk6 §6.1'de kuruluyor. Ya bu kriterler chunk6'ya taşınmalı (P-4 deseni) ya da chunk5'e asgari izin iskeletinin alındığı açıkça yazılmalı.

**F7 (minör) — Mali bağlantının mekaniği eksik:** §2.2 bağlantı tablosunda yalnız `pgsql` ve `pgsql_admin` var — `3eegitim_app_financial` rolü için üçüncü bağlantı tanımı (`pgsql_financial` gibi) yazılmamış; ayrıca bekçi kanca §2.5'te yalnız `DB::connection('pgsql')`'e kaydediliyor — financial bağlantısına da kaydedilmesi gerektiği belirtilmeli, yoksa o bağlantıdaki sorgular bekçisiz kalır (RLS durdurur ama katman eksilir).

**F8 (not) — Tanımsız bileşen adı:** chunk7 §7.4 "`EnsureTenantContext` middleware'inden muaftır" diyor; bu middleware hiçbir chunk'ta tanımlanmadı (chunk2'nin mekanizması DB-seviyesi bekçi kancası). Ya middleware gerçekten var ve chunk2'de tanımlanmalı (F4'ün istek-yaşam-döngüsü boşluğuyla birlikte) ya da referans düzeltilmeli. Ayrıca chunk9 §9.7'deki "(P-1 doğrulaması)" etiketi yanlış — migration güvenliği önceki review'da bulgu değil kontrol sorusuydu; kozmetik.

## ❓ Open Questions (to be answered before next stage)

1. F2'nin yönü: ofis kapsam listesi `customers`'tan dar bir `SECURITY DEFINER` fonksiyonla mı okunacak (S-9 envanterine girer), yoksa `customers` gerçekten muaf mı olacak? Karar §2.8 tablosunu ve S-9 listesini değiştirir.
2. F3'ün yönü: `audit_writes`/`audit_access` nullable tenant mı olacak, yoksa ofis-alanı olayları için ayrı bir audit tablosu mu? (Nullable seçilirse okuma kısıtı §3.6'daki `audit_activity` disipliniyle aynı olmalı.)
3. F4(b): istek yaşam döngüsünde bağlam kurulumunu üstlenen bileşen (middleware) chunk2'ye mi chunk5'e mi yazılacak?

## Verdict: ❌ REJECTED

Plan v2.0, v1.0'ın dokuz bulgusunun tamamını gerçek içerikle kapatmış, yeni kimlik modelini doğru taşıyan ve birçok yerde (S-9 envanteri, ayrı izin dünyaları, 404/403 ayrımı, açık varsayımlar) kanun standardını yakalayan güçlü bir yeniden yazım. Ancak dört majör boşluk kaldı ve dördü de aynı bölgede — RLS/bağlam mekaniğinin uç durumları: DELETE'in `WITH CHECK` dışında kalması (F1), ofis kapsam listesinin tavuk-yumurtası (F2), ofis olaylarının audit tenant durumu (F3) ve auth-öncesi/istek-içi bağlam kurulumunun eksik akışları (F4). Bunlar "implementasyon çözer" denecek detaylar değil; planın kendi kuralıyla implementasyonun durup soracağı noktalar. F5-F8 küçük ve çoğu majörlerin çözümüne bağlanıyor. Majörler kapatılıp re-review istendiğinde planın kalanı implementasyona hazır görünüyor.
