# Review — Omurga Chunk 2 (Tenancy, RLS, Tenant Context) Implementasyonu — 2026-07-26

> **Review type:** impl-review
> **Previous review:** N/A (chunk2'nin ilk impl review'u)
> **Upstream document(s) read:** `docs/plans/omurga-plan_chunk2.md` (kabul kriterleri §2.12 + kapsam düzeltmesi notu) · `docs/plans/omurga-plan.md` (index, v2.3) · `docs/specs/omurga-spec.md` v1.7 §3, §3.0–3.2, §4 (B-1…B-6, T-1…T-7, V-1…V-5, I-1…I-4)
> **Other cross-referenced documents:** `CLAUDE.md` (17 kırmızı çizgi, dil kuralı, onay kuralları) · `docs/plans/omurga-plan_chunk5.md` §5.9 (taşınan kriterlerin D-1…D-5 kaydı) · `docs/reviews/review-omurga-chunk1-impl-2026-07-26-r2.md` (önceki chunk APPROVED) · mevzuat cross-ref gerekmedi (bu chunk mevzuat-bağımlı kural içermiyor)
> **Stage locks in effect:** Spec v1.7 + Plan v2.3 locked. Kapsam sınırı: kriter 7, 9c, 11, 14 ve test R-12, R-14, R-16 Sets onayıyla chunk5'e taşındı (plan chunk2 kapsam düzeltmesi notu, 26.07.2026) — bu maddeler bu review'da bulgu değildir.

**Kanıt tabanı:** branch `feature/EGT-31-tenancy-rls` (5 commit, `6a09184`…`93ebe81`, 44 dosya). TeamCity build **#699: SUCCESS — 107 passed, 0 failed, 0 ignored** (skip'lenebilir üç mali test dahil koşmuş). Pint `--test` lokal doğrulandı: passed. PHPStan (`composer analyse`): 0 hata. Tüm matrix satırları dosya+satır düzeyinde bu depodan okunarak doğrulanmıştır.

---

## Upstream Coverage Matrix

### §2.12 Kabul kriterleri (chunk2'de kalan)

