# Açık Konular / Belirsizlikler

*CLAUDE.md kuralı: kurallar çelişiyorsa veya net değilse buraya yazılır. Kapanınca tarih + karar notuyla işaretlenir.*

> **Production'a dokunan her kalem [EGT-51](https://3egroup.youtrack.cloud/issue/EGT-51)'de toplanır** (Sets kararı, 26.07.2026). Burası belirsizliklerin ve kararların yeri; production hazırlık/taşıma işleri o ticket'ta birikir ve çıkış o ticket üzerinden yürür. Bir kalem hem belirsizlik hem production işiyse burada kalır ve EGT-51'e not düşülür — tek kaynak EGT-51'dir, burası açıklamadır.

## Açık

- **"En az 3 katılımcı" bakanlık teyidi** — mevzuat metninde bulunamadı; parametrik (başlangıç 3). Operasyon/bakanlık teyidi bekleniyor.
- **e-Fatura entegratörü seçimi** — adaptör provider-bağımsız kurulacak (AD-12); entegratör belli olunca provider yazılacak.
- **Magic link + içerik paketi erişiminde ek doğrulama default'u** — auth yöntemleri admin'den ayarlanabilir (AD-07); içerik paketi için default güvenlik seviyesi spec aşamasında kararlaştırılacak.
- **Tenant-bazlı şifreleme anahtar yönetimi mekanizması** (KMS vs. türetilmiş anahtar) — AD-05 design aşamasında detaylandırılacak.
- ✅ ~~**Sertifika şablonunun resmî formatı** — bakanlık/mevzuat şablon şartı var mı, spec öncesi teyit.~~ **Kapandı (21.10.2026'da doğrulandı):** Bakanlık sertifikasını sistem basmaz (AD-28); sistemin bastığı tek belge mevzuat dışı eğitimin şablondan üretilen belgesidir (EGT-225, tasarım `52`).
- ✅ ~~**Fiyatlarda KDV/para birimi davranışı** — teklif/fatura spec'inde netleşecek.~~ **Kapandı (21.10.2026'da doğrulandı):** kalem bazında KDV oranı, eğitim tanımından ya da paketten (EGT-222, `vat_rate`); para birimi alanı yok — şemada `currency` kolonu bulunmuyor, tutarlar TL olarak okunuyor.
- **YouTube kanal hesabı + OAuth kurulumu** (AD-13) — implementasyon öncesi operasyonel kurulum.
~~TeamCity build config~~ → kuruldu: `Egitim_Egitim`, lokal klasör modeli, doğrulama build #687 SUCCESS (24.07.2026); build adımı plan §1.10 sırasına çekildi, ilk yeşil tam koşum #690 (25.07.2026).
~~Production domain/DNS kararı~~ → **ÇÖZÜLDÜ (25.07.2026):** domain alındı, Cloudflare zone `3egrp.com` yönetimde, `CF_API_TOKEN` ~/.zshrc'de — DNS işleri API üzerinden yapılıyor, dış birine ihtiyaç yok. Postmark DKIM/Return-Path/SPF doğrulandı, inbound MX yayında. Detay: `docs/infra/dns-kayitlari.md`.
- **Postmark hesap onayı** — sunucu ve gönderim domain'i hazır, DKIM/Return-Path/SPF doğrulandı; ancak hesap onay bekliyor: Postmark yalnız From ile aynı domain'e göndermeye izin veriyor, bu yüzden gerçek gönderim testi yapılamadı. Sets'in Postmark panelinden onay talebi vermesi gerekiyor.

- **Backoffice tehdit varsayımı (AD-09, 26.07.2026).** Kullanıcı yönetiminin backoffice'te kapsamsız olması, backoffice yöneticisinin **şirket sahibi** olduğu varsayımına dayanır. Backoffice'e şirket sahibi olmayan bir taraf girerse (dış IT, danışman, ileride destek görevlisi) varsayım yeniden değerlendirilmeli: kendine ofis yetkisi verme (self-grant) o noktada bir yetki yükseltme yoluna dönüşür. Bugün kural değişikliği gerektirmiyor; backoffice rol ailesi genişlediğinde tekrar bakılacak.

- ~~F-2: "son aktif" ↔ L-1 çelişkisi~~ → **KARAR (Sets, 26.07.2026): ofiste görünür, L-1'e adlandırılmış istisna.** Müşteri kullanıcısının son aktif damgası ofiste müşteri kartında görünür; istisna **tek bir türetilmiş alanla** sınırlı (spec §7.5 L-1a, AD-21, chunk8 §8.5). Giriş geçmişi, oturum listesi, giriş sayısı, geçirilen süre gösterilmez; istisnaya başka alan eklemek spec değişikliğidir.
- ~~F-3: kimlik doğrulama olayları ofis feed'inde~~ → **KARAR (Sets, 26.07.2026): feed'de kalır.** Heyetin "çevrimiçi süre elle çıkarılabilir" gerekçesi kabul edilmedi: giriş olayı kullanım süresini göstermez — sabah giriş yapıp gün boyu panele bakmayan kişi de aynı satırı üretir. Kırmızı çizgi 17'nin koruması **üretim tarafında** durur (chunk9 S-2): sistem süre/skor/sıralama üretmez. Spec §7.5 L-1b, chunk3 §3.6.

- **⚠ `.env` ve `.env.testing` aynı cluster rolleri için FARKLI şifre taşıyordu (26.07.2026).** `3eegitim_app` ve `3eegitim_app_financial` **cluster genelinde** rollerdir — tek bir şifreleri olabilir. İki env dosyası farklı değer yazdığı için biri her zaman yanlıştı: pratikte roller `.env.testing` değerlerini taşıyordu, yani **dev uygulama bağlantısı hiç çalışmıyordu** ve fark edilmemişti (bugüne kadar her şey `pgsql_admin` veya test veritabanı üzerinden koşmuş). Rol migration'ı var olan rolün şifresini bilinçli olarak döndürmüyor (koşan worker'ları deploy ortasında kesmemek için), dolayısıyla `.env` değeri role hiç ulaşmıyordu. `.env` paylaşılan iki rol için `.env.testing` ile hizalandı. **Kalıcı çözüm gerekiyor (→ EGT-51 [2]):** paylaşılan rol şifreleri tek kaynaktan gelmeli veya iki dosyanın uyuşmadığını yakalayan bir kontrol olmalı — "dev = build = local prod = prod, yalnız port/env farkı" kuralının tam olarak kaçırdığı yer burası. *(Bu kalem, dev rollerini elle senkronize ederken mali rolün şifresini de değiştirip test suite'ini geçici olarak kırmam sonucu ortaya çıktı.)*

- ~~Dış tanık repository'si ve adaptörü~~ → **KURULDU ve DOĞRULANDI (26.07.2026).** Repo: `Sets/3eegitim-anchor`. Adaptör `App\Integrations\AuditWitness\GitAuditWitness` (deploy key ile SSH; PAT değil çünkü süresi dolan bir token, tanığın sessizce yazılmaz hale gelmesi demektir). Üç şart canlı doğrulandı: **public** (`visibility: public`), **force-push engelli** (`GH013: Cannot force-push to this branch`), **silme engelli** (`main` üzerinde `non_fast_forward` + `deletion` ruleset'i aktif). Uçtan uca deneme yapıldı: mühür üretildi, yayımlandı, commit referansı kayda geçti (`eb18642`). Deponun README'sine **bağımsız doğrulama prosedürü** yazıldı (zincir kontrolü betiği, yaprak/katlama kuralı, yayım zamanının nereden okunacağı).
  - **Güncelleme (26.07.2026, heyet 135).** Karşılaştırma işi **yazıldı ve günlük koşuyor**: `VerifyAuditChain` artık (a) her partinin kökünü satırlardan yeniden hesaplayıp mühürdekiyle karşılaştırıyor, (b) yerel mühürleri tanığın yayımladıklarıyla **iki yönlü** karşılaştırıyor, (c) mührün `hash_algorithm` alanını okuyup tanımadığı kuralla mühürlenmiş halkayı sessizce geçmek yerine raporluyor. Tanık kopyası her okumada `fetch` ile tazeleniyor — eski hâli, uygulamanın kendi yazabildiği klonu okuyordu.
  - **Kalan:** production'a deploy key ve `AUDIT_ANCHOR_*` env değerlerinin taşınması → **EGT-51 [1]**.