| Upstream § / madde | Reviewed doc'ta durumu | Kanıt |
|---|---|---|
| Kriter 1 — dört rol migration ile; uygulama `3eegitim_app` ile bağlanıyor; `rolsuper` → false | ⚠ | Roller migration'da: `database/migrations/0001_01_01_000100_create_application_roles.php:44-58`; test rolü superuser değil: `tests/RlsTestCase.php:96-113`, `tests/Feature/OmurgaRls/DatabaseRoleTest.php:26-39`. **Ama** `.env.example:52` `DB_USERNAME=postgres` — runtime `pgsql` bağlantısının (PgBouncer, "the connection the application actually uses") şablon değeri superuser → **F-1** |
| Kriter 2 — hiçbir role `BYPASSRLS` yok, şema sorgusuyla kanıt | ✓ | Migration `NOSUPERUSER NOBYPASSRLS` zorluyor: `000100:161-166`; şema sorgusu testi: `DatabaseRoleTest.php:26-39`; suite başı çift kontrol: `RlsTestCase.php:102-112` |
| Kriter 3 — `pgsql` PgBouncer portu, `pgsql_admin` doğrudan port; migration `pgsql_admin` üzerinde | ⚠ | Portlar: `config/database.php:107` (`DB_PORT`), `:151` (`DB_DIRECT_PORT`); wiring testi `DatabaseRoleTest.php:101-111`. Test migrate'leri `--database=pgsql_admin`: `tests/Concerns/RefreshDatabase.php:36-45`, `tests/Concerns/TruncatesDatabase.php:34-43`. **Ama** üretim/dev migrate yolu bağlanmamış: `composer.json` `setup` → `php artisan migrate --force` (default bağlantı) → **F-2** |
| Kriter 4 — `userlist.txt.example` ve README uygulama rolleri + superuser uyarısı | ✓ | `deployment/pgbouncer/userlist.txt.example:9-32` (roller + "do not change which role the application connects as"); `deployment/pgbouncer/README.md` ("Kurulum atlanırsa ne olur" + deney 1 uyarısı + `pool_mode=transaction`/AD-04 bölümü) |
| Kriter 5 — `app.tenant_ids()` ve `app.write_tenant_id()` kurulu; NULL **ve** boş string | ✓ | `000200_create_tenant_context_functions.php:36-58` (`IF v IS NULL OR v = ''`); üçü de `STABLE`, `COMMENT ON` (:109-128), `GRANT EXECUTE` (:98-107); commit sonrası `''` testi: `TenantIsolationTest.php:142-152` |
| Kriter 6 — her tenant-kapsamlı tabloda FORCE + komut bazlı dört politika; istisna listesi bilinen, listede olmayan sapma testi kırar | ✓ | `app/Support/Database/TenantPolicy.php:85-110` (4 politika + ENABLE/FORCE); canlı katalog testi: `SchemaConformanceTest.php:63-104`; istisna listesi kodda planla birebir (`customers`, `audit_writes`, `audit_access`, `audit_activity` — `:28-44`); kasıtlı ihlal probu (`unpoliced_probe`): `:138-159`; fixture'ın uygun sayıldığı pozitif kontrol: `:106-136` |
| Kriter 7 — `customers` id bazlı politika | — | **Chunk5'e taşındı** (kapsam düzeltmesi). Şablon hazır: `TenantPolicy::applyToCustomers()` `TenantPolicy.php:49-76`, plan §2.8'in üç sapmasını (id kolonu, `WITH CHECK (true)` INSERT, DELETE politikası yok) birebir üretiyor. Chunk5 D-1'de kayıtlı (`omurga-plan_chunk5.md:136`) |
| Kriter 8 — `TenantContext::run()` transaction açıyor/set/commit; iç içe çağrıda yeni transaction yok | ✓ | `app/Support/Tenancy/TenantContext.php:114-121` (dışta yoksa `transaction()`, varsa aynı transaction'da `withScope`); `SET LOCAL` yalnız int'lenmiş değerle: `:149-160`; testler R-11/R-11b: `TenantIsolationTest.php:165-197` |
| Kriter 9 — bekçi transaction açmıyor, bağlam set etmiyor, yalnız exception; **iki bağlantıya kayıtlı** | ✓ | `TenantContextGuard.php:48-72` (sorgu koşmaz, yalnız fırlatır); `TenancyServiceProvider.php:31` (`GUARDED_CONNECTIONS = ['pgsql','pgsql_financial']`), `:56-69` (`ConnectionEstablished` ile bağlantı başına — purge sonrası düşme düzeltmesi); ikinci bağlantı testi: `FinancialRoleTest.php:63-79`; CI'da skip yok (#699: 0 ignored) |
| Kriter 9b — `EnsureTenantContext` kapsamı hesaplıyor, transaction **açmıyor**; muaf rota listesi kesin | ⚠ | Middleware: `app/Http/Middleware/EnsureTenantContext.php:28-62`; transaction açmadığı test: `TenantScopeResolutionTest.php:85-94`; muaf liste `config/tenancy.php:92-96` desen eşleşmesiyle (`Str::is`). **Ama** kapsama yalnız `web` grubu — backoffice Filament paneli bu middleware'i taşımıyor → **F-4** |
| Kriter 9c — `all_customer_ids` / `magic_link_lookup` | — | **Chunk5'e taşındı**; D-2 kaydı `omurga-plan_chunk5.md:137` |
| Kriter 10 — `exempt_tables` kesin liste; listede olmayan + `BelongsToTenant` kullanmayan model arch testini kırıyor | ✓ | `config/tenancy.php:74-84` (kesin liste, `customers`/`customer_users`/`magic_links` bilinçli dışarıda); arch testi: `tests/Arch/TenancyTest.php:30-60` (sınıflandırma), `:62-75` (liste driftı — 22 kayıt kilidi + üç tablonun muaf olmadığı) |
| Kriter 11 — `authenticate_lookup` | — | **Chunk5'e taşındı**; D-3 kaydı `omurga-plan_chunk5.md:138` |
| Kriter 12 — mali rol ayrımı kurulu; liste boşken mekanizma fixture ile kanıtlı | ✓ | `000300_restrict_financial_tables.php` + `app/Support/Database/FinancialTables.php:35-48` (`REVOKE` app / `GRANT` financial); temel rol `permission denied`: `FinancialRoleTest.php:37-47`; mali rolün RLS'e tabi kaldığı: `:81-93` |
| Kriter 13 — `BelongsToTenant` global scope eklemiyor; `tenant_id` guarded ve değiştirilemez | ✓ | `app/Models/Concerns/BelongsToTenant.php:24-77` (global scope yok; `creating` doldurma yalnız tek elemanlı kapsamdan; `updating` tenant değişimini fırlatıyor; `$guarded`'a ekleme **+ `$fillable`'dan çıkarma**); testler: `BelongsToTenantTest.php:46-110` (fillable sızıntı probu `:34` kasıtlı `tenant_id` listeleyerek) |
| Kriter 14 — `customer_users` unique indexler | — | **Chunk5'e taşındı**; D-4 kaydı `omurga-plan_chunk5.md:139` |
| Kriter 15 — R testleri yeşil; `omurga-rls` paralel değil | ✓ | `composer.json` `test`: tam koşum `--exclude-group=omurga-rls --parallel` + RLS grubu ayrı, `--parallel`'siz; commit `b1eecbc` CI'yı aynı hizaya getirdi; TeamCity #699 SUCCESS 107/0/0 |

### §2.11 Zorunlu testler (taşınanlar hariç)

| Test | Durum | Kanıt |
|---|---|---|
| R-1 kapsam A → B görünmez | ✓ | `TenantIsolationTest.php:34-38` |
| R-2 uygulama katmanı atlanınca da 0 satır | ✓ | `:40-49` (raw `DB::select` ile tenant 2'yi adıyla sorguluyor) |
| R-3 bağlamsız sorgu → exception | ✓ | `:51-54` (uygulama katmanı) + `:56-65` (muaf listeye ekleyip DB katmanının da reddettiği — AD-03 iki katman probu) |
| R-4 çapraz INSERT → RLS ihlali | ✓ | `:83-88` |
| R-5 / R-5b / R-5c çok-tenant INSERT/DELETE/UPDATE → exception | ✓ | `:90-97` / `:99-112` (satır sayısı korunuyor) / `:114-118` |
| R-6 çok-tenant SELECT her iki tenant | ✓ | `:120-129`; tek tenant'a daraltılmış silmenin çalıştığı `:131-140` |
| R-7 COMMIT sonrası bağlam boş | ✓ | `:142-152` (`''`/NULL ikisi de) |
| R-8 ardışık iş birimlerinde sızıntı yok | ✓ | `:154-163` — not: suite doğrudan porttan koşar (plan §2.11'in kendi tasarımı); PgBouncer havuz davranışının kendisi plan aşamasında canlı deney 7 ile kanıtlanmıştı, testin kanıtladığı commit sınırı mekanizması |
| R-9 test rolü superuser değil | ✓ | `RlsTestCase.php:96-113` (her testte, ilk iş) + `DatabaseRoleTest.php:86-99` (rol adı + tablo sahipliği yok) |
| R-10 `TenantContext` dışında tenant sorgusu → exception | ✓ | `:51-54` (`TenantContextMissingException`) |
| R-11 / R-11b iç içe daraltma/genişletme/geri yükleme | ✓ | `:165-176` / `:178-181` / `:183-197` (iç exception'da `finally` geri yükleme) |
| R-13 `tenant_id` güncelleme → exception | ✓ | `BelongsToTenantTest.php:68-77` |
| R-15 mali tabloya temel rolle SELECT → permission denied | ✓ | `FinancialRoleTest.php:37-47` |
| R-17 muaf liste dışındaki rota kapsamsız çalışmıyor | ⚠ | `TenantScopeResolutionTest.php:49-102` muaf/muaf-olmayan çözümü ve `web` grubu kaydını kanıtlıyor; **Filament rotaları kapsam dışı kalıyor** → F-4 |
| R-12, R-14, R-16 | — | Chunk5 D-5 (`omurga-plan_chunk5.md:140`) |

### Plan gövdesi — kriterlerde birebir görünmeyen yükümlülükler

| Upstream § / madde | Durum | Kanıt |
|---|---|---|
| §2.2 grant yapısı (Q-2: app/test aynı yetki profili) | ✓ | `NOLOGIN` grup rolleri + üyelik: `000100:44-58`; tek fark `CREATEDB`, testle kilitli: `DatabaseRoleTest.php:61-84` |
| §2.2 şifreler config'ten, migration'da sabit yok; `down()` rol düşürmez | ✓ | `000100:93-117` (yoksa `RuntimeException`, varsa şifre korunur), `:61-78` |
| §2.4 `write_tenant_id_or_null()` yalnız audit için hazır | ✓ | `000200:60-78` + COMMENT; chunk2'de politika kullanımı yok (chunk3 §3.6'ya uygun) |
| §2.5 bekçi muafiyeti dar (yalnız audit INSERT, yazar-işaretli) | ✓ | `TenantContextGuard.php:34-46` (`whileWritingAudit`), `:67-86` (INSERT-only + tablo kontrolü). Not: `config('audit.tables')` `audit_stamps`'ı da içeriyor; `audit_stamps` muaf listede olduğundan davranış farkı üretmiyor |
| §2.5.1 kimliksiz/adsız rota fail-closed | ✓ | `EnsureTenantContext.php:49-52` (adsız rota kapsam ister); boş kapsam ilk sorguda fırlatır: `TenantContext.php:96-101`, `:165-177` |
| §2.8.1 geçici ofis kapsam çözümü | ❌ | Plan: `SimpleScopeResolver` "tüm tenant'lar" + `app.all_customer_ids()`. Impl: `SessionScopeResolver` ofis kimliğine **boş** kapsam (`SessionScopeResolver.php:31-44`, binding `TenancyServiceProvider.php:42-46`). Sapma güvenli yönde ama plan metninde kayıtsız → **F-5** |
| §2.9 arch kuralları (SET LOCAL'siz `SET` yok; model sınıflandırma; RLS-politika varlığı) | ✓/⚠ | `TenancyTest.php:14-28` (SET taraması), `:30-60` (sınıflandırma); politika varlığı canlı katalogdan `SchemaConformanceTest` ile. **Ama** `tests/Concerns/RefreshDatabase.php:22`'deki "arch testi base trait'i yakalar" iddiasının karşılığı yok → **F-3** |
| §2.10 `SET LOCAL ROLE` yerine ayrı bağlantı; rol seçimi bağlantı adı üzerinden | ✓ | `config/database.php:118-136` (`pgsql_financial`); `TenantConnection`/`DefaultTenantConnection` (güvenli varsayılan: mali erişimi olmayan rol); test `FinancialRoleTest.php:95-104` chunk6'nın kullanacağı seçim noktasını kullanıyor |
| §2.11 iki suite; RLS truncation + sarmalayan transaction yok | ✓ | `tests/Pest.php:43-51` (dizin→grup, `OmurgaRls` → `RlsTestCase`); `TruncatesDatabase` (DatabaseTruncation, admin bağlantısı); grup disiplini arch testi `TestDisciplineTest.php:44-110` |
| Kapsam düzeltmesinin chunk5 karşılığı (D-1…D-5) | ✓ | `omurga-plan_chunk5.md:130-140` |

### Zorunlu üç tarama

1. **Red-line taraması (17 madde):** 1–5, 7–17 temiz — raw `DB::` kullanımı yalnız Support/migration/test katmanında ve RLS'e tabi bağlantılarda; `env()` config dışına çıkmıyor (arch `EnvTest`, CI yeşil); hardcoded credential yok (şifreler env→config, örnek dosyalar placeholder); hard delete yok; AI attribution yok (5 commit mesajı kontrol edildi); iş kuralı/mevzuat sabiti bu chunk'ta yok; kişi bazlı izleme yok. **Madde 1-2 ekosistemi için tek gölge F-1'dir** (şablon superuser'a bağlıyor — kod değil, ortam şablonu).
2. **Wire taraması:** `TenancyServiceProvider` → `bootstrap/providers.php` ✓; `EnsureTenantContext` → `bootstrap/app.php:32-39` (web grubu) ✓ (Filament boşluğu F-4); bekçi → provider `boot()` ✓; `SessionScopeResolver` → container binding ✓; `FinancialTables` → migration `000300` ✓; `TenantPolicy::apply` → `RlsTestCase` + gelecek migration'ların jeneratörü ✓. `applyToCustomers` ve `whileWritingAudit` bugün caller'sız — **ikisi de planın açık kararı** (kapsam düzeltmesi: "şablon bu chunk'ta yazılır ve hazır durur"; §2.5 N2: chunk3 kullanacak), ORPHAN sayılmadı.
3. **Dil taraması:** kod, test adları, yorumlar, config anahtarları, exception mesajları tamamı İngilizce ✓. `deployment/pgbouncer/README.md` Türkçe — operasyon dokümanı, doküman seti Türkçe (uyumlu). Bu branch'te koda Türkçe yorum eklenmemiş (`setup-test-database.sh`'daki Türkçe yorumlar chunk1'den, bu branch dokunmamış — kapsam dışı).

---

## ✅ What works

- **İzolasyon suite'i gerçekten bir şey kanıtlıyor.** Non-superuser + tablo sahibi olmayan rol + sarmalayan transaction yok + her testte `rolsuper`/`rolbypassrls` ön kontrolü (`RlsTestCase:96-113`). "Superuser'la koşan RLS testi her koşulda geçer" tuzağı hem kapatılmış hem **gösterilmiş** (`TenantIsolationTest:67-81` admin bağlantısının 3 satırı bağlamsız okuduğu test — deney 1'in canlı kanıtı suite içinde).
- **Komut bazlı dört politika + `WITH CHECK`-DELETE tuzağı** kodda, testte ve şema uygunluk denetiminde tutarlı; istisna listesi plan §2.12 tablosuyla birebir ve "istisna eklemek plan değişikliğidir" ilkesi kodda yorumla taşınmış.
- **`BelongsToTenant`'ın `$fillable` düzeltmesi** doğru ve testi kasıtlı ihlal kurgusuyla yazılmış (test modeli `tenant_id`'yi bilerek fillable listeliyor).
- **Bekçinin `ConnectionEstablished` ile bağlantı başına kaydı** — purge/yeniden açılmada sessiz düşme gerçek bir üretim riskiydi; düzeltme doğru katmanda.
- **Grant'ların `NOLOGIN` grup rolünde toplanması** Q-2'nin "aynı yetki profili" şartını kopya listeyle değil yapıyla çözüyor; `CREATEDB` farkı küme düzeyinde, satır erişimine etkisiz ve **tek fark olduğu testle kilitli** (`DatabaseRoleTest:61-84`) — Q-2'nin ruhunu bozmuyor.
- **Boş kapsamlı ofis kimliği kararının davranışı** güvenlik açısından doğru yönde: geniş geçici kapsam üretime kadar "çalışıyor" görünürdü, boş kapsam ilk sorguda isimli exception'la düşüyor. (Plan kaydı sorunu için F-5'e bakınız — bulgu davranış değil, kayıt.)
- **PgBouncer dokümantasyonu** (README + userlist.example) "eksik rolü postgres'e düşürme" anti-pattern'ini sebep-sonuçla anlatıyor; `pool_mode=session`'a kaçışın da neden çözüm olmadığı yazılmış.
- CI kanıtı sağlam: #699 SUCCESS, 107/0/**0 ignored** — mali bağlantının skip'lenebilir üç testi CI'da gerçekten koşmuş; Pint ve PHPStan bu review sırasında bağımsız doğrulandı.

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

**F-1 — Ortam şablonu uygulamanın runtime bağlantısını superuser'a bağlıyor (kriter 1 ⚠; §2.2 "uygulama hiçbir ortamda superuser bağlanmaz").** `.env.example:52` `DB_USERNAME=postgres` — ve bu değer, dosyanın kendi yorumuyla "PgBouncer — the connection the application actually uses" olan `pgsql` bağlantısını besliyor (`config/database.php:109`). `userlist.txt.example` `postgres` girdisini de içerdiğinden bu kurulum PgBouncer'dan geçer ve ayağa kalkar: deney 1'in sessiz izolasyon kaybı, şablonun varsayılanı olarak. Hiçbir runtime kontrolü bunu yakalamıyor (kriter 1'in `SELECT rolsuper → false` kanıtı yalnız test rolü için var; phpunit `DB_USERNAME`'i `3eegitim_test`'e force ettiği için suite bu şablon değerini hiç görmez). Chunk2 aynı bloğa rol değişkenlerini eklerken (`git diff` `.env.example`) 52. satırı uygulama rolüne çevirmemiş. "dev = build = local prod = prod aynı compose'lardan, fark yalnız `.env`" ilkesi bu şablonu her ortamın başlangıç noktası yapar.

**F-2 — Kriter 3'ün "migration `pgsql_admin` üzerinde" şartı yalnız test altyapısında karşılanıyor.** `tests/Concerns/RefreshDatabase.php` ve `TruncatesDatabase.php` `--database=pgsql_admin` geçiyor; ama üretim/dev migrate yolunu `pgsql_admin`'e bağlayan hiçbir şey yok: `composer.json` `setup` düz `php artisan migrate --force` çalıştırıyor (default bağlantı), `deployment/` ve `docs/` içinde `--database=pgsql_admin` geçen tek satır yok. F-1'in şablonuyla birleşince düz migrate "çalışır" — DDL, PgBouncer transaction pooling üzerinden, superuser ile: planın aynı paragrafta yasakladığı iki şey birden (§2.2: "DDL transaction pooling'de sorun çıkarır ve rol yetkisi gerekir").

**F-3 — Var olmayan bir arch denetimine atıf; base `RefreshDatabase` kullanımı denetimsiz.** `tests/Concerns/RefreshDatabase.php:22`: "tests/Arch/TenancyTest.php makes sure nobody reaches for the base directly" — `TenancyTest.php`'de (ve başka hiçbir arch testinde) böyle bir denetim yok; `Illuminate\Foundation\Testing\RefreshDatabase`'i doğrudan `use` eden bir test dosyası hiçbir şeye takılmaz. Sonuç: migration'lar default bağlantıda koşar, tabloların sahibi test rolü olur ve dosyanın kendi docblock'unun anlattığı tehlike ("RLS testi bir unutulmuş FORCE kadar yakında") o dosya için sessizce geri gelir. Çalışmayan (burada: hiç var olmayan) denetime yapılan atıf yanlış güven üretir — bu dosya bu chunk'ta yazıldı (`6a09184`), kapsam içidir.