- **⚠ HUKUK TEYİDİ — audit zincirinin delil sınıfı (26.07.2026, AD-06).** Kendi zincirimiz **takdiri delildir**; 5070'in güvenli elektronik imzaya tanıdığı statü BTK'ya kayıtlı bir ESHS'den gelir ve bizde yoktur. Bilirkişi zinciri doğrulayabilir, ancak itiraz halinde ispat yükü bizde kalır. Teyit edilmesi gereken: **sertifika/bildirim süreçlerinde İdare'ye karşı nitelikli zaman damgası zorunluluğu var mı?** Varsa ESHS'ye veya bir kamu anchor'ına dönülür — veri modeli hazır, sabitlenen şey zaten zincir hash'i. Avukat teyidi alınmadan "delil niteliği" iddiası müşteriye sunulmaz. → **EGT-51 [3]**

- **Bağımsız doğrulama aracı ve prosedürü yazılmadı (AD-06, 26.07.2026).** Zincirin bilirkişi karşısında işe yaraması, üçüncü bir tarafın **bizim yardımımız olmadan** doğrulayabilmesine bağlı. `VerifyAuditChain` işi içeriden koşuyor; dışarıdan çalıştırılabilir araç ayrı iş kalemi → **EGT-51 [4]**. ⚠ **Prosedür tarafı 26.07.2026'da kapandı ve fiilen kanıtlandı:** kural `docs/arch/tanik-deposu-readme.md`'de yazılı (tek kaynak; tanık deposundaki kopya yayımlanmış hâli) ve kök `psql` + Python ile, uygulama kodu hiç kullanılmadan yeniden hesaplanıp fonksiyonun ürettiğiyle birebir eşleşti. Kalan yalnız **paketlenmiş araç**.

- **chunk3 kriter 11b chunk3'te sınanamıyor (26.07.2026).** *"Müşteri kimliği `audit.view` izniyle bile NULL tenant'lı audit satırlarını göremiyor"* — müşteri kimlikleri (`customer_users`) chunk5'te, izin altyapısı chunk6'da doğuyor. Kısıt zaten N-2 gereği **policy katmanında** (chunk6); kriterin testi chunk6 §6.9'a taşınmalı. RLS bu satırları bağlamı olan herkese gösterir ve bu bilinçlidir — ofis girişi başka türlü audit'lenemezdi.

- **`analytics_usage_daily` çapraz-tenant okuma mekanizması tanımsız (heyet 132 R3-7, 26.07.2026 — chunk8 implementasyonundan önce kapanmalı).** chunk3 §3.6.1 B-3 ile backoffice ham `audit_usage`'ı çapraz-tenant okumuyor; usage'a tek meşru yolu `analytics_usage_daily` agregatı. Ama o agregatın okunma mekanizması hiçbir belgede yok: chunk2 §2.8'in **kesin** bağlamsız-tablo listesinde değil (listede olmayan her tablo tenant-kapsamlıdır ve §2.4 desenini uygular — arch testi zorlar), TimescaleDB continuous aggregate'i RLS politikası taşımaz, bekçi karşısındaki durumu ve G-1 ile ilişkisi yazılmamış. chunk8 §8.6 ekranı tanımlıyor, okuma yolunu değil. İki muhtemel adres: (a) §2.8 listesine gerekçeli kayıt, (b) G-1 deseninde dar `SECURITY DEFINER` okuma fonksiyonu + S-9 envanteri. Karar chunk8 turunundur.