**F-4 — `EnsureTenantContext` kapsaması `web` grubuyla sınırlı; backoffice Filament rotaları middleware'siz (kriter 9b ⚠, R-17 ⚠).** Middleware yalnız `bootstrap/app.php:32-39`'da `web` grubuna ekli. Backoffice paneli `web` grubundan geçmiyor (yorum bunu açıkça söylüyor: `BackofficePanelProvider.php:57-58`) ve panelin middleware listesinde (`:47-62`) `EnsureTenantContext` yok. R-17'nin cümlesi — "muaf rota listesi dışındaki bir rota `EnsureTenantContext` olmadan çalışmıyor" — Filament host'unda sağlanmıyor; `TenantScopeResolutionTest:96-102`'nin "runs on every web request" güvencesi tam da bu sınıfın uyardığı şekilde ("route that mattered") paneli görmüyor. Bugünkü davranış fail-closed (bağlanmamış `TenantScope` boş → `TenantContext::ambientScope()` fırlatır, `TenantContext.php:165-177`) ve panelde henüz tenant verisi dokunuşu yok; ama en geniş ofis kapsamlarını taşıyacak host'ta kapsam hesaplama katmanı kurulu değil ve bunu kilitleyen hiçbir test yok.

**F-5 — §2.8.1'in geçici çözümünden sapma plan metninde kayıtsız.** Plan §2.8.1 "Geçici (bu plan)": `SimpleScopeResolver` "tüm tenant'lar" döndürür, liste `app.all_customer_ids()`'ten gelir. Implementasyon bunun yerine ofis kimliğine **boş kapsam** veriyor (`SessionScopeResolver.php:31-44`). Kapsam düzeltmesi notu kriter 9c'yi (fonksiyonu) chunk5'e taşıdı — geçici çözümün chunk2'de kurulamayacağı bundan zorunlu olarak çıkıyor — ama notun "chunk2'de kalan her şey ... değişmedi" listesi resolver davranışını içermiyor ve §2.8.1'in tarifi düzeltilmemiş. Ara dönemin (chunk2→chunk5/6) ofis davranışı planın hiçbir yerinde yazmıyor; chunk5/6 implementer'ı §2.8.1'i okuyup var olmayan bir `SimpleScopeResolver` bekleyecek. "Plan kanundur" ve "istisna eklemek plan değişikliği gerektirir" ilkeleri altında bu, kayda bağlanmamış bir plan-impl uyumsuzluğudur. (Sapmanın kendisi güvenli yöndedir — bulgu davranış değil, kayıttır.)

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