- ✅ **Kapandı (21.10.2026'da doğrulandı): `ActorType::BackofficeUser` var, backoffice satırları kendi değeriyle yazılıyor.** ~~**`actor_type` ve backoffice kimliği (heyet 130 açık soru 2).**~~ Backoffice yöneticisinin ürettiği audit satırı hangi `actor_type` değerini alır? `backoffice_users` kararına bağlı: tek `users` çıkarsa backoffice işlemleri `office_user` olarak satış yöneticisinin feed'ine girer. Chunk5'te `backoffice_users` kararıyla birlikte çözülür.

- **Parametre daraltma mekanizması nerede kurulacak (AD-09, 26.07.2026).** "Global backoffice'te, daraltma ofiste, **genişletme onaya düşer**" *(düzeltme: AD-09 revizesi, Sets 28.07.2026 — bu satır önce "genişletme yasak" diyordu)* kuralının tek bir çözümleyicide yaşaması gerekiyor (etkin değer = zincirin en dar halkası; genişletme denemesi exception). Omurga bu parametreleri henüz üretmiyor; mekanizmanın sahibi **Spec 2 / chunk5** olmalı. Her dikeyin kendi `min()` mantığını yazması bu kuralın sessizce delinmesi demektir.

### chunk2 implementasyonunda açılan kalemler (26.07.2026)

- **§2.8.1 geçici ofis kapsam çözümünden sapma (heyet F-5, karara bağlandı).** Plan `SimpleScopeResolver` → "tüm tenant'lar" diyordu; `app.all_customer_ids()` `customers` tablosunu gerektirdiği ve o tablo chunk5'e taşındığı için kurulamadı. Ara dönemde ofis kimliği **boş kapsam** alır ve ilk sorguda düşer. Plan §2.8.1 düzeltme notuyla güncellendi. ⚠ **Düzeltme 27.07.2026 (heyet 147 F-2):** bu kalem *"Chunk6 `TreeScopeResolver` ile kapanır"* diyordu — yanlış. `TreeScopeResolver` **Spec 3'ün ürünüdür**; chunk6 onu üretmez, yalnız arayüzü ve geçici implementasyonu taşır (chunk6 §6.5). Kalem **Spec 3 turunda** kapanır. Bu arada ofis kimliği hiçbir tenant verisine erişemez ve bu, prod çıkış kısıtının gerekçesidir (chunk6 P-7).
- **Bekçinin SQL tablo çıkarımının bilinen sınırları (heyet açık soru 1).** `TenantContextGuard::tablesIn` yorumları temizler ve virgüllü join listelerini açar; buna rağmen el yazması egzotik SQL'de uygulama katmanını atlatan bir şekil kalabilir. Atlarsa **RLS yakalar** — iki katmandan biri düşer, izolasyon düşmez. Chunk9 §9.5 taramasına kural eklenmesi değerlendirilmeli.
- ✅ ~~**Arch `SET` taraması `set_config()`'i görmüyor (heyet açık soru 2).**~~ **Kapandı 21.10.2026 (EGT-239):** `TenancyTest` artık kiracı anahtarına `false` ile yapılan `set_config` çağrısını da reddediyor. Plan testin tarifini "SET LOCAL'siz `SET`" olarak veriyor, dolayısıyla plan uyumsuzluğu değil. `SELECT set_config('app.tenant_ids', ..., false)` oturum düzeyinde bağlam kurar ve desene takılmaz — chunk9 kural taramasında kapsanmalı.
- **`config('audit.tables')` ↔ `exempt_tables` çakışması (heyet açık soru 3).** `audit_stamps` her iki listede; bugün davranış farkı yok (muaf tablo zaten bekçiyi tetiklemiyor). Chunk3 audit tablolarını RLS'li yaparken bu çakışmanın bilinçli kalıp kalmayacağı netleşmeli.

### chunk1 implementasyonunda açılan kalemler (25.07.2026)

- **timestamptz kararı (planda yok, canlı deneyle bulundu).** `timestamptz` olmadan UTC saklama garantisi kurulamıyor: PostgreSQL `timestamp` verilen duvar saatini olduğu gibi tutar ve Laravel yazarken dönüştürmez. Ayrıca Laravel'in varsayılan tarih biçimi offset taşımadığı için `timestamptz` tek başına da yetmiyor — İstanbul saatiyle verilen bir an "21:30" düz metni olarak gidip UTC 21:30 olarak yazılıyordu (canlı doğrulandı). İki katmanlı çözüm: (a) tüm an kolonları `timestamptz` — `tests/Arch/TimeTest.php` her migration'ı denetliyor, (b) `App\Support\Database\UtcTimestampGrammar` offset'i her yazımda gönderiyor. **Chunk2+ migration'ları `timestampTz()` kullanmak zorunda.**
- **Test rolü chunk1'de kuruldu.** Rol/veritabanı migration'ı plan chunk2 §2.2'nin işi; ancak `phpunit.xml` `DB_USERNAME=3eegitim_test` zorunlu kıldığı için chunk1 kendi kabul kriterlerini rol olmadan doğrulayamıyor. Geçici çözüm: `deployment/scripts/setup-test-database.sh` (idempotent, rolün superuser/BYPASSRLS olmadığını doğrulayarak biter). Chunk2'nin migration'ı kanonik sahiptir ve mevcut rolü atlar.
- **Test rolüne `CREATEDB`.** Paralel koşum her worker'a ayrı veritabanı açar (chunk9 §9.1 paralel koşum şartı). `CREATEDB` satır güvenliğini etkilemez; `BYPASSRLS` hiçbir role verilmedi.
- **Paket rotaları host ayrımının dışında (heyet F-2, kapatıldı ama kalıcı bir sınır var).** Horizon, Filament export/import ve Livewire rotaları kendilerini domain'siz kaydeder. Kısıtlananlar: Horizon (`config/horizon.php` middleware), Filament indirme uçları (`filament.actions` grubuna **prepend** — `web` grubu route model binding çözdüğü için append yetmiyordu), Livewire `update` (component metot çağrısı taşıyan uç). **Kısıtlanmayanlar bilinçli:** Livewire statik asset rotaları (JS/CSS, veri yok) ve iki dosya ucu (`upload-file`, `preview-file` — paket hook vermiyor; ikisi de geçerli imza istiyor). İstisna listesi `tests/Feature/Omurga/PackageRouteHostTest.php` içinde adlandırıldı; yeni bir domain'siz rota çıkarsa test kırılır. **Paket yükseltmelerinde bu liste gözden geçirilmeli.**
- **Q-1 (heyet, chunk2'ye devredildi): `config/tenancy.php`'deki `context_free_routes` ve `financial_tables` chunk2'nin (§2.5.1, §2.10) kanonik listeleri.** Chunk1'e "yer hazırlama" olarak çekildiler. Chunk2 implementasyonunda bu listelerin plan metniyle drift kontrolünü yapan bir test yazılmalı.
- **Q-2 KARARA BAĞLANDI (Sets, 26.07.2026) — chunk2'de uygulanacak: test rolü üretim rolüyle aynı yetki yapısına sahip olur, tabloların sahibi olmaz.**
  - **Grant'lar tek yerde tanımlanır:** `NOLOGIN` grup rolü (`3eegitim_app_grants`). `3eegitim_app` ve `3eegitim_test` bu grubun üyesidir. Yetki profili kopyalanmaz, **paylaşılır** — böylece test ile üretim arasında yetki kayması yapısal olarak imkânsız hale gelir. Mali sınıf kendi grubunu alır (`3eegitim_financial_grants`).
  - **Sahiplik ayrılır:** migration'lar `pgsql_admin` ile koşar (plan §9.6 dağıtım sırası 1 ve chunk2 §2.5 zaten böyle diyor), dolayısıyla tabloların sahibi test rolü değildir. AD-26'nın "RLS testi owner rolüyle koşulmaz" kuralı sağlanır; `FORCE ROW LEVEL SECURITY` ikinci kapı olarak kalır, tek dayanak olmaz.
  - **⚠ Paralel koşum kısıtı:** Laravel paralel testte worker başına veritabanı açar ve bunu **bağlantı kullanıcısıyla** yapar. Üretim uygulama rolünde `CREATEDB` olmayacağına göre, test rolünde de olamaz. Çözüm iki bağlantı: şema ve worker veritabanları `pgsql_admin` bağlantısıyla, testin kendi sorguları (özellikle `omurga-rls` grubu) `3eegitim_test` bağlantısıyla. Chunk9 §9.1 zaten RLS suite'ini ayrı ve paralel-değil tanımlıyor; bu ayrım onunla tutarlı.
  - **Seeder değil migration:** chunk2 §2.2 rolleri migration'a veriyor (şifreler config'ten, migration'da sabit şifre yok). Aynı olgunun iki mekanizmaya bölünmemesi için seeder kullanılmaz. `pgsql_admin`'in kendisi migration'dan önce var olmak zorunda olduğu için provizyon adımı olarak kalır (`setup-test-database.sh` / rollout adım 1).
  - **Drift testi:** `3eegitim_test` ile `3eegitim_app`'in rol üyelikleri ve öznitelikleri (`rolsuper`, `rolbypassrls`, `rolcreatedb`) birebir aynı mı; test rolü hiçbir tablonun sahibi mi değil — chunk2'de test edilir.
  - Chunk1'in geçici kurulumu (test rolü migration'ları koşuyor, `CREATEDB` var) chunk2 bunu devraldığında **geri alınır**.
- ~~Q-3: `.env` örnek dosyalarının yorum dili~~ → **karar (Sets, 26.07.2026): hepsi aynı hizada olsun, İngilizce.** `.env.example` ve `.env.testing.example` yorumları İngilizceye çevrildi. Gerekçe: env kod dışı konfigürasyon olduğu için dil kuralının kapsamı tartışmalıydı; tek hizada tutmak tartışmayı kapatıyor.
- **Dil kuralı arch testinin iki istisnası.** (a) Kurum adları için kısa özel-isim listesi (şu an yalnız İYS), (b) Türkçe metin davranışını sınayan test dosyaları için `arch-allow: turkish-content` işareti — her istisna tek grep ile görünür.

## Kapandı

- ~~Ayrı `backoffice_users` tablosu önerisi~~ → **Sets kararı (26.07.2026): ayrılıyor; üç bağımsız kimlik alanı.** Spec v1.11 §3, AD-09; heyet re-review APPROVED (`docs/reviews/review-omurga-spec-2026-07-26-r2.md`). Belirleyici gerekçe maliyet karşılaştırması değil, **karmaşıklığın adresi** oldu: ofis net bir yapıdır ve öyle kalmalıdır, backoffice ihtiyaca göre şekillenecek karmaşık yetki yapıları gerektirecektir; istisnayı `users`'a taşımak ofisin standardını bozardı. Bootstrap argümanı da ayırma lehineydi (sistem ilk açıldığında yalnız backoffice kullanıcıları vardır). ⚠ Kapanışın **düzeltilmiş yanı**: eski kalem "bugünkü sınır zaten var: `system.access_backoffice` izni" diyordu — o izin kaldırıldı; kapı artık `backoffice_users` kaydının kendisidir. Ayrıca backoffice'in **yetki tavanı yoktur** (spec §3.3); bugünkü dar kapsam bir tercihtir (§3.4), kısıt değil.

- ~~Plan çelişkisi: uygulama saat dilimi (§1.2 `Europe/Istanbul` ↔ §1.5 UTC saklama)~~ → **Sets kararı (25.07.2026): veritabanı UTC, gösterim her zaman Türkiye.** `app.timezone = UTC`, `app.display_timezone = Europe/Istanbul`; plan chunk1 §1.2 düzeltildi.

- ~~MBP build agent PHP 8.5 + redis/pdo_pgsql~~ → kurulu ve doğrulandı; build makinesine ayrıca PostgreSQL/Redis stack'i ve `libpq` kuruldu, build #690 yeşil (25.07.2026, EGT-48).

- ~~İYS entegrasyon kapsamı~~ → day-one, İletiMerkezi İYS paketi üzerinden; SMS de İletiMerkezi (AD-17/AD-18) (24.07.2026).
- ~~Bakanlık bildirim süresi 3 gün mü 15 gün mü~~ → 3 gün, mevzuattan doğrulandı (24.07.2026, çatı dokümanı).
- ~~IMDG kapsama kuralları~~ → 4→2+3, 9→7, 8→5+6 — yönerge md. 7'den doğrulandı; sertifikada kapsanan görevler zorunlu (24.07.2026).
- ~~"2000 sınırı"~~ → doğrusu 1/7/2020 + çift koşul (yönerge md. 21/5) (24.07.2026).
- ~~Video/KVKK~~ → YouTube unlisted + rıza akışı (AD-13) (24.07.2026).
- ~~Panel/frontend kapsamı~~ → yalnız ana yönetim Filament; diğer 5 panel özel tasarım + Inertia/Vue (AD-09 güncellendi, 24.07.2026).

## Bilinçli kabul: iki audit feed sınıfı chunk3'te orphan (Sets kararı, 26.07.2026)

`App\Queries\Audit\OfficeAuditFeed` ve `BackofficeAuditFeed` chunk3'te **production entry-point'ten çağrılmıyor** — tüketicileri chunk6 (izin zorlaması) ve chunk8 (ekranlar). CLAUDE.md onay kuralı 4 "yeni public symbol caller'a sahip olmalı" der; bu, o kuralla bilinçli ve kayda geçmiş bir çatışmadır.

**Neden kabul ediliyor:** plan omurga için "iş ekranı üretmez" diyor (`omurga-plan.md` §Kapsam dışı). Sınıfları ekranlarıyla birlikte yazmak, omurga chunk'ına ekran sokmak demekti. Heyet 135 de bunu bulgu saymadı, kabul kaydı istedi.

**Kapanış şartı:** chunk6 ve chunk8 kabul kriterlerine "bu sınıf production yolundan çağrılıyor" maddesi eklendi. O maddeler kapanana kadar bu kalem açık kalır — kabul, kalıcı muafiyet değil **ertelemedir**.

- **Çok-kurallı zincir doğrulaması yazılmadı (AD-06, 26.07.2026 — heyet 136 F-r3-2).** `VerifyAuditChain` yalnız **güncel** yaprak kuralını hesaplayabiliyor; başka bir kuralla mühürlenmiş halka "yeniden hesaplanamıyor" bulgusu üretiyor. Bugün doğru davranış — hiçbir ortamda eski kurallı mühür yok, prod deploy yapılmadı — ve raporlamak sessizce geçmekten iyidir. **Ama:** yerde halka varken kural bir daha değişirse, eski halkalar **süresiz günlük critical log** üretmeye başlar. O gün iki seçenekten biri gerekir: (a) eski kural fonksiyonlarını sürüm adıyla yanyana tutup mührün `hash_algorithm`'ine göre seçmek, (b) belli bir tarihten eski halkalar için "doğrulanamaz ama dokunulmamış" statüsünü açıkça kabul edip raporlamadan düşürmek. Karar, ilk gerçek kural değişiminde verilir. → **EGT-51 [K2]**

- **Yaprak kuralının `jsonb` bileşeni PostgreSQL'in sözleşmesel garantisi değil (AD-06, 26.07.2026 — heyet 136 F-r3-3).** `old_values`/`new_values`/`context` alanları yaprakta `::text` ile yazılıyor ve bu, jsonb'nin kanonik biçimine (anahtar sıralaması, tekilleştirme, boşluk normalizasyonu, sayı biçimi) bağlı. Biçim PG içinde deterministik ve oturumdan bağımsız — zincir kendi içinde sağlam. Ancak PostgreSQL bu serileştirmeyi sürümler arası **taahhüt etmiyor**: bir major upgrade biçimi değiştirirse v2 kökleri toplu uyuşmazlık üretir. Kırılma **gürültülüdür** (günlük doğrulama bağırır, kuralın sürümlenmesi zorunlu hale gelir), sessiz değil. İki gereklilik: (1) major PostgreSQL yükseltmesi öncesi bu kalem okunacak ve kural sürümü ihtiyacı değerlendirilecek, (2) dış doğrulayıcı PG dışı bir araç kullanıyorsa jsonb serileştirmesini birebir taklit etmek zorunda — tanık deposunun README'si bu biçimi açıkça yazar. → **EGT-51 [K3]**

- ~~Audit zaman damgaları saniye çözünürlüğünde~~ → **ÇÖZÜLDÜ (Sets kararı, 26.07.2026): mikrosaniyeye çıkarıldı** (`precision=6`, dört audit tablosunda; şema testiyle sabitlendi). Yaprak kuralı **değişmedi** — `to_char(... '.US')` zaten mikrosaniye render ediyordu, kesirli kısım artık bilgi taşıyor. Üretimde halka olmadığı için bedelsizdi; sonrasında aynı değişiklik her yaprağı yeniden yazar ve kuralı v3'e çıkarırdı. Aşağıdaki özgün not kayıt olarak duruyor:

- *(özgün kayıt)* **Audit zaman damgaları saniye çözünürlüğündeydi (26.07.2026, bağımsız doğrulama sırasında bulundu).** `audit_writes`, `audit_access` ve `audit_stamps` üzerindeki tüm `timestamptz` kolonları `precision=0` — Laravel'in `timestampsTz()` varsayılanı, bilinçli bir karar değil. Sonuçlar: (a) aynı saniye içindeki iki olay aynı damgayı taşır, sıralama `id`'den gelir (zincir etkilenmez, yaprak sırası `id` bazlıdır); (b) yaprak kuralındaki `.US` kalıbı bugün her zaman `000000` üretir — kural geçerli ve deterministik, ama kesirli kısım bilgi taşımıyor. **Değiştirmek isterseniz bedeli var:** kolon tipini değiştirmek migration ister ve **her yaprağı değiştirir** → yaprak kuralı `v3`'e çıkar, tanık deposu README'si ve `app.audit_leaf_line` birlikte güncellenir. Bu yüzden şimdi değiştirilmedi; delil süreçlerinde saniye altı ayrım gerekiyorsa Sets'in kararıyla yapılır. → **EGT-51 [K1]** — prod'a çıkıldıktan sonra değiştirmek çok daha pahalı (yerde halka olur), karar çıkış öncesi verilmeli.

## Temiz kurulumda ortaya çıkan iki sağlama kusuru (26.07.2026, PG18 yükseltmesi sırasında)

İkisi de **yalnız sıfırdan kurulan bir veritabanında** görünüyordu; mevcut dev volume'u ikisini de maskeliyordu. Yani ikisi de ilk gerçek belirtisini **production kurulumunda** verecekti. Yükseltme için volume'u yenilemek olmasa bulunmayacaklardı.

**1. Uygulama rolü superuser doğuyordu — kırmızı çizgi 1.**
`docker-compose.postgresql.yml` cluster'ı `POSTGRES_USER: ${DB_USERNAME}` ile bootstrap ediyordu ve `.env`'de bu değer `3eegitim_app`. `initdb`, `POSTGRES_USER` neyse onu **superuser** yaratır → temiz kurulumda `3eegitim_app super=true bypassrls=true` doğuyordu ve chunk2 deney 1 gereği RLS **sessizce** devre dışı kalıyordu. Mevcut dev volume'u `postgres` ile kurulmuş, `3eegitim_app` sonradan migration'la normal rol olarak eklenmişti — bu yüzden hiçbir test yakalayamadı.

- **Düzeltme:** bootstrap `POSTGRES_USER: ${DB_ADMIN_USERNAME:-postgres}` + `POSTGRES_PASSWORD: ${DB_ADMIN_PASSWORD}`. Doğrulandı: temiz kurulumda yalnız `postgres` superuser, üç uygulama rolü `super=false bypassrls=false`.
- **Testin durumu:** `DatabaseRoleTest` — *"gives no application role a way past row security"* zaten vardı ve **doğruydu**; kusur testte değil ortam sağlamasındaydı. Test ancak temiz bootstrap edilmiş bir cluster'a karşı koşarsa bunu yakalar.
- **Kalan iş → EGT-51:** deploy anında çalışan bir doğrulama (uygulama rolleri superuser/BYPASSRLS değil) rollout kontrol listesine girmeli. Test paketi production'a karşı koşmaz; bu kusuru production'da yakalayacak tek şey deploy-time kontrolüdür.

**2. `pgcrypto` hiçbir migration tarafından kurulmuyordu.**
Zincir `digest()` kullanıyor ve o pgcrypto'da. Uzantı yalnız container'ın `init-extensions.sql`'inde yaratılıyordu — bu bir migration değil ve temiz kurulumda **yok**. Test veritabanı kurulum script'i bilinçli olarak yalnız migration'ı olan uzantıları kuruyor; eski test veritabanı pgcrypto'yu geçmişten taşıdığı için 28 test bunu maskeliyordu. Temiz veritabanında hepsi `function digest(text, unknown) does not exist` ile düştü.

- **Etkisi:** production'da **hiçbir mühür yazılamazdı** — zincir sessizce hiç oluşmazdı.
- **Düzeltme:** `CREATE EXTENSION IF NOT EXISTS pgcrypto` merkle migration'ına eklendi (unaccent'in app-şema migration'ındaki deseniyle aynı). pgcrypto trusted extension'dır; veritabanı sahibi superuser olmadan kurabilir, paralel test bunu gerektiriyor.
- **Ders (PA-225'in tekrarı):** ortamda **var olan** ile migration'da **yazılı olan** ayrı şeylerdir. "Çalışıyor" kanıt değildir; kanıt, sıfırdan kurulan bir veritabanında çalışmasıdır.

**3. Paralel rol yaratma yarışı yalnız `duplicate_object` yakalıyordu.**
`0001_01_01_000100_create_application_roles` rolleri `DO $$ … EXCEPTION WHEN duplicate_object THEN NULL $$` ile yaratıyordu. Roller **cluster genelindedir** ve paralel test worker başına bir veritabanı açar; sekiz worker aynı anda `CREATE ROLE` denediğinde kaybeden oturumun aldığı hata **`duplicate_object` (42710) değil `unique_violation` (23505)** olabiliyor — katalog kontrolünü geçip `pg_authid.rolname` indeksine takıldığında. Yakalanmadığı için migration düşüyordu.

- **Neden görünmedi:** roller **zaten var** olduğu sürece kaybeden oturum katalog kontrolünde durur ve 42710 alır. Yarış ancak roller **gerçekten yokken** — yani cluster ömründe **tam bir kez**, temiz kurulumda — kaybedebilir. PG18 yükseltmesinde CI tam olarak orada düştü.
- **Düzeltme:** `EXCEPTION WHEN duplicate_object OR unique_violation THEN NULL`.
- **Doğrulama:** yerel cluster sıfırdan kuruldu (volume silindi) ve paralel paket koşuldu — 8 worker rolleri aynı anda yaratırken 91 test geçti.

**Üçünün ortak dersi.** Üçü de aynı kalıptan: *mevcut ortam, kodun eksiğini taşıyordu.* Kusurun tetikleyicisi hep "ilk kez" olmak — ilk bootstrap, ilk uzantı kurulumu, ilk rol yaratma. Bunlar production'da tam olarak bir kez yaşanır ve orada yanlış giderse geri dönüşü pahalıdır. **Kural:** altyapıya dokunan her değişiklikten sonra veritabanı sıfırdan kurulup tam paket koşulur; "mevcut ortamda çalışıyor" kanıt sayılmaz.

- **Tenant anahtar rotasyonu yazılmadı (AD-05, chunk4 §4.2).** `key_version` şifreli değerlerin ve `document_versions` satırlarının yanında tutuluyor, yani rotasyon yolu **kapalı değil**; rotasyonun kendisi (yeni sürümle yeniden şifreleme, iki sürümü bir arada okuma, eski sürümü emekliye ayırma) yapılmadı ve plan da yapılmayacağını yazıyor.

  ⚠ **Gerekçe düzeltmesi (Sets, 26.07.2026).** İlk yazımı *"kök anahtar sızarsa"* senaryosuna dayanıyordu — yanlış çerçeve. Kök anahtarın sızması sunucunun ele geçirilmesini gerektirir; ona karşı yapılacak şey mimari değil sunucu sertleştirme ve güvenlik denetimidir. Kalem **aciliyetsizdir**: bu projedeki verilerin kâğıt nüshaları zaten ofiste dolaşıyor. `key_version` duruyor çünkü sonradan eklenemeyecek tek şey odur.
---

## Chunk 6 — özel nitelikli belgeye müşteri erişimi (27.07.2026 — KAPANDI)

**Açık soru olarak yazıldı, aynı gün Sets kapattı.**

İlk hâlinde `DocumentPolicy` özel nitelikli belge için yalnız ofis iznini (`document.view_special_category`) arıyordu ve bu izin müşteri ailesinde yok. Sonuç: bir firma **kendi yüklediği** sağlık raporunu okuyamıyordu — güvenli yön, ama yanlış yön.

**Karar (Sets):** müşteri tarafı katılımcıların evraklarını da yükler; firma kendi ekibinin bilgilerini görür ve bunu daraltan tek şey owner'ın vermediği izindir, 3E'nin koyduğu tavan değil.

- `customer.upload_documents` ve `customer.view_special_category_documents` müşteri sözlüğüne eklendi.
- Özel nitelikli belge yine **ayrı izin** — `customer.view_documents`'ın içine gömülmedi: sağlık raporu "okunabilir bir belge" değil, owner'ın ayrıca söylemesi gereken bir karar.
- Ofis tarafına `document.upload` eklendi; §6.2 iki tarafta da yükleme iznini atlamıştı, yani chunk4'ün kurduğu dosya deposunu açan bir izin yoktu.

---

## Giriş hız sınırı yalnız ofiste zorlanıyor (28.07.2026)

`config/security.php`'nin `login_throttle` ayarı (`max_attempts: 5`, `lockout_minutes: 15`) chunk5'ten beri duruyor ve **hiçbir kod okumuyordu** — spec A-1 (*"başarısız giriş denemeleri hız sınırına tabidir"*) üç kimlik alanının hiçbirinde zorlanmıyordu. Ayar vardı, zorlama yoktu.

Ofis T1'de kapandı: `app/Support/Auth/ThrottlesSignIn.php`, ofis girişine ve ikinci faktör ucuna bağlı (ofis T1 planı görev 5 adım 2b, görev 6 adım 5).

⚠ **Müşteri girişi (`SignInCustomerUser`) hâlâ sınırsız.** Yüzeyi kuran tur müşteri portalıdır; kapanış adresi **EGT-24 / EGT-38**. Backoffice tarafı Filament'in kendi giriş akışını kullanıyor ve ayrı incelenmeli.

⚠ **Prod çıkışından ÖNCE kapanmalı — EGT-51.** Müşteri giriş ucu ekranı olmasa da **bugün canlı bir endpoint'tir**; boşluk ekran turuna değil uca aittir. EGT-24 prod çıkışından sonraya sarkarsa açık prod'a çıkar. Bu yüzden kalem hem EGT-24'e (yüzey) hem **EGT-51'e (production hazırlık kapısı)** bağlıdır; ikisinden hangisi önce gelirse orada kapanır.

⚠ Bu boşluk ofis spec'i §8.1'in altını da oyuyordu: *"kısıtla"*yı hız sınırına bağlayıp *"sistemin ürettiği geçici durum"* demiştik ve `63`'ün *"Girişi Kısıtlı (30 dk)"* rozeti buna dayanıyor. Mekanizma olmadan rozet hiç çıkmaz ve T2'nin *"kısıtı kaldır"* aksiyonunun kaldıracağı bir şey olmazdı.


⚠ **Aynı sınıf ikinci bir kusur: `AnonymousRequestLimiter`'ın IP bacağı (28.07.2026, heyet 152 bulgusu R3-1).** Sınıf tek `$max` alıp **iki anahtara birden** uyguluyor (`app/Support/Auth/AnonymousRequestLimiter.php:30-47`). Şifre sıfırlama için makul; **magic link için değil**. `RequestMagicLink:53-58` eşiği `password_reset_throttle`'dan okuyor — **3 istek / 60 dakika** — ve katılımcılar eğitim yerine *birlikte* gelir: 25 kişilik bir sınıfın aynı salon wifi'sinden aynı dakikalarda link istemesi, **4. katılımcıdan itibaren** IP anahtarına takılır.

⚠ **Ve sessizce takılır.** `RequestMagicLink` sınıra çarpınca `return 0` yapar; kişi *"bağlantı gönderildi"* ekranını görür, posta hiç gelmez. Ürünün çekirdek akışının kapısında, hata mesajı üretmeyen bir tıkanma.

**Çözüm deseni hazır:** ofis T1'in `ThrottlesSignIn`'i (plan görev 5 adım 2b) her anahtara **kendi eşiğini** veren `budgets()` yapısını getiriyor; `AnonymousRequestLimiter` de ona geçirilmeli — IP bacağı sınıf mevcudunu (mevzuat azamisi 30) rahatça karşılayan ayrı bir eşik almalı. **Adres:** katılımcı akışını kuran tur (`EGT-27` / `EGT-38`); prod çıkışından önce kapanmalı — **EGT-51**.

## Passkey — üç kimlik alanı, tek authenticatable model (27.07.2026)

`spatie/laravel-passkeys` **tek** bir authenticatable model tanır (`config/passkeys.php` → `models.authenticatable`) ve `passkeys` tablosunu ona foreign key ile bağlar. Bugün backoffice'e ayarlı ve bu yeterli — passkey yalnız orada var.

⚠ Ofis ya da müşteri tarafı passkey isterse **ayar değiştirmek çözüm değildir**: tablo tek modele bağlı. O gün her alanın kendi tablosu gerekir, izin ailelerindeki desenle (chunk6 §6.1). Kayıt burada, karar o turda.

**Adres:** ofis passkey'i **EGT-56** (Ofis T3). Kendi tablosu, kendi modeli ve ofis relying-party id'si o turda doğar — bir passkey kayıt edildiği host'a bağlıdır, backoffice passkey'i ofis host'unda çalışmaz.

## `backoffice_users.passcode_enabled` — düştü (27.07.2026, KAPANDI)

E-posta ile iletilen tek kullanımlık kod aynı gün eklendi ve aynı gün yöntem olmaktan çıkarıldı (magic link zaten odur). Kolon bir gün ölü kaldı; **Sets onayladı ve `2026_07_30_000100_drop_passcode_authentication` ile düştü.** Ekleyen migration'a dokunulmadı — koşmuş migration tarihtir (kırmızı çizgi 6).

## Passkey ile giriş bağlandı (27.07.2026, KAPANDI)

`SignInWithPasskey` + giriş sayfasındaki buton. Paketin kendi controller'ı kullanılamadı: **varsayılan guard'da** giriş yapıyor (bu uygulamada varsayılan guard yok), donmuş hesabı kontrol etmiyor ve yöntem ayarına bakmıyor. Tören paketin kaldı; etrafındaki her şey bizim.

**Aynı turda kapatılan gerçek açık:** `PasskeyAuthentication::isEnabled()` "bu hesabın passkey'i var mı" diye cevaplıyor ve **alanı olmayan** bir doğrulama formu sunuyordu. Filament giriş sayfası ilk etkin sağlayıcıda duruyor; sonuç, passkey kaydetmiş birinin şifreyle girip boş formda butona basarak içeri girmesiydi. Yani **passkey kaydetmek şifreyle girişi sessizce tek faktöre düşürüyordu.** Kural artık oturuma soruluyor (`App\Support\Auth\SecondFactor`, spec v1.15 §5.2).

---

## Passkey lokalde çalışıyor — lokal ortam HTTPS'e geçti (27.07.2026, KAPANDI)

Açık şuydu: `http://backoffice.3eegitim.test` üzerinde `window.isSecureContext === false`, `window.PublicKeyCredential === undefined`. Tarayıcılar WebAuthn'u yalnız güvenli bağlamda açar; passkey lokalde ne kaydedilebiliyordu ne kullanılabiliyordu.

**Çözüldü.** `.test` hostları kaldırıldı; `egitim.dev.sets.fyi` ailesi Cloudflare DNS (gri bulut) → 10.31.1.25, sertifika Let's Encrypt DNS-01. Ayrıntı ve kurulum adımları `docs/infra/ortamlar.md`.

Doğrulandı — sanal authenticator ile uçtan uca: passkey kaydı yapıldı (`rpId = backoffice.egitim.dev.sets.fyi`), oturum kapatıldıktan sonra **yalnız passkey ile giriş** yapıldı ve panele düşüldü, ikinci faktör sorulmadı. Aynı hesap şifreyle girdiğinde 2FA kurulum ekranına düştü — kuralın iki yarısı da canlıda.

⚠ **Kalan tek şey gerçek cihaz.** Sanal authenticator töreni doğru koşturur ama parmak izi donanımını taklit eder; biyometri akışının kendisi ancak telefonda/parmak izi okuyucuda görülür. Adres LAN'a bağlı olduğu için telefon aynı wifi'de olmalı.

## Elle silinmesi gereken tek satır: `/etc/hosts` (27.07.2026)

`.test` kayıtları duruyor ve zararsız — ama artık hiçbir şeye karşılık gelmiyorlar. Silmek sudo ister, o yüzden bende değil:

```bash
sudo sed -i '' '/3eegitim.test/d' /etc/hosts
```

## Filament login sayfası, girmiş kullanıcıya 500 veriyordu (27.07.2026, KAPANDI)

Passkey testi sırasında çıktı ve **passkey ile ilgisi yok**: Laravel'in `AuthenticateSession` middleware'i VARSAYILAN guard'a yazılmış (`$request->user()`, `viaRemember()`, `password_hash_<driver>`), bu uygulamada ise varsayılan guard bilinçli olarak yok.

Kimlik doğrulanmış rotalarda görünmüyordu, çünkü middleware önceliği `Authenticate`i önce koşturuyor ve o `shouldUse(backoffice)` diyor. **Giriş sayfasında `Authenticate` yoktur** — girmemiş olan için vardır — dolayısıyla açık bir panel sekmesi `/login`'i yenilediği anda `Auth guard [] is not defined` ile 500 alıyordu. Filament'in kendi `Login::mount()`'u tam bu durumu panele yönlendirmek için yazılmış, ama oraya hiç varamıyordu.

**Düzeltme:** `AuthenticateBackofficeSession` — panelin guard'ını session kontrolünden önce söyler. İki regresyon testi eklendi (girmiş olan yönlenir, misafir giriş sayfasını görür).

## İkinci faktör kurulduktan sonra sayfa ilerlemiyordu (27.07.2026, KAPANDI)

Passkey kaydediliyor, oturum ikinci faktörü karşılamış oluyor, panel bir yenileme uzakta — ama kurulum sayfası bunu **haber almıyordu**. "Devam et" butonunu o sayfa çiziyor ve iç içe bir Livewire bileşeni ebeveynini yeniden çizdiremez. Sonuç: kişi yeni passkey'ini gördüğü ekranda kalıyor, yalnız F5'e basmayı akıl eden içeri girebiliyordu.

**Düzeltme:** kayıt biten bileşen `second-factor-enrolled` yayınlar, sayfa dinler ve `intended()` ile panele yönlendirir. Kurulum sayfasının işi zaten bitmiştir.

## Şifre adımı artık ayrı modal — ve gerçek bir kapı (27.07.2026)

Önce form alanıydı: passkey ekleme formunun içinde bir "Şifreniz" kutusu, silme de aynı kutuyu kullanıyordu. Sets: *"bir yerde eğer step olarak password girmesi gerekiyorsa bu ayrı bir modal ile halledilsin"*. Doğru okuma — form içindeki alan formun parçası gibi görünür, pencere ise adımdır.

Dört yerde geçerli: passkey ekleme, passkey silme, 2FA kurma, 2FA kapatma.

⚠ **Buton gizlemek kapı değildir.** Her Filament action'ı tarayıcıdan adıyla mount edilebilir; ilk hâlinde gizli 2FA butonu tek `mountAction('setUpAppAuthentication')` çağrısı uzaktaydı. Kapı artık tek kullanımlık bir izinle çalışıyor: pencere izni yazar, arkasındaki işlem `mountUsing` içinde onu tüketir, yoksa sessizce açılmaz. Tarayıcıda denendi: izinsiz çağrı hiçbir pencere açmıyor.

⚠ İki tuzak yol boyunca çıktı ve ikisi de sessizdi: (a) `mountUsing` bir **setter**'dır — 2FA kurulumu sırrını ve kurtarma kodlarını orada üretir, kapıyı oraya yazınca sır hiç üretilmedi (`Undefined array key "encrypted"`); artık sarmalanıyor, eziliyor değil. (b) Şema içindeki bir action **şema yoluyla** çözülür; devir sırasında bağlam taşınmayınca Filament sayfada `<ad>Action()` metodu arar, bulamaz ve **hiçbir şey söylemeden** vazgeçer — pencere kapanır, QR gelmez.

## Passkey töreninin kendisi testle kanıtlanmaz (27.07.2026)

`tests/Fixtures/FakePasskeyCeremony` imza doğrulamasının yerine geçer, çünkü challenge her denemede rastgele üretilir — kaydedilmiş bir assertion ikinci kez geçerli olamaz, elle yazılmış olan da yalnız "elle yazılmış olan kabul edildi"yi kanıtlar. Testler **etrafındaki kuralları** kanıtlar: yöntem ayarı, tek kullanımlık challenge, donmuş hesap kapısı, hangi guard'da oturum açıldığı, oturumun ne hatırladığı.

Törenin kendisi paketin kendi paketinde ve **gerçek cihazda** doğrulanır. Bu, yukarıdaki TLS kalemi kapanmadan yapılamaz.

## Filament sayfasında Livewire `call()` bileşen metoduna ulaşmıyor (28.07.2026)

`AuthMethodSettings` (plan chunk10 §10.5) bir Filament `Page`; formu `content` içine `EmbeddedSchema` ile gömülü ve submit handler'ı `livewireSubmitHandler('persist')`. Ekran **render oluyor**, yetki kapısı **çalışıyor**, ama testte:

```php
Livewire::test(AuthMethodSettings::class)->call('persist');   // metoda hiç girmiyor
```

**Ölçüldü, tahmin değil:** sayfaya hiçbir iş yapmayan `public function probeXyz()` eklendi, `call('probeXyz')` da metoda girmedi. Yani sorun `persist`'e özgü değil, bu bileşende Livewire çağrı hattı testte hiç çalışmıyor. `MethodNotFoundException` de atılmıyor — `HandleComponents::callMethod`'daki `trigger('call', …)` kancası erken dönüyor olmalı (`vendor/livewire/livewire/src/Mechanisms/HandleComponents/HandleComponents.php:680`).

Denenip **elenenler**: `Livewire::actingAs()` yerine `$this->actingAs()` + `Filament::setCurrentPanel()`; aksiyon adı ile metot adının çakışması (`save`/`save` → `saveSettings`/`persist`); `content`'i formun kendisi yapmak (bu ayrıca **sonsuz özyineleme** üretiyor — Filament şemayı ancak kurduktan SONRA cache'liyor); `optimize:clear`.

**✅ Tarayıcıda ÇALIŞIYOR** *(Sets doğruladı, 28.07.2026 06:15 — "Giriş yöntemi ayarları güncellendi" bildirimi geldi)*. Yani bu bir ürün kusuru değil, **test harness sınırı**: buton üretimde aksiyona bağlı, `Livewire::test(...)->call(...)` ise bu sayfada bileşene ulaşmıyor.

**Bugünkü karşılığı:** ekranın davranışı `UpdateAuthSettings` aksiyonu üzerinden test ediliyor — üç tablonun doğru olanına yazma, yetki kapısı, passkey'in müşteri alanına açılmaması, audit satırı. Testin kapsamadığı tek şey butonun handler'a bağlı olduğu; o **tarayıcıda** doğrulandı.

⚠ Aynı desen §10.3, §10.6, §10.7 ve fiyat matrisinde de kullanılacak. Bu ekranlarda da aynı bölünme geçerli olacak: **davranış aksiyon testiyle**, **bağlantı Sets'in tarayıcı teyidiyle**. Kök neden hâlâ bilinmiyor; Filament/Livewire sürümü yükseldiğinde tekrar denenmeli.

## Kullanım analitiği ekranı, TimescaleDB continuous aggregate'ine bağlı (28.07.2026)

§10.8'in **erişim günlüğü** yarısı yazıldı ve çalışıyor. **Kullanım analitiği** yarısı yazılmadı, çünkü ham `audit_usage` üzerinden okumak mimarinin bilinçli olarak reddettiği yol:

`TenantContextGuard::isEvidenceSelect()` yorumu birebir şunu söylüyor — *"`audit_usage` deliberately not in this list: the usage log is never shown row by row, the backoffice reads it through the daily aggregate"*. AD-21 de kaynağı **continuous aggregate (saatlik/günlük rollup)** diye yazıyor.

Ham tabloya sorgu attığımda guard doğru davrandı ve reddetti. Guard'ı genişletmek, bilerek dar yazılmış bir kararı onaysız geri almak olurdu.

⚠ **Ve bu mekanik bir iş değil, gerçek bir tasarım sorusu:** k-anonimlik `count(distinct user_id)` istiyor, TimescaleDB continuous aggregate'leri `count(distinct)` desteklemiyor. Üç yol var ve seçim Sets'e ait:

1. **`hyperloglog`** (timescaledb_toolkit) — yaklaşık distinct sayımı. Eşik 2 gibi küçük bir sayıda yaklaşıklık tehlikeli: 1 kişiyi 2 sayarsa k-anonimlik delinir.
2. **Zamanlanmış materialized view** — continuous aggregate değil, cron ile yenilenen tam sayım. Kesin, ama AD-21'in "continuous aggregate" ifadesinden sapar.
3. **İki katmanlı rollup** — continuous aggregate `(bucket, role, route, user_id)` kırılımında sayar (distinct yok, grup var), ekran onun üstünde `count(*)` ile distinct kişiyi bulur. Kesin, continuous aggregate kalır, ama satır sayısı kişi × ekran × saat kadar büyür.

**✅ KAPANDI (Sets, 28.07.2026 07:32).** Seçim yapıldı ve sorunun kendisi ortadan kalktı:

> *"Derdimiz anonimliği kaldırıp watchdog kurmak değil, analytics oluşturmak ve kullanıcı davranışlarına göre panel iyileştirmesi yapmak… bu kadar katı kurgulamaya, ek maliyet oluşturmaya gerek yok."*

Rollup yalnızca `count(distinct)` için, o da yalnızca k-anonimlik eşiği için gerekiyordu. Eşik **ayara** dönüşünce (varsayılan 2, ekrandan 1 yapılıp kapatılabilir) düz gruplu sorgu yetti — **hiç ek altyapı kurulmadı.**

Yerine `TenantContextGuard::whileReadingUsageAggregate()` eklendi: beşinci ve en dar istisna. Üç koşulu SQL'in kendisinden doğruluyor — tek tablo, `group by` var, ve `user_id` yalnız `count(distinct …)` içinde geçebilir. Yani "çağıran bunun agregat olduğunu söylüyor" değil, "sorgu gerçekten agregat". Kişi bazlı bir sorgu bu bayrağın arkasında da reddedilir ve bunu bir test kanıtlıyor.

Eşik hakkında not: Sets kişi limitinin gereksiz olduğunu söyledi, gerekçesi doğru (kişi bazlı rapor ekranı yok ve olmayacak). Yine de varsayılan 2 bırakıldı — koruduğu şey **kaza**: tek kişilik bir rolde *"direktör × muhasebe ekranı: 40 görüntüleme"* o tek kişinin kullanımıdır, kimse öyle tasarlamasa da. Maliyeti bir karşılaştırma; ayardan 1 yapılınca kapanıyor ve bunun kapandığını da ayrı bir test kanıtlıyor.

## `satis` grubunda bir kez düşüp tekrarlanmayan test *(02.08.2026)*

`TrainingRequestTest` içindeki bir test bir koşuda düştü, ardından üst üste dört
koşuda geçti. ⚠ Retry ile geçirilmedi — AD-26'nın istediği gibi kaydediliyor.

**Muhtemel sebep:** bu suite testler arasında veritabanını temizlemiyor
(`tests/Helpers/Planlama.php` bunu açıkça söylüyor: "The suite does not clear the
database between runs"). Aynı sebep 01.08.2026'da `InstructorPanelTest`'te
gerçek bir hata olarak ortaya çıktı: test, önceki koşulardan kalan kullanıcılara
dayanıyordu ve temiz veritabanında düştü.

⛔ **Yapılacak:** grubun testlerini paylaşılan durumdan bağımsız hâle getirmek —
her testin ihtiyacı olan aktörü kendisi yaratması. **Ticket: EGT-75.**

**DÜZELTİLDİ (08.08.2026).** `TrainingRequestTest`'in fikstürü `first() ?? create()` idi: bir şey varsa onu okuyordu. Artık **her zaman kendi tanımını yaratıyor**. Aynı gün ikinci bir örnek daha çıktı ve düzeltildi: `ApprovalScreenTest`'e yazdığım iki yeni test **başka dosyada tanımlı** fonksiyonları (`paperProving`, `trainingCosting`) çağırıyordu — `--filter` ile geçip grup koşusunda düşüyordu. Fikstürler kendi dosyasına taşındı.


## Verilmiş bir onayın geri alınması — ÇÖZÜLDÜ *(Sets kararı, 08.08.2026)*

Sets sordu: *"satış yöneticisinin onayladığını [operasyon direktörü] iptal de edebilir?"* — çünkü **operasyon direktörü ofisin en üst yetkilisidir**.

**Bugünkü durum, ölçüldü:**

| Onay türü | Verilmiş karar geri alınabiliyor mu |
|---|---|
| İndirim tavanı | **Evet, dolaylı** — direktör satıcının kaydından tavanı değiştirir veya kaldırır (`SetSellerDiscountLimit`, `user.update.all`). Kuyruk yalnız bekleyen kararı işler. |
| Masraf | **Hayır** — `ApproveExpense` `already_decided` ile kilitler |
| Eğitmen evrağı | **Hayır** — `ApproveInstructorDocument`, `review_state !== 'pending'` |
| Eğitim maliyeti | **Hayır** — `ApproveTrainingCost` `already_decided` |
| İptal onayı | Kontrol edilmedi |

⚠ **Bu kilit kasıtlı olabilir**: AD-11 kritik kaydı silmez, iptal/revizyon ister — yani "geri alma" muhtemelen kararı silmek değil, **gerekçeli ikinci bir karar** yazmak olmalı (ilk karar kayıtta kalır, denetim izi bozulmaz).

**Sets'in kararı:** *gerekçeli ikinci karar yazılsın.* İlk karar kayıtta kalır, ayrı izin istenmez — `*.approve` yeterli.

**Uygulanan biçim:** karar **silinmiyor, sorusu yeniden açılıyor.** `ReopenApprovalDecision` kaydı kuyruğa geri koyar; ikinci karar **normal akıştan** (aynı `decide` yolundan) geçer, yani "kim karar verebilir" ve "kimse kendi işini onaylayamaz" kuralları tek yerde kalır. Yeniden açma `approval.decision_reopened` olayıyla loglanır: **eski karar** `old_values`'ta, **gerekçe** `new_values`'ta.

Yüzey: `/onaylar/gecmis` satırında **"Kararı yeniden aç"** — yalnız yetkisi olana ve yalnız yeniden açılabilir türlerde çizilir.

**Hangi türlerde:**

| Tür | Durum |
|---|---|
| Eğitmen evrağı | ✅ yeniden açılır |
| Eğitim maliyeti | ✅ yeniden açılır |
| Masraf | ✅ **ödenmemişse** yeniden açılır — aşağı bak |
| İndirim tavanı | zaten satıcı kaydından değiştirilebiliyor |
| Eğitim iptali | eğitimin kendi statüsü; yeniden açma ayrı bir iş |
| Teklif | karar **metne** bağlı; yeni karar yeni gönderimle gelir |

✅ **Masraf da yeniden açılabilir — ödemesi yapılmamışsa** *(Sets, 08.08.2026: "ilk yönetici onayladı, üst yönetici gördü ve reddetti, onay iptali yapılabilmesi gerekir eğer ödemesi yapılmamışsa; insan hatası olasılığını 0'a indirerek sistem kuramayız")*.

`instructor_expenses_freeze_decision` tetikleyicisi kaldırılmadı, **daraltıldı** (`2026_10_04_000100_a_decided_expense_may_be_asked_again.php`): `approved → submitted` artık serbest — yani "karar değişti" değil, **"soru yeniden soruldu"**; doğrudan `approved → rejected` hâlâ reddediliyor.

İki koruma birlikte çalışıyor:

1. **Ödendiyse kapalı.** `ReopenApprovalDecision::hasBeenPaid()` `instructor_payment_allocations`'a bakar (`target_type = 'expense'`); satır varsa eylem `approvals.errors.expense_is_paid` ile reddeder. Çıkmış parayı, onu doğuran talebi düzelterek geri almak muhasebe değildir.
2. **Maliyet geri sarılır.** Yeniden açma, tutarı eğitimin maliyet kaleminden düşer (`actual_amount`); kalemde bir şey kalmıyorsa `null` yazılır — "henüz gerçekleşen yok" ile "gerçekleşen sıfır" aynı cümle değildir.

Her yeniden açma `approval.decision_reopened` olarak, **gerekçesiyle** iz kaydına yazılır.

⛔ **Tarayıcıda çıkan ikinci bulgu (08.08.2026):** yeniden açılan karar, geçmiş ekranında hâlâ *"onaylandı"* olarak ve **yeniden aç butonuyla** duruyordu — buton, "yeniden açılacak bir şey yok" reddine açılan bir kapıydı. Geçmiş iz kaydını okur (doğrusu budur: karar gerçekten verildi), ama *"yeniden sorulabilir mi"* sorusu **kaydın bugünkü hâline** aittir. Kural tek yere alındı (`app/Support/Approvals/DecisionState.php`) ve `SettledApprovals` artık tablo başına tek sorguyla kaydın hâlâ karara bağlı olup olmadığını soruyor; olmayan satır butonu değil **"yeniden açıldı"** rozetini gösteriyor. Test: `ApprovalScreenTest` → *"asks an approved expense again while it is unpaid, and refuses once it is paid"*.