1. **Bekçinin tablo çıkarımı (`TenantContextGuard::tablesIn`) — bilinen kaçak şekilleri.** Regex `from|join|into|update` sonrası tek tanımlayıcı yakalıyor. Query builder çıktısının tamamını kapsıyor; el yazması SQL'de şu şekiller uygulama katmanını atlar (RLS yakalar): (a) virgüllü eski stil join — `FROM a, b`'de `b` görülmez; (b) anahtar kelime ile tablo adı arasına giren SQL yorumu — `FROM /* x */ t`'de `t` görülmez. Şema nitelikli ad (`FROM public.t`) ise `public`'i tablo sanır → muaf listede olmadığından fırlatır: **fail-closed**, kaçak değil. `TRUNCATE` yakalanmaz ama uygulama rollerinde `TRUNCATE` grant'ı yok. Bu profil iki katman ilkesi altında kabul edilebilir mi, yoksa chunk9 §9.5 taramasına "el yazması SQL'de virgüllü join yasak" gibi bir kural mı eklenmeli — karar Sets'in.
2. **Arch `SET` taramasının deseni `set_config()`'i görmüyor.** `TenancyTest:22` yalnız `SET app.tenant_ids` metnini arıyor; `SELECT set_config('app.tenant_ids', ..., false)` session-level bağlam kurar ve desene takılmaz. Plan testin tarifini "SET LOCAL'siz SET" olarak verdiği için plan-uyumsuzluk değil; kural taraması chunk9'da genişletilecekse not düşülmeli.
3. **`config('audit.tables')` bekçi muafiyetinde `audit_stamps`'ı da kapsıyor** (`stamps` girdisi). `audit_stamps` muaf listede olduğundan bugün davranış farkı sıfır; chunk3 audit tablolarını RLS'li yaparken bu çakışmanın (muaf liste ↔ audit listesi) bilinçli mi kalacağı netleşmeli.
4. **Fixture→gerçek tablo geçişi:** taşınan D-1…D-5 dışında eksik görünmüyor — şema uygunluk testi canlı katalogdan okuduğu için ilk gerçek tenant tablosu otomatik kapsanır; `BelongsToTenant` kullanan ilk gerçek model arch sınıflandırmasına otomatik girer. Chunk3/chunk5'in önünü kesen bir karar tespit edilmedi (`write_tenant_id_or_null`, bekçi muafiyeti, `applyToCustomers` üçü de hazır ve plana uygun).

## Verdict: ❌ REJECTED

Mekanizmanın çekirdeği — roller, dört politika, bağlam fonksiyonları, `TenantContext`, bekçi, model katmanı — plana yüksek sadakatle ve alışılmışın üstünde bir test disipliniyle kurulmuş; CI kanıtı bağımsız doğrulandı. Ancak matrix'te üç ⚠ (kriter 1, kriter 3, kriter 9b/R-17) ve bir ❌ (§2.8.1) var; beş bulgu açık. Özellikle F-1, bu chunk'ın kurmak için var olduğu güvencenin ("uygulama hiçbir ortamda superuser bağlanmaz") ortam şablonundaki tam tersidir ve deney 1'in sessiz kayıp senaryosunu varsayılan kurulum yoluna koyar. Bulgular kapatılıp re-review talep edilmelidir.
