---
title: 'Frontend Kalitesi'
source: 'https://academia.sh/tr/kurslar/frontend-kalitesi'
language: tr
updated: '2026-08-17T04:40:58+00:00'
license: 'CC BY-SA 4.0'
lessons: 27
outcomes:
    - 'Başarım bütçesi tanımlar ve ölçümle doğrular'
    - 'Erişilebilirlik ölçütlerini standart düzeyinde uygular'
    - 'İstemci tarafı güvenlik risklerini savunma katmanlarıyla azaltır'
---

# Frontend Kalitesi

Yayınlanmış bir arayüzün ölçülebilir kalite eksenleri; başarımın kullanıcı merkezli ölçütlerle bütçelenmesi, erişilebilirliğin standart ölçütlere bağlanması, istemci tarafı güvenlik savunmaları ve davranışı doğrulayan test ile izleme düzeni.

## Başarım

Kullanıcı merkezli ölçütlerin tanımı, kritik oluşturma yolunun kısaltılması, görsel ve yazı tipi yükleme stratejileri, ağ katmanının başlıklarla ayarlanması, çalışma zamanındaki uzun görevler ve bütün bunları bir bütçeye bağlayıp gerilemeyi yakalama.

### Kullanıcı Merkezli Ölçütler

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/kullanici-merkezli-olcutler

Yayınlanmış bir arayüzün hızını kullanıcının algıladığı olaylarla ölçmek; yükleme, etkileşim ve görsel kararlılık ölçütlerinin tanımları, düzen kayması puanının hesabı ve dağılımın yüzdeliklerle özetlenmesi.

Kuzey Yamaç Ölçüm İstasyonu uygulaması üretiliyor, paketleniyor ve yayınlanıyor. Derleme
çıktısı bir barındırma hedefine çıkıyor, sürümler geri alınabiliyor, ortam değişkenleri
ayrılmış durumda. Yayın hattının yanıtladığı soru "kod kullanıcıya ulaşıyor mu" idi ve
yanıtı evet.

Yanıtlanmayan soru şudur: ulaşan şey kullanıcı için ne kadar iyi çalışıyor? Bu ders o
sorunun ölçülebilir hâlini kurar. Ölçütler kullanıcının algıladığı olaylara bağlanır;
bütçeye bağlanmaları bu konunun son dersine kalır.

#### Teknik Kilometre Taşları Yanlış Soruyu Yanıtlar

Sayfanın yüklenmesi, tarayıcı için tanımlı olayları olan bir süreçtir: belge ayrıştırması
biter, alt kaynaklar iner, yükleme olayı tetiklenir. Bu olaylar ölçülebilir ve
kaydedilebilir. Sorun ölçülebilirlikte değil, neyi temsil ettiklerindedir.

İstasyon listesi sayfası düşünülsün. Liste sunucudan gelen HTML içinde geliyor ve hemen
görünüyor; sayfanın altındaki bir harita bileşeni ise büyük bir betik indirip iki saniye
sonra hazır oluyor. Yükleme olayı harita hazır olduğunda tetiklenir. Kullanıcı ise
listesini bir saniyede görmüş, aradığı istasyona çoktan dokunmuştur.

Ters durum da vardır: belge erken biter, ama ekranda okunabilir hiçbir şey yoktur çünkü
içerik betikle üretilmektedir. Yükleme olayı erkendir, kullanıcının gördüğü boş ekrandır.

İki durumda da teknik kilometre taşı doğrudur ve deneyimi yanlış anlatır. Ölçütün öznesi
tarayıcının iç akışı değil, kullanıcının ekranda gördüğü ve yaptığı şey olmalıdır.

#### Üç Soru, Beş Ölçüt

Kullanıcı merkezli ölçütler üç soruyu ayırır.

**Ne zaman bir şey gördüm?** Bu sorunun iki ölçüsü vardır. **İlk bayt süresi (time to
first byte)** isteğin gönderilmesiyle yanıtın ilk baytının gelmesi arasındaki süredir;
Ağ Panelinde Tanı dersinde aşamalara bölünen zamanlamanın ilk parçasıdır. Sunucu tarafını
ölçer ve istemcide yapılacak eniyilemenin tavanını belirler: sonraki her ölçüt bu sürenin
üstüne biner. **İlk içerik boyası (first contentful paint)** ekranda belgeden gelen
herhangi bir içeriğin — metin, görsel, tuval çizimi — ilk kez çizildiği andır. Boş ekranın
bittiğini söyler, sayfanın hazır olduğunu söylemez.

**Ne zaman asıl içeriği gördüm?** **En büyük içerik boyası (largest contentful paint)**,
görünüm alanındaki en büyük içerik ögesinin çizildiği andır. Ölçütün fikri şudur: bir
sayfanın "yüklendi" hissi, en çok yer kaplayan ögenin gelmesiyle oluşur. İstasyon listesi
sayfasında bu öge listenin kendisidir; ölçüm ayrıntısı sayfasında istasyon fotoğrafıdır.
Ölçüt yükleme boyunca güncellenir; daha büyük bir öge sonradan çizilirse aday değişir ve
ölçüt ileri kayar.

**Ne zaman dokunduğuma yanıt aldım?** **Sonraki boyaya etkileşim (interaction to next
paint)**, kullanıcının bir etkileşiminden — tıklama, dokunma, tuş — ekranın bunu yansıtan
sonraki boyamasına kadar geçen süredir. Ölçülen şey işleyicinin süresi değil, kullanıcının
gördüğü gecikmenin tamamıdır: girdinin işlenmeyi beklediği süre, işleyicinin süresi ve
karenin üretilmesi. Sayfa boyunca birçok etkileşim olur; ölçüt bunların en kötülerini
temsil eden tek bir değer verir.

**Ekran ayağımın altından kaydı mı?** **Kümülatif düzen kayması (cumulative layout
shift)**, beklenmedik yer değiştirmelerin toplamıdır. Web'in Temelleri ve HTML kursunda
içerik kayması adıyla tanıtılan olgunun niceliksel karşılığıdır ve tek ölçüt içinde süre
değil, oransız bir puan verir.

#### Görsel Kararlılığın Puanı

Kayma puanı iki payın çarpımıdır. **Etki payı**, kayan ögenin iki ardışık karedeki görünür
alanlarının birleşiminin görünüm alanına oranıdır: ekranın ne kadarı etkilendi? **Uzaklık
payı**, ögenin kat ettiği en büyük mesafenin görünüm alanının büyük kenarına oranıdır: ne
kadar uzağa gitti?

Aşağıdaki hesap, kayan ögelerin görünüm alanı genişliğini kapladığını varsayar; bu
varsayımla etki bölgesi iki boyutlu bir alandan tek bir dikey aralığa iner. Daha dar
ögelerde aynı çarpım genişlik oranıyla ölçeklenir.

```js
// kayma.mjs — tek bir duzen kaymasinin puani ve oturum penceresi toplami

const GORUNTU = { genislik: 390, yukseklik: 844 }; // gorunum alani, CSS pikseli

// Her kayma: kararsiz ogenin kayma oncesi ve sonrasi dikey araligi.
const kaymalar = [
  { ad: "duyuru serigi acildi", once: [96, 700], sonra: [196, 800] },
  { ad: "istasyon gorseli yerlesti", once: [200, 444], sonra: [224, 468] },
  { ad: "olcum rozeti geldi", once: [512, 560], sonra: [520, 568] },
];

function kaymaPuani(kayma) {
  const [oncekiUst, oncekiAlt] = kayma.once;
  const [sonrakiUst, sonrakiAlt] = kayma.sonra;

  // Etki bolgesi: iki karedeki gorunur alanlarin birlesimi.
  const birlesimUst = Math.max(0, Math.min(oncekiUst, sonrakiUst));
  const birlesimAlt = Math.min(GORUNTU.yukseklik, Math.max(oncekiAlt, sonrakiAlt));
  const etkiPayi = (birlesimAlt - birlesimUst) / GORUNTU.yukseklik;

  // Uzaklik payi: ogenin kat ettigi en buyuk mesafe / gorunum alaninin buyuk kenari.
  const mesafe = Math.abs(sonrakiUst - oncekiUst);
  const uzaklikPayi = mesafe / Math.max(GORUNTU.genislik, GORUNTU.yukseklik);

  return { etkiPayi, uzaklikPayi, mesafe, puan: etkiPayi * uzaklikPayi };
}

console.log("kayma".padEnd(28) + "mesafe".padStart(8) + "etki".padStart(8) +
  "uzaklik".padStart(9) + "puan".padStart(8));
let toplam = 0;
for (const k of kaymalar) {
  const s = kaymaPuani(k);
  toplam += s.puan;
  console.log(
    k.ad.padEnd(28) + `${s.mesafe}px`.padStart(8) +
    s.etkiPayi.toFixed(3).padStart(8) + s.uzaklikPayi.toFixed(3).padStart(9) +
    s.puan.toFixed(4).padStart(8));
}
console.log("-".repeat(61));
console.log("oturum penceresi toplami".padEnd(28) + toplam.toFixed(4).padStart(33));
console.log(`esik degerlendirmesi: ${toplam <= 0.1 ? "iyi" : toplam <= 0.25 ? "gelistirilmeli" : "kotu"}`);
```

```
kayma                         mesafe    etki  uzaklik    puan
duyuru serigi acildi           100px   0.834    0.118  0.0988
istasyon gorseli yerlesti       24px   0.318    0.028  0.0090
olcum rozeti geldi               8px   0.066    0.009  0.0006
-------------------------------------------------------------
oturum penceresi toplami                               0.1085
esik degerlendirmesi: gelistirilmeli
```

Üç kaymanın etkisi birbirinden çok farklıdır. Sayfanın üstüne sonradan açılan yüz piksellik duyuru
şeridi, altındaki her şeyi taşıdığı için ekranın yüzde seksen üçünü etkiler ve tek başına puanın
neredeyse tamamını üretir. Ölçüm rozetinin sekiz piksellik oynaması ise on binde altı eder.

Buradan iki kural çıkar. Kaymanın maliyeti kayan ögenin boyutuyla değil, **altında ne
kadar içerik olduğuyla** orantılıdır: sayfanın en üstündeki küçük bir öge, en altındaki
büyük bir ögeden pahalıdır. Ve puan çarpım olduğu için iki payın biri sıfıra yaklaştığında
sonuç sıfıra yaklaşır — görünüm alanının dışında olan bir kayma hiç sayılmaz.

Toplama kuralı da tanımın parçasıdır: kaymalar **oturum penceresi** içinde toplanır ve
sayfanın puanı bu pencerelerin en büyüğüdür. Bir pencere, aralarında uzun boşluk olmayan
kaymaları bir arada tutar; uzun süre açık kalan bir sayfa, saatler boyunca biriken küçük
kaymalar yüzünden kötü puan almaz.

Kullanıcı hareketiyle başlayan kaymalar ölçüte girmez. Bir düğmeye basıldığında açılan
panelin altındakileri itmesi beklenen bir sonuçtur; ölçülen şey **beklenmedik** kaymadır.

#### Alan Verisi ve Laboratuvar Verisi

Aynı ölçüt iki ayrı yolla toplanır ve ikisi farklı soruları yanıtlar.

**Laboratuvar verisi**, denetlenen koşullarda üretilmiş ölçümdür: sabit ağ kısıtlaması,
sabit aygıt profili, eklentisiz tarayıcı, soğuk önbellek. Yeniden üretilebilir olduğu için
karşılaştırmaya elverişlidir; iki derlemenin farkını ölçmenin tek güvenilir yolu budur.
Yanıtlamadığı soru, gerçek kullanıcıların ne yaşadığıdır.

**Alan verisi**, gerçek ziyaretlerden toplanan ölçümdür. Aygıt çeşitliliğini, ağ
koşullarını ve kullanıcı davranışını içerir. Yanıtlamadığı soru ise nedendir: bir ölçütün
kötüleştiğini söyler, hangi kaynağın geciktiğini söylemez.

İkisi birbirinin yerine geçmez. Alan verisi sorunun **var olduğunu**, laboratuvar verisi
**nerede olduğunu** gösterir. Sonraki boyaya etkileşim gibi ölçütler ancak kullanıcı
etkileşimiyle oluştuğu için laboratuvar koşulunda doğrudan üretilemez; ölçülebilmesi için
etkileşimin taklit edilmesi gerekir ve taklit, gerçek kullanım örüntüsünü temsil etmez.

#### Dağılımı Özetlemek

Alan verisi tek bir sayı değil, bir dağılımdır ve dağılımın hangi sayıyla özetlendiği
sonucu değiştirir.

```js
// dagilim.mjs — ayni sayfanin iki farkli ozeti: ortalama ve yuzdelikler

// Dagilim uretilmis bir ornektir, olculmus alan verisi degildir:
// belirlenimci bir uretecle 500 ziyaret uretilir.
let tohum = 20260728;
const rastgele = () => (tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648;

const ziyaretler = Array.from({ length: 500 }, () => {
  // Ziyaretlerin yaklasik ucte biri yavas kosulda (uzak ag, yavas aygit).
  const yavas = rastgele() < 0.3;
  return yavas
    ? Math.round(3400 + rastgele() * 2100)
    : Math.round(1200 + rastgele() * 800);
});

function yuzdelik(dizi, p) {
  const sirali = [...dizi].sort((a, b) => a - b);
  const konum = (p / 100) * (sirali.length - 1);
  const alt = Math.floor(konum);
  const ust = Math.ceil(konum);
  return sirali[alt] + (sirali[ust] - sirali[alt]) * (konum - alt);
}

const ortalama = ziyaretler.reduce((a, b) => a + b, 0) / ziyaretler.length;
console.log("en buyuk icerik boyasi ozeti (uretilmis ornek dagilim, 500 ziyaret)");
for (const [ad, deger] of [
  ["ortalama", ortalama],
  ["ortanca (50.)", yuzdelik(ziyaretler, 50)],
  ["75. yuzdelik", yuzdelik(ziyaretler, 75)],
  ["95. yuzdelik", yuzdelik(ziyaretler, 95)],
]) {
  const sinif = deger <= 2500 ? "iyi" : deger <= 4000 ? "gelistirilmeli" : "kotu";
  console.log(`${ad.padEnd(16)}${(deger / 1000).toFixed(2)} s   -> ${sinif}`);
}

const iyiOran = ziyaretler.filter((v) => v <= 2500).length / ziyaretler.length;
console.log(`esigin altinda kalan ziyaret orani: ${(iyiOran * 100).toFixed(1)}%`);
```

```
en buyuk icerik boyasi ozeti (uretilmis ornek dagilim, 500 ziyaret)
ortalama        2.38 s   -> iyi
ortanca (50.)   1.73 s   -> iyi
75. yuzdelik    3.56 s   -> gelistirilmeli
95. yuzdelik    5.15 s   -> kotu
esigin altinda kalan ziyaret orani: 72.4%
```

Dağılım, programın içinde belirlenimci bir üreteçle kurulmuştur; ölçülmüş bir alan verisi
değil, iki özetleme yönteminin farkını göstermek için üretilmiş bir örnektir. Ziyaretlerin
üçte biri yavaş koşulda, geri kalanı hızlı koşulda varsayılmıştır.

Sonuç aynı veriye iki çelişik cevap verir. Ortalama iki virgül otuz sekiz saniyedir ve iyi
sınıfına düşer; yetmiş beşinci yüzdelik üç virgül elli altı saniyedir ve geliştirilmeli
sınıfına düşer. Aradaki fark bir hesap hatası değil, ortalamanın çalışma biçimidir: hızlı
ziyaretler yavaşların ağırlığını gizler.

Yüzdelik bunu yapamaz. Yetmiş beşinci yüzdelik, "kullanıcıların dörtte üçü bu değerden
daha iyisini gördü" demektir ve doğrudan bir kullanıcı payına karşılık gelir. Eşiğin
altında kalan ziyaret oranı da aynı bilgiyi verir: bu dağılımda ziyaretlerin yüzde
yetmiş ikisi iyi eşiğinin altındadır.

Ölçütlerin standart eşikleri de dağılım üzerinden tanımlıdır. En büyük içerik boyası için
iki buçuk saniyeye kadar iyi, dört saniyeye kadar geliştirilmeli sayılır; kümülatif düzen
kayması için sınırlar sıfır virgül bir ve sıfır virgül yirmi beştir; sonraki boyaya
etkileşim için iki yüz ve beş yüz milisaniyedir. Sınıflandırma tek bir ziyarete değil,
seçilen yüzdeliğe uygulanır.

#### İstasyon Sayfalarında Ölçüt Seçimi

Uygulamanın iki sayfası aynı ölçütlerle değerlendirilmez.

**İstasyon listesi sayfası** bir gezinme başlangıcıdır. Kullanıcı buraya listeyi görmek
için gelir ve hemen bir satıra dokunur. Belirleyici ölçütler en büyük içerik boyası ile
kümülatif düzen kaymasıdır: liste geç gelirse bekler, geç kayarsa yanlış satıra dokunur.

**Ölçüm ayrıntısı sayfası** bir çalışma yüzeyidir. Kullanıcı tarih aralığı değiştirir,
grafiği süzer, birim çevirir. Belirleyici ölçüt sonraki boyaya etkileşimdir; yükleme bir
kez olur, etkileşim onlarca kez.

Ölçüt seçimi bu yüzden sayfanın işine bağlıdır. Bütün ölçütleri her sayfa için aynı ağırlıkla
izlemek, hangi işin bozulduğunu gizler.

#### Özet

- Teknik kilometre taşları tarayıcının iç akışını ölçer; kullanıcı merkezli ölçütler
  kullanıcının gördüğü ve yaptığı şeyi ölçer, ikisi birbirinden bağımsız kötüleşebilir.
- Üç soru beş ölçüte bağlanır: ilk bayt süresi ile ilk içerik boyası boş ekranın bitişini,
  en büyük içerik boyası asıl içeriğin gelişini, sonraki boyaya etkileşim yanıt süresini,
  kümülatif düzen kayması görsel kararlılığı ölçer.
- Düzen kayması puanı etki payı ile uzaklık payının çarpımıdır; maliyeti belirleyen, kayan
  ögenin boyutu değil altında kalan içeriğin miktarıdır.
- Alan verisi sorunun var olduğunu, laboratuvar verisi nerede olduğunu gösterir; biri
  yeniden üretilebilir, diğeri temsil edicidir.
- Dağılım ortalamayla özetlendiğinde yavaş ziyaretler gizlenir; yüzdelik doğrudan bir
  kullanıcı payına karşılık geldiği için ölçüt eşikleri yüzdeliğe uygulanır.

#### Sonraki Adım

Ölçütler tanımlandı ve hangi sayısıyla okunacakları belirlendi. Bir sonraki soru,
ölçülen sürelerin nereye harcandığıdır. En büyük içerik boyası geç gerçekleşiyorsa gecikme
üç yerden birindedir: sunucunun yanıt üretmesi, yanıtın taşınması ya da tarayıcının ekrana
bir şey çizebilmek için beklemek zorunda olduğu kaynaklar. Üçüncüsü tümüyle geliştiricinin
denetimindedir ve çoğu zaman en büyük paydır. Sonraki ders, ilk boyamanın hangi kaynakları
beklediğini ve bu bekleyişin kaç gidiş-dönüşe mal olduğunu hesaplanabilir bir model
üzerinden ele alır.

### Kritik Oluşturma Yolu

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/kritik-olusturma-yolu

İlk boyamanın beklemek zorunda olduğu kaynaklar; çizimi engelleyen biçem ile ayrıştırmayı engelleyen betiğin ayrımı, gidiş-dönüş sayısının hesaplanması ve engellemeyi kaldıran yazımlar.

Önceki ders ölçütleri tanımladı: en büyük içerik boyası geç gerçekleşiyorsa bir sorun
vardır. Gecikmenin üç kaynağı olabilir — sunucunun yanıtı üretmesi, yanıtın taşınması ve
tarayıcının ekrana bir şey çizebilmek için beklemek zorunda olduğu kaynaklar. Üçüncüsü
tümüyle geliştiricinin denetimindedir.

Bu ders o bekleyişi tanımlar. **Kritik oluşturma yolu (critical rendering path)**, ilk
boyamanın gerçekleşebilmesi için tamamlanması zorunlu olan işler zinciridir. Zincirin
uzunluğu ölçülebilir ve kısaltılabilir.

#### İki Farklı Engelleme

Tarayıcı belgeyi yukarıdan aşağı ayrıştırır ve yol boyunca karşılaştığı kaynakları ister.
İki tür kaynak bu akışı durdurur ve ikisi farklı biçimde durdurur.

**Çizimi engelleyen biçem.** Bir dış biçem dosyası indirilip çözümlenmeden tarayıcı hiçbir
şey çizmez. Gerekçesi basamaklamadır: CSS ile Görsel Sunum kursunda kurulan kurala göre bir
ögenin hesaplanmış değeri, henüz inmemiş bir dosyadaki bildirimle değişebilir. Eksik
biçemle çizmek, saniyeler sonra bambaşka görünen bir sayfa demektir. Tarayıcı bu yüzden
bekler. Belgenin ayrıştırılması sürer — biçem dosyası ayrıştırmayı durdurmaz — ama boyama
durur.

**Ayrıştırmayı engelleyen betik.** Belge içine yazılmış, `defer` ya da `async` taşımayan
bir betik, indirilip çalıştırılana kadar ayrıştırmayı durdurur. Gerekçesi betiğin belgeyi
değiştirebilmesidir: betik, kendisinden sonraki içeriği üretebilir ya da silebilir, bu
yüzden tarayıcı devam edemez. Ayrıştırma durduğu için ondan sonrası keşfedilemez; boyama
da zaten olamaz.

İki engelleme birleşir. Betikten önce gelen bir biçem dosyası, betiğin çalışmasını da
bekletir: betik hesaplanmış değerleri okuyabileceği için tarayıcı biçemin çözümlenmesini
beklemek zorundadır. Böylece tek bir sıralama kararı üç işi birbirine bağlar.

#### Üç Değişken

Bekleyişin uzunluğu üç ayrı büyüklüğe bağlıdır ve bunları ayırmak, hangi eniyilemenin işe
yarayacağını belirler.

**Bayt sayısı** bant genişliğine bağlı olan bölümdür. Sıkıştırma ve ölü kod eleme buraya
etki eder.

**Gidiş-dönüş sayısı** gecikmeye bağlı olan bölümdür. Bir bağlantı üzerinde veri
gönderilirken tıkanıklık penceresi baştan küçüktür ve her turda büyür; bu yüzden büyük bir
dosya tek turda gelmez. Ağ Modelleri ve Protokoller kursunda tanımlanan bu davranış,
başarım açısından şu sonucu doğurur: ilk baytlar ucuz, sonrakiler pahalıdır.

**Zincir derinliği** ise keşif sırasına bağlıdır. Bir kaynağın varlığı ancak başka bir
kaynak indirildikten sonra öğreniliyorsa, ikisi koşut istenemez; süreleri toplanır.

Aşağıdaki model üçünü birlikte hesaplar.

```js
// kritik-yol.mjs — ilk boyanin kac gidis-donuse mal oldugunu modelleyen hesap

const MSS = 1460;               // bayt: tek segmentin tasidigi veri
const BASLANGIC_PENCERESI = 10; // segment: ilk turda gonderilebilen segment sayisi

// Baglanti, tikaniklik penceresini turlar boyunca tasir; pencere her turda ikiye katlanir.
function baglanti() {
  let pencere = BASLANGIC_PENCERESI;
  return {
    get pencere() { return pencere; },
    // Bir asama: es zamanli istenen kaynaklar ayni pencereyi paylasir.
    asama(baytlar) {
      let kalan = baytlar.reduce((a, b) => a + b, 0);
      let tur = 0;
      do {
        kalan -= pencere * MSS;
        pencere *= 2;
        tur += 1;
      } while (kalan > 0);
      return tur;
    },
  };
}

function ilkBoya(senaryo) {
  const b = baglanti();
  let tur = senaryo.baglantiKurulumu;
  const dokum = [["baglanti kurulumu", senaryo.baglantiKurulumu, "-"]];
  for (const asama of senaryo.asamalar) {
    const oncekiPencere = b.pencere;
    const t = b.asama(asama.baytlar);
    tur += t;
    dokum.push([asama.ad, t, `${oncekiPencere} -> ${b.pencere} segment`]);
  }
  return { tur, dokum };
}

// Kuzey Yamac Olcum Istasyonu — istasyon listesi sayfasi.
// Baglanti kurulumu: ad cozumleme 1 + tasima el sikismasi 1 + guvenli el sikisma 1 = 3 tur.
const senaryolar = [
  {
    ad: "A: engelleyen bicem ve betik basta",
    baglantiKurulumu: 3,
    asamalar: [
      { ad: "HTML govdesi", baytlar: [18_400] },
      { ad: "engelleyen bicem + betik", baytlar: [62_000, 141_000] },
      { ad: "betigin kesfettigi bicem", baytlar: [24_000] },
    ],
  },
  {
    ad: "B: betik ertelendi",
    baglantiKurulumu: 3,
    asamalar: [
      { ad: "HTML govdesi", baytlar: [18_400] },
      { ad: "engelleyen bicem", baytlar: [62_000] },
    ],
  },
  {
    ad: "C: kritik bicem gomuldu",
    baglantiKurulumu: 3,
    asamalar: [
      { ad: "HTML govdesi (+ gomulu bicem)", baytlar: [18_400 + 9_800] },
    ],
  },
];

console.log("--- yavas baslangic: k. tur sonunda gonderilebilmis toplam bayt ---");
let birikim = 0;
let pencere = BASLANGIC_PENCERESI;
for (let k = 1; k <= 4; k++) {
  birikim += pencere * MSS;
  console.log(`${k}. tur  pencere ${String(pencere).padStart(3)} segment  toplam ${String(birikim).padStart(7)} bayt`);
  pencere *= 2;
}

console.log("\n--- ilk boyaya kadar gidis-donus dokumu ---");
const sonuclar = [];
for (const s of senaryolar) {
  const r = ilkBoya(s);
  sonuclar.push({ ad: s.ad, tur: r.tur });
  console.log(`\n${s.ad}`);
  for (const [ad, t, p] of r.dokum) {
    console.log(`  ${ad.padEnd(30)} ${t} tur   ${p}`);
  }
  console.log(`  ${"TOPLAM".padEnd(30)} ${r.tur} tur`);
}

console.log("\n--- senaryolarin karsilastirmasi ---");
const taban = sonuclar[0].tur;
for (const s of sonuclar) {
  console.log(`${s.ad.padEnd(34)} ${String(s.tur).padStart(2)} tur   A'ya oran: ${(s.tur / taban).toFixed(2)}`);
}
```

```
--- yavas baslangic: k. tur sonunda gonderilebilmis toplam bayt ---
1. tur  pencere  10 segment  toplam   14600 bayt
2. tur  pencere  20 segment  toplam   43800 bayt
3. tur  pencere  40 segment  toplam  102200 bayt
4. tur  pencere  80 segment  toplam  219000 bayt

--- ilk boyaya kadar gidis-donus dokumu ---

A: engelleyen bicem ve betik basta
  baglanti kurulumu              3 tur   -
  HTML govdesi                   2 tur   10 -> 40 segment
  engelleyen bicem + betik       3 tur   40 -> 320 segment
  betigin kesfettigi bicem       1 tur   320 -> 640 segment
  TOPLAM                         9 tur

B: betik ertelendi
  baglanti kurulumu              3 tur   -
  HTML govdesi                   2 tur   10 -> 40 segment
  engelleyen bicem               2 tur   40 -> 160 segment
  TOPLAM                         7 tur

C: kritik bicem gomuldu
  baglanti kurulumu              3 tur   -
  HTML govdesi (+ gomulu bicem)  2 tur   10 -> 40 segment
  TOPLAM                         5 tur

--- senaryolarin karsilastirmasi ---
A: engelleyen bicem ve betik basta  9 tur   A'ya oran: 1.00
B: betik ertelendi                  7 tur   A'ya oran: 0.78
C: kritik bicem gomuldu             5 tur   A'ya oran: 0.56
```

İlk tablo tıkanıklık penceresinin büyümesini gösteriyor: ilk turda on dört kilobaytın
biraz üzerinde veri gönderilebiliyor, dördüncü turun sonunda iki yüz kilobaytı aşıyor. Bu
sayı bir sınır değil, bir başlangıç varsayımıdır; başlangıç penceresi yapılandırmaya
bağlıdır ve model değeri en başta bir sabit olarak açıyor.

Üç senaryo aynı sayfayı üç ayrı yazımla kuruyor. A senaryosunda engelleyen biçem ve betik
belgenin başında duruyor, üstelik betik indirildikten sonra ikinci bir biçem dosyası
keşfediliyor. B senaryosunda betik erteleniyor; hem kendi baytları hem de keşfettiği
zincir kritik yoldan çıkıyor. C senaryosunda ilk ekranın biçemi belgenin içine gömülüyor
ve dış istek kalmıyor.

Sonuç oranlarla okunur: B senaryosu A'nın yüzde yetmiş sekizi, C senaryosu yüzde elli
altısı kadar bekliyor. Mutlak süre verilemez, çünkü tur süresi ağa bağlıdır — ama tur
sayısı yazımın kendisinden çıkar ve karşılaştırılabilir.

Model iki noktada gerçeği sadeleştirir. Aşamalar içindeki koşut isteklerin aynı pencereyi
paylaştığını varsayar ve pencerenin boşta kalma sonrası küçülmesini hesaba katmaz. İkisi de
sonucu B ve C lehine değil, A lehine kaydırır; yani gerçek fark modelin gösterdiğinden
küçük değil, büyüktür.

#### Engellemeyi Kaldırmak

Kritik yolu kısaltmanın yolları, engellemenin iki türüne karşılık gelir.

**Betiği kritik yoldan çıkarmak.** `defer` bildirimi, betiğin ayrıştırmaya koşut
indirilmesini ve belge ayrıştırması bittikten sonra, yazıldığı sırayla çalıştırılmasını
sağlar. `async` da koşut indirir ama indiği anda çalıştırır; sıra garantisi yoktur ve
çalıştığı an ayrıştırmayı yine keser. Birbirine bağımlı betikler için `defer`, bağımsız ve
tek başına çalışan betikler için `async` uygundur. Modül olarak yüklenen betikler
öntanımlı olarak ertelenmiş davranır.

**Biçemi koşullu hâle getirmek.** Bir biçem bağlantısına `media` özniteliği yazıldığında,
koşul o anda tutmuyorsa dosya çizimi engellemez. Yazdırma biçemleri ve yalnızca geniş
ekranlarda geçerli biçemler bu yolla kritik yoldan çıkarılır. Dosya yine indirilir, ancak
düşük öncelikle ve boyamayı bekletmeden.

**Kritik biçemi gömmek.** İlk ekranı çizmeye yeten bildirimler belgenin içine yazılır;
kalan biçem engellemeyen bir yolla yüklenir. Bu, C senaryosundaki kazancın kaynağıdır ve
bedeli vardır: gömülü biçem ayrı önbelleklenemez, her sayfa isteğiyle yeniden gelir ve
belgeyi büyütür. Karar, tekrar ziyaret oranına bağlıdır — ilk ziyaretin ağırlıklı olduğu
bir sayfada kazanç, her gün açılan bir panelde kayıptır.

**Geç keşfedilen kaynağı öne çekmek.** Bir kaynak ancak biçem çözümlendikten ya da betik
çalıştıktan sonra keşfediliyorsa, `preload` bildirimiyle belge başında istenebilir. Bu
bildirim bir eniyileme değil, bir düzeltmedir: zinciri kısaltmanın doğrudan yolu varken
kullanılmamalıdır, çünkü yanlış yazıldığında kaynağı iki kez indirtir ya da gerçekten
kritik olan bir kaynağın bant genişliğini alır.

#### Keşif Zinciri

A senaryosundaki dördüncü satır — "betiğin keşfettiği biçem" — tek başına bir turdan fazla
maliyet üretir, çünkü o tur ancak betik indirilip çalıştıktan sonra başlayabilir.

Tarayıcının ön tarayıcısı belgeyi ayrıştırmadan önce hızlıca gözden geçirir ve gördüğü
kaynakları erkenden ister. Ön tarayıcının göremediği tek şey, belgede yazılı olmayan
kaynaklardır: betik tarafından eklenen bağlantılar, biçem dosyası içinden çağrılan başka
biçemler, bir yapılandırma dosyası okunduktan sonra belirlenen adresler.

Bu yüzden kritik yol eniyilemesinin en etkili adımı çoğu zaman bayt azaltmak değil,
**zinciri düzleştirmektir**: kritik kaynakların hepsinin belgenin ilk baytlarında görünür
olması.

#### İstasyon Listesi Sayfasının Belge Başı

```html
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kuzey Yamaç Ölçüm İstasyonu — İstasyonlar</title>

  <!-- ilk ekranın biçemi: gömülü, dış istek yok -->
  <style>
    .istasyon-listesi { display: grid; gap: 12px; }
    .istasyon-listesi > li { min-height: 72px; }
  </style>

  <!-- kalan biçem: çizimi engellemez, indikten sonra uygulanır -->
  <link rel="stylesheet" href="/varlik/istasyon.9f2c1a.css" media="print"
        onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/varlik/istasyon.9f2c1a.css"></noscript>

  <!-- yazdırma biçemi hiçbir zaman çizimi engellemez -->
  <link rel="stylesheet" href="/varlik/yazdirma.3ce8b0.css" media="print">

  <!-- uygulama betiği: ayrıştırmaya koşut iner, ayrıştırma bitince sırayla çalışır -->
  <script src="/varlik/giris.4a1c.js" defer></script>
</head>
```

İkinci bağlantı bir yazım hilesidir ve açıklanması gerekir: `media="print"` yazılan bir
biçem çizimi engellemez; dosya indikten sonra `onload` işleyicisi ortamı `all` yaparak
biçemi uygular. `noscript` içindeki ikinci bağlantı, betik çalışmadığında biçemin yine de
gelmesini sağlar. Hile, biçem bağlantısı için doğrudan bir "engelleme" bildirimi
bulunmamasından doğar.

Gömülü biçemdeki `min-height` bildirimi başarım değil kararlılık içindir: liste satırları
veri gelmeden önce yüksekliklerini alır ve önceki derste hesaplanan düzen kayması oluşmaz.
İki hedef aynı bildirimde buluşur.

#### Özet

- Kritik oluşturma yolu, ilk boyamanın beklemek zorunda olduğu işler zinciridir; çizimi
  engelleyen biçem boyamayı, ayrıştırmayı engelleyen betik hem ayrıştırmayı hem boyamayı
  durdurur.
- Bekleyişin üç bileşeni ayrı ayrı ele alınır: bayt sayısı bant genişliğine, gidiş-dönüş
  sayısı gecikmeye, zincir derinliği keşif sırasına bağlıdır.
- Tıkanıklık penceresi turlarla büyüdüğü için ilk baytlar ucuzdur; bir kaynağı kritik
  yoldan çıkarmak, onu küçültmekten çok daha büyük kazanç verebilir.
- `defer`, `async` ve `media` bildirimleri engellemeyi kaldırır; kritik biçemi gömmek dış
  isteği tümüyle siler ama önbelleklenebilirlikten vazgeçer.
- Ön tarayıcı yalnızca belgede yazılı kaynakları görür; zinciri düzleştirmek çoğu zaman
  bayt azaltmaktan etkilidir.

#### Sonraki Adım

Kritik yol kısaldığında ilk boyama erkene çeker, ama en büyük içerik boyası çoğu sayfada
bir metin bloğu değil bir görseldir. İstasyon listesindeki kart görselleri ve ölçüm
ayrıntısı sayfasındaki istasyon fotoğrafı, belgeden çok daha fazla bayt taşır; yazı tipi
dosyaları ise metnin ne zaman okunabilir olacağını belirler. İkisinin de kendi boyut,
biçim ve yükleme kararları vardır. Sonraki ders bu kararları ele alır ve hangi aday
dosyanın hangi koşulda indirileceğini hesaplar.

### Görsel ve Yazı Tipi Optimizasyonu

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/gorsel-ve-yazi-tipi-optimizasyonu

Görselin aktarım, çözme ve çizim maliyetlerinin ayrılması; aday kümesinden doğru dosyanın seçilmesinin hesabı, biçim sınıflarına göre karar ve yazı tipi yükleme stratejisinin zaman çizelgesi.

Kritik yol kısaldığında ilk boyama erkene çeker. En büyük içerik boyası ise çoğu sayfada
bir metin bloğu değil, bir görseldir: istasyon listesindeki kart görselleri ve ölçüm
ayrıntısı sayfasındaki istasyon fotoğrafı, belgenin kendisinden çok daha fazla bayt taşır.
Yazı tipi dosyaları ise metnin ne zaman okunabilir hâle geleceğini belirler.

Bu ders iki varlık türünün boyut, biçim ve yükleme kararlarını ele alır.

#### Bir Görselin Üç Ayrı Maliyeti

Görsel eniyilemesi çoğunlukla tek bir sayıya indirgenir: dosya boyutu. Oysa aynı görsel üç
ayrı yerde maliyet üretir ve bunlar birbirinden bağımsızdır.

**Aktarım maliyeti** indirilen bayt sayısıdır. Sıkıştırma bunu düşürür.

**Çözme maliyeti** sıkıştırılmış baytların piksellere çevrilmesidir. İşlemci zamanı harcar
ve sonucunda bellekte, sıkıştırmadan bağımsız bir yer kaplar: piksel başına dört bayt.
Ağ Panelinde Tanı dersindeki aktarılan boyut–kaynak boyutu ayrımının görsellerdeki
karşılığı budur ve burada fark çok daha büyüktür. İyi sıkışan bir fotoğraf ağı yormaz,
belleği yorar.

**Çizim maliyeti** çözülmüş pikselin ekrandaki kutuya ölçeklenmesidir. Kaynak piksel sayısı
hedef kutudan büyükse fazlalık her karede yeniden ölçeklenir.

İkinci ve üçüncü maliyet, seçilen dosyanın **piksel boyutuna** bağlıdır; dosya boyutuna
değil. Aşağıdaki hesap bu ayrımı sayılara döker.

```js
// aday.mjs — aday kumesinden hangi dosyanin secildigi ve cozulmus piksel maliyeti

// istasyon karti gorseli: en boy orani 3:2
const adaylar = [400, 800, 1200, 1600, 2400]; // genislik tanimlayicilari (w)
const EN_BOY = 2 / 3;

// sizes bildirimi: 720 pikselden dar gorunum alaninda tam genislik, uzerinde 360 piksel
const yerlesimGenisligi = (gorunumGenisligi) =>
  gorunumGenisligi <= 720 ? gorunumGenisligi - 32 : 360;

function secilenAday(gorunumGenisligi, pikselOrani) {
  const gereken = yerlesimGenisligi(gorunumGenisligi) * pikselOrani;
  return adaylar.find((a) => a >= gereken) ?? adaylar[adaylar.length - 1];
}

const cozulmusBayt = (genislik) => Math.round(genislik * genislik * EN_BOY * 4);

console.log("gorunum".padStart(8) + "oran".padStart(6) + "yerlesim".padStart(10) +
  "gereken".padStart(9) + "secilen".padStart(9) + "cozulmus".padStart(11) +
  "en buyuk aday".padStart(15) + "kat".padStart(7));
for (const [gorunum, oran] of [[390, 2], [390, 3], [768, 1], [768, 2], [1280, 2]]) {
  const secilen = secilenAday(gorunum, oran);
  const gereken = yerlesimGenisligi(gorunum) * oran;
  const secilenBayt = cozulmusBayt(secilen);
  const enBuyukBayt = cozulmusBayt(adaylar[adaylar.length - 1]);
  console.log(
    `${gorunum}px`.padStart(8) + `${oran}x`.padStart(6) +
    `${yerlesimGenisligi(gorunum)}px`.padStart(10) + `${gereken}px`.padStart(9) +
    `${secilen}w`.padStart(9) +
    `${(secilenBayt / 1024 / 1024).toFixed(2)} MB`.padStart(11) +
    `${(enBuyukBayt / 1024 / 1024).toFixed(2)} MB`.padStart(15) +
    `${(enBuyukBayt / secilenBayt).toFixed(1)}x`.padStart(7));
}
console.log("cozulmus = genislik x yukseklik x 4 bayt (kanal basina 1 bayt, RGBA)");
console.log("kat = en buyuk aday secilseydi cozulmus bellegin kac kati olacagi");
```

```
 gorunum  oran  yerlesim  gereken  secilen   cozulmus  en buyuk aday    kat
   390px    2x     358px    716px     800w    1.63 MB       14.65 MB   9.0x
   390px    3x     358px   1074px    1200w    3.66 MB       14.65 MB   4.0x
   768px    1x     360px    360px     400w    0.41 MB       14.65 MB  36.0x
   768px    2x     360px    720px     800w    1.63 MB       14.65 MB   9.0x
  1280px    2x     360px    720px     800w    1.63 MB       14.65 MB   9.0x
cozulmus = genislik x yukseklik x 4 bayt (kanal basina 1 bayt, RGBA)
kat = en buyuk aday secilseydi cozulmus bellegin kac kati olacagi
```

Seçim iki adımda yapılır. Önce `sizes` bildiriminden ögenin **yerleşim genişliği** çözülür;
bu, ortam sorgularıyla yazılmış bir kural kümesidir ve tarayıcı görüntü alanına bakarak
tek bir CSS piksel değeri üretir. Sonra bu değer piksel oranıyla çarpılır ve aday
kümesinden bu sayıya yeten en küçük dosya seçilir.

Tablonun anlattığı asıl şey son sütundur. Aynı görselin en büyük adayı her koşulda
çözüldüğünde on dört buçuk megabaytlık piksel verisi üretir; oysa dar bir ekranda dört yüz
piksellik dosya yeterlidir ve otuz altı kat daha az bellek harcar. Bu fark dosya
boyutlarında görünmez — iki dosyanın sıkıştırılmış hâlleri arasındaki oran çok daha
küçüktür.

Aynı tabloda ikinci bir gözlem var: geniş ekranda kart daha küçüktür. Duyarlı düzende
sütun sayısı arttıkça tek bir kartın genişliği düşer, dolayısıyla masaüstü daima daha
büyük görsel ister varsayımı yanlıştır. `sizes` bildirimi düzenden türetilmelidir,
tahminden değil.

#### Biçim Seçimi

Biçim kararı ürün adlarıyla değil, içerik sınıflarıyla verilir.

**Vektör mü, ızgara mı?** Simgeler, çizelgeler, logolar ve çizgi grafikleri geometriyle
tanımlıdır; vektör biçiminde saklandıklarında her ölçekte keskin kalır ve çözülmüş
bellekleri piksel sayısına değil geometri karmaşıklığına bağlıdır. Fotoğraf ızgara
biçimindedir; vektöre çevrilemez.

**Kayıplı mı, kayıpsız mı?** Kayıplı kodlayıcı, gözün ayırt etmediği bilgiyi atarak
sıkıştırır ve fotoğraf için doğru seçimdir. Kayıpsız kodlayıcı her pikseli korur; keskin
kenarların bulunduğu içerikte — ekran görüntüsü, metin içeren görsel, düz renk alanları —
kayıplı sıkıştırmanın bıraktığı bulanıklık görünür hâle gelir.

**Saydamlık gerekiyor mu?** Alfa kanalı destekleyen bir biçim seçilmelidir; gerekmiyorsa
seçilmemelidir, çünkü dördüncü kanal veri ekler.

**Aşamalı çözünürlük veriliyor mu?** Bazı biçimler dosyanın ilk baytlarından düşük
çözünürlüklü bir sürüm üretilmesine izin verir. Büyük bir görselde bu, kullanıcının bir
şey görmesiyle her şeyi görmesi arasındaki süreyi ayırır.

Farklı biçimlerin farklı istemcilerde desteklenmesi `picture` ögesiyle çözülür: kaynak
listesindeki her seçenek kendi içerik türünü bildirir, istemci tanımadığı türü atlar ve
sonunda `img` ögesindeki karşılığa düşer. Bu, yetenek saptamanın işaretleme düzeyindeki
karşılığıdır; hangi biçimin destekleneceğine dair varsayım gerektirmez.

#### Ne Zaman İstenir

Doğru dosya seçildikten sonra iki karar kalır: ne zaman istenecek ve yeri baştan ayrılacak
mı?

**Gecikmeli yükleme** görünüm alanının dışındaki görsellerin istenmesini erteler. Kuralın
tek istisnası önemlidir: en büyük içerik boyasına aday olan görsel gecikmeli
yüklenmemelidir. Ertelenen istek, ölçütün kendisini geciktirir.

**Öncelik bildirimi** aynı sorunun diğer ucudur. Belgede geçen bütün görseller öntanımlı
olarak düşük öncelikle istenir; ilk ekrandaki kahraman görsel için `fetchpriority="high"`
yazıldığında istek diğerlerinin önüne geçer.

**Yer ayırma** başarım değil kararlılık kararıdır, ama aynı ögede verilir. Görsele
`width` ve `height` öznitelikleri yazıldığında tarayıcı en boy oranını dosya inmeden bilir
ve kutuyu baştan ayırır. Yazılmadığında, görsel indiği anda altındaki her şey kayar —
ilk derste hesaplanan puanın en yaygın kaynağı budur.

#### Yazı Tipinin Zaman Çizelgesi

CSS ile Görsel Sunum kursundaki Yazı Tipi Yükleme dersi `font-display` değerlerini iki
döneme göre ayırmıştı: engelleme süresi boyunca metin çizilmez, değiştirme süresi boyunca
yedek yazı tipiyle çizilir. Burada sorulan soru farklıdır: dosya belirli bir anda gelirse
kullanıcı ne görür?

```js
// yazitipi-cizelge.mjs — dosya varis anina gore metnin durumu
// Belirtimde tanimli uc donem: engelleme, degistirme, geri cekilme.
const DONEMLER = {
  block:    { engelleme: 3000, degistirme: Infinity },
  swap:     { engelleme: 100,  degistirme: Infinity },
  fallback: { engelleme: 100,  degistirme: 3000 },
  optional: { engelleme: 100,  degistirme: 0 },
};

function cizelge(deger, varisAni) {
  const { engelleme, degistirme } = DONEMLER[deger];
  const gorunmezSure = Math.min(varisAni, engelleme);
  if (varisAni <= engelleme) {
    return { gorunmezSure, yedekSure: 0, sonuc: "istenen yazi tipi" };
  }
  const degistirmeSonu = engelleme + degistirme;
  if (varisAni <= degistirmeSonu) {
    return { gorunmezSure, yedekSure: varisAni - engelleme, sonuc: "istenen yazi tipi" };
  }
  return { gorunmezSure, yedekSure: Infinity, sonuc: "yedek yazi tipi kalir" };
}

const sure = (ms) => (ms === Infinity ? "sayfa boyunca" : `${ms} ms`);

console.log("--- dosya varis anina gore metnin durumu ---");
console.log("deger".padEnd(10) + "varis".padStart(8) + "gorunmez".padStart(10) +
  "yedekle".padStart(15) + "   son durum");
for (const deger of Object.keys(DONEMLER)) {
  for (const varis of [80, 900, 4200]) {
    const c = cizelge(deger, varis);
    console.log(
      deger.padEnd(10) + `${varis} ms`.padStart(8) +
      `${c.gorunmezSure} ms`.padStart(10) + sure(c.yedekSure).padStart(15) +
      "   " + c.sonuc);
  }
}
```

```
--- dosya varis anina gore metnin durumu ---
deger        varis  gorunmez        yedekle   son durum
block        80 ms     80 ms           0 ms   istenen yazi tipi
block       900 ms    900 ms           0 ms   istenen yazi tipi
block      4200 ms   3000 ms        1200 ms   istenen yazi tipi
swap         80 ms     80 ms           0 ms   istenen yazi tipi
swap        900 ms    100 ms         800 ms   istenen yazi tipi
swap       4200 ms    100 ms        4100 ms   istenen yazi tipi
fallback     80 ms     80 ms           0 ms   istenen yazi tipi
fallback    900 ms    100 ms         800 ms   istenen yazi tipi
fallback   4200 ms    100 ms  sayfa boyunca   yedek yazi tipi kalir
optional     80 ms     80 ms           0 ms   istenen yazi tipi
optional    900 ms    100 ms  sayfa boyunca   yedek yazi tipi kalir
optional   4200 ms    100 ms  sayfa boyunca   yedek yazi tipi kalir
```

Dosya hızlı geldiğinde beş değer de aynı sonucu verir; ayrım yavaş koşulda ortaya çıkar.
Dokuz yüz milisaniyede gelen bir dosya için `block` metni dokuz yüz milisaniye boyunca
görünmez bırakır, `swap` ve `fallback` yüz milisaniye sonra yedek yazı tipiyle çizer,
`optional` ise dosyayı o sayfada hiç kullanmaz.

Karar, hangi ölçütün korunduğuna göre verilir. `swap` ilk içerik boyasını korur ve düzen
kaymasını göze alır: yedek yazı tipiyle çizilen metin, asıl yazı tipi geldiğinde ölçü
değiştirir. `optional` düzen kararlılığını korur ve tasarlanan yazı tipinden vazgeçer.
`block` her ikisini de bozar ve yalnızca yedeği anlamsız olan durumlarda — simge yazı
tipleri gibi — savunulabilir.

Süre değerleri belirtimde öneri olarak verilir ve gerçekleştirime göre değişir; modeldeki
sayılar bu önerilerden alınmıştır. Değişen sayı, değerlerin birbirine göre sıralamasını
değiştirmez.

Yazı tipi maliyetini azaltmanın üç yolu vardır ve üçü de bu tabloyu iyileştirir: dosyayı
kendi metnine göre alt kümelere ayırmak indirilecek baytı azaltır, dosyayı `preload` ile
erken istemek varış anını öne çeker, yedek yazı tipini `size-adjust` ve benzeri
tanımlarla asıl yazı tipine ölçüce yaklaştırmak ise değişim anındaki kaymayı sıfıra
yaklaştırır.

#### İstasyon Kartının İşaretlemesi

```html
<article class="istasyon-karti">
  <img
    src="/gorsel/kuzey-yamac-800.img"
    srcset="/gorsel/kuzey-yamac-400.img   400w,
            /gorsel/kuzey-yamac-800.img   800w,
            /gorsel/kuzey-yamac-1200.img 1200w"
    sizes="(max-width: 720px) calc(100vw - 32px), 360px"
    width="1200" height="800"
    fetchpriority="high"
    alt="Kuzey Yamaç istasyonunun kar örtüsü altındaki ölçüm direği">
  <h2>Kuzey Yamaç 1</h2>
</article>
```

Dosya uzantısı örnekte yer tutucudur; seçilen biçime göre yazılır. Kural uzantıda değil
yapıdadır: aday kümesi gerçek piksel genişlikleriyle bildirilir, `sizes` bildirimi düzenden
türetilir, `width` ve `height` en boy oranını baştan verir ve öncelik bildirimi yalnızca
ilk ekrandaki karta yazılır. Birden çok biçim sunulacaksa aynı `img` ögesi bir `picture`
içine alınır ve her `source` kendi içerik türünü bildirir; `img` son çare olarak kalır.

Listenin görünüm alanı dışında kalan kartlarında öncelik bildirimi bulunmaz; onlara
`loading="lazy"` yazılır. Aynı bileşenin iki farklı örneği farklı bildirimler alır ve bu
ayrım bileşen arayüzünde bir seçenek olarak dışarı çıkarılır.

#### Özet

- Bir görselin aktarım, çözme ve çizim maliyetleri ayrıdır; çözülmüş bellek dosya boyutuna
  değil piksel sayısına bağlıdır ve aradaki fark kat kat olabilir.
- Aday seçimi iki adımdır: `sizes` bildiriminden yerleşim genişliği çözülür, piksel oranıyla
  çarpılır ve yeten en küçük aday seçilir; bildirim düzenden türetilmelidir.
- Biçim kararı ürün adıyla değil içerik sınıfıyla verilir: vektör mü ızgara mı, kayıplı mı
  kayıpsız mı, saydamlık gerekli mi, aşamalı çözünürlük veriliyor mu.
- En büyük içerik boyasına aday olan görsel gecikmeli yüklenmez; öncelik bildirimi yalnızca
  ilk ekrandaki görsele yazılır ve yer ayırma her görselde yapılır.
- Yazı tipi görüntüleme davranışı bir ölçüt tercihidir: `swap` ilk içerik boyasını,
  `optional` düzen kararlılığını korur.

#### Sonraki Adım

Buraya kadarki kararların hepsi belgenin ve varlıkların kendisiyle ilgiliydi: hangi dosya,
ne kadar büyük, ne zaman istenir. Bir katman daha var ve orada tek bir bayt bile
azaltmadan kazanç elde edilebilir. Aynı kullanıcı istasyon listesini ikinci kez açtığında
aynı dosyaları yeniden indirmek zorunda mıdır? Bağlantı kurulumu her seferinde baştan mı
yapılır? Bu soruların yanıtı istekte değil, yanıtın taşıdığı başlıklarda ve belgeye
yazılan bağlantı ipuçlarındadır. Sonraki ders bu başlıkları yerel bir sunucu üzerinde
ölçerek ele alır.

### Ağ Başarımı

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/ag-basarimi

Tazelik ile doğrulamanın ayrılması, önbellek başlıklarının yerel bir sunucu üzerinde ölçülmesi, içerik karmalı adların değişmezlik sözü ve bağlantı ipuçlarının neyi kazandırdığı.

Önceki üç ders belgenin ve varlıkların kendisini ele aldı: hangi dosya, ne kadar büyük, ne
zaman istenir. Bir katman daha var ve orada tek bir bayt bile azaltmadan kazanç elde
edilebilir. Aynı kullanıcı istasyon listesini ikinci kez açtığında aynı dosyaları yeniden
indirmek zorunda mıdır?

Bu sorunun yanıtı istekte değil, yanıtın taşıdığı başlıklardadır. Tarayıcı ve Web Platformu
kursundaki Önbellek Stratejileri dersi program denetimli önbelleği ele almıştı; buradaki
konu, program yazılmadan sunucunun başlıklarla neyi belirlediğidir.

#### Tazelik ve Doğrulama İki Ayrı Karardır

HTTP önbelleği iki ayrı soruyu yanıtlar ve bunları karıştırmak yanlış yapılandırmanın en
yaygın nedenidir.

**Tazelik ömrü** şu soruyu yanıtlar: bu yanıt ne kadar süre sorgusuz kullanılabilir?
`Cache-Control` başlığındaki `max-age` yönergesi bir süre verir; süre dolana kadar tarayıcı
**hiç istek yapmaz**. En ucuz istek, yapılmayan istektir.

**Doğrulayıcı** şu soruyu yanıtlar: elimdeki kopya hâlâ geçerli mi? `ETag` başlığı içeriğe
bağlı bir imza taşır. Tazelik süresi dolduğunda tarayıcı bu imzayı `If-None-Match` başlığıyla
geri gönderir; içerik değişmemişse sunucu gövdesiz bir yanıt döndürür.

İki karar birbirini tamamlar. Tazelik isteği tümüyle önler; doğrulama isteği ucuzlatır. `no-cache`
yönergesi bir yanlış anlamaya yol açar: saklamayı yasaklamaz, yalnızca her kullanımdan önce
doğrulamayı zorunlu kılar. Saklamayı yasaklayan yönerge `no-store` yönergesidir.

#### Başlıkları Ölçmek

Aşağıdaki sunucu üç kaynağı üç ayrı politikayla sunar ve koşullu isteği gerçekten
karşılar.

```js
// onbellek-sunucusu.mjs — tazelik ve dogrulama basliklarini uc ayri kaynakta gosterir
import { createServer } from "node:http";
import { createHash } from "node:crypto";

const govdeler = {
  "/varlik/istasyon.9f2c1a.css": {
    tur: "text/css",
    icerik: ".istasyon-listesi{display:grid;gap:12px}",
    // Icerik karmali ad: adres degisirse icerik degismistir.
    onbellek: "public, max-age=31536000, immutable",
  },
  "/api/istasyonlar": {
    tur: "application/json",
    icerik: JSON.stringify({ istasyonlar: ["kuzey-yamac-1", "kuzey-yamac-2"] }),
    // Her kullanimda dogrulanir; govde yalnizca degistiyse gonderilir.
    onbellek: "no-cache",
  },
  "/api/duyuru": {
    tur: "application/json",
    icerik: JSON.stringify({ duyuru: "Bakim penceresi: 03:00-04:00" }),
    // Bir dakika taze; sonraki bes dakika eskisi sunulup arkada tazelenir.
    onbellek: "public, max-age=60, stale-while-revalidate=300",
  },
};

const etiket = (icerik) =>
  `"${createHash("sha256").update(icerik).digest("hex").slice(0, 16)}"`;

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const kaynak = govdeler[istek.url];
  if (!kaynak) {
    yanit.writeHead(404, { "Content-Type": "text/plain" });
    return yanit.end("yok\n");
  }

  const dogrulayici = etiket(kaynak.icerik);
  const basliklar = {
    "Cache-Control": kaynak.onbellek,
    ETag: dogrulayici,
    Vary: "Accept-Encoding",
  };

  // Kosullu istek: istemcinin elindeki dogrulayici hala gecerliyse govde gonderilmez.
  if (istek.headers["if-none-match"] === dogrulayici) {
    yanit.writeHead(304, basliklar);
    return yanit.end();
  }

  yanit.writeHead(200, {
    ...basliklar,
    "Content-Type": kaynak.tur,
    "Content-Length": Buffer.byteLength(kaynak.icerik),
  });
  yanit.end(kaynak.icerik);
}).listen(8241, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8241"));
```

```bash
#!/usr/bin/env bash
# onbellek-sunucusu.mjs'yi baslatir, tazelik ve dogrulama davranisini olcer, durdurur.
node onbellek-sunucusu.mjs > /dev/null &
sunucu=$!
sleep 1
temel=http://127.0.0.1:8241

for yol in /varlik/istasyon.9f2c1a.css /api/istasyonlar /api/duyuru; do
  printf '%s\n' "$yol"
  curl -sS -o /dev/null -D - "$temel$yol" \
    | grep -iE '^(HTTP/|cache-control|etag|content-length)' \
    | tr -d '\r' | sed 's/^/  /'
done

printf '\n--- kosullu istek ---\n'
etiket=$(curl -sS -o /dev/null -D - "$temel/api/istasyonlar" \
  | awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')
printf 'istemcideki dogrulayici: %s\n' "$etiket"

curl -sS -o /dev/null -H "If-None-Match: $etiket" "$temel/api/istasyonlar" \
  -w 'kosullu istek  -> durum %{http_code}, indirilen govde %{size_download} bayt\n'
curl -sS -o /dev/null "$temel/api/istasyonlar" \
  -w 'kosulsuz istek -> durum %{http_code}, indirilen govde %{size_download} bayt\n'

kill "$sunucu"
```

```
/varlik/istasyon.9f2c1a.css
  HTTP/1.1 200 OK
  Cache-Control: public, max-age=31536000, immutable
  ETag: "5102555c20f3670b"
  Content-Length: 40
/api/istasyonlar
  HTTP/1.1 200 OK
  Cache-Control: no-cache
  ETag: "c0c1001d3d358ea1"
  Content-Length: 49
/api/duyuru
  HTTP/1.1 200 OK
  Cache-Control: public, max-age=60, stale-while-revalidate=300
  ETag: "caefa267f7a6d1f0"
  Content-Length: 41

--- kosullu istek ---
istemcideki dogrulayici: "c0c1001d3d358ea1"
kosullu istek  -> durum 304, indirilen govde 0 bayt
kosulsuz istek -> durum 200, indirilen govde 49 bayt
```

Doğrulayıcı değerleri içeriğin özetinden üretildiği için bu çıktıda sabittir; içerik
değiştiğinde değişir. Bağlantı noktası olarak seçilen 8241 keyfîdir ve boş olmalıdır.

Ölçülen fark açıktır: koşullu istek gövdesiz döner. Gidiş-dönüş yine yapılmıştır — koşullu
istek gecikmeyi ortadan kaldırmaz, yalnızca aktarımı ortadan kaldırır. Tazelik ömrü ise
gidiş-dönüşü de ortadan kaldırır. İkisi arasındaki bu fark, hangi kaynağa hangi politikanın
yazılacağını belirler.

#### Değişmez Varlık, Değişken Belge

`immutable` yönergesi bir söz verir: bu adresteki içerik hiç değişmeyecek. Söz ancak
adres içerikten türetiliyorsa tutulabilir. Oluşturma Stratejileri ve Altyapı kursundaki
önbellek kırma yaklaşımı tam olarak bunu sağlar: dosya adına içerik karması yazıldığında,
içerik değiştiğinde adres de değişir ve eski adresin sonsuza kadar taze kalması bir sorun
yaratmaz.

Belge bu sözü veremez, çünkü adresi sabittir ve içeriği her yayında değişir. Belgeye
uygulanan doğru politika, uzun tazelik değil doğrulamadır.

Aradaki üçüncü durum için `stale-while-revalidate` yönergesi vardır: tazelik süresi dolduktan
sonra belirtilen süre boyunca eski kopya hemen sunulur ve tazeleme arka planda yapılır. Kullanıcı
beklemez; veri bir tur gecikmeyle tazelenir. Duyuru şeridi gibi gecikmeye toleranslı içerik için
uygundur; hesap bakiyesi gibi içerik için değildir.

Bu üç politikanın tekrar ziyarette ne kadar bayt aktardığı hesaplanabilir.

```js
// tekrar-ziyaret.mjs — baslik politikasinin tekrar ziyarette aktarilan bayta etkisi

const BASLIK_YUKU = 320; // bayt: istek + yanit basliklarinin yaklasik payi

// Istasyon listesi sayfasinin kaynaklari ve secilen politika.
const kaynaklar = [
  { ad: "belge",         bayt:  18_400, politika: "no-cache" },
  { ad: "bicem",         bayt:  62_000, politika: "immutable" },
  { ad: "giris betigi",  bayt: 148_112, politika: "immutable" },
  { ad: "ortak betik",   bayt: 101_760, politika: "immutable" },
  { ad: "yazi tipi",     bayt:  41_600, politika: "immutable" },
  { ad: "kart gorseli",  bayt:  86_400, politika: "immutable" },
  { ad: "istasyon veri", bayt:   9_240, politika: "max-age=60" },
];

const tamBoyut = (k) => BASLIK_YUKU + k.bayt;
const ilkZiyaret = kaynaklar.reduce((t, k) => t + tamBoyut(k), 0);

// Tekrar ziyarette bir kaynak icin aktarilan bayt.
function tekrarBayti(kaynak, gecenSaniye, degisenler) {
  const degisti = degisenler.has(kaynak.ad);
  switch (kaynak.politika) {
    case "immutable":
      // Icerik karmali ad: degistiyse adres degismistir, yeni kaynak bastan iner.
      return degisti ? tamBoyut(kaynak) : 0;
    case "no-cache":
      // Her kullanimda dogrulanir: degismediyse yalniz basliklar gelir.
      return degisti ? tamBoyut(kaynak) : BASLIK_YUKU;
    case "max-age=60":
      if (gecenSaniye <= 60) return 0;               // hala taze: istek yok
      return degisti ? tamBoyut(kaynak) : BASLIK_YUKU;
    default:
      return tamBoyut(kaynak);
  }
}

const senaryolar = [
  { ad: "30 saniye sonra, surum ayni", gecen: 30, degisenler: new Set() },
  { ad: "300 saniye sonra, surum ayni", gecen: 300, degisenler: new Set() },
  {
    ad: "yeni surum yayinlandi",
    gecen: 300,
    degisenler: new Set(["belge", "bicem", "giris betigi"]),
  },
];

console.log(`ilk ziyaret: ${(ilkZiyaret / 1024).toFixed(1)} KB`);
for (const s of senaryolar) {
  console.log(`\n--- ${s.ad} ---`);
  console.log("kaynak".padEnd(14) + "tekrar".padStart(10) + "  politika");
  let toplam = 0;
  for (const k of kaynaklar) {
    const b = tekrarBayti(k, s.gecen, s.degisenler);
    toplam += b;
    console.log(k.ad.padEnd(14) + `${(b / 1024).toFixed(2)} KB`.padStart(10) + "  " + k.politika);
  }
  console.log("-".repeat(36));
  console.log("TOPLAM".padEnd(14) + `${(toplam / 1024).toFixed(2)} KB`.padStart(10));
  console.log(`ilk ziyarete oran: ${(toplam / ilkZiyaret).toFixed(4)}`);
}

console.log("\nhicbir tazelik basligi yazilmasaydi her tekrar ziyaret oran 1.0000 olurdu.");
```

```
ilk ziyaret: 458.7 KB

--- 30 saniye sonra, surum ayni ---
kaynak            tekrar  politika
belge            0.31 KB  no-cache
bicem            0.00 KB  immutable
giris betigi     0.00 KB  immutable
ortak betik      0.00 KB  immutable
yazi tipi        0.00 KB  immutable
kart gorseli     0.00 KB  immutable
istasyon veri    0.00 KB  max-age=60
------------------------------------
TOPLAM           0.31 KB
ilk ziyarete oran: 0.0007

--- 300 saniye sonra, surum ayni ---
kaynak            tekrar  politika
belge            0.31 KB  no-cache
bicem            0.00 KB  immutable
giris betigi     0.00 KB  immutable
ortak betik      0.00 KB  immutable
yazi tipi        0.00 KB  immutable
kart gorseli     0.00 KB  immutable
istasyon veri    0.31 KB  max-age=60
------------------------------------
TOPLAM           0.63 KB
ilk ziyarete oran: 0.0014

--- yeni surum yayinlandi ---
kaynak            tekrar  politika
belge           18.28 KB  no-cache
bicem           60.86 KB  immutable
giris betigi   144.95 KB  immutable
ortak betik      0.00 KB  immutable
yazi tipi        0.00 KB  immutable
kart gorseli     0.00 KB  immutable
istasyon veri    0.31 KB  max-age=60
------------------------------------
TOPLAM         224.41 KB
ilk ziyarete oran: 0.4892

hicbir tazelik basligi yazilmasaydi her tekrar ziyaret oran 1.0000 olurdu.
```

Kaynak boyutları hesabın girdisidir, ölçülmüş bir uygulamanın çıktısı değildir; başlık payı
da bir varsayımdır. Hesaplanan şey politikanın sonucudur.

İlk iki senaryoda tekrar ziyaret neredeyse bedavadır. Üçüncü senaryo asıl dersi verir:
yeni sürüm yayınlandığında yalnızca **değişen** parçalar yeniden iner. Ortak betik
değişmediği için adresi de değişmemiştir ve hiç istenmez. Paketleme kararlarının başarım
sonucu buradadır — sık değişen kodla seyrek değişen kod aynı parçada toplanırsa, her yayın
ikisini birden yeniden indirtir.

#### Vary ve Önbellek Anahtarı

Bir yanıt istek başlıklarına göre değişiyorsa bu, `Vary` başlığıyla bildirilmelidir.
Bildirilmezse ara önbellekler tek bir kopyayı herkese sunar: sıkıştırılmış bir yanıt
sıkıştırma desteklemeyen bir istemciye, bir dile göre üretilmiş sayfa başka dildeki
kullanıcıya gidebilir.

Kural, doğruluk kuralıdır ve başarımı da etkiler: `Vary` listesine gereksiz başlık
eklemek, önbellek anahtarını gereğinden fazla böler ve isabet oranını düşürür. Kullanıcıya
özgü bir başlık listeye girerse önbellek pratikte devre dışı kalır.

#### Bağlantı İpuçları

Belgede yazılan bağlantı ipuçları, tarayıcıya bir işi erken yapmasını bildirir. Her biri
farklı bir aşamayı öne çeker.

`dns-prefetch` yalnızca ad çözümlemesini yapar. En ucuz ipucudur ve en az kazandırır.

`preconnect` ad çözümlemesini, taşıma bağlantısını ve güvenli el sıkışmayı önden yapar.
Kritik yol modelinde "bağlantı kurulumu" olarak sayılan turları, kaynak istenmeden önce
harcar. Bedeli vardır: kurulan ve kullanılmayan bağlantı hem istemcide hem sunucuda
kaynak tutar, bu yüzden yalnızca kesin kullanılacak kökenler için yazılır.

`preload` belirli bir kaynağı erken ister. Kritik Oluşturma Yolu dersinde anlatıldığı gibi
bu bir düzeltmedir: geç keşfedilen kritik kaynağı öne çeker. `as` özniteliği zorunludur;
yanlış yazıldığında kaynak iki kez indirilir.

`prefetch` bir **sonraki** gezinmede gerekecek kaynağı boşta indirir. Öncelik en düşüktür
ve kullanıcı o sayfaya gitmezse harcanmış bant genişliğidir.

Sıra bir öncelik sırasıdır: önce zinciri düzleştirmek, sonra `preconnect`, en son
`preload`. Ölçüm göstermeden yazılan ipucu, çoğu zaman kritik kaynağın bant genişliğini
alarak zarar verir.

#### Özet

- Tazelik ömrü isteği tümüyle önler, doğrulayıcı isteği gövdesiz hâle getirir; ikisi ayrı
  sorunları çözer ve `no-cache` saklamayı değil doğrulamasız kullanımı yasaklar.
- İçerik karmalı adres, `immutable` sözünün tutulabilmesinin koşuludur; sabit adresli belge
  bu sözü veremez ve doğrulamayla yönetilir.
- Yeni bir yayında yalnızca adresi değişen parçalar yeniden iner; paketleme kararı bu
  yüzden bir önbellek kararıdır.
- `Vary` başlığı önce doğruluk, sonra başarım konusudur: eksikliği yanlış yanıt sunar,
  fazlalığı önbelleği böler.
- Bağlantı ipuçları farklı aşamaları öne çeker ve hepsinin bir bedeli vardır; ölçüm
  göstermeden yazılan ipucu kritik kaynağın önünü keser.

#### Sonraki Adım

Ağ tarafı düzenlendiğinde baytlar zamanında gelir ve tekrar ziyaret neredeyse istek
yapmaz. Buna karşın uygulama hâlâ ağır hissettirebilir: liste kaydırılırken takılıyor,
süzgeç kutusuna yazılan harf ekranda geç görünüyor, tarih aralığı değiştirildiğinde sayfa
bir an donuyor olabilir. Bu sorunların hiçbiri ağda değildir; hepsi indirilmiş kodun tek
bir sırada çalışmasından doğar. Sonraki ders bu sırayı ele alır: uzun görevlerin nasıl
ölçüldüğünü, bölüntülemenin ne kazandırdığını ve yerleşim sarsıntısının satır sayısıyla
nasıl ölçeklendiğini hesaplar.

### Çalışma Zamanı Başarımı

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/calisma-zamani-basarimi

Ana iş parçacığındaki uzun görevlerin toplam engelleme süresiyle ölçülmesi, işin bölüntülenmesinin ölçülen etkisi, yerleşim sarsıntısının satır sayısıyla ölçeklenmesi ve görünmeyeni çizmemenin yolları.

Ağ tarafı düzenlendiğinde baytlar zamanında gelir ve tekrar ziyaret neredeyse istek
yapmaz. Uygulama yine de ağır hissettirebilir: istasyon listesi kaydırılırken takılıyor,
süzgeç kutusuna yazılan harf ekranda geç görünüyor, tarih aralığı değiştirildiğinde sayfa
bir an donuyor olabilir.

Bu sorunların hiçbiri ağda değildir. Hepsi indirilmiş kodun tek bir sırada çalışmasından
doğar. Tarayıcı ve Web Platformu kursundaki Başarım Kaydı dersi bu sırayı görünür kılan
aracı ele almıştı; bu ders sıranın nasıl boşaltılacağını ele alır.

#### Uzun Görev ve Toplam Engelleme Süresi

Ana iş parçacığı bir görev sırasıdır. Bir görev başladıktan sonra bölünemez: sürdüğü sürece
kullanıcı girdisi işlenemez, kare üretilemez. Elli milisaniyeyi aşan görevler **uzun görev**
sayılır ve eşiğin gerekçesi girdi yanıtıdır.

Ölçüt, uzun görevlerin sayısı değil, eşiği aşan bölümlerinin toplamıdır.

```js
// engelleme.mjs — toplam engelleme suresi ve ayni isin bolunmus hali

const UZUN_GOREV = 50; // ms

const gorevler = [
  { ad: "belge ayristirma", sure: 42 },
  { ad: "paket degerlendirme", sure: 186 },
  { ad: "istasyon listesi olusturma", sure: 128 },
  { ad: "olcum bicimleme", sure: 61 },
  { ad: "girdi isleyicisi", sure: 18 },
  { ad: "kaydirma isleyicisi", sure: 33 },
];

console.log("gorev".padEnd(28) + "sure".padStart(8) + "engelleyen".padStart(12));
let toplamEngelleme = 0;
for (const g of gorevler) {
  const engelleyen = Math.max(0, g.sure - UZUN_GOREV);
  toplamEngelleme += engelleyen;
  console.log(g.ad.padEnd(28) + `${g.sure} ms`.padStart(8) + `${engelleyen} ms`.padStart(12));
}
console.log("-".repeat(48));
console.log("toplam engelleme suresi".padEnd(28) + `${toplamEngelleme} ms`.padStart(20));

// Ayni is, 50 ms'yi asmayan parcalara bolunurse engelleme sifira iner.
const bolunmus = gorevler.flatMap((g) => {
  const parca = Math.ceil(g.sure / UZUN_GOREV);
  return Array.from({ length: parca }, () => g.sure / parca);
});
const bolunmusEngelleme = bolunmus.reduce((t, s) => t + Math.max(0, s - UZUN_GOREV), 0);
console.log(`ayni is ${bolunmus.length} parcaya bolunurse: ${bolunmusEngelleme} ms engelleme`);
console.log(`toplam is suresi degismedi: ${gorevler.reduce((t, g) => t + g.sure, 0)} ms`);
```

```
gorev                           sure  engelleyen
belge ayristirma               42 ms        0 ms
paket degerlendirme           186 ms      136 ms
istasyon listesi olusturma    128 ms       78 ms
olcum bicimleme                61 ms       11 ms
girdi isleyicisi               18 ms        0 ms
kaydirma isleyicisi            33 ms        0 ms
------------------------------------------------
toplam engelleme suresi                   225 ms
ayni is 12 parcaya bolunurse: 0 ms engelleme
toplam is suresi degismedi: 468 ms
```

Görev süreleri hesabın girdisidir; hesaplanan şey ölçütün kendisidir. Dört yüz altmış sekiz
milisaniyelik işin iki yüz yirmi beş milisaniyesi engelleyicidir. Kırk iki milisaniyelik
ayrıştırma hiç sayılmaz, çünkü eşiğin altındadır — ölçüt işin tamamını değil, kullanıcının
yanıt bekleyeceği fazlalığı ölçer.

Son iki satır kritik olanı söylüyor: aynı iş elli milisaniyeyi aşmayan parçalara
bölündüğünde engelleme sıfıra iner ve toplam iş süresi hiç değişmez. Yapılan şey işi
azaltmak değil, **araya çıkış noktaları koymaktır.**

#### Bölüntülemenin Ölçülmesi

Modelin söylediği şey ölçülebilir. Aşağıdaki program aynı işi üç yazımla yapar ve her
birinde olay döngüsünün ne kadar süreyle bloke olduğunu bir yoklayıcıyla kaydeder.

```js
// boluntule.mjs — ayni isin tek parcada ve boluntulenmis olarak olculmesi
// Yanit yoklayicisi: her turda kendini yeniden kurar, gecikmesini kaydeder.
function yoklayiciBaslat(kayit) {
  let calisiyor = true;
  let onceki = performance.now();
  const tur = () => {
    if (!calisiyor) return;
    const simdi = performance.now();
    kayit.push(simdi - onceki);
    onceki = simdi;
    setTimeout(tur, 0);
  };
  setTimeout(tur, 0);
  return () => { calisiyor = false; };
}

const SATIR = 300_000;
function satirMetni(i) {
  // Her satir icin bir miktar is: bicimlenmis bir olcum satiri uretmek.
  const sicaklik = -12 + ((i * 37) % 240) / 10;
  const zaman = new Date(Date.UTC(2026, 0, 1, 0, 0, i % 60)).toISOString();
  return `kuzey-yamac-${String(i).padStart(5, "0")} ${zaman} ${sicaklik.toFixed(1)} C`.length;
}

async function tekParcada() {
  let toplam = 0;
  for (let i = 0; i < SATIR; i++) toplam += satirMetni(i);
  return toplam;
}

async function boluntulenmis(parcaBoyu) {
  let toplam = 0;
  for (let i = 0; i < SATIR; i++) {
    toplam += satirMetni(i);
    if ((i + 1) % parcaBoyu === 0) await new Promise((coz) => setImmediate(coz));
  }
  return toplam;
}

async function olc(ad, is) {
  const gecikmeler = [];
  const durdur = yoklayiciBaslat(gecikmeler);
  await new Promise((coz) => setTimeout(coz, 20)); // yoklayici isinsin
  gecikmeler.length = 0;
  const bas = performance.now();
  const sonuc = await is();
  const gecen = performance.now() - bas;
  await new Promise((coz) => setTimeout(coz, 20)); // son yoklama kaydedilsin
  durdur();
  const enBuyuk = Math.max(...gecikmeler);
  return { ad, gecen, enBuyuk, sonuc, tur: gecikmeler.length };
}

const olcumler = [
  await olc("tek parcada", tekParcada),
  await olc("2000 satirlik parcalar", () => boluntulenmis(2000)),
  await olc("500 satirlik parcalar", () => boluntulenmis(500)),
];

console.log("--- ayni is, farkli boluntuleme (sureler ortama baglidir) ---");
console.log("yaklasim".padEnd(24) + "toplam".padStart(10) + "en uzun engelleme".padStart(19) +
  "yoklama".padStart(9));
for (const o of olcumler) {
  console.log(o.ad.padEnd(24) + `${o.gecen.toFixed(1)} ms`.padStart(10) +
    `${o.enBuyuk.toFixed(1)} ms`.padStart(19) + String(o.tur).padStart(9));
}
const taban = olcumler[0];
console.log("\nen uzun engellemenin tek parcaya orani:");
for (const o of olcumler) {
  console.log(`  ${o.ad.padEnd(24)} ${(o.enBuyuk / taban.enBuyuk).toFixed(3)}`);
}
console.log(`butun yaklasimlar ayni sonucu uretti: ${new Set(olcumler.map((o) => o.sonuc)).size === 1}`);
```

```
--- ayni is, farkli boluntuleme (sureler ortama baglidir) ---
yaklasim                    toplam  en uzun engelleme  yoklama
tek parcada               126.9 ms           128.2 ms       18
2000 satirlik parcalar    128.5 ms             2.3 ms      144
500 satirlik parcalar     135.5 ms             1.9 ms      152

en uzun engellemenin tek parcaya orani:
  tek parcada              1.000
  2000 satirlik parcalar   0.018
  500 satirlik parcalar    0.015
butun yaklasimlar ayni sonucu uretti: true
```

Süreler makineye ve yüke bağlıdır; her çalıştırmada değişirler. Değişmeyen şey oranlardır. Toplam
süre üç yazımda birbirine yakın kalır — bölüntüleme işi azaltmaz, hatta tur başına küçük bir ek
yük getirir. En uzun engelleme ise yaklaşık iki mertebe düşer: yüz yirmi sekiz milisaniyelik tek
blok, birkaç milisaniyelik bloklara dağılır.

Yoklama sayısı bunun nedenini gösterir. Tek parçalı yazımda iş sürerken sıraya hiç
dönülmez; bölüntülenmiş yazımda her parçadan sonra dönülür ve bekleyen girdi işlenebilir.

Bu ölçüm bir tarayıcıda değil, tek iş parçacıklı bir çalışma zamanında yapılmıştır; ana iş
parçacığının davranışını doğru modeller, ama tarayıcıdaki çıkış noktası `setImmediate`
değildir. Tarayıcıda karşılığı, denetimi olay döngüsüne bırakan bir bekleme ya da boşta
çalışma çağrısıdır.

Bölüntülemenin bir de sınırı vardır: parça küçüldükçe tur başına ek yük büyür. Beş yüz
satırlık parçalarda toplam süre iki bin satırlık parçalardan yüksektir. Doğru parça boyu
"olabildiğince küçük" değil, kare bütçesine sığan en büyük parçadır.

#### İşi Sıradan Tümüyle Çıkarmak

Bölüntüleme işi ana sırada tutar ve araya girmeye izin verir. İkinci bir yol, işi sıradan
tümüyle çıkarmaktır.

Tarayıcı ve Web Platformu kursunda tanıtılan çalışanlar ayrı bir iş parçacığında koşar ve
ana sırayı hiç meşgul etmez. Uygun olduğu işler bellidir: saf hesap, ayrıştırma,
sıkıştırma, büyük veri kümesinin süzülmesi ve sıralanması. Uygun olmadığı iş de bellidir —
belge ağacına erişim. Ölçüm ayrıntısı sayfasında on binlerce ölçümün özetlenmesi çalışana
taşınabilir; sonuç ana sıraya yalnızca çizilecek satırlar olarak döner.

Bedeli veri aktarımıdır. İleti geçişi kopyalama gerektirir ve büyük dizilerde bu kopyalama
kazancı yiyebilir; aktarılabilir nesneler bunun için vardır.

#### Yerleşim Sarsıntısı Satır Sayısıyla Ölçeklenir

Düzen Sistemleri ve Duyarlı Tasarım kursundaki Animasyon Başarımı dersi zorlanmış yerleşim
hesabını tanımlamıştı: bir yazma işlemi yerleşimi geçersiz kılar, sonraki bir ölçü okuması
hesabı hemen zorlar. Okuma ile yazmanın sıralanması bu hesabı her turda tekrarlar ve buna
**yerleşim sarsıntısı** denir.

Orada sayılan şey hesap adediydi. Buradaki soru, adedin liste büyüdükçe ne olduğudur.

```js
// sarsinti.mjs — yerlesim sarsintisinin satir sayisiyla nasil olcekledigi

const KARE_BUTCESI = 1000 / 60; // ms, saniyede 60 kare varsayimiyla
const HESAP_MALIYETI = 0.05;    // ms: tek bir yerlesim hesabinin varsayilan maliyeti
const GORUNUR_SATIR = 20;       // gorunum alanina sigan satir sayisi

// Uc yaklasim: her biri N satir icin kac yerlesim hesabi zorlar?
const yaklasimlar = {
  "dongu icinde oku-yaz": (n) => Math.max(0, n - 1),
  "okumalar sonra yazmalar": () => 0,
  "yalniz gorunur satirlar": (n) => Math.max(0, Math.min(n, GORUNUR_SATIR) - 1),
};

console.log("satir".padStart(7) +
  Object.keys(yaklasimlar).map((a) => a.padStart(26)).join(""));
for (const n of [10, 50, 200, 1000, 5000]) {
  const hucreler = Object.values(yaklasimlar).map((f) => {
    const hesap = f(n);
    const sure = hesap * HESAP_MALIYETI;
    return `${hesap} hesap / ${sure.toFixed(1)} ms`.padStart(26);
  });
  console.log(String(n).padStart(7) + hucreler.join(""));
}

console.log(`\nkare butcesi: ${KARE_BUTCESI.toFixed(2)} ms`);
console.log("kac kare butcesi tuketilir:");
console.log("satir".padStart(7) +
  Object.keys(yaklasimlar).map((a) => a.padStart(26)).join(""));
for (const n of [10, 50, 200, 1000, 5000]) {
  const hucreler = Object.values(yaklasimlar).map((f) =>
    (f(n) * HESAP_MALIYETI / KARE_BUTCESI).toFixed(1).padStart(26));
  console.log(String(n).padStart(7) + hucreler.join(""));
}

console.log("\nhesap maliyeti bir varsayimdir; degistiginde sutunlarin orani degismez.");
```

```
  satir      dongu icinde oku-yaz   okumalar sonra yazmalar   yalniz gorunur satirlar
     10          9 hesap / 0.5 ms          0 hesap / 0.0 ms          9 hesap / 0.5 ms
     50         49 hesap / 2.5 ms          0 hesap / 0.0 ms         19 hesap / 1.0 ms
    200       199 hesap / 10.0 ms          0 hesap / 0.0 ms         19 hesap / 1.0 ms
   1000       999 hesap / 50.0 ms          0 hesap / 0.0 ms         19 hesap / 1.0 ms
   5000     4999 hesap / 250.0 ms          0 hesap / 0.0 ms         19 hesap / 1.0 ms

kare butcesi: 16.67 ms
kac kare butcesi tuketilir:
  satir      dongu icinde oku-yaz   okumalar sonra yazmalar   yalniz gorunur satirlar
     10                       0.0                       0.0                       0.0
     50                       0.1                       0.0                       0.1
    200                       0.6                       0.0                       0.1
   1000                       3.0                       0.0                       0.1
   5000                      15.0                       0.0                       0.1

hesap maliyeti bir varsayimdir; degistiginde sutunlarin orani degismez.
```

On satırlık bir listede üç yaklaşım arasında fark yoktur; sorun bu yüzden geliştirme
sırasında görünmez. Beş bin satırda ilk yaklaşım on beş kare bütçesi tüketir, yani çeyrek
saniye boyunca hiçbir kare üretilmez.

İki çözümün karakteri farklıdır. Okumaları ve yazmaları öbeklemek maliyeti **sıfıra**
indirir ve liste büyüklüğünden bağımsızdır; yalnızca görünür satırları işlemek maliyeti
**sabitler** ve liste ne kadar büyürse büyüsün değişmez. İkincisi, sarsıntıyı olduğu gibi
bırakır ama ölçeklenmesini keser.

#### Görünmeyeni Çizmemek

Görünür olanı işlemek fikri yerleşim sarsıntısıyla sınırlı değildir; tarayıcının çizim
hattının tamamı için geçerlidir.

**Sanallaştırma** listenin yalnızca görünen bölümünü belge ağacına koyar. Kaydırıldıkça
düğümler yeniden kullanılır. Bedeli, ağaçta bulunmayan içeriğin sayfa içi aramada ve
erişilebilirlik ağacında da bulunmamasıdır.

**İçerik görünürlüğü bildirimi** aynı hedefe işaretlemeyi bozmadan yaklaşır: `content-visibility`
özelliği bir kutunun ekran dışındayken çizim ve yerleşim işlerinin atlanmasını sağlar.
Atlanan işin kutusu yine de bir yer kaplamalıdır; yoksa kaydırma çubuğu içerik göründükçe
zıplar. Bu yüzden bildirimle birlikte tahmini bir boyut verilir.

**Sınırlama** bildirimi ise bir kutunun etkisini kendi sınırlarında tutar. Animasyon
Başarımı dersinde tanıtılan `contain` bildirimi, bir kart içindeki değişikliğin yerleşim
hesabını sayfanın tamamına yaymasını önler.

#### Etkileşim Gecikmesinin Üç Bileşeni

Sonraki boyaya etkileşim tek bir sayıdır ama üç ayrı gecikmenin toplamıdır ve düzeltmeleri
farklıdır.

**Girdi gecikmesi**, kullanıcı etkileşimi ile işleyicinin başlaması arasındaki süredir.
Nedeni, o sırada süren başka bir görevdir. Çözümü bölüntüleme ya da işi çalışana taşımaktır;
işleyiciyi hızlandırmak bu bölüme dokunmaz.

**İşleme süresi**, işleyicilerin kendi süresidir. Çözümü işleyicinin küçültülmesi ve
ertelenebilir işin ertelenmesidir: kullanıcının gördüğü değişikliği hemen yap, kaydı ve
istatistiği sonraya bırak.

**Sunum gecikmesi**, işleyici bittikten sonra karenin üretilmesine kadar geçen süredir.
Nedeni işleyicinin tetiklediği yerleşim ve boyama işinin büyüklüğüdür. Çözümü, güncellenen
alanı küçültmektir.

Kaydın hangi bileşenin büyüdüğünü göstermesi, hangi düzeltmenin denenmeyeceğini de söyler.
Ölçmeden yapılan eniyileme çoğu zaman yanlış bileşene uygulanır.

#### Özet

- Toplam engelleme süresi, uzun görevlerin elli milisaniyelik eşiği aşan bölümlerinin
  toplamıdır; işi bölüntülemek toplam süreyi değiştirmeden bu ölçütü sıfıra indirebilir.
- Ölçüm bunu doğrular: aynı iş bölüntülendiğinde en uzun engelleme iki mertebe düşerken
  toplam süre yakın kalır; parça küçüldükçe tur başına ek yük artar.
- Çalışanlar işi ana sıradan tümüyle çıkarır; sınırları belge ağacına erişememek ve veri
  kopyalama maliyetidir.
- Yerleşim sarsıntısının maliyeti satır sayısıyla doğrusal büyür; öbekleme maliyeti sıfıra
  indirir, görünür satırlarla çalışmak sabitler.
- Sonraki boyaya etkileşim girdi gecikmesi, işleme süresi ve sunum gecikmesinin toplamıdır
  ve her bileşenin düzeltmesi ayrıdır.

#### Sonraki Adım

Beş derste ölçütler tanımlandı, kritik yol kısaltıldı, varlıklar boyutlandırıldı, ağ
katmanı ayarlandı ve çalışma zamanı boşaltıldı. Bütün bunların ortak açığı şudur: hepsi bir
kez yapılmış iyileştirmelerdir. Yeni bir özellik eklendiğinde paket büyür, yeni bir görsel
konduğunda ilk ekranın ağırlığı artar, bir kütüphane ortak parçaya girdiğinde tekrar
ziyaret kazancı erir. Bu erimenin bir sonraki yayında fark edilmesi gerekir, aylar sonra
şikâyetle değil. Sonraki ders ölçütleri yazılı bir bütçeye bağlar ve iki derleme çıktısını
karşılaştırarak gerilemeyi yakalayan bir denetleyici kurar.

### Başarım Bütçesi

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/basarim-butcesi

Başarım kararlarının yazılı bir sınıra bağlanması; bütçe türleri, sınırın nereden türetildiği, derleme çıktısını denetleyen bir program ve iki derlemeyi karşılaştırarak gerilemenin yakalanması.

Önceki beş ders bir dizi iyileştirme kurdu: ölçütler tanımlandı, kritik yol kısaltıldı,
varlıklar boyutlandırıldı, ağ katmanı ayarlandı, çalışma zamanı boşaltıldı. Hepsinin ortak
açığı aynıdır — bir kez yapılmış olmaları.

Yeni bir özellik eklendiğinde paket büyür; bir kütüphane ortak parçaya girdiğinde tekrar ziyaret
kazancı erir. Erimenin bir sonraki yayında fark edilmesi gerekir. Bu ders o mekanizmayı kurar.

#### Bütçe Bir Hedef Değildir

İkisi karıştırılır ve karıştırıldığında bütçe işlevini yitirir.

**Hedef**, ulaşılmak istenen değerdir. Yönü vardır, yaptırımı yoktur; aşıldığında bir şey
olmaz.

**Bütçe**, aşılmasına izin verilmeyen sınırdır. Aşıldığında bir şey olur: derleme başarısız
sayılır, birleştirme durur, sürüm çıkmaz. Yaptırımı olmayan bir sayı bütçe değildir.

Ayrımın pratik sonucu şudur: bütçe, ulaşılabilir bir yerde çizilir. Şu anki değerin çok
altına konan bir sınır ilk günden kırmızı yanar ve kısa sürede yok sayılmaya başlar. Doğru
yaklaşım, sınırı bugünkü değerin biraz üstüne koymak ve zamanla indirmektir.

#### Üç Bütçe Türü

Bütçeler ölçtükleri şeye göre ayrılır ve üçü de gerekir, çünkü her biri diğerinin
kaçırdığını yakalar.

**Ölçüt bütçesi** kullanıcı merkezli ölçütlere sınır koyar: en büyük içerik boyası şu
değerin altında kalacak, kümülatif düzen kayması şu puanı aşmayacak. Doğrudan deneyimi
ölçtüğü için en anlamlı bütçedir; en gürültülü olanı da budur, çünkü ölçüm koşullarına
duyarlıdır.

**Nicelik bütçesi** sayılabilir büyüklüklere sınır koyar: parça boyutları, istek sayısı,
görsel ağırlığı, yazı tipi dosyası sayısı. Belirlenimcidir — aynı derleme her zaman aynı
sayıyı üretir — ve bu yüzden yaptırıma en uygun bütçedir. Deneyimle bağı dolaylıdır: küçük
bir paket her zaman hızlı bir sayfa demek değildir.

**Kural bütçesi** sayı değil koşul içerir: ilk ekranda çizimi engelleyen dış biçem
bulunmayacak, belgede eşzamanlı betik olmayacak, en büyük içerik boyasına aday görsel
gecikmeli yüklenmeyecek. Bir sayıya indirgenemeyen kararları korur.

Sağlam bir düzen üçünü birleştirir: nicelik ve kural bütçeleri her derlemede, ölçüt
bütçesi düzenli aralıklarla denetlenir.

#### Sınır Nereden Gelir

Bir sayının bütçe olabilmesi için gerekçesi olmalıdır. Üç gerekçelendirme yolu vardır.

**Ölçütün standart eşiğinden türetme.** İlk derste anılan sınıflandırma sınırları — en
büyük içerik boyası için iki buçuk saniye, kümülatif düzen kayması için sıfır virgül bir,
sonraki boyaya etkileşim için iki yüz milisaniye — doğrudan ölçüt bütçesi olarak
yazılabilir. Denetim yetmiş beşinci yüzdeliğe uygulanır.

**Ölçütten niceliğe geri hesap.** Hedef bir ölçüt değeri seçilir, hedef bir ağ ve aygıt
profili varsayılır, aradaki bütçe baytlara çevrilir. Kritik Oluşturma Yolu dersindeki
gidiş-dönüş modeli bu hesabın iskeletidir: kaç tur harcanabileceği belirlenince kaç bayt
gönderilebileceği çıkar.

**Bugünkü değerden pay bırakarak.** En pratik ve en az iddialı yol. Ölçülen değerin üzerine
küçük bir pay eklenir ve sınır oraya konur. Bu bütçe iyileştirme getirmez; **kötüleşmeyi
durdurur.** Çoğu takım için doğru başlangıç budur.

Hangi yol seçilirse seçilsin bütçe yazılı olmalı ve depoda durmalıdır. Kimsenin
göremediği bir sınır, olmayan bir sınırdır.

#### Denetleyici

Aşağıdaki program bir derleme dökümünü bütçeyle karşılaştırır, sonra iki derlemeyi
karşılaştırıp gerilemeyi bildirir.

```js
// butce.mjs — parca boyutlarini butceyle karsilastirir ve iki derleme arasindaki gerilemeyi bulur

const KB = 1024;

// Butce: her grup icin toplama kurali, ust sinir (bayt) ve buyume toleransi (oran).
// "toplam" grubun tamamini olcer; "enBuyuk" gruptaki en agir tek parcayi olcer.
const BUTCE = {
  giris:  { toplama: "toplam",  sinir: 170 * KB, tolerans: 0.03 },
  ortak:  { toplama: "toplam",  sinir: 120 * KB, tolerans: 0.03 },
  rota:   { toplama: "enBuyuk", sinir:  45 * KB, tolerans: 0.05 },
  bicem:  { toplama: "toplam",  sinir:  40 * KB, tolerans: 0.05 },
};
const TOPLAM_SINIR = 360 * KB;

// Derleme dokumu: her parcanin adi, grubu ve sikistirilmis boyutu.
const oncekiDerleme = [
  { ad: "giris.4a1c.js",            grup: "giris", bayt: 148_112 },
  { ad: "ortak.9d02.js",            grup: "ortak", bayt: 101_760 },
  { ad: "rota-istasyonlar.1f77.js", grup: "rota",  bayt:  38_240 },
  { ad: "rota-olcum.55be.js",       grup: "rota",  bayt:  21_904 },
  { ad: "istasyon.6b3e.css",        grup: "bicem", bayt:  31_648 },
];

const yeniDerleme = [
  { ad: "giris.7e90.js",            grup: "giris", bayt: 151_296 },
  { ad: "ortak.c214.js",            grup: "ortak", bayt: 124_368 },
  { ad: "rota-istasyonlar.02ab.js", grup: "rota",  bayt:  39_120 },
  { ad: "rota-olcum.55be.js",       grup: "rota",  bayt:  21_904 },
  { ad: "rota-rapor.8c4d.js",       grup: "rota",  bayt:  17_856 },
  { ad: "istasyon.a9f1.css",        grup: "bicem", bayt:  32_960 },
];

const kb = (bayt) => `${(bayt / KB).toFixed(1)} KB`;

function gruplaraTopla(derleme) {
  const olcu = new Map();
  for (const [grup, { toplama }] of Object.entries(BUTCE)) {
    const parcalar = derleme.filter((p) => p.grup === grup).map((p) => p.bayt);
    olcu.set(grup, toplama === "enBuyuk"
      ? Math.max(0, ...parcalar)
      : parcalar.reduce((a, b) => a + b, 0));
  }
  return olcu;
}

const tumToplam = (derleme) => derleme.reduce((t, p) => t + p.bayt, 0);

// --- 1. Butce denetimi ---
function denetle(derleme, baslik) {
  const toplamlar = gruplaraTopla(derleme);
  let gecti = true;
  console.log(`--- ${baslik} ---`);
  console.log("grup".padEnd(8) + "kural".padEnd(9) + "boyut".padStart(11) +
    "sinir".padStart(11) + "kullanim".padStart(10) + "  sonuc");
  for (const [grup, { sinir, toplama }] of Object.entries(BUTCE)) {
    const boyut = toplamlar.get(grup);
    const kullanim = boyut / sinir;
    const sonuc = boyut <= sinir ? "gecti" : "KALDI";
    if (boyut > sinir) gecti = false;
    console.log(grup.padEnd(8) + toplama.padEnd(9) + kb(boyut).padStart(11) +
      kb(sinir).padStart(11) + `${(kullanim * 100).toFixed(1)}%`.padStart(10) + "  " + sonuc);
  }
  const genelToplam = tumToplam(derleme);
  const toplamSonuc = genelToplam <= TOPLAM_SINIR ? "gecti" : "KALDI";
  if (genelToplam > TOPLAM_SINIR) gecti = false;
  console.log("-".repeat(55));
  console.log("TOPLAM".padEnd(8) + "toplam".padEnd(9) + kb(genelToplam).padStart(11) +
    kb(TOPLAM_SINIR).padStart(11) +
    `${((genelToplam / TOPLAM_SINIR) * 100).toFixed(1)}%`.padStart(10) + "  " + toplamSonuc);
  console.log(`butce denetimi: ${gecti ? "gecti" : "KALDI"}\n`);
  return gecti;
}

denetle(oncekiDerleme, "onceki derleme");
const yeniGecti = denetle(yeniDerleme, "yeni derleme");

// --- 2. Gerileme tespiti: sinirin altinda kalan buyume de bildirilir ---
console.log("--- iki derlemenin karsilastirmasi ---");
const eski = gruplaraTopla(oncekiDerleme);
const yeni = gruplaraTopla(yeniDerleme);

console.log("grup".padEnd(8) + "onceki".padStart(11) + "yeni".padStart(11) +
  "fark".padStart(11) + "oran".padStart(9) + "  uyari");
const uyarilar = [];
for (const [grup, { tolerans }] of Object.entries(BUTCE)) {
  const a = eski.get(grup);
  const b = yeni.get(grup);
  const oran = (b - a) / a;
  const asildi = oran > tolerans;
  if (asildi) uyarilar.push(`${grup}: %${(oran * 100).toFixed(1)} buyume (tolerans %${tolerans * 100})`);
  console.log(grup.padEnd(8) + kb(a).padStart(11) + kb(b).padStart(11) +
    `${b - a >= 0 ? "+" : ""}${((b - a) / KB).toFixed(1)} KB`.padStart(11) +
    `${oran >= 0 ? "+" : ""}${(oran * 100).toFixed(1)}%`.padStart(9) +
    "  " + (asildi ? "TOLERANS ASILDI" : "-"));
}

// Yeni gelen parcalar ayrica bildirilir: adres degistigi icin ad karsilastirmasi
// icerik karmasi cikarilarak yapilir.
const kok = (ad) => ad.replace(/\.[0-9a-f]{4,}\./, ".");
const eskiAdlar = new Set(oncekiDerleme.map((p) => kok(p.ad)));
const yeniParcalar = yeniDerleme.filter((p) => !eskiAdlar.has(kok(p.ad)));

console.log(`\nyeni parcalar: ${yeniParcalar.map((p) => `${kok(p.ad)} (${kb(p.bayt)})`).join(", ") || "(yok)"}`);
if (uyarilar.length === 0) console.log("gerileme uyarisi: (yok)");
else {
  console.log("gerileme uyarisi:");
  for (const u of uyarilar) console.log(`  ${u}`);
}

const cikis = yeniGecti && uyarilar.length === 0 ? 0 : 1;
console.log(`\ncikis kodu: ${cikis}`);
process.exitCode = cikis;
```

```
--- onceki derleme ---
grup    kural          boyut      sinir  kullanim  sonuc
giris   toplam      144.6 KB   170.0 KB     85.1%  gecti
ortak   toplam       99.4 KB   120.0 KB     82.8%  gecti
rota    enBuyuk      37.3 KB    45.0 KB     83.0%  gecti
bicem   toplam       30.9 KB    40.0 KB     77.3%  gecti
-------------------------------------------------------
TOPLAM  toplam      333.7 KB   360.0 KB     92.7%  gecti
butce denetimi: gecti

--- yeni derleme ---
grup    kural          boyut      sinir  kullanim  sonuc
giris   toplam      147.8 KB   170.0 KB     86.9%  gecti
ortak   toplam      121.5 KB   120.0 KB    101.2%  KALDI
rota    enBuyuk      38.2 KB    45.0 KB     84.9%  gecti
bicem   toplam       32.2 KB    40.0 KB     80.5%  gecti
-------------------------------------------------------
TOPLAM  toplam      378.4 KB   360.0 KB    105.1%  KALDI
butce denetimi: KALDI

--- iki derlemenin karsilastirmasi ---
grup         onceki       yeni       fark     oran  uyari
giris      144.6 KB   147.8 KB    +3.1 KB    +2.1%  -
ortak       99.4 KB   121.5 KB   +22.1 KB   +22.2%  TOLERANS ASILDI
rota        37.3 KB    38.2 KB    +0.9 KB    +2.3%  -
bicem       30.9 KB    32.2 KB    +1.3 KB    +4.1%  -

yeni parcalar: rota-rapor.js (17.4 KB)
gerileme uyarisi:
  ortak: %22.2 buyume (tolerans %3)

cikis kodu: 1
```

Derleme dökümleri programın girdisidir; ölçülmüş bir projenin çıktısı değil, denetleyicinin
davranışını göstermek için yazılmış iki listedir.

#### Çıktının Söyledikleri

Bütçe tanımındaki `toplama` alanı önemli bir ayrımı taşır. Giriş, ortak ve biçem grupları
için sınır **toplama** uygulanır: bu parçaların hepsi her sayfada yüklenir. Rota grubu için
sınır **en büyük tek parçaya** uygulanır, çünkü kullanıcı bir seferde tek bir rotayı
yükler. Yanlış toplama kuralı, yeni bir rota eklendiğinde bütçeyi haksız yere kırar.

Yeni derleme iki yerde kalıyor. Ortak parça kendi sınırını yüzde bir küsur aşmış; toplam ise
sınırı yüzde beş aşmış. Toplamı aşan asıl neden ortak parçanın büyümesidir, ama yeni eklenen rota
parçası da toplama katkı yapar. İki satırın birlikte okunması, sorunun tek bir değişiklikten mi
yoksa birikimden mi geldiğini söyler.

Karşılaştırma tablosu bütçenin yakalayamadığını yakalıyor. Giriş parçası yüzde iki virgül
bir büyümüş ve sınırın hâlâ çok altında; uyarı üretmiyor. Ortak parça yüzde yirmi iki
büyümüş ve tolerans yüzde üç olduğu için uyarı üretiyor — bu parça sınırın altında kalsaydı
bile uyarı yine üretilirdi.

Ayrım budur. **Bütçe mutlak bir sınırı korur; tolerans değişim hızını korur.** Yalnız bütçe
denetlenirse, her yayında yüzde iki büyüyen bir paket aylarca sessiz kalır ve sınırı bir gün
aniden aşar; o gün hangi değişikliğin sorumlu olduğu artık bulunamaz.

Yeni parçaların ayrıca bildirilmesi de bu yüzdendir. İçerik karmalı adlar her yayında
değiştiği için karşılaştırma karma çıkarılarak yapılır; yoksa her parça yeni görünür.

#### Bütçenin Yayın Hattındaki Yeri

Denetleyicinin çıkış kodu döndürmesi tasarımın parçasıdır: derleme sonrası çalışan
doğrulama adımlarında sıfırdan farklı bir çıkış kodu adımı durdurur. Kabuk Programlama
kursunda kurulan kural burada yaptırımın taşıyıcısıdır.

İki karar gerekir. **Nerede çalışacak?** Değişiklik başına çalışan bir denetim, sorunu
birleştirmeden önce bulur; yalnız ana dalda çalışan bir denetim, sorunu bulduğunda sorumlu
değişikliği aramak gerekir.

**Ne yapacak?** Bütçe aşımının derlemeyi durdurması ile uyarı üretmesi arasındaki seçim,
bütçenin ciddiyetini belirler. Durdurmayan bütçe hedefe dönüşür. Buna karşın bir kaçış
kapısı gerekir: sınırın bilinçli olarak yükseltilebilmesi ve yükseltmenin bir kayıt
bırakması. Bütçe dosyasında yapılan değişiklik incelemeye tabi olduğunda kaçış deliği da
denetlenmiş olur.

#### Gürültü ve Yanlış Alarm

Nicelik bütçeleri belirlenimcidir; aynı girdiden aynı sayı çıkar. Ölçüt bütçeleri değildir
ve doğrudan uygulandıklarında yanlış alarm üretirler.

Üç önlem gerekir. Ölçüm **birden çok kez** yapılır ve ortanca alınır; tek bir koşu karar
için yeterli değildir. Ölçüm koşulları **sabitlenir**: aynı ağ kısıtlaması, aynı aygıt
profili, eklentisiz tarayıcı — Ağ Panelinde Tanı dersindeki yeniden üretilebilirlik
koşulları burada bütçenin ön şartıdır. Ve eşik, ölçümün doğal değişkenliğinden **belirgin
biçimde büyük** seçilir; gürültünün içinde kalan bir tolerans, sürekli kırmızı yanan ve bu
yüzden yok sayılan bir denetim üretir.

Bir de kapsam kararı vardır. Bütçe her sayfaya değil, temsilci sayfalara uygulanır:
istasyon listesi sayfası ve ölçüm ayrıntısı sayfası. İlk ders bu iki sayfanın farklı
ölçütlerle değerlendirildiğini göstermişti; bütçeleri de bu yüzden ayrı yazılır.

#### Özet

- Bütçe, aşıldığında bir şeyin olduğu sınırdır; yaptırımı olmayan sayı hedeftir ve
  kötüleşmeyi durdurmaz.
- Üç bütçe türü birbirinin açığını kapatır: ölçüt bütçesi deneyimi ölçer ama gürültülüdür,
  nicelik bütçesi belirlenimcidir ama dolaylıdır, kural bütçesi sayıya inmeyen kararları
  korur.
- Bütçe grubunun toplama kuralı sonucu değiştirir: her sayfada yüklenen parçalar toplanır,
  seçenekli parçalarda en büyüğü ölçülür.
- Mutlak sınır ile büyüme toleransı ayrı şeyleri korur; yalnız sınır denetlendiğinde küçük
  ve düzenli büyümeler sınıra ulaşana kadar görünmez kalır.
- Ölçüt bütçeleri ancak ölçüm koşulları sabitlenip birden çok koşunun ortancası alındığında
  yaptırıma bağlanabilir.

#### Sonraki Adım

Başarım artık bir görüş meselesi değil: tanımlı ölçütleri, yazılı sınırları ve her
derlemede çalışan bir denetimi var. Kalite ekseninin tamamı bu değildir. Bir sayfa hızlı
yüklenip yine de kullanılamaz olabilir — klavyeyle gezilemeyen bir liste, ekran okuyucuya
hiçbir şey söylemeyen bir düğme, karşıtlığı yetersiz bir metin. Bu sorunlar da bir görüş
meselesi olmak zorunda değildir; onların da tanımlı ölçütleri, ölçülebilir eşikleri ve
uygunluk düzeyleri vardır. Sonraki ders erişilebilirlik standartlarını ele alır ve
kullanılabilirliği aynı disiplinle — sınanabilir ölçütlerle — tanımlar.

## Erişilebilirlik

Erişilebilirlik ölçütlerinin yapısı ve uygunluk düzeyleri; yerel ögelerin erişilebilirlik ağacındaki karşılığı, erişilebilir ad hesabı, ARIA'nın doğru ve yanlış kullanımı, klavye erişimi, canlı bölgeler, karşıtlık eşikleri ve otomatik denetimin sınırları.

### Erişilebilirlik Standartları

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/erisilebilirlik-standartlari

Erişilebilirliği ölçülebilir kılan dört ilke, ölçüt numaralarının yapısı, uygunluk düzeylerinin bağlaç kuralı ve uygunluğun sayfa değil süreç ölçeğinde tanımlı olması.

Başarım bu noktada ölçülebilir bir bütçeye bağlandı: her ölçüte bir eşik yazıldı, eşiği
aşan değişiklik bir gerileme sayıldı, gerileme yayından önce yakalandı. Bu disiplinin
değeri kararı tartışmadan çıkarıp ölçüme taşımasıydı — "sayfa hızlı mı" sorusunun yerini
"hangi ölçüt hangi eşiği aşıyor" sorusu aldı.

Aynı disiplin ikinci bir kalite ekseninde gerekir. Bir arayüzün kullanılabilir olup
olmadığı, gören bir kullanıcının fareyle denediği bir izlenim olarak bırakılırsa
ölçülemez; ölçülemeyen şey gerilediğinde fark edilmez. Sorunun yanıtı bu derste kurulacak
yapıdadır: kullanılabilirlik, sayılabilir ve doğru–yanlış olarak sınanabilir ölçütlere
bağlanır.

#### Dört İlke

Erişilebilirlik ölçütleri dört ilke altında toplanır. İlkeler ölçüt değildir; sınanamazlar.
Ölçütleri gruplayan ve her birinin hangi soruya yanıt verdiğini söyleyen çerçevedir.

**Algılanabilirlik (perceivable).** İçerik ve arayüz bileşenleri, kullanıcının
algılayabileceği biçimde sunulmalıdır. Bir bilgi yalnızca tek bir duyuya bağlıysa o duyuyu
kullanamayan kullanıcı için yoktur. Görselin alternatif metni, videonun altyazısı, rengin
tek taşıyıcı olmaması ve metnin karşıtlık eşiği bu ilkeye girer.

**Çalıştırılabilirlik (operable).** Arayüz bileşenleri ve gezinme kullanılabilir olmalıdır.
Buradaki ölçüt kullanıcının bilgiye ulaşması değil, arayüzü **işletebilmesidir**: klavyeyle
her denetime erişmek, girdiği yerden çıkabilmek, odağın nerede olduğunu görmek, yeterli
zamana sahip olmak.

**Anlaşılabilirlik (understandable).** Bilgi ve arayüzün işleyişi anlaşılır olmalıdır. Dil
bildirimi, öngörülebilir davranış, hataların tanımlanması ve düzeltme önerisi bu ilkededir.
Bir form alanı odak aldığında sayfanın kendiliğinden değişmesi, çalışan ama anlaşılmayan
bir arayüz üretir.

**Sağlamlık (robust).** İçerik, yardımcı teknolojiler dahil olmak üzere çok çeşitli
kullanıcı aracıyla yorumlanabilecek kadar sağlam olmalıdır. Bu ilke ürettiğimiz
işaretlemenin belirli bir gerçekleştirimin davranışına değil, tanımlı karşılıklara
dayanmasını ister. Bir bileşenin adının, rolünün ve değerinin program yoluyla okunabilir
olması bu ilkenin ölçütüdür.

Dört ilkenin sırası rastgele değildir ve bir bağımlılık zinciri kurar: algılanamayan bir
şey çalıştırılamaz, çalıştırılamayan bir şeyin anlaşılırlığı sorulmaz, hiçbiri sağlam
biçimde bildirilmemişse üçü de yardımcı teknolojiye ulaşmaz.

#### Ölçüt Numarası Ne Söyler

Her ölçütün üç parçalı bir numarası vardır ve numara konuşulan şeyi tek başına
konumlandırır.

İlk sayı **ilkedir**: 1 algılanabilirlik, 2 çalıştırılabilirlik, 3 anlaşılabilirlik, 4
sağlamlık. İkinci sayı ilkenin altındaki **yönergedir**; yönergeler de sınanamaz, ölçütleri
konuya göre toplar (2.4 gezinilebilirlik, 3.3 girdi yardımı gibi). Üçüncü sayı o yönergenin
altındaki **başarı ölçütüdür** ve sınanabilir tek birim odur.

Böylece 2.4.7 numarası okunduğunda, ölçütün adı bilinmese bile çalıştırılabilirlik
ilkesinin gezinme yönergesine ait olduğu bilinir. Numaralar yayımlandıktan sonra yeniden
kullanılmaz: bir ölçüt eklendiğinde yeni numara alır, var olan numaraların anlamı
değişmez. Bu, iki kişinin aynı numaradan aynı şeyi anlamasını sağlar ve hata kayıtlarında
ölçüt numarasının yazılmasını değerli kılar.

Ölçütlerin ortak özelliği **sınanabilir** olmalarıdır: her biri, bir içerik parçası için
doğru ya da yanlış olarak yanıtlanabilecek biçimde yazılmıştır. "Sayfa erişilebilir olmalı"
bir ölçüt değildir; "metin ile arka planı arasındaki kontrast oranı en az 4.5:1 olmalı"
ölçüttür.

#### Uygunluk Düzeyleri

Ölçütler üç uygunluk düzeyine dağıtılır: A, AA ve AAA. Düzey, ölçütün önemini değil
**karşılanma maliyetini ve kapsam genişliğini** anlatır. A düzeyi karşılanmadığında
kullanıcı grupları arayüzü hiç kullanamaz; AA düzeyi kullanımı ciddi biçimde zorlaştıran
engelleri kaldırır; AAA düzeyi her içerik türüne uygulanamayabilecek ek gereklerdir.

Düzeyler birikimlidir ve **bağlaç** kuralıyla değerlendirilir: AA uygunluğu, bütün A ve
bütün AA ölçütlerinin karşılanması demektir. Tek bir A ölçütü kalırsa yüzey hiçbir düzeye
uygun değildir. Aşağıdaki betik bu kuralı Kuzey Yamaç Ölçüm İstasyonu arayüzünün üç yüzeyi
üzerinde uygular.

```js
// uygunluk.mjs — olcut numarasindan ilke turetimi ve uygunluk duzeyinin hesabi

const ILKE = { 1: "algilanabilirlik", 2: "calistirilabilirlik", 3: "anlasilabilirlik", 4: "saglamlik" };

const OLCUT = {
  "1.1.1": ["Metin Olmayan İçerik", "A"],
  "1.3.1": ["Bilgi ve İlişkiler", "A"],
  "1.4.1": ["Renk Kullanımı", "A"],
  "1.4.3": ["Kontrast (En Az)", "AA"],
  "1.4.6": ["Kontrast (Gelişmiş)", "AAA"],
  "1.4.11": ["Metin Dışı Kontrast", "AA"],
  "2.1.1": ["Klavye", "A"],
  "2.1.2": ["Klavye Tuzağı Yok", "A"],
  "2.4.1": ["Blokları Atlama", "A"],
  "2.4.3": ["Odak Sırası", "A"],
  "2.4.7": ["Odak Görünür", "AA"],
  "2.5.3": ["Addaki Etiket", "A"],
  "3.3.1": ["Hata Tanımlama", "A"],
  "3.3.2": ["Etiketler veya Yönergeler", "A"],
  "3.3.3": ["Hata Önerisi", "AA"],
  "3.3.5": ["Yardım", "AAA"],
  "4.1.2": ["Ad, Rol, Değer", "A"],
  "4.1.3": ["Durum İletileri", "AA"],
};

const DUZEYLER = ["A", "AA", "AAA"];

// Kuzey Yamac istasyonunun uc yuzeyi: "gecti" | "kaldi" | "uygulanmaz"
const KALAN = {
  "olcum-listesi": ["1.4.3", "1.4.6", "4.1.3"],
  "olcum-girisi": ["1.4.6", "3.3.5", "4.1.2"],
  "gonderim-onayi": ["3.3.5"],
};
const UYGULANMAZ = {
  "olcum-listesi": ["3.3.1", "3.3.2", "3.3.3"],
  "olcum-girisi": [],
  "gonderim-onayi": ["3.3.1", "3.3.2", "3.3.3"],
};

const sonuclar = (yuzey) =>
  Object.fromEntries(
    Object.keys(OLCUT).map((n) => [
      n,
      KALAN[yuzey].includes(n) ? "kaldi" : UYGULANMAZ[yuzey].includes(n) ? "uygulanmaz" : "gecti",
    ]),
  );

const duzeyKarsilandi = (s, duzey) =>
  Object.entries(OLCUT).filter(([, [, d]]) => d === duzey).every(([n]) => s[n] !== "kaldi");

function uygunlukDuzeyi(s) {
  let sonuc = "uygun degil";
  for (const d of DUZEYLER) {
    if (!duzeyKarsilandi(s, d)) break;
    sonuc = d;
  }
  return sonuc;
}

console.log("yuzey            gecti  kaldi  uygulanmaz  yuzde  uygunluk");
for (const yuzey of Object.keys(KALAN)) {
  const s = sonuclar(yuzey);
  const d = Object.values(s);
  const gecti = d.filter((v) => v === "gecti").length;
  const kaldi = d.filter((v) => v === "kaldi").length;
  const yok = d.filter((v) => v === "uygulanmaz").length;
  console.log(
    `${yuzey.padEnd(16)} ${String(gecti).padStart(5)} ${String(kaldi).padStart(6)} ${String(yok).padStart(11)} ` +
      `${((100 * gecti) / (gecti + kaldi)).toFixed(1).padStart(6)} ${uygunlukDuzeyi(s).padStart(11)}`,
  );
}

console.log("\nkalan olcutler:");
for (const yuzey of Object.keys(KALAN)) {
  for (const n of KALAN[yuzey]) {
    const [ad, duzey] = OLCUT[n];
    console.log(`  ${yuzey.padEnd(16)} ${n.padEnd(6)} ${duzey.padEnd(4)} ${ILKE[n[0]].padEnd(20)} ${ad}`);
  }
}

const SUREC = ["olcum-listesi", "olcum-girisi", "gonderim-onayi"];
const enDusuk = SUREC
  .map((y) => uygunlukDuzeyi(sonuclar(y)))
  .reduce((a, b) => (DUZEYLER.indexOf(a) < DUZEYLER.indexOf(b) ? a : b));
console.log(`\nsurec: ${SUREC.join(" -> ")}`);
console.log(`  surecin uygunluk duzeyi: ${enDusuk}`);

const tum = SUREC.flatMap((y) => Object.values(sonuclar(y)));
const g = tum.filter((v) => v === "gecti").length;
const k = tum.filter((v) => v === "kaldi").length;
console.log(`  olcut basari yuzdesi: ${((100 * g) / (g + k)).toFixed(1)}`);
console.log(`  AA icin denetlenen olcut sayisi: ${Object.values(OLCUT).filter(([, d]) => d !== "AAA").length}`);
```

```
yuzey            gecti  kaldi  uygulanmaz  yuzde  uygunluk
olcum-listesi       12      3           3   80.0           A
olcum-girisi        15      3           0   83.3 uygun degil
gonderim-onayi      14      1           3   93.3          AA

kalan olcutler:
  olcum-listesi    1.4.3  AA   algilanabilirlik     Kontrast (En Az)
  olcum-listesi    1.4.6  AAA  algilanabilirlik     Kontrast (Gelişmiş)
  olcum-listesi    4.1.3  AA   saglamlik            Durum İletileri
  olcum-girisi     1.4.6  AAA  algilanabilirlik     Kontrast (Gelişmiş)
  olcum-girisi     3.3.5  AAA  anlasilabilirlik     Yardım
  olcum-girisi     4.1.2  A    saglamlik            Ad, Rol, Değer
  gonderim-onayi   3.3.5  AAA  anlasilabilirlik     Yardım

surec: olcum-listesi -> olcum-girisi -> gonderim-onayi
  surecin uygunluk duzeyi: uygun degil
  olcut basari yuzdesi: 85.4
  AA icin denetlenen olcut sayisi: 16
```

Üç satır üç ayrı durumu gösteriyor. Ölçüm listesi bütün A ölçütlerini karşılıyor ama iki
AA ölçütünü kaçırıyor; düzeyi A'dır. Ölçüm girişi on beş ölçüt geçmesine rağmen tek bir A
ölçütünde (4.1.2 Ad, Rol, Değer) kaldığı için hiçbir düzeye uygun değildir. Gönderim onayı
yalnızca bir AAA ölçütünü kaçırıyor ve AA düzeyindedir.

**Uygulanmaz** sütunu bir muafiyet değildir. Gönderim onayı sayfasında form bulunmadığı
için hata ölçütleri o yüzeyde değerlendirilmez; sayfaya bir form eklendiği anda üç ölçüt
yeniden kapsama girer. Uygulanmaz sonucu, ölçütün o içerikte karşılığı olmamasıdır;
karşılığı olan ölçütün atlanması değildir.

#### Uygunluk Sayfanın Değil Sürecin Özelliğidir

Betiğin son bölümü uygunluğun neden yüzey yüzey bildirilemeyeceğini gösteriyor. Ölçüm
bildirimi üç yüzeyden geçen bir süreçtir: kullanıcı listeden bir kaydı seçer, giriş
formunu doldurur, onay yüzeyinde sonucu görür. Zincirin bir halkası kırıldığında süreç
tamamlanamaz; bu nedenle sürecin uygunluk düzeyi halkaların en düşüğüdür.

Bu kuralın üç uzantısı vardır ve üçü de uygulamada karar değiştirir.

**Sayfanın tamamı sayılır.** Bir yüzeyin uygunluğu, o yüzeyin bir bölümünün değil
tamamının uygunluğudur. Sayfanın bir köşesindeki üçüncü taraf içeriğin bileşeni de aynı
kapsamdadır.

**Bir süreç bütün olarak sayılır.** Yalnızca listeleme yüzeyinin uygun olması, ölçüm
bildirme sürecinin uygun olduğu anlamına gelmez.

**Uygun olmayan içerik engel çıkarmamalıdır.** Bir bölüm uygunluk kapsamı dışında bırakılsa
bile, sayfanın geri kalanının kullanımını engellememelidir. Odağı yakalayan, kendiliğinden
ses çalan ya da klavye dolaşımını kilitleyen bir bileşen, kapsam dışı sayılarak
temizlenemez.

Bir dördüncü koşul teknoloji seçimine bakar: kullanılan işaretleme ve betikler, yardımcı
teknolojilerin desteklediği biçimde kullanılmalıdır. Bu koşul, bir bileşenin tanımlı bir
karşılığı olmayan bir yolla bildirilmesini uygunluk dışında bırakır — bileşen bir
gerçekleştirimde çalışsa bile.

#### Bütçe ile Ölçütün Farkı

Başarım bütçesiyle uygunluk kuralı arasındaki fark, çıktının son satırlarında görünür.
Üç yüzeyde ölçütlerin yüzde 85.4'ü geçiyor; buna rağmen sürecin uygunluk düzeyi yok. Bu
oran hiçbir kararı beslemez.

Nedeni iki ölçünün matematiksel yapısının farklı olmasıdır. Başarım ölçütleri bir dağılım
üzerinde tanımlanır: bir yükleme süresi ölçütü, kullanıcıların yüzde 75'inde belirli bir
eşiğin altında kalmayı ister ve kalan yüzde 25 bilinçli olarak kabul edilir. Erişilebilirlik
ölçütleri ise bir bağlaçtır: bütün ölçütler karşılanır ya da uygunluk yoktur. Ortalama
alınmaz, çünkü kullanıcı ortalama bir sayfada gezmez — kullanamadığı tek yüzeyde durur.

Bu, erişilebilirliğin bütçelenemeyeceği anlamına gelmez; bütçenin biçimi değişir. Başarımda
bütçe bir eşik listesidir, erişilebilirlikte bir **sıfır sayımıdır**: yayına çıkan sürümde
karşılanmamış A ve AA ölçüt sayısı sıfır olmalıdır. Bilinen ve henüz düzeltilmemiş
kusurlar, ölçüt numarasıyla, etkilenen yüzeyle ve düzeltme tarihiyle açık bir listede
tutulur. Gerileme tanımı da aynıdır: dün geçen bir ölçüt bugün kalıyorsa gerileme vardır.

#### Özet

- Erişilebilirlik ölçütleri dört ilke altında toplanır: algılanabilirlik,
  çalıştırılabilirlik, anlaşılabilirlik ve sağlamlık. İlkeler sınanmaz, ölçütleri gruplar.
- Ölçüt numarasının ilk sayısı ilkeyi, ikincisi yönergeyi, üçüncüsü sınanabilir ölçütü
  verir; numaralar yeniden kullanılmaz ve hata kayıtlarında ortak bir dil sağlar.
- Uygunluk düzeyleri birikimlidir: AA uygunluğu bütün A ve AA ölçütlerinin karşılanmasıdır;
  tek bir A ölçütü kaldığında yüzey hiçbir düzeye uygun değildir.
- Uygunluk yüzey ölçeğinde değil süreç ölçeğinde anlamlıdır; sayfanın tamamı sayılır, süreç
  bütün olarak sayılır ve kapsam dışı içerik geri kalanı engellememelidir.
- Başarım bütçesi bir dağılım eşiğidir, erişilebilirlik uygunluğu bir bağlaçtır; ölçüt
  başarı yüzdesi karar üretmez, karşılanmamış A ve AA ölçüt sayısı üretir.

#### Sonraki Adım

Bu ders ölçütlerin çerçevesini kurdu ama ölçütlerin denetlediği yapının ne olduğunu
açmadı. 4.1.2 ölçütü bir bileşenin adının, rolünün ve değerinin program yoluyla
okunabilmesini istiyor; bu üçlü nerede durur, kim üretir? Yanıt Anlamsal İşaretleme ve
Medya konusunda tanıtılan erişilebilirlik ağacındadır: tarayıcı belge ağacından ikinci bir
ağaç türetir ve yardımcı teknolojiler o ağacı okur. Sonraki ders bu türetmeyi kural
düzeyinde ele alır, hangi ögenin hangi rolü ürettiğini ve bir ögenin erişilebilir adının
hangi sırayla hesaplandığını gösterir.

### Anlamsal İşaretlemenin Rolü

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/anlamsal-isaretlemenin-rolu

Erişilebilirlik ağacının belge ağacından türetilmesi, yerel ögelerin ürettiği örtük roller, erişilebilir adın hesaplanma sırası ve anlamsız kapsayıcıyla ödenen bedelin sayımı.

Önceki ders 4.1.2 ölçütünü listeye aldı: bir arayüz bileşeninin **adı**, **rolü** ve
**değeri** program yoluyla okunabilmelidir. Ölçütün sınanabilir olması, bu üçlünün bir
yerde gerçekten bulunmasına bağlıdır. Bir düğmenin adı sayfanın görünümünde değil, belgeden
türetilen ikinci bir yapıda durur.

O yapı, Anlamsal İşaretleme ve Medya konusunda tanıtılan **erişilebilirlik ağacıdır**. Bu
ders ağacın nasıl türetildiğini kural düzeyinde ele alır: hangi düğüm ağaca girer, girenin
rolü nereden gelir ve adı hangi sırayla hesaplanır. Üçünün de yanıtı işaretlemededir;
hiçbiri sunum katmanından okunmaz.

#### Ağaç Ağaçtan Türer

Tarayıcı, Belge Nesne Modeli ağacını dolaşarak ikinci bir ağaç kurar. İkinci ağacın
düğümleri ögelerin kendisi değil, onların **erişilebilirlik karşılıklarıdır**: her düğüm bir
rol, bir ad, bir açıklama ve bir durum kümesi taşır.

Türetme sırasında bazı düğümler ağaca hiç girmez. Budama kuralları şunlardır: sunum
katmanında görüntülenmeyen ya da görünürlüğü kaldırılmış ögeler girmez; `hidden`
özniteliği taşıyan ögeler girmez; `aria-hidden="true"` bildirimi taşıyan öge ve bütün alt
ağacı girmez; alternatif metni boş bırakılmış bir görsel sunumsal sayılarak girmez.

Buradan bir ayrım çıkar. Bir ögeyi ekrandan kaldırmanın iki yolu vardır ve ikisi ağaçta
farklı sonuç verir. Ögeyi sunum katmanında görüntüleme dışı bırakmak onu ağaçtan da siler;
ögeyi ekranın dışına taşımak ya da saydamlaştırmak ağaçta bırakır. Atlama bağlantısının
görünmeden çalışabilmesi ikinci yola dayanır: bağlantı ağaçtadır, odak alır, yalnızca göze
görünmez.

#### Örtük Rol

Ağaca giren her düğümün bir rolü vardır ve bu rol, hiçbir bildirim yazılmadığında ögenin
kendisinden gelir. Buna **örtük rol (implicit role)** denir. Karşılıkların bir bölümü
koşulludur: aynı öge, bir özniteliğe göre farklı rol üretir.

`a` ögesi yalnızca bir `href` özniteliği taşıdığında bağlantı rolü üretir; taşımadığında
ağaçta anlamsız bir düğümdür. `input` ögesinin rolü tür özniteliğinden gelir: metin alanı,
arama alanı, sayı alanı ve onay kutusu ayrı rollerdir. `th` ögesinin rolü kapsam
özniteliğine göre sütun başlığı ya da satır başlığıdır. Bölüm ögelerinin yer imi rollerine
karşılığı ve o karşılıkların koşulları Web'in Temelleri ve HTML kursunda kurulmuştu; burada
aynı türetme, denetim ögeleri için sürdürülüyor.

#### Erişilebilir Adın Hesabı

Rolden sonraki soru addır. **Erişilebilir ad (accessible name)**, bir düğümü kullanıcıya
tanıtan dizgidir ve tek bir kaynaktan gelmez: tanımlı bir öncelik sırasıyla hesaplanır.
Aşağıdaki betik sıranın yerel bölümünü uygular. ARIA kaynakları sıranın en başında durur ve
sonraki dersin konusudur; burada onlar olmadığında ne olduğu görülüyor.

```js
// ad.mjs — yerel ogelerin ortuk rolu ve erisilebilir adin hesabi (ARIA kaynaklari haric)

function rol(d) {
  const o = d.oz ?? {};
  switch (d.oge) {
    case "a": return o.href === undefined ? "generic" : "link";
    case "button": return "button";
    case "input":
      return { checkbox: "checkbox", radio: "radio", number: "spinbutton",
               search: "searchbox", submit: "button" }[o.type ?? "text"] ?? "textbox";
    case "select": return "combobox";
    case "table": return "table";
    case "th": return o.scope === "row" ? "rowheader" : "columnheader";
    case "td": return "cell";
    case "tr": return "row";
    case "fieldset": return "group";
    case "img": return o.alt === "" ? "— (sunumsal)" : "image";
    case "ul": return "list";
    case "li": return "listitem";
    case "h1": case "h2": case "h3": return "heading";
    case "output": return "status";
    default: return "generic";
  }
}

// ARIA'da icerikten ad alan roller
const ICERIKTEN = new Set(["button", "link", "heading", "cell", "columnheader",
  "rowheader", "listitem", "checkbox", "radio", "option"]);

function metin(d) {
  if (typeof d === "string") return d;
  const o = d.oz ?? {};
  if (o.hidden) return "";
  if (d.oge === "img") return typeof o.alt === "string" ? o.alt : "";
  return (d.cocuklar ?? []).map(metin).join(" ").replace(/\s+/g, " ").trim();
}

// Sira: yerel kaynak -> icerik metni (izin veren roller) -> son care
function ad(d, etiketler) {
  const o = d.oz ?? {};
  if (o.hidden) return ["", "gizli"];
  if (d.oge === "img" && typeof o.alt === "string" && o.alt !== "") return [o.alt, "alt"];
  for (const [oge, alt] of [["table", "caption"], ["fieldset", "legend"], ["figure", "figcaption"]]) {
    if (d.oge !== oge) continue;
    const c = (d.cocuklar ?? []).find((x) => x.oge === alt);
    if (c) return [metin(c), alt];
  }
  if (["input", "select", "textarea"].includes(d.oge)) {
    if (etiketler[o.id]) return [etiketler[o.id], "label ogesi"];
    if (o.title) return [o.title, "title (son care)"];
    if (o.placeholder) return [o.placeholder, "placeholder (son care)"];
    return ["", "ad yok"];
  }
  if (ICERIKTEN.has(rol(d))) {
    const m = metin(d);
    if (m) return [m, "icerik metni"];
  }
  if (o.title) return [o.title, "title (son care)"];
  return ["", "ad yok"];
}

// --- Kuzey Yamac: suzgec paneli ve olcum tablosundan ornekler ----------------
const ETIKET = {
  "suz-kod": "İstasyon kodu",
  "suz-uyari": "Yalnız uyarılı ölçümler",
  deger: "Ölçülen değer (°C)",
};

const ORNEK = [
  ["<button>Süz</button>", { oge: "button", cocuklar: ["Süz"] }],
  ['<button><img alt="Süzgeci temizle"></button>',
   { oge: "button", cocuklar: [{ oge: "img", oz: { alt: "Süzgeci temizle" } }] }],
  ['<button><img alt=""></button>',
   { oge: "button", cocuklar: [{ oge: "img", oz: { alt: "" } }] }],
  ['<input id="suz-kod" type="search"> + label',
   { oge: "input", oz: { id: "suz-kod", type: "search" } }],
  ['<input type="search" placeholder="istasyon">',
   { oge: "input", oz: { id: "ara", type: "search", placeholder: "istasyon" } }],
  ['<input id="deger" type="number"> + label',
   { oge: "input", oz: { id: "deger", type: "number" } }],
  ['<input id="suz-uyari" type="checkbox"> + label',
   { oge: "input", oz: { id: "suz-uyari", type: "checkbox" } }],
  ["<table><caption>Günlük ölçümler</caption>",
   { oge: "table", cocuklar: [{ oge: "caption", cocuklar: ["Günlük ölçümler"] }] }],
  ["<fieldset><legend>Ölçüm kaynağı</legend>",
   { oge: "fieldset", cocuklar: [{ oge: "legend", cocuklar: ["Ölçüm kaynağı"] }] }],
  ['<a href="/istasyonlar/">İstasyonlar</a>',
   { oge: "a", oz: { href: "/istasyonlar/" }, cocuklar: ["İstasyonlar"] }],
  ["<a>İstasyonlar</a>", { oge: "a", cocuklar: ["İstasyonlar"] }],
  ['<div class="dugme">Kaydet</div>', { oge: "div", cocuklar: ["Kaydet"] }],
  ['<th scope="col">Sıcaklık</th>',
   { oge: "th", oz: { scope: "col" }, cocuklar: ["Sıcaklık"] }],
  ['<img src="uyari.svg" alt="">', { oge: "img", oz: { alt: "" } }],
];

console.log("isaretleme".padEnd(48) + "rol".padEnd(14) + "erisilebilir ad".padEnd(27) + "kaynak");
for (const [kaynak, dugum] of ORNEK) {
  const [a, nereden] = ad(dugum, ETIKET);
  console.log(
    kaynak.padEnd(48) + rol(dugum).padEnd(14) +
    (a === "" ? "(bos)" : '"' + a + '"').padEnd(27) + nereden,
  );
}
```

```
isaretleme                                      rol           erisilebilir ad            kaynak
<button>Süz</button>                            button        "Süz"                      icerik metni
<button><img alt="Süzgeci temizle"></button>    button        "Süzgeci temizle"          icerik metni
<button><img alt=""></button>                   button        (bos)                      ad yok
<input id="suz-kod" type="search"> + label      searchbox     "İstasyon kodu"            label ogesi
<input type="search" placeholder="istasyon">    searchbox     "istasyon"                 placeholder (son care)
<input id="deger" type="number"> + label        spinbutton    "Ölçülen değer (°C)"       label ogesi
<input id="suz-uyari" type="checkbox"> + label  checkbox      "Yalnız uyarılı ölçümler"  label ogesi
<table><caption>Günlük ölçümler</caption>       table         "Günlük ölçümler"          caption
<fieldset><legend>Ölçüm kaynağı</legend>        group         "Ölçüm kaynağı"            legend
<a href="/istasyonlar/">İstasyonlar</a>         link          "İstasyonlar"              icerik metni
<a>İstasyonlar</a>                              generic       (bos)                      ad yok
<div class="dugme">Kaydet</div>                 generic       (bos)                      ad yok
<th scope="col">Sıcaklık</th>                   columnheader  "Sıcaklık"                 icerik metni
<img src="uyari.svg" alt="">                    — (sunumsal)  (bos)                      ad yok
```

Tablonun okunması gereken üç yeri var.

**İçerikten ad yalnız bazı rollerde alınır.** Betikteki küme bunu uyguluyor: düğme,
bağlantı, başlık ve hücre rolleri adını içindeki metinden alır; metin alanı, tablo ve grup
rolleri almaz. Bu yüzden `İstasyonlar` metni bağlantı içinde ad üretiyor, aynı metin
`href` taşımayan bir `a` ögesinde hiçbir şey üretmiyor. Ayrım keyfi değildir: bir metin
alanının içindeki metin **değeridir**, adı değildir; ikisi karışırsa kullanıcının yazdığı
şey alanın adı sanılır.

**İçerik gezinmesi alt ögelerin adını toplar.** Süzgeci temizleyen düğmenin içinde metin
yok, bir görsel var; görselin alternatif metni düğmenin adı oluyor. Üçüncü satır bunun ters
yüzüdür: aynı düğmede alternatif metin boş bırakıldığında görsel sunumsal sayılıp
budanıyor ve geriye adsız bir düğme kalıyor. Ekranda simge görünür, ağaçta ad yoktur. Bu,
simge düğmelerinde en sık üretilen kusurdur ve 4.1.2 ölçütünü doğrudan kaldırır.

**Son çare kaynakları ada dönüşür ama iş görmez.** Yer tutucu metni olan arama alanı bir ad
kazanıyor. Bu ad, alan doldurulduğunda ekranda kaybolan bir metindir ve Renk ve Karşıtlık
dersinde ölçülen düşük karşıtlığı da taşır. Hesabın onu ad olarak kabul etmesi, tasarımın onu
etiket yerine koyabileceği anlamına gelmez. Aynı şey `title` özniteliği için de geçerlidir:
hesapta bulunur, işaret aygıtına bağlıdır ve kalıcı bir etiketin yerini tutmaz.

#### Değer ve Durum

Üçlünün son ögesi değerdir ve rolüne göre değişir: metin alanının değeri yazılan metin, onay
kutusunun değeri işaretli olup olmaması, sayı alanının değeri sayının kendisi ve alt–üst
sınırlarıdır. Bunların yanında **durumlar** bulunur: devre dışı, zorunlu, geçersiz, salt
okunur, genişletilmiş.

Yerel ögelerde bu bildirimlerin tamamı özniteliklerden gelir ve ayrıca yazılmaz. `required`
yazıldığında zorunluluk ağaca geçer; `disabled` yazıldığında hem etkinlik durumu bildirilir
hem de öge odak dışına çıkar; sayı alanına `min` ve `max` yazıldığında değer aralığı ağaçta
görünür. Web'in Temelleri ve HTML kursundaki kısıt doğrulaması bu nedenle iki iş birden yapar:
tarayıcıya kuralı, erişilebilirlik ağacına durumu bildirir.

#### Anlamsız Kapsayıcının Bedeli

Ölçüm tablosunun bir satırını silen düğme `div` ögesiyle kurulduğunda ekranda aynı görünür.
Ağaçta ise altı bildirim birden eksilir ve her biri elle yerine konmak zorundadır: rol
(düğme olduğu), erişilebilir ad, odaklanabilirlik, boşluk ve giriş tuşlarıyla
çalıştırılabilirlik, devre dışı durumunun bildirimi, form gönderimiyle ilişkisi.

Bu altı maddenin beşi işaretlemede tek bir öge adı değiştirilerek geri kazanılır. Yerel
öge kullanma tercihi bir üslup kararı değil, yeniden üretilmesi gereken bildirim sayısının
sıfıra indirilmesidir. Aynı sayım tablo için de yapılır: `div` yığınıyla kurulmuş bir
çizelgede satır ve sütun ilişkileri, başlık hücrelerinin hangi veriye ait olduğu ve toplam
satır sayısı ağaca hiç geçmez; kullanıcı hücreler arasında dolaşırken bulunduğu sütunun
adını duyamaz.

Karar ölçütü şudur: yapılacak iş için tanımlı bir öge varsa o kullanılır. Tanımlı öge
yoksa —sekmeli bir panel, bir ağaç görünümü, bir birleşik giriş alanı— eksik bildirimlerin
elle yazılması gerekir ve bunun aracı ARIA'dır.

#### Özet

- Erişilebilirlik ağacı belge ağacından türetilir; görüntülenmeyen, `hidden` taşıyan,
  `aria-hidden` bildirimi olan ve alternatif metni boş görsel düğümler budanır.
- Örtük rol ögeden gelir ve koşulludur: `a` ögesi ancak `href` taşırsa bağlantı, `input`
  ögesinin rolü türünden, `th` ögesinin rolü kapsamından belirlenir.
- Erişilebilir ad tanımlı bir sırayla hesaplanır; içerikten ad yalnızca izin veren rollerde
  alınır ve içerik gezinmesi alt ögelerin alternatif metnini toplar.
- Alternatif metni boş bir simge içeren düğme adsız kalır; yer tutucu ve `title` hesapta ad
  üretse de kalıcı etiketin yerini tutmaz.
- Değer ve durum bildirimleri yerel ögelerde özniteliklerden gelir; anlamsız kapsayıcıyla
  kurulan bir denetimde rol, ad, odaklanabilirlik, tuş davranışı ve durum elle üretilmelidir.

#### Sonraki Adım

Son bölüm bir boşluk bıraktı: tanımlı ögesi olmayan bir bileşen nasıl bildirilir? Ölçüm
listesinin üstündeki süzgeç paneli açılıp kapanan bir bölümdür, uyarı bildirimleri
kendiliğinden belirir, ölçüm tablosu sıralanabilir sütunlar taşır. Bunların hiçbirinin
yerel karşılığı yoktur ve üçü de ağaçta doğru görünmek zorundadır. Sonraki ders bu boşluğu
dolduran bildirim kümesini —rolleri, durumları ve özellikleri— ele alır; aynı kümenin yanlış
kullanıldığında ağacı nasıl bozduğunu da aynı ad hesabı üzerinde gösterir.

### ARIA Kullanımı

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/aria-kullanimi

Erişilebilirlik ağacına yazılan rol, durum ve özellik bildirimleri; ad hesabının tamamı, birinci kuralın gerekçesi ve kötüye kullanımın ürettiği somut kırılmalar.

Önceki ders yerel ögelerin ağaca ne bildirdiğini kurdu ve bir boşluk bıraktı: tanımlı
karşılığı olmayan bileşenler. Süzgeç paneli açılıp kapanır, ölçüm tablosunun sütunları
sıralanabilir, uyarı bildirimleri sonradan belirir. Bu davranışların hiçbirinin tek bir
öge karşılığı yoktur; ağaçta doğru görünmeleri için eksik bildirimlerin yazılması gerekir.

Bu bildirimleri yazan küme ARIA'dır. Aynı küme, en sık yanlış kullanılan araçtır ve yanlış
kullanıldığında arayüzü hiç bildirim yazılmamış hâlinden daha kullanılmaz yapar. Ders iki
şeyi birlikte kurar: kümenin ne yaptığını ve neyi bozduğunu.

#### ARIA Neyi Değiştirir

ARIA bildirimleri **yalnızca erişilebilirlik ağacını** değiştirir. Bir öznitelik yazmak o
ögeye davranış eklemez: odaklanabilir yapmaz, tuş dinlemez, tıklamayı işlemez, görünümünü
değiştirmez.

Bu tek cümle yanlış kullanımların çoğunun kaynağını açıklar. Bir `div` ögesine düğme rolü
yazan kod, ekranda düğme gibi görünen ve ağaçta düğme olarak bildirilen ama boşluk tuşuna
yanıt vermeyen bir şey üretir. Bildirim ile davranış ayrıştığında ortaya çıkan durum,
bildirim hiç yazılmamış olmasından kötüdür: kullanıcıya düğme olduğu söylenmiş ama kullanma yolu verilmemiştir.

#### Rol, Durum, Özellik

Küme üç tür bildirim taşır.

**Rol**, ögenin ne olduğunu söyler ve örtük rolün yerine geçer. Bir kez yazılır, çalışma
sırasında değişmesi beklenmez.

**Durum (state)**, ögenin o andaki hâlidir ve etkileşimle değişir: genişletilmiş mi
(`aria-expanded`), işaretli mi (`aria-checked`), seçili mi (`aria-selected`), meşgul mü
(`aria-busy`), geçersiz mi (`aria-invalid`).

**Özellik (property)**, ögenin daha kalıcı niteliğidir: adı (`aria-label`,
`aria-labelledby`), açıklaması (`aria-describedby`), neyi denetlediği (`aria-controls`),
zorunlu olup olmadığı (`aria-required`), hangi sırayla sıralandığı (`aria-sort`).

Ayrım biçimsel değildir: durum bildirimleri **değiştikçe güncellenmek zorundadır**. Panel
açıldığında `aria-expanded` değeri güncellenmiyorsa ağaç, ekranda görünenle çelişen bir
bilgi taşır. Yanlış bilgi, eksik bilgiden daha maliyetlidir.

#### Birinci Kural

ARIA'nın yazım kurallarının birincisi şudur: **gereken anlamı taşıyan yerel bir öge varsa
ARIA kullanılmaz.** `div` ögesine düğme rolü yazmak yerine `button` yazılır; liste rolü
yerine `ul` kullanılır; başlık rolü yerine başlık ögesi kullanılır.

Gerekçesi önceki dersteki sayımdır. Yerel öge rolü, adı, odaklanabilirliği, tuş
davranışını, durum bildirimlerini ve form ilişkisini birlikte getirir; rol bildirimi
yalnızca birincisini getirir. Kalan beşinin elle yazılması hem daha uzun hem de her
gerçekleştirimde yeniden hata yapılabilir bir iştir.

Kalan kurallar bu birincinin uzantısıdır: yerel bir ögenin anlamı zorunlu olmadıkça
değiştirilmez; rol yazılan her etkileşimli öge klavyeyle çalıştırılabilir olmalıdır;
odaklanabilir bir öge ağaçtan gizlenmez; etkileşimli her ögenin bir erişilebilir adı
olmalıdır.

#### Ad Hesabının Tamamı

Önceki dersin ad hesabı yerel kaynaklarla sınırlıydı. Sıranın en üstünde ARIA kaynakları
durur ve yerel her şeyi bastırır. Aşağıdaki betik tam sırayı uygular ve aynı işaretleme
kümesi üzerinde kural denetimi çalıştırır.

```js
// aria.mjs — ad hesabinin tamami (ARIA kaynaklari en ustte) ve kotuye kullanim denetimi

const ETKILESIMLI = new Set(["button", "link", "checkbox", "textbox", "searchbox", "spinbutton"]);
const ICERIKTEN = new Set(["button", "link", "heading", "cell", "columnheader", "checkbox", "listitem"]);
const ADA_KAPALI = new Set(["generic", "presentation", "none", "paragraph"]);   // yazardan ad almaz

function rol(d) {
  const o = d.oz ?? {};
  if (o.role) return o.role;
  switch (d.oge) {
    case "a": return o.href === undefined ? "generic" : "link";
    case "button": return "button";
    case "input": return o.type === "checkbox" ? "checkbox" : o.type === "search" ? "searchbox" : "textbox";
    case "table": return "table";
    case "section": return o["aria-label"] || o["aria-labelledby"] ? "region" : "generic";
    case "h2": return "heading";
    default: return "generic";
  }
}

const duz = (s) => s.replace(/\s+/g, " ").trim();

// Icerik metni; aria-hidden alt agaci atlanir
function metin(d, gorunur = true) {
  if (typeof d === "string") return d;
  const o = d.oz ?? {};
  if (gorunur && (o["aria-hidden"] === true || o.hidden)) return "";
  if (d.oge === "img") return o.alt ?? "";
  return duz((d.cocuklar ?? []).map((c) => metin(c, gorunur)).join(" "));
}

// 1 aria-labelledby -> 2 aria-label -> 3 yerel kaynak -> 4 icerik -> 5 son care
function ad(d, belge) {
  const o = d.oz ?? {};
  const r = rol(d);
  const yazardanAlir = !ADA_KAPALI.has(r);

  if (yazardanAlir && o["aria-labelledby"]) {
    const parcalar = o["aria-labelledby"].split(" ").map((k) => belge[k]).filter(Boolean);
    // basvurulan dugum gizli olsa da metni kullanilir
    if (parcalar.length > 0) return [duz(parcalar.map((p) => metin(p, false)).join(" ")), "aria-labelledby"];
  }
  if (yazardanAlir && o["aria-label"]) return [duz(o["aria-label"]), "aria-label"];
  if (["input", "select", "textarea"].includes(d.oge) && belge["label:" + o.id]) {
    return [metin(belge["label:" + o.id]), "label ogesi"];
  }
  if (ICERIKTEN.has(r)) {
    const m = metin(d);
    if (m) return [m, "icerik metni"];
  }
  return ["", "ad yok"];
}

// --- Kuzey Yamac: suzgec paneli ve olcum tablosu bilesenleri -----------------
const BELGE = {
  "baslik": { oge: "h2", cocuklar: ["Süzgeç"] },
  "sayi": { oge: "span", cocuklar: ["3 ölçüt"] },
  "gizli-ad": { oge: "span", oz: { hidden: true }, cocuklar: ["Ölçüm satırını sil"] },
  "label:kod": { oge: "label", cocuklar: ["İstasyon kodu"] },
};

const ORNEK = [
  ['<button>Süz</button>',
   { oge: "button", cocuklar: ["Süz"], odaklanir: true }],
  ['<button aria-label="Kaydet">Ölçümü sil</button>',
   { oge: "button", oz: { "aria-label": "Kaydet" }, cocuklar: ["Ölçümü sil"], odaklanir: true }],
  ['<button aria-labelledby="baslik sayi">Aç</button>',
   { oge: "button", oz: { "aria-labelledby": "baslik sayi" }, cocuklar: ["Aç"], odaklanir: true }],
  ['<button aria-labelledby="yok-boyle">Sil</button>',
   { oge: "button", oz: { "aria-labelledby": "yok-boyle" }, cocuklar: ["Sil"], odaklanir: true }],
  ['<div role="button" tabindex="0">Sil</div>',
   { oge: "div", oz: { role: "button" }, cocuklar: ["Sil"], odaklanir: true }],
  ['<div role="button">Sil</div>',
   { oge: "div", oz: { role: "button" }, cocuklar: ["Sil"], odaklanir: false }],
  ['<input id="kod" aria-label="Kod"> + label',
   { oge: "input", oz: { id: "kod", "aria-label": "Kod" }, odaklanir: true }],
  ['<div aria-label="Süzgeç paneli">…</div>',
   { oge: "div", oz: { "aria-label": "Süzgeç paneli" }, cocuklar: ["3 ölçüt"], odaklanir: false }],
  ['<section aria-label="Süzgeç paneli">…',
   { oge: "section", oz: { "aria-label": "Süzgeç paneli" }, cocuklar: ["3 ölçüt"], odaklanir: false }],
  ['<button aria-hidden="true">Dışa aktar</button>',
   { oge: "button", oz: { "aria-hidden": true }, cocuklar: ["Dışa aktar"], odaklanir: true }],
  ['<button aria-labelledby="gizli-ad">×</button>',
   { oge: "button", oz: { "aria-labelledby": "gizli-ad" }, cocuklar: ["×"], odaklanir: true }],
  ['<table role="presentation">…</table>',
   { oge: "table", oz: { role: "presentation" }, cocuklar: ["Sıcaklık"], odaklanir: false }],
];

console.log("isaretleme".padEnd(51) + "rol".padEnd(14) + "erisilebilir ad".padEnd(22) + "kaynak");
for (const [kaynak, d] of ORNEK) {
  const [a, nereden] = ad(d, BELGE);
  console.log(kaynak.padEnd(51) + rol(d).padEnd(14) + (a === "" ? "(bos)" : '"' + a + '"').padEnd(22) + nereden);
}

console.log("\nkural denetimi:");
let sayi = 0;
for (const [kaynak, d] of ORNEK) {
  const o = d.oz ?? {};
  const r = rol(d);
  const [a] = ad(d, BELGE);
  const gorunen = metin(d);
  const bulgular = [];
  if (o["aria-hidden"] === true && d.odaklanir) bulgular.push("odaklanabilir oge agactan gizlendi");
  if (o["aria-labelledby"] && !o["aria-labelledby"].split(" ").some((k) => BELGE[k]))
    bulgular.push("aria-labelledby gecersiz kimlige basvuruyor");
  if ((o["aria-label"] || o["aria-labelledby"]) && ADA_KAPALI.has(r))
    bulgular.push("yazardan ad almayan rolde ad bildirimi yok sayildi");
  if (ETKILESIMLI.has(r) && !d.odaklanir) bulgular.push("etkilesimli rol odaklanabilir degil");
  if (ETKILESIMLI.has(r) && a === "") bulgular.push("etkilesimli oge adsiz");
  if (["presentation", "none"].includes(o.role) && ["table", "ul", "ol", "li"].includes(d.oge))
    bulgular.push("yerel yapi anlami kaldirildi: satir ve sutun iliskileri agacta yok");
  if (ETKILESIMLI.has(r) && /\p{L}/u.test(gorunen) && a &&
      !a.toLocaleLowerCase("tr").includes(gorunen.toLocaleLowerCase("tr")))
    bulgular.push('gorunen metin ("' + gorunen + '") erisilebilir adin icinde yok');
  for (const b of bulgular) {
    sayi++;
    console.log("  " + kaynak.padEnd(51) + b);
  }
}
console.log(`\n${ORNEK.length} ornekte ${sayi} bulgu`);
```

```
isaretleme                                         rol           erisilebilir ad       kaynak
<button>Süz</button>                               button        "Süz"                 icerik metni
<button aria-label="Kaydet">Ölçümü sil</button>    button        "Kaydet"              aria-label
<button aria-labelledby="baslik sayi">Aç</button>  button        "Süzgeç 3 ölçüt"      aria-labelledby
<button aria-labelledby="yok-boyle">Sil</button>   button        "Sil"                 icerik metni
<div role="button" tabindex="0">Sil</div>          button        "Sil"                 icerik metni
<div role="button">Sil</div>                       button        "Sil"                 icerik metni
<input id="kod" aria-label="Kod"> + label          textbox       "Kod"                 aria-label
<div aria-label="Süzgeç paneli">…</div>            generic       (bos)                 ad yok
<section aria-label="Süzgeç paneli">…              region        "Süzgeç paneli"       aria-label
<button aria-hidden="true">Dışa aktar</button>     button        (bos)                 ad yok
<button aria-labelledby="gizli-ad">×</button>      button        "Ölçüm satırını sil"  aria-labelledby
<table role="presentation">…</table>               presentation  (bos)                 ad yok

kural denetimi:
  <button aria-label="Kaydet">Ölçümü sil</button>    gorunen metin ("Ölçümü sil") erisilebilir adin icinde yok
  <button aria-labelledby="baslik sayi">Aç</button>  gorunen metin ("Aç") erisilebilir adin icinde yok
  <button aria-labelledby="yok-boyle">Sil</button>   aria-labelledby gecersiz kimlige basvuruyor
  <div role="button">Sil</div>                       etkilesimli rol odaklanabilir degil
  <div aria-label="Süzgeç paneli">…</div>            yazardan ad almayan rolde ad bildirimi yok sayildi
  <button aria-hidden="true">Dışa aktar</button>     odaklanabilir oge agactan gizlendi
  <button aria-hidden="true">Dışa aktar</button>     etkilesimli oge adsiz
  <table role="presentation">…</table>               yerel yapi anlami kaldirildi: satir ve sutun iliskileri agacta yok

12 ornekte 8 bulgu
```

Betikteki `odaklanir` alanı, ögenin gerçekten odak alıp almadığını temsil eder; `div`
satırlarında `tabindex` yazılıp yazılmamasının karşılığıdır.

#### Bulguların Okunması

**Ad bildirimi görünen metni bastırır.** İkinci satırda düğmenin üzerinde "Ölçümü sil"
yazıyor, ağaçta adı "Kaydet". Konuşarak arayüz kullanan bir kişi gördüğü metni söyler ve
söylediği ad hiçbir ögeye uymaz. 2.5.3 ölçütü tam da bunu yasaklar: görünen etiket metni,
erişilebilir adın içinde geçmelidir. Aynı kırılma yedinci satırda daha sinsi biçimde
oluşuyor — alanın bir `label` ögesi var, ama `aria-label` bildirimi onu bastırdığı için
ekranda "İstasyon kodu" yazan alanın adı "Kod".

**Geçersiz başvuru sessizce düşer.** Dördüncü satırda `aria-labelledby` var olmayan bir
kimliğe işaret ediyor; hesap o adımı atlıyor ve ad içerik metninden geliyor. Sonuç bu
örnekte zararsız görünür, ancak asıl bildirim yok sayılmıştır. İçeriği olmayan bir simge
düğmesinde aynı hata adsız bir düğme bırakır.

**Gizli düğüme yapılan başvuru geçerlidir.** On birinci satır bunu gösteriyor: gizlenmiş
bir `span` içindeki metin, ona başvuran düğmenin adı oluyor. Ekranda yalnız bir çarpı
işareti duran silme düğmesine ad vermenin tanımlı yolu budur.

**Yazardan ad almayan roller vardır.** Sekizinci satırda bir `div` ögesine yazılan
`aria-label` yok sayılıyor; dokuzuncu satırda aynı bildirim `section` ögesine yazıldığında
hem ögenin rolünü bölge yapıyor hem de ad üretiyor. Ad bildirimi ögeye değil, ögenin
rolüne yazılır.

**Odaklanabilir bir öge ağaçtan gizlenemez.** Onuncu satır iki bulgu birden üretiyor:
düğme odak alıyor ama ağaçta yok. Klavyeyle gezen bir kullanıcı odağın kaybolduğu bir
durakla karşılaşır; sekme tuşu bir yere gider, orada hiçbir şey bildirilmez.

**Yapı anlamını kaldırmak veriyi kaldırır.** Son satırda tablonun rolü sunumsala
çevrilmiş. Düzen amacıyla kullanılan bir tabloda bu doğru karardır; veri taşıyan ölçüm
tablosunda satır ve sütun ilişkileri ağaçtan tamamen düşer ve hücreler arasında dolaşan
kullanıcı hangi sütunda olduğunu öğrenemez.

#### Süzgeç Panelinde Doğru Kullanım

Aşağıdaki işaretleme, yerel ögeyle karşılanmayan iki yapıyı kuralına uygun bildirir:
açılıp kapanan panel ve sıralanabilir sütun.

```html
<h2 id="suzgec-basligi">Süzgeç</h2>
<button aria-expanded="false" aria-controls="suzgec-govde">
  Süzgeç ölçütleri
</button>
<div id="suzgec-govde" hidden>
  <label for="suz-kod">İstasyon kodu</label>
  <input id="suz-kod" type="search">
  <p><label><input type="checkbox" name="uyari"> Yalnız uyarılı ölçümler</label></p>
</div>

<table aria-labelledby="tablo-basligi">
  <caption id="tablo-basligi">Günlük ölçümler</caption>
  <thead>
    <tr>
      <th scope="col" aria-sort="descending">
        <button>Tarih</button>
      </th>
      <th scope="col" aria-sort="none"><button>Sıcaklık</button></th>
    </tr>
  </thead>
</table>
```

Üç karar dikkat gerektirir. Açma denetimi bir `button` ögesidir; rol yazılmamıştır, çünkü
gereken rol zaten vardır. `aria-expanded` durumu panel her açılıp kapandığında
güncellenmelidir; sabit kalırsa ağaç yanlış bilgi taşır. Sıralama durumu başlık hücresine
yazılır, hücrenin içindeki düğmeye değil: sıralanan şey sütundur, düğme yalnızca sıralamayı
başlatan denetimdir.

#### Özet

- ARIA bildirimleri yalnızca erişilebilirlik ağacını değiştirir; davranış, odaklanabilirlik
  ya da görünüm eklemez.
- Rol ögenin ne olduğunu, durum o andaki hâlini, özellik daha kalıcı niteliğini bildirir;
  durum bildirimleri etkileşimle birlikte güncellenmek zorundadır.
- Birinci kural, gereken anlamı taşıyan yerel öge varken ARIA kullanmamaktır; yerel öge
  rolle birlikte ad, odak, tuş davranışı ve durumları da getirir.
- Ad hesabında ARIA kaynakları yerel kaynakları bastırır; görünen metinle erişilebilir adın
  ayrışması 2.5.3 ölçütünü kaldırır.
- Geçersiz kimlik başvurusu sessizce düşer, gizli düğüme yapılan başvuru geçerlidir ve
  `aria-label` yazardan ad almayan rollerde yok sayılır.
- Odaklanabilir bir ögeyi ağaçtan gizlemek ve veri tablosunun yapı anlamını kaldırmak,
  bildirim yazmamaktan daha ağır kırılmalar üretir.

#### Sonraki Adım

Kural denetiminin bir satırı bu derste açılmadan bırakıldı: düğme rolü yazılmış ama odak
almayan öge. Bildirimin doğru olması yetmez, ögenin klavyeyle çalıştırılabilir olması da
gerekir ve bu iki gereklilik ayrı ölçütlerde tanımlıdır. Sonraki ders arayüzün tamamına
klavyeden bakar: hangi ögelerin sekme sırasında bulunduğunu, sıranın nasıl hesaplandığını,
açılan bir panelin odağı nasıl kuşattığını ve kuşatmanın hangi noktada tuzağa dönüştüğünü
ele alır.

### Klavye Erişimi

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/klavye-erisimi

Klavyeyle çalıştırılabilirliğin gerekleri, odak sırasıyla görsel sıranın karşılaştırılması, odak kuşatmasının tuzağa dönüştüğü sınır ve tek karakterli kısayolların koşulu.

Önceki dersin kural denetimi bir satırı açık bıraktı: düğme rolü bildirilmiş ama odak
almayan öge. Bildirim doğruydu, davranış yoktu. Bu ayrım iki farklı ölçütün konusudur ve
ikincisi bu dersin çıkış noktasıdır.

Klavye erişimi yalnızca klavye kullanan bir azınlığın sorunu değildir. Ekran okuyucular
sayfayı klavye arayüzü üzerinden dolaşır; ses komutuyla ve anahtar denetimiyle çalışan
araçlar da aynı arayüzü kullanır. Klavyeyle çalışmayan bir bileşen bu araçların hiçbirinde
çalışmaz. Buradan pratik bir sonuç çıkar: klavye denetimi, yardımcı teknoloji denemesinden
önce yapılabilecek en ucuz ve en geniş kapsamlı sınamadır.

#### Ölçüt Ne İster

2.1.1 ölçütü, içeriğin bütün işlevlerinin bir klavye arayüzü üzerinden çalıştırılabilmesini
ister. İfadenin iki ayrıntısı önemlidir. Birincisi "klavye arayüzü" denmesi: fiziksel bir
klavye değil, o arayüzü kullanan her araç kastedilir. İkincisi, ölçütün tek tek tuş
vuruşları için **belirli bir zamanlama** gerektirmemeyi de istemesidir; belirli bir hızda
basılması gereken bir tuş dizisi ölçütü karşılamaz.

Tek istisna, girdinin yolunun kendisinin bilgi taşıdığı işlevlerdir — serbest el çizimi
gibi. Bir haritayı sürükleyerek kaydırmak bu istisnaya girmez, çünkü aynı işlev kaydırma
düğmeleriyle de sağlanabilir.

2.1.2 ölçütü zincirin diğer ucudur: klavyeyle bir bileşene girilebiliyorsa, aynı arayüzle
çıkılabilmelidir. Bu iki ölçüt birlikte bir bütün kurar; birincisi girişi, ikincisi çıkışı
güvence altına alır.

#### Odak Sırası Görsel Sırayla Uyuşmalı

2.4.3 ölçütü, odaklanabilir ögelerin anlamı ve çalıştırılabilirliği koruyan bir sırada
gezilmesini ister. Sekme sırası belge sırasından gelir; görsel sıra sunum katmanından
gelir. İkisi ayrıştığında klavye kullanıcısı ekranda ileri geri sıçrar.

Ölçüm listesi sayfasında bu ayrışma iki nedenle oluşuyor: süzgeç paneli işaretlemeye
sonradan, tablodan sonra eklendi ve sunum katmanı onu tablonun üstüne yerleştirdi; dışa
aktarma düğmesine de "önce gelsin" niyetiyle artı bir `tabindex` değeri yazıldı.

```js
// odaksira.mjs — sekme sirasi ile gorsel siranin karsilastirilmasi (2.4.3)

// gorsel: [satir, sutun] — sunum katmaninin yerlestirdigi yer
const SAYFA = [
  { ad: "a.atlama-bagi", belge: 1, tabindex: null, gorsel: [0, 1] },
  { ad: "button#sirala-tarih", belge: 2, tabindex: null, gorsel: [3, 1] },
  { ad: "button#sirala-sicaklik", belge: 3, tabindex: null, gorsel: [3, 2] },
  { ad: "button.sil[T-01]", belge: 4, tabindex: null, gorsel: [4, 1] },
  { ad: "input#suz-kod", belge: 5, tabindex: null, gorsel: [1, 1] },
  { ad: "input#suz-uyari", belge: 6, tabindex: null, gorsel: [1, 2] },
  { ad: "button#suz", belge: 7, tabindex: null, gorsel: [1, 3] },
  { ad: "button#disa-aktar", belge: 8, tabindex: 1, gorsel: [2, 1] },
];

// M14/K04'te kurulan kural: once artan tabindex, sonra belge sirasi
function sekmeSirasi(ogeler) {
  const pozitif = ogeler.filter((o) => (o.tabindex ?? 0) > 0)
    .sort((a, b) => a.tabindex - b.tabindex || a.belge - b.belge);
  const sifir = ogeler.filter((o) => (o.tabindex ?? 0) === 0 || o.tabindex === null)
    .sort((a, b) => a.belge - b.belge);
  return [...pozitif, ...sifir];
}

const gorselSira = (ogeler) =>
  [...ogeler].sort((a, b) => a.gorsel[0] - b.gorsel[0] || a.gorsel[1] - b.gorsel[1]);

function tersDusenler(ogeler) {
  const sekme = sekmeSirasi(ogeler).map((o) => o.ad);
  const gorsel = gorselSira(ogeler).map((o) => o.ad);
  const ciftler = [];
  for (let i = 0; i < gorsel.length; i++) {
    for (let j = i + 1; j < gorsel.length; j++) {
      if (sekme.indexOf(gorsel[i]) > sekme.indexOf(gorsel[j])) ciftler.push([gorsel[i], gorsel[j]]);
    }
  }
  return ciftler;
}

function rapor(baslik, ogeler) {
  console.log(baslik);
  const sekme = sekmeSirasi(ogeler);
  const gorsel = gorselSira(ogeler);
  console.log("  no  sekme sirasi              gorsel sira");
  for (let i = 0; i < sekme.length; i++) {
    console.log(`  ${String(i + 1).padStart(2)}  ${sekme[i].ad.padEnd(24)}  ${gorsel[i].ad}`);
  }
  const ciftler = tersDusenler(ogeler);
  console.log(`  tersine dusen cift sayisi: ${ciftler.length}`);
  for (const [a, b] of ciftler.slice(0, 4)) {
    console.log(`    "${a}" gorselde once, sekmede "${b}" ogesinden sonra`);
  }
}

rapor("suzgec paneli kaynakta tablodan sonra, tabindex=1 var:", SAYFA);

// Duzeltme: suzgec paneli kaynakta yukari alinir, arti tabindex kaldirilir
const DUZELTILMIS = SAYFA.map((o) => {
  const yeni = { ...o, tabindex: null };
  if (o.ad.startsWith("input#suz") || o.ad === "button#suz") yeni.belge = o.belge - 3;
  if (o.ad === "button#disa-aktar") yeni.belge = 5;
  if (o.ad.startsWith("button#sirala") || o.ad.startsWith("button.sil")) yeni.belge = o.belge + 4;
  return yeni;
});

console.log("");
rapor("suzgec paneli kaynakta yukari alindi, tabindex kaldirildi:", DUZELTILMIS);
```

```
suzgec paneli kaynakta tablodan sonra, tabindex=1 var:
  no  sekme sirasi              gorsel sira
   1  button#disa-aktar         a.atlama-bagi
   2  a.atlama-bagi             input#suz-kod
   3  button#sirala-tarih       input#suz-uyari
   4  button#sirala-sicaklik    button#suz
   5  button.sil[T-01]          button#disa-aktar
   6  input#suz-kod             button#sirala-tarih
   7  input#suz-uyari           button#sirala-sicaklik
   8  button#suz                button.sil[T-01]
  tersine dusen cift sayisi: 13
    "a.atlama-bagi" gorselde once, sekmede "button#disa-aktar" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#disa-aktar" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#sirala-tarih" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#sirala-sicaklik" ogesinden sonra

suzgec paneli kaynakta yukari alindi, tabindex kaldirildi:
  no  sekme sirasi              gorsel sira
   1  a.atlama-bagi             a.atlama-bagi
   2  input#suz-kod             input#suz-kod
   3  input#suz-uyari           input#suz-uyari
   4  button#suz                button#suz
   5  button#disa-aktar         button#disa-aktar
   6  button#sirala-tarih       button#sirala-tarih
   7  button#sirala-sicaklik    button#sirala-sicaklik
   8  button.sil[T-01]          button.sil[T-01]
  tersine dusen cift sayisi: 0
```

Sekiz ögelik bir sayfada on üç ters çift oluşuyor. Sayı, iki hatanın çarpımıdır: artı
`tabindex` değeri tek başına yedi çift üretir, kaynak sırasındaki yanlış yerleşim kalanı.
İkinci rapor düzeltmenin yönünü gösteriyor — sıra, sunum katmanında değil **kaynakta**
düzeltilir. Görsel sırayı sunum katmanı belirlediği için, kaynağı ona uydurmaktan başka
tutarlı bir yol yoktur.

Ters çift sayısı bir denetim ölçüsü olarak kullanılabilir: sıfırdan büyük her değer, en az
bir kullanıcının ekranda geriye sıçradığı anlamına gelir. Ölçüm tablosunun her satırındaki
silme düğmesi sekme sırasını uzatıyorsa, Tarayıcı ve Web Platformu kursunda kurulan dönen
sekme durağı düzeni grubu tek durağa indirir; bu, sırayı bozmadan uzunluğu düşürür.

#### Odak Kuşatması ve Tuzak

Ölçüm giriş formu bir pencere olarak açıldığında arkasındaki sayfa erişilebilir kalmamalıdır:
sekme tuşu pencerenin içinde dönmelidir. Bu düzene **odak kuşatması** denir ve yanlış
kurulduğunda 2.1.2 ölçütünü kaldıran bir tuzağa dönüşür. Aşağıdaki betik üç kurulumu bir
durum makinesi olarak modelliyor ve her birinden hangi durumların erişilebilir olduğunu
tarayarak denetliyor.

```js
// tuzak.mjs — odak kusatmasinin durum makinesi ve klavye tuzagi denetimi (2.1.2)

const ARKA = ["input#suz-kod", "button#suz", "button#yeni-olcum", "button.sil[T-01]"];
const PENCERE = ["input#tarih", "input#deger", "button#kaydet", "button#kapat"];
const TUSLAR = ["Tab", "Shift+Tab", "Escape"];

// tur: "kusatmasiz" | "cikissiz" | "dogru"
function makine(tur) {
  const cikisVar = tur !== "cikissiz";
  return (d, tus) => {
    if (!d.acik) {
      const i = ARKA.indexOf(d.odak);
      if (tus === "Tab") return { acik: false, odak: ARKA[(i + 1) % ARKA.length] };
      if (tus === "Shift+Tab") return { acik: false, odak: ARKA[(i + ARKA.length - 1) % ARKA.length] };
      return d;
    }
    const halka = tur === "kusatmasiz" ? [...PENCERE, ...ARKA] : PENCERE;
    const i = halka.indexOf(d.odak);
    if (tus === "Tab") return { acik: true, odak: halka[(i + 1) % halka.length] };
    if (tus === "Shift+Tab") return { acik: true, odak: halka[(i + halka.length - 1) % halka.length] };
    if (tus === "Escape" && cikisVar) return { acik: false, odak: "button#yeni-olcum" };
    return d;
  };
}

const anahtar = (d) => (d.acik ? "acik" : "kapali") + "|" + d.odak;

function denetle(tur) {
  const sonraki = makine(tur);
  const baslangic = { acik: true, odak: PENCERE[0] };
  const gorulen = new Map([[anahtar(baslangic), baslangic]]);
  const kuyruk = [baslangic];
  while (kuyruk.length > 0) {
    const d = kuyruk.shift();
    for (const tus of TUSLAR) {
      const y = sonraki(d, tus);
      if (gorulen.has(anahtar(y))) continue;
      gorulen.set(anahtar(y), y);
      kuyruk.push(y);
    }
  }
  const durumlar = [...gorulen.values()];
  return {
    ulasilan: durumlar.length,
    kacak: durumlar.some((d) => d.acik && ARKA.includes(d.odak)),
    cikis: durumlar.some((d) => !d.acik),
  };
}

console.log("kurulum       ulasilan durum  arka plana kacak  klavyeyle cikis  2.1.2");
for (const tur of ["kusatmasiz", "cikissiz", "dogru"]) {
  const s = denetle(tur);
  console.log(
    tur.padEnd(14) +
      String(s.ulasilan).padStart(14) +
      (s.kacak ? "  var" : "  yok").padEnd(18) +
      (s.cikis ? " var" : " yok").padEnd(17) +
      (s.cikis ? "gecti" : "KALDI"),
  );
}

// Dogru kurulumda tus dizisi
console.log("\ndogru kurulumda odak izi:");
let d = { acik: true, odak: PENCERE[0] };
const sonraki = makine("dogru");
console.log("  " + "baslangic".padEnd(18) + " " + "pencere acik".padEnd(14) + " odak: " + d.odak);
for (const tus of ["Tab", "Tab", "Tab", "Tab", "Shift+Tab", "Escape", "Tab"]) {
  d = sonraki(d, tus);
  console.log(`  ${tus.padEnd(18)} ${(d.acik ? "pencere acik" : "pencere kapali").padEnd(14)} odak: ${d.odak}`);
}
```

```
kurulum       ulasilan durum  arka plana kacak  klavyeyle cikis  2.1.2
kusatmasiz                12  var              var             gecti
cikissiz                   4  yok              yok             KALDI
dogru                      8  yok              var             gecti

dogru kurulumda odak izi:
  baslangic          pencere acik   odak: input#tarih
  Tab                pencere acik   odak: input#deger
  Tab                pencere acik   odak: button#kaydet
  Tab                pencere acik   odak: button#kapat
  Tab                pencere acik   odak: input#tarih
  Shift+Tab          pencere acik   odak: button#kapat
  Escape             pencere kapali odak: button#yeni-olcum
  Tab                pencere kapali odak: button.sil[T-01]
```

Üç satır üç ayrı kusuru ayırıyor. **Kuşatmasız** kurulum ölçütü geçiyor — çıkış vardır —
ama pencere açıkken arka plandaki dört durağa odak kaçıyor. Kullanıcı görmediği, üstü
örtülü ögelere odaklanır; bu bir uygunluk ihlali değil, kullanımı bozan bir kusurdur.
**Çıkışsız** kurulum yalnızca dört duruma ulaşıyor: pencerenin dışına çıkan hiçbir yol yok.
Bu, tanımı gereği klavye tuzağıdır ve 2.1.2 ölçütünü kaldırır. **Doğru** kurulumda sekiz
durumun tamamına ulaşılıyor, kaçak yok ve çıkış var.

Odak izinin son iki satırı ayrı bir kuralı gösteriyor: pencere kapandığında odak, pencereyi
açan düğmeye döner. Dönmezse odak belgenin köküne düşer ve kullanıcı dolaşıma en baştan
başlar. Bu davranış Tarayıcı ve Web Platformu kursunda odağı program taşıdığında doğan
sorumluluk olarak kurulmuştu; burada ölçütün gereği hâline geliyor.

Kuşatmanın kendisi de tek bir yolla kurulmaz. Sekme tuşunu yakalayıp odağı halkada
döndürmek bir yoldur; pencere dışındaki içeriği ağaçtan gizlemek ve etkileşim dışı bırakmak
diğeridir. İkincisi yeğlenir, çünkü kuşatmayı yalnızca sekme tuşuna değil, ekran
okuyucunun kendi gezinme komutlarına karşı da kurar — sekme tuşu yakalanmış bir pencerede
kullanıcı yer imi listesinden arka plana geçebilir.

#### Odağın Görünürlüğü

Klavyeyle çalışan bir arayüzde odağın nerede olduğu her an görünmelidir; 2.4.7 ölçütü
bunu ister. Odak göstergesini sunum katmanından kaldırmak, ölçütü tek satırda kaldırır.
Gösterge değiştirilebilir: kalınlığı, rengi ve biçimi tasarım kararıdır. Kaldırılamaz.

İki ek koşul vardır. Gösterge, komşu renklerden ayırt edilebilecek kadar karşıtlık taşımalıdır —
bu, metin dışı kontrast ölçütünün konusudur ve karşıtlık dersinde hesaplanacaktır. Odaklanan öge
ayrıca ekranda **örtülü kalmamalıdır** — bu da WCAG 2.2 ile gelen ayrı bir ölçütün konusudur:
sayfanın altına yapışan bir çubuk ya da üstte duran bir başlık, odaklanan ögeyi tamamen
kapatıyorsa gösterge var ama görünmez demektir.

#### Kısayollar

Ölçüm listesinde süzgeç alanına atlamak için tek harflik bir kısayol yazmak akla yakın
gelir. 2.1.4 ölçütü bu tür kısayollara üç koşuldan en az birini dayatır: kısayol
kapatılabilmeli, kullanıcı tarafından bir değiştirici tuşla birlikte kullanılacak biçimde
yeniden atanabilmeli, ya da yalnızca ilgili bileşen odaktayken etkin olmalıdır.

Gerekçe iki kullanım biçiminde görünür. Sesle yazan bir kullanıcının çevresindeki konuşma
metne çevrilebilir ve tek harfler kısayol olarak yorumlanır; klavyede tek tuşa basmakta
zorlanan bir kullanıcı istemeden kısayol tetikler. Ölçüm giriş formunda bu, açıklama
alanına yazarken satırların silinmesi anlamına gelir.

Uygulanabilir karar şudur: tek karakterli kısayollar belge geneline değil, odaktaki
bileşene bağlanır. Ölçüm tablosunda ok tuşlarıyla satır gezinmesi tablo odaktayken çalışır;
tablo odakta değilken aynı tuşlar sayfayı kaydırır. Belge geneline bağlanan kısayollar bir
değiştirici tuşla yazılır ve kullanıcıya kapatma yolu bırakılır.

#### Özet

- 2.1.1 bütün işlevlerin klavye arayüzünden ve tuş vuruşu zamanlamasına bağlı olmadan
  çalıştırılmasını ister; 2.1.2 girilen her yerden aynı arayüzle çıkılmasını ister.
- Sekme sırası kaynaktan, görsel sıra sunum katmanından gelir; ayrışma tersine düşen
  çiftler üretir ve düzeltme sunum katmanında değil kaynakta yapılır.
- Artı `tabindex` değerleri bütün sıranın önüne geçerek ters çift sayısını tek başına
  yükseltir.
- Odak kuşatması üç kurulumda farklı sonuç verir: kuşatmasız kurulum arka plana kaçak
  bırakır, çıkışsız kurulum klavye tuzağıdır, doğru kurulumda kapanışta odak açan denetime
  döner.
- Odak göstergesi biçim olarak değiştirilebilir ama kaldırılamaz; ayrıca yeterli karşıtlık
  taşımalı ve başka bir ögeyle örtülmemelidir.
- Tek karakterli kısayollar kapatılabilir, yeniden atanabilir ya da yalnızca bileşen
  odaktayken etkin olmalıdır.

#### Sonraki Adım

Bu ders odağın nereye gittiğini hesapladı ama odak oraya vardığında kullanıcının **ne
öğrendiğini** sormadı. Pencere açıldığında yeni bir bağlamın açıldığı nasıl bildirilir?
Süzgeç uygulandığında tabloda kaç satır kaldığı, ekranı görmeyen bir kullanıcıya nasıl
ulaşır? Sonraki ders duyuru düzeneklerini ele alır: canlı bölgelerin kibar ve atılgan
ayrımını, duyuruların kuyruklanmasını ve yer imleriyle başlıkların kurduğu gezinme
düzlemlerini.

### Ekran Okuyucu Deneyimi

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/ekran-okuyucu-deneyimi

Belgenin gezinme düzlemleri ve düzlem kusurlarının denetimi, duyurunun neyi taşıdığı, canlı bölgelerin kibar ve atılgan önceliği ile duyuru kuyruğunun davranışı.

Önceki ders odağın nereye gittiğini hesapladı. Odak oraya vardığında kullanıcının ne
öğrendiği ayrı bir sorudur ve yanıtı iki kanaldan gelir: ögenin kendisinden gelen bilgi ve
ögeden bağımsız olarak değişen içeriğin bildirimi.

Bu dersin konusu iki kanaldır. Ekran okuyucuların iç işleyişi ve söyledikleri sözcükler
araca göre değişir; burada anlatılan, **ağaçta tanımlı olan bilginin ne olduğu** ve
duyuruların hangi öncelik kurallarına göre sıraya girdiğidir. Bu kurallar araca değil
tanıma bağlıdır.

#### Sayfa Doğrusal Okunmaz

Ekran okuyucu kullanıcısı bir sayfayı baştan sona dinlemez; bu, uzun bir belgede
kullanılamaz bir yol olurdu. Kullanılan yol, belgenin **gezinme düzlemleri** arasında
atlamaktır: yer imlerinden bölüme, başlıklardan konuya, bağlantı listesinden hedefe,
denetim listesinden forma.

Her düzlem belgeden türetilir ve türetme kusurları düzlemi kullanılmaz kılar. Aşağıdaki
betik ölçüm listesi sayfasından üç düzlemi çıkarıyor ve her birini kendi ölçütüyle
denetliyor.

```js
// duzlem.mjs — bir belgeden uc gezinme duzleminin turetilmesi ve kusur denetimi

const YER_IMI = { header: "banner", nav: "navigation", main: "main", aside: "complementary",
  footer: "contentinfo", section: "region", form: "form" };

// ust duzey: belgenin dogrudan cocugu mu
const BELGE = [
  { oge: "header", ustDuzey: true },
  { oge: "nav", ustDuzey: false, etiket: "Ağ genelinde gezinme" },
  { oge: "main", ustDuzey: true },
  { oge: "h1", metin: "Kuzey Yamaç Ölçüm İstasyonu" },
  { oge: "form", ustDuzey: false, etiket: "Süzgeç" },
  { oge: "a", metin: "Ayrıntı", hedef: "/olcum/T-01" },
  { oge: "h3", metin: "Günlük Ölçümler" },
  { oge: "a", metin: "Ayrıntı", hedef: "/olcum/T-02" },
  { oge: "a", metin: "buraya tıklayın", hedef: "/yontem.html" },
  { oge: "h2", metin: "Uyarılar" },
  { oge: "nav", ustDuzey: false },
  { oge: "a", metin: "Ölçüm yöntemi belgesi", hedef: "/yontem.html" },
  { oge: "footer", ustDuzey: true },
];

const BELIRSIZ = new Set(["buraya tıklayın", "tıklayın", "devamı", "daha fazla", "bağlantı"]);

function yerImleri(belge) {
  const liste = [];
  for (const d of belge) {
    let rol = YER_IMI[d.oge];
    if (!rol) continue;
    if ((d.oge === "header" || d.oge === "footer") && !d.ustDuzey) continue;
    if ((d.oge === "section" || d.oge === "form") && !d.etiket) continue;
    liste.push({ rol, etiket: d.etiket ?? null });
  }
  return liste;
}

const basliklar = (belge) =>
  belge.filter((d) => /^h[1-6]$/.test(d.oge)).map((d) => ({ duzey: Number(d.oge[1]), metin: d.metin }));

const baglantilar = (belge) => belge.filter((d) => d.oge === "a");

console.log("yer imi duzlemi:");
for (const y of yerImleri(BELGE)) console.log(`  ${y.rol}${y.etiket ? ': "' + y.etiket + '"' : ""}`);

console.log("\nbaslik duzlemi:");
for (const b of basliklar(BELGE)) console.log(`  ${"  ".repeat(b.duzey - 1)}h${b.duzey} ${b.metin}`);

console.log("\nbaglanti duzlemi:");
for (const b of baglantilar(BELGE)) console.log(`  "${b.metin}" -> ${b.hedef}`);

console.log("\nkusur denetimi:");
const bulgular = [];

// 1. Ayni turden adsiz yer imi birden fazla
const sayim = {};
for (const y of yerImleri(BELGE)) sayim[y.rol] = (sayim[y.rol] ?? 0) + 1;
for (const y of yerImleri(BELGE)) {
  if (sayim[y.rol] > 1 && !y.etiket) bulgular.push(`adsiz ${y.rol} yer imi, ayni rolden ${sayim[y.rol]} tane var`);
}

// 2. Baslik duzeyi atlamasi
let onceki = 0;
for (const b of basliklar(BELGE)) {
  if (onceki && b.duzey > onceki + 1) bulgular.push(`baslik duzeyi atlandi: h${onceki} -> h${b.duzey} ("${b.metin}")`);
  onceki = b.duzey;
}

// 3. Ayni metin farkli hedef / farkli metin ayni hedef / belirsiz metin
const metinler = {};
const hedefler = {};
for (const b of baglantilar(BELGE)) {
  (metinler[b.metin] ??= new Set()).add(b.hedef);
  (hedefler[b.hedef] ??= new Set()).add(b.metin);
  if (BELIRSIZ.has(b.metin.toLocaleLowerCase("tr"))) bulgular.push(`baglamdan kopuk baglanti metni: "${b.metin}"`);
}
for (const [m, h] of Object.entries(metinler)) {
  if (h.size > 1) bulgular.push(`"${m}" metni ${h.size} farkli hedefe gidiyor`);
}
for (const [h, m] of Object.entries(hedefler)) {
  if (m.size > 1) bulgular.push(`${h} hedefine ${m.size} farkli metinle baglaniliyor`);
}

for (const b of bulgular) console.log("  - " + b);
console.log(`  toplam ${bulgular.length} bulgu`);
```

```
yer imi duzlemi:
  banner
  navigation: "Ağ genelinde gezinme"
  main
  form: "Süzgeç"
  navigation
  contentinfo

baslik duzlemi:
  h1 Kuzey Yamaç Ölçüm İstasyonu
      h3 Günlük Ölçümler
    h2 Uyarılar

baglanti duzlemi:
  "Ayrıntı" -> /olcum/T-01
  "Ayrıntı" -> /olcum/T-02
  "buraya tıklayın" -> /yontem.html
  "Ölçüm yöntemi belgesi" -> /yontem.html

kusur denetimi:
  - adsiz navigation yer imi, ayni rolden 2 tane var
  - baslik duzeyi atlandi: h1 -> h3 ("Günlük Ölçümler")
  - baglamdan kopuk baglanti metni: "buraya tıklayın"
  - "Ayrıntı" metni 2 farkli hedefe gidiyor
  - /yontem.html hedefine 2 farkli metinle baglaniliyor
  toplam 5 bulgu
```

Beş bulgunun her biri ayrı bir düzlemi bozuyor.

**Adsız ikinci gezinme bloğu** yer imi listesinde ayırt edilemez: kullanıcı iki
"navigation" girdisi görür ve hangisinin sayfa içi gezinme olduğunu ancak ikisini de
açarak öğrenir. Çözüm ada bakar, yapıya değil.

**Başlık düzeyi atlaması** ana hattı bozar. Çıktının girintileri sorunu gösteriyor: h1'den
sonra gelen h3, ardından gelen h2'nin altına düşmesi gereken bir konuyu onun üstüne
koyuyor. Başlık düzlemi belgenin içindekiler dizinidir; atlanan düzey, dizinde var olmayan
bir kırılma üretir.

**Bağlantı düzleminde üç kusur** birlikte görünüyor. Bağlamdan kopuk metin, listede tek
başına okunduğunda hiçbir şey söylemez. Aynı metnin iki farklı hedefe gitmesi, listede
ayırt edilemeyen iki giriş üretir — ölçüm tablosundaki her satırın "Ayrıntı" bağlantısı
tam bu durumdur ve çözümü bağlantı metnine ölçüm kodunu eklemektir. Aynı hedefe iki farklı
metinle bağlanmak ise ters yönde bir kusurdur: kullanıcı iki ayrı sayfa olduğunu sanır.

#### Duyuru Neyi Taşır

Odak bir ögeye vardığında kullanıcıya iletilen bilgi, o ögenin ağaçtaki dört alanından
gelir: **rolü**, **erişilebilir adı**, **değeri** ve **durumları**; buna bir de
`aria-describedby` ile bağlanan **ek açıklama** eklenir. İletim sırası ve sözcükler araca
göre değişir, alanların içeriği değişmez.

Buradan iki pratik kural çıkar. Birincisi, ada rol adı yazılmaz: adı "Süz düğmesi" olan bir
düğme, rolüyle birlikte iki kez düğme diye anılır. İkincisi, ek açıklama ada eklenmez,
adın **ardından** gelir; uzun yardım metni ada değil açıklamaya yazılır, yoksa denetimin
adı bir paragrafa dönüşür.

#### Canlı Bölgeler ve Öncelik

Sayfa yenilenmeden değişen içerik ayrı bir sorundur: kullanıcı orada değilse değişimi
öğrenemez. **Canlı bölge (live region)**, içeriği değiştiğinde bildirilen bölümdür ve Web
Temelleri ve HTML kursunda hata özeti için kurulmuştu.

Bölgenin iki önceliği vardır. **Kibar (polite)** öncelik, bildirimi sıraya koyar ve
kullanıcının o anki okumasını bölmez; durum iletileri buraya girer. **Atılgan (assertive)**
öncelik, bekleyenleri geçersiz kılarak öne geçer; yalnızca kullanıcının işini durduran
bildirimler —veri kaybı, gönderim hatası— buraya girer. `role="status"` bildirimi kibar,
`role="alert"` bildirimi atılgan bir bölge kurar.

Aşağıdaki betik bu önceliği bir kuyruk olarak modelliyor. Model, iletimin ne kadar
süreceğini değil, tanımdaki önceliğin sıraya ne yaptığını gösterir.

```js
// duyuru.mjs — canli bolge duyurularinin kibar/atilgan onceligiyle kuyruklanma modeli

const BOLGE = {
  "suzgec-durumu": { rol: "status", oncelik: "kibar" },
  "gonderim-durumu": { rol: "status", oncelik: "kibar" },
  "hata-ozeti": { rol: "alert", oncelik: "atilgan" },
};

const SURE = 2; // bir duyurunun kapladigi an sayisi

function calistir(baslik, olaylar, sonAn) {
  console.log(baslik);
  console.log("  an  " + "gelen".padEnd(38) + "duyurulan".padEnd(24) + "kuyruk");
  let kuyruk = [];          // bekleyen kibar duyurular
  let etkin = null;         // { metin, kalan }
  let degistirilen = 0, atilan = 0;

  for (let an = 0; an <= sonAn; an++) {
    const gelen = olaylar.filter((o) => o.an === an);
    let gelenMetin = [];
    for (const o of gelen) {
      const b = BOLGE[o.bolge];
      gelenMetin.push(`${o.bolge}: ${o.metin}`);
      if (b.oncelik === "atilgan") {
        atilan += kuyruk.length + (etkin ? 1 : 0);
        kuyruk = [];
        etkin = { metin: o.metin, kalan: SURE };      // suren duyuruyu boler
      } else {
        const i = kuyruk.findIndex((k) => k.bolge === o.bolge);
        if (i >= 0) { kuyruk[i] = { ...o }; degistirilen++; }   // bolgenin icerigi yenilendi
        else kuyruk.push({ ...o });
      }
    }
    if (!etkin && kuyruk.length > 0) etkin = { metin: kuyruk.shift().metin, kalan: SURE };

    const duyurulan = etkin && etkin.kalan === SURE ? etkin.metin : etkin ? "(suruyor)" : "—";
    console.log(
      `  ${String(an).padStart(2)}  ${(gelenMetin.join(" | ") || "—").padEnd(38)}` +
        `${duyurulan.padEnd(24)}${kuyruk.length === 0 ? "—" : kuyruk.map((k) => k.metin).join(" | ")}`,
    );
    if (etkin && --etkin.kalan === 0) etkin = null;
  }
  console.log(`  yenilenen bekleyen duyuru: ${degistirilen}, atilgan duyurunun attigi: ${atilan}`);
}

const HER_TUSTA = [
  { an: 0, bolge: "suzgec-durumu", metin: "12 ölçüm listeleniyor" },
  { an: 1, bolge: "suzgec-durumu", metin: "5 ölçüm listeleniyor" },
  { an: 2, bolge: "suzgec-durumu", metin: "3 ölçüm listeleniyor" },
  { an: 3, bolge: "gonderim-durumu", metin: "Kayıt gönderiliyor" },
  { an: 4, bolge: "hata-ozeti", metin: "Kayıt gönderilemedi" },
  { an: 7, bolge: "gonderim-durumu", metin: "Kayıt gönderildi" },
];

const GECIKMELI = [
  { an: 2, bolge: "suzgec-durumu", metin: "3 ölçüm listeleniyor" },
  { an: 3, bolge: "gonderim-durumu", metin: "Kayıt gönderiliyor" },
  { an: 6, bolge: "hata-ozeti", metin: "Kayıt gönderilemedi" },
  { an: 9, bolge: "gonderim-durumu", metin: "Kayıt gönderildi" },
];

calistir("her tus vurusunda duyuru:", HER_TUSTA, 9);
console.log("");
calistir("gecikmeli tetiklemeyle tek duyuru:", GECIKMELI, 11);
```

```
her tus vurusunda duyuru:
  an  gelen                                 duyurulan               kuyruk
   0  suzgec-durumu: 12 ölçüm listeleniyor  12 ölçüm listeleniyor   —
   1  suzgec-durumu: 5 ölçüm listeleniyor   (suruyor)               5 ölçüm listeleniyor
   2  suzgec-durumu: 3 ölçüm listeleniyor   3 ölçüm listeleniyor    —
   3  gonderim-durumu: Kayıt gönderiliyor   (suruyor)               Kayıt gönderiliyor
   4  hata-ozeti: Kayıt gönderilemedi       Kayıt gönderilemedi     —
   5  —                                     (suruyor)               —
   6  —                                     —                       —
   7  gonderim-durumu: Kayıt gönderildi     Kayıt gönderildi        —
   8  —                                     (suruyor)               —
   9  —                                     —                       —
  yenilenen bekleyen duyuru: 1, atilgan duyurunun attigi: 1

gecikmeli tetiklemeyle tek duyuru:
  an  gelen                                 duyurulan               kuyruk
   0  —                                     —                       —
   1  —                                     —                       —
   2  suzgec-durumu: 3 ölçüm listeleniyor   3 ölçüm listeleniyor    —
   3  gonderim-durumu: Kayıt gönderiliyor   (suruyor)               Kayıt gönderiliyor
   4  —                                     Kayıt gönderiliyor      —
   5  —                                     (suruyor)               —
   6  hata-ozeti: Kayıt gönderilemedi       Kayıt gönderilemedi     —
   7  —                                     (suruyor)               —
   8  —                                     —                       —
   9  gonderim-durumu: Kayıt gönderildi     Kayıt gönderildi        —
  10  —                                     (suruyor)               —
  11  —                                     —                       —
  yenilenen bekleyen duyuru: 0, atilgan duyurunun attigi: 0
```

İlk çalıştırma iki kayıp üretiyor. Süzgeç alanına yazılan her karakter bir duyuru
tetiklediği için bekleyen sayı duyurusu bir kez yenileniyor: kullanıcı "12" duyurusunu
dinlerken sayı önce beşe, sonra üçe düşüyor ve duyurulan ilk sayı zaten geçersiz. İkinci
kayıp daha ağır: atılgan hata bildirimi geldiğinde bekleyen "Kayıt gönderiliyor" duyurusu
atılıyor. Kullanıcı, gönderimin başladığını hiç duymadan başarısız olduğunu duyuyor.

İkinci çalıştırma iki düzeltmeyi birlikte uyguluyor. Süzgeç duyurusu her tuşta değil,
yazma durduktan sonra bir kez tetikleniyor; bu, Tarayıcı ve Web Platformu kursundaki
gecikmeli tetiklemenin erişilebilirlik karşılığıdır. Hata bildirimi de kuyruk boşaldıktan
sonraya düşüyor. Kayıp sayısı sıfıra iniyor.

Buradan üç kural çıkar. Kibar bölgeye yazılan metin **son durumu** bildirmelidir, ara
durumları değil. Atılgan öncelik bekleyenleri attığı için yalnızca kullanıcının işini
durduran bildirimlerde kullanılır. Bir bölge sık güncelleniyorsa duyuru güncellemeden değil,
**durulmadan** sonra yazılır.

#### Bölgenin Kurulumu

Canlı bölgenin belgede **önceden** bulunması gerekir; sonradan eklenen bir bölgenin
içeriği bildirilmeyebilir. Boş bir kap sayfa kurulurken konur ve içeriği sonradan
doldurulur.

Bölge içeriği kısa ve durum bildiren bir cümle olmalıdır. Tabloyu canlı bölgeye almak —
yani tablonun kendisini `aria-live` bildirimiyle işaretlemek— her satır değişiminde bütün
içeriğin bildirilmesine yol açar. Doğru kurulum, tablonun dışında bir durum bölgesi
tutmak ve oraya "3 ölçüm listeleniyor" yazmaktır. Tablonun kendisi sessiz kalır; kullanıcı
sayıyı duyduktan sonra tabloya kendi gider.

4.1.3 ölçütü bunu bir gereklilik hâline getirir: odak değiştirmeden sunulan durum iletileri
program yoluyla belirlenebilir olmalı ve odak taşımadan bildirilmelidir. Ölçütün iki yönü
vardır — ileti bildirilmelidir, ama bildirim için **odak çalınmamalıdır**. Süzgeç
sonucundan sonra odağı tabloya taşıyan kod, ölçütü ters yönden kaldırır.

#### Özet

- Ekran okuyucu kullanıcısı belgeyi doğrusal okumaz; yer imi, başlık ve bağlantı
  düzlemleri arasında atlar ve her düzlem belgeden türetilir.
- Düzlem kusurları sayılabilir: adsız yinelenen yer imi, atlanan başlık düzeyi, bağlamdan
  kopuk bağlantı metni, aynı metinle farklı hedef ve farklı metinle aynı hedef.
- Odak bir ögeye vardığında iletilen bilgi rol, ad, değer, durum ve ek açıklamadan gelir;
  ada rol adı yazılmaz, uzun metin ada değil açıklamaya yazılır.
- Kibar öncelik duyuruyu sıraya koyar, atılgan öncelik bekleyenleri atarak öne geçer;
  atılgan bildirim yalnızca işi durduran durumlar içindir.
- Sık güncellenen bir bölgede duyuru her değişimde değil durulmadan sonra yazılır ve son
  durumu bildirir.
- Canlı bölge belgede önceden bulunmalı, içeriği kısa olmalı ve bildirim odağı
  taşımamalıdır.

#### Sonraki Adım

Bu ders bilginin görsel olmayan kanallardan nasıl aktarıldığını kurdu. Aynı bilginin görsel
kanaldaki karşılığı ölçülebilir bir eşiğe bağlıdır: ekranda duran metnin ve bileşen
sınırlarının çevresinden ayırt edilebilmesi. Sonraki ders karşıtlığı hesaplanabilir bir
nicelik olarak ele alır — bağıl parlaklıktan kontrast oranının türetilmesini, metin ve
metin dışı ögelerin farklı eşiklerini ve süzgeç panelinin bütün durumlarının bu eşiklere
karşı denetlenmesini.

### Renk ve Karşıtlık

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/renk-ve-karsitlik

Bileşen durumlarının kontrast eşiklerine karşı toplu denetimi, odak halkasının iki komşuya birden bakan sınavı, rengin tek kanal olamaması ve metin ölçeklemesinin getirdiği eşikler.

Önceki ders bilginin görsel olmayan kanallardan nasıl aktarıldığını kurdu. Görsel kanalda
karşılığı olan soru şudur: ekranda duran bir metin ya da bir bileşen sınırı, çevresinden
ayırt edilebiliyor mu? Bu sorunun yanıtı ölçülebilir bir niceliğe bağlıdır ve bu, kalite
ekseninde sayılabilir ölçütlerin en doğrudan olanıdır.

Kontrast oranının tanımı, formülü ve sınırları Arayüz Tasarımının Temelleri kursundaki
Karşıtlık ve Erişilebilirlik dersinde kuruldu: iki rengin bağıl parlaklıklarının 0.05
kaydırmalı oranı, 1:1 ile 21:1 arasında değişir, tonu görmez ve doğrusal bir algı ölçüsü
değildir. Bu ders aynı formülü gerçekleştirim tarafında kullanır ve tasarım denetiminin
atladığı iki yeri ele alır: bileşenlerin **durumları** ve odak göstergesinin **iki
komşusu**.

#### Durumlar da Denetlenir

Bir renk paleti denetlendiğinde denetlenen şey çoğunlukla varsayılan durumdur: metin
rengi, zemin rengi, düğme dolgusu. Oysa bir bileşen ekranda tek bir renk çiftiyle durmaz.
Düğmenin üzerine gelindiğinde dolgu değişir, devre dışı kaldığında hem dolgu hem metin
değişir, odaklandığında etrafına bir halka gelir. Her durum yeni bir renk çiftidir ve her
çift ayrı bir eşleşmedir.

Aşağıdaki betik ölçüm listesi arayüzünün bileşen durumlarını denetliyor.

```js
// karsitlik.mjs — bilesen durumlarinin kontrast denetimi ve odak halkasinin iki yanli sinavi

const cz = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
function kanal(v) {
  const s = v / 255;
  return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
}
const parlaklik = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b);
function kontrast(a, b) {
  const [x, y] = [parlaklik(cz(a)), parlaklik(cz(b))].sort((p, q) => q - p);
  return (x + 0.05) / (y + 0.05);
}

const RENK = {
  yuzey: "#ffffff",
  yuzeyIkincil: "#f2f4f7",
  metin: "#16202a",
  metinIkincil: "#5b6b7a",
  metinDevreDisi: "#9aa7b2",
  eylem: "#1f5fa8",
  eylemUzeri: "#ffffff",
  eylemVurgu: "#17497f",
  uyariZemin: "#fdf0c8",
  uyariMetin: "#6b4e00",
  kenarlik: "#c3ccd6",
  siralamaOku: "#b4bcc4",
};

const PT_PX = 96 / 72;
const buyukMu = (px, ag) => px >= 18 * PT_PX || (ag >= 700 && px >= 14 * PT_PX);

// [ad, on plan, zemin, tur, px, agirlik]
const DURUM = [
  ["dugme varsayilan", "eylemUzeri", "eylem", "metin", 16, 600],
  ["dugme uzerine gelme", "eylemUzeri", "eylemVurgu", "metin", 16, 600],
  ["dugme devre disi", "metinDevreDisi", "yuzeyIkincil", "metin", 16, 600],
  ["suzgec etiketi", "metin", "yuzey", "metin", 16, 400],
  ["suzgec ipucu", "metinIkincil", "yuzey", "metin", 13, 400],
  ["tablo basligi", "metin", "yuzeyIkincil", "metin", 14, 700],
  ["satir metni", "metin", "yuzey", "metin", 16, 400],
  ["uyari rozeti", "uyariMetin", "uyariZemin", "metin", 13, 600],
  ["alan kenarligi", "kenarlik", "yuzey", "metin-disi", 0, 0],
  ["siralama gostergesi", "siralamaOku", "yuzeyIkincil", "metin-disi", 0, 0],
];

console.log("durum                on plan   zemin     esik  olculen  sonuc");
let kalan = 0;
for (const [ad, on, zemin, tur, px, ag] of DURUM) {
  const esik = tur === "metin-disi" ? 3.0 : buyukMu(px, ag) ? 3.0 : 4.5;
  const k = kontrast(RENK[on], RENK[zemin]);
  if (k < esik) kalan++;
  console.log(
    ad.padEnd(20) + " " + RENK[on].padEnd(10) + RENK[zemin].padEnd(10) +
      esik.toFixed(1).padStart(4) + k.toFixed(2).padStart(9) + "  " + (k >= esik ? "gecti" : "KALDI"),
  );
}
console.log(`${DURUM.length} eslesmenin ${kalan} tanesi esigi gecmiyor`);

// Odak halkasi iki komsuya birden bakar: ic dolgu ve dis yuzey
console.log("\ntek renkli odak halkasi, iki yanli sinav (esik 3.0):");
console.log("halka      dolguya karsi  yuzeye karsi  sonuc");
for (const halka of ["#2a7de1", "#16202a", "#ffffff", "#000000"]) {
  const a = kontrast(halka, RENK.eylem);
  const b = kontrast(halka, RENK.yuzey);
  console.log(
    halka.padEnd(11) + a.toFixed(2).padStart(13) + b.toFixed(2).padStart(14) +
      "  " + (a >= 3 && b >= 3 ? "gecti" : "KALDI"),
  );
}

// Hangi griler iki yani birden gecer
const uygunGriler = [];
for (let v = 0; v <= 255; v++) {
  const g = "#" + v.toString(16).padStart(2, "0").repeat(3);
  if (kontrast(g, RENK.eylem) >= 3 && kontrast(g, RENK.yuzey) >= 3) uygunGriler.push(v);
}
console.log(`\niki yani birden gecen gri sayisi: ${uygunGriler.length} / 256`);
console.log(`  izin verilen aralik: ${uygunGriler[0]}–${uygunGriler[uygunGriler.length - 1]}`);

// Iki bantli halka: her bant kendi komsusuna bakar
const icBant = "#ffffff", disBant = "#16202a";
console.log("\niki bantli halka:");
console.log(`  ic bant ${icBant} — dolguya (${RENK.eylem}) karsi ${kontrast(icBant, RENK.eylem).toFixed(2)}`);
console.log(`  dis bant ${disBant} — yuzeye (${RENK.yuzey}) karsi ${kontrast(disBant, RENK.yuzey).toFixed(2)}`);
console.log(`  bantlar arasi ${kontrast(icBant, disBant).toFixed(2)}`);

// 1.4.1: bilgi yalniz renkle mi tasiniyor
console.log("\nolcum niteligi gostergeleri (1.4.1):");
for (const [ad, kanallar] of [
  ["esik asimi", ["renk"]],
  ["aygit hatasi", ["renk", "simge", "metin"]],
  ["elle girilmis", ["renk"]],
  ["dogrulanmis", ["renk", "metin"]],
]) {
  const tekKanal = kanallar.length === 1 && kanallar[0] === "renk";
  console.log(`  ${ad.padEnd(14)} kanallar: ${kanallar.join(", ").padEnd(22)} ${tekKanal ? "KALDI" : "gecti"}`);
}
```

```
durum                on plan   zemin     esik  olculen  sonuc
dugme varsayilan     #ffffff   #1f5fa8    4.5     6.44  gecti
dugme uzerine gelme  #ffffff   #17497f    4.5     9.15  gecti
dugme devre disi     #9aa7b2   #f2f4f7    4.5     2.23  KALDI
suzgec etiketi       #16202a   #ffffff    4.5    16.48  gecti
suzgec ipucu         #5b6b7a   #ffffff    4.5     5.48  gecti
tablo basligi        #16202a   #f2f4f7    4.5    14.96  gecti
satir metni          #16202a   #ffffff    4.5    16.48  gecti
uyari rozeti         #6b4e00   #fdf0c8    4.5     6.80  gecti
alan kenarligi       #c3ccd6   #ffffff    3.0     1.62  KALDI
siralama gostergesi  #b4bcc4   #f2f4f7    3.0     1.74  KALDI
10 eslesmenin 3 tanesi esigi gecmiyor

tek renkli odak halkasi, iki yanli sinav (esik 3.0):
halka      dolguya karsi  yuzeye karsi  sonuc
#2a7de1             1.57          4.10  KALDI
#16202a             2.56         16.48  KALDI
#ffffff             6.44          1.00  KALDI
#000000             3.26         21.00  gecti

iki yani birden gecen gri sayisi: 14 / 256
  izin verilen aralik: 0–13

iki bantli halka:
  ic bant #ffffff — dolguya (#1f5fa8) karsi 6.44
  dis bant #16202a — yuzeye (#ffffff) karsi 16.48
  bantlar arasi 16.48

olcum niteligi gostergeleri (1.4.1):
  esik asimi     kanallar: renk                   KALDI
  aygit hatasi   kanallar: renk, simge, metin     gecti
  elle girilmis  kanallar: renk                   KALDI
  dogrulanmis    kanallar: renk, metin            gecti
```

Üzerine gelme durumu geçiyor, çünkü dolgu koyulaştığında beyaz metnin karşıtlığı artıyor.
Ters yönde bir vurgu —dolguyu açan bir üzerine gelme durumu— aynı metinle eşiği kaçırırdı;
durum listesinin denetlenmesinin nedeni budur.

#### Üç Başarısızlık

**Devre dışı düğme** 2.23 oranıyla kalıyor. Ölçüt etkin olmayan bileşenleri kapsam dışında
bırakır, dolayısıyla bu satır bir uygunluk ihlali değildir. Ancak Klavye Erişimi dersinde
kurulan kural burada ikinci bir maliyet çıkarır: devre dışı bir denetim odak da almaz.
Kullanıcı düğmeyi ne okuyabilir ne de üzerine gidebilir; neden çalışmadığını öğrenmenin
yolu kalmaz. Ölçüt izin verse de karar, düğmeyi etkin bırakıp basıldığında nedeni
bildirmektir.

**Alan kenarlığı** 1.62 oranıyla kalıyor ve bu bir 1.4.11 ihlalidir. Süzgeç panelindeki arama
alanının nerede başlayıp bittiğini gösteren tek şey o kenarlıktır: alan boşken içinde metin
yoktur ve zemini yüzeyle aynıdır. Kenarlık görünmüyorsa alan da görünmüyordur.

**Sıralama göstergesi** 1.74 oranıyla kalıyor. Sütunun hangi yönde sıralandığını bildiren
ok, metin dışı bir görsel bilgidir ve durumu tanımlamak için gereklidir. Önceki derste bu
bilgi `aria-sort` bildirimiyle ağaca yazılmıştı; ekranda gören kullanıcı için karşılığı
okun kendisidir ve ikisi aynı bilginin iki kanalıdır.

#### Odak Halkasının İki Komşusu

Odak göstergesi başka her şeyden farklı bir sorun çıkarır: bir ögenin **etrafına** çizilir
ve iki farklı rengin sınırında durur. İçeride bileşenin dolgusu, dışarıda sayfanın yüzeyi
vardır. Ölçüt komşu renklere karşı karşıtlık istediği için sınav iki yanlıdır.

Çıktının ikinci bölümü dört aday halkayı bu sınavdan geçiriyor. Bileşenin kendi ana
rengiyle akraba bir mavi halka dolguya karşı 1.57 veriyor — düğmenin üstünde görünmez.
Koyu bir halka yüzeye karşı 16.48 veriyor ama dolguya karşı 2.56 ile eşiğin altında
kalıyor. Beyaz bir halka bunun tersi. Yalnızca saf siyah iki yanı birden geçiyor.

Gri taraması sınırın ne kadar dar olduğunu gösteriyor: 256 gri tonundan yalnızca 14'ü, yani
0 ile 13 arasındakiler, hem düğme dolgusuna hem beyaz yüzeye karşı 3:1 sağlıyor. Bu, tek
renkli bir odak halkasının pratikte siyaha yakın olmak zorunda olduğu anlamına gelir — ve
palette birden çok dolgu rengi varsa aralık daha da daralır, çoğunlukla boşalır.

Çözüm halkayı tek renkli bırakmamaktır. İki bantlı halkada iç bant bileşenin dolgusuna,
dış bant sayfanın yüzeyine bakar; her bant yalnızca kendi komşusuna karşı eşiği sağlamak
zorundadır. Çıktının son bölümü bunu veriyor: iç bant dolguya karşı 6.44, dış bant yüzeye
karşı 16.48. Halka artık her iki tarafta da görünür ve bileşenin dolgu rengi değiştiğinde
yalnızca iç bandın yeniden denetlenmesi yeter.

#### Renk Tek Kanal Olamaz

Son bölüm 1.4.1 ölçütünü uyguluyor ve kontrast hesabından bağımsızdır: bir bilgi rengin
tek başına taşıdığı bir ayrımla veriliyorsa, o bilgi rengi ayırt edemeyen kullanıcıya
ulaşmaz. Ölçüm tablosunda dört nitelik göstergesinden ikisi yalnızca satır rengiyle
bildiriliyor.

İkinci kanal eklemenin üç yolu vardır: metin (rozetin içine "eşik aşımı" yazmak), biçim
(satır başına bir işaret koymak) ve konum (uyarılı ölçümleri ayrı bir bölümde toplamak).
Metin en güvenilir olanıdır, çünkü aynı zamanda erişilebilirlik ağacına da geçer — ikinci
görsel kanal ile ağaçtaki bildirim tek hamlede sağlanır.

Buradan bir denetim kuralı çıkar: renkli her göstergenin yanında, o göstergenin taşıdığı
bilgiyi renksiz bir belgede de aktarabilecek bir kanal bulunmalıdır. Sınama basittir —
arayüzün ekran görüntüsü griye çevrildiğinde hangi bilgiler kayboluyor?

#### Metnin Ölçekleneceğini Varsaymak

İki ölçüt daha ölçülebilir ve kontrastla aynı denetime girer. 1.4.4 ölçütü metnin yüzde
200'e kadar büyütülebilmesini ve bu sırada içerik ya da işlev kaybı olmamasını ister.
1.4.12 ölçütü ise kullanıcının satır yüksekliğini, paragraf aralığını, harf ve sözcük
aralığını belirli değerlere çıkarabilmesini ve metnin kırpılmamasını ister.

İkisinin de gerçekleştirim tarafındaki karşılığı aynıdır: metin taşıyan kapların yüksekliği
sabitlenmez. Sabit yükseklikli bir rozet, iki katına çıkan yazı boyutunda metni kırpar;
taşmayı gizleyen bir bildirim, kırpılan metni erişilemez kılar. Düzen Sistemleri ve Duyarlı
Tasarım kursundaki içerik tabanlı boyutlandırma bu iki ölçütün doğrudan karşılığıdır.

#### Özet

- Kontrast denetimi varsayılan durumla sınırlı kalamaz; bir bileşenin her durumu yeni bir
  renk çiftidir ve ayrı bir eşleşme olarak denetlenir.
- Devre dışı bileşenler kontrast ölçütünden muaftır, ancak odak da alamadıkları için
  muafiyet çoğunlukla yanlış kararın örtüsüdür.
- Alan kenarlığı ve sıralama göstergesi gibi metin dışı görsel bilgiler 3:1 eşiğine tabidir;
  bunlar bileşenin varlığını ya da durumunu tanımlayan tek işarettir.
- Odak halkası iki komşu rengin sınırında durur; tek renkli bir halkanın iki yanı birden
  geçmesi çok dar bir aralıkta mümkündür, iki bantlı halka her bandı kendi komşusuna karşı
  sınar.
- Renk tek taşıyıcı olamaz; ikinci kanal metin, biçim ya da konumdur ve metin seçildiğinde
  aynı hamlede erişilebilirlik ağacına da geçer.
- Metnin iki katına büyütülebilmesi ve aralıklarının artırılabilmesi ölçülebilir
  gerekliliklerdir; karşılıkları sabit yükseklikli kaplardan kaçınmaktır.

#### Sonraki Adım

Bu dersteki denetimlerin tamamı bir betikle çalıştı: renk çiftleri girdi, geçen ve kalan
eşleşmeler çıktı. Aynı şey önceki derslerin bir bölümü için de geçerliydi — erişilebilir ad
hesabı, sekme sırası ve düzlem kusurları da hesaplanabilirdi. Bu, kaçınılmaz bir soruyu
getirir: erişilebilirliğin ne kadarı otomatik denetlenebilir? Sonraki ders bu sorunun
sayısal yanıtını verir, otomatik denetleyicinin yapı olarak göremediği ölçüt sınıflarını
adlandırır ve elle doğrulamanın hangi sırayla yapılacağını kurar.

### Erişilebilirlik Testi

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/erisilebilirlik-testi

Otomatik denetleyicinin karar verebildiği kural sınıfları, ölçütlerin otomatikleştirilebilirlik dağılımı, gerçek bulgularda kaçan oranı ve elle doğrulamanın maliyete göre sıralanmış düzeni.

Önceki derslerin denetimlerinin çoğu bir betikle çalıştı: erişilebilir ad hesabı, sekme
sırası, düzlem kusurları ve kontrast oranları girdi verilip çıktı alınan hesaplardı. Bu,
kaçınılmaz bir soruyu getirir. Erişilebilirliğin ne kadarı bir programa yaptırılabilir ve
geriye ne kalır?

Sorunun yanıtı iki sayıdır ve ikisi de bu derste hesaplanacak: ölçütlerin ne kadarının
makinece karar verilebildiği ve bir arayüzdeki gerçek kusurların ne kadarının otomatik
denetimle bulunduğu. İki sayı birbirinden farklıdır ve ikincisi daha önemlidir.

#### Otomatik Denetleyici Ne Yapar

Bir **otomatik erişilebilirlik denetleyicisi**, çizilmiş belgeyi ve ondan türetilen
erişilebilirlik ağacını okur, hesaplanmış biçem değerlerine bakar ve bir kural kümesini
uygular. Uygulayabildiği kurallar dört sınıfa girer.

**Varlık kuralları.** Bir bildirimin bulunup bulunmadığına bakar: görselde alternatif metin
özniteliği var mı, etkileşimli ögenin erişilebilir adı boş mu, belgede dil bildirimi var mı.

**Biçim geçerliliği kuralları.** Yazılan bildirimin tanımlı olup olmadığına bakar: rol adı
tanımlı bir rol mü, kullanılan durum o rolde geçerli mi, `aria-labelledby` var olan bir
kimliğe mi başvuruyor, aynı kimlik belgede iki kez mi geçiyor.

**Hesaplanabilir değer kuralları.** Sayısal bir eşiği doğrudan hesaplar: düz renkli iki
katman arasındaki kontrast oranı, `tabindex` değerlerinden türeyen sekme sırası, dokunma
hedefinin boyutu.

**Teklik ve tutarlılık kuralları.** Belge ölçeğinde sayar: birden çok `main` yer imi var mı,
başlık düzeyleri atlanmış mı, aynı türden adsız yer imi birden fazla mı.

Bu dört sınıfın ortak özelliği, kararın **belgeden okunabilir** olmasıdır. Kararın niyete,
anlama ya da zaman içindeki bir etkileşim dizisine bağlı olduğu yerde kural yazılamaz.

#### Ölçütler ve Bulgular

Aşağıdaki betik iki hesabı birlikte yapıyor: ölçütlerin otomatikleştirilebilirlik dağılımı
ve önceki derslerde Kuzey Yamaç arayüzünde bulunan gerçek kusurlarda otomatik denetimin
bulma oranı.

```js
// kapsam.mjs — olcutlerin otomatiklestirilebilirlik siniflari ve gercek bulgularda kapsam

// sinif: "otomatik" (makinece kararlanabilir) | "kismi" (bir bolumu) | "elle"
const OLCUT = [
  ["1.1.1", "Metin Olmayan İçerik", "kismi"],
  ["1.3.1", "Bilgi ve İlişkiler", "kismi"],
  ["1.3.5", "Girdi Amacının Belirlenmesi", "kismi"],
  ["1.4.1", "Renk Kullanımı", "elle"],
  ["1.4.3", "Kontrast (En Az)", "kismi"],
  ["1.4.4", "Metnin Yeniden Boyutlandırılması", "elle"],
  ["1.4.11", "Metin Dışı Kontrast", "kismi"],
  ["1.4.12", "Metin Aralığı", "kismi"],
  ["2.1.1", "Klavye", "elle"],
  ["2.1.2", "Klavye Tuzağı Yok", "elle"],
  ["2.4.1", "Blokları Atlama", "kismi"],
  ["2.4.2", "Sayfa Başlığı", "otomatik"],
  ["2.4.3", "Odak Sırası", "elle"],
  ["2.4.4", "Bağlantı Amacı", "kismi"],
  ["2.4.6", "Başlıklar ve Etiketler", "elle"],
  ["2.4.7", "Odak Görünür", "kismi"],
  ["2.5.3", "Addaki Etiket", "kismi"],
  ["3.1.1", "Sayfanın Dili", "otomatik"],
  ["3.2.2", "Girdide Değişim", "elle"],
  ["3.3.1", "Hata Tanımlama", "kismi"],
  ["3.3.2", "Etiketler veya Yönergeler", "kismi"],
  ["4.1.2", "Ad, Rol, Değer", "kismi"],
  ["4.1.3", "Durum İletileri", "elle"],
];

const sayim = {};
for (const [, , s] of OLCUT) sayim[s] = (sayim[s] ?? 0) + 1;
console.log("olcut sinifi  sayi  oran");
for (const s of ["otomatik", "kismi", "elle"]) {
  console.log(`${s.padEnd(13)} ${String(sayim[s]).padStart(4)}  ${((100 * sayim[s]) / OLCUT.length).toFixed(1)}%`);
}
console.log(`toplam        ${String(OLCUT.length).padStart(4)}`);

// Kuzey Yamac arayuzunde onceki derslerde bulunan gercek kusurlar
// [bulgu, olcut, otomatik denetleyici yakalar mi]
const BULGU = [
  ["simge dugmesinde bos alternatif metin", "4.1.2", true],
  ["aria-labelledby gecersiz kimlige basvuruyor", "4.1.2", true],
  ["aria-label gorunen etiketi bastiriyor", "2.5.3", true],
  ["role=button odak almiyor", "2.1.1", true],
  ["odaklanabilir oge aria-hidden tasiyor", "4.1.2", true],
  ["veri tablosunda role=presentation", "1.3.1", false],
  ["arti tabindex degeri", "2.4.3", true],
  ["odak sirasi gorsel sirayla uyusmuyor", "2.4.3", false],
  ["pencereden klavyeyle cikilamiyor", "2.1.2", false],
  ["pencere kapaninca odak donmuyor", "2.4.3", false],
  ["adsiz ikinci gezinme yer imi", "1.3.1", true],
  ["baslik duzeyi atlanmis", "1.3.1", true],
  ["baglamdan kopuk baglanti metni", "2.4.4", true],
  ["ayni metin iki farkli hedefe gidiyor", "2.4.4", false],
  ["atilgan duyuru bekleyeni atiyor", "4.1.3", false],
  ["alan kenarligi 3:1 esiginin altinda", "1.4.11", false],
  ["metin kontrasti 4.5:1 esiginin altinda", "1.4.3", true],
  ["nitelik yalniz renkle bildiriliyor", "1.4.1", false],
];

const yakalanan = BULGU.filter(([, , y]) => y).length;
console.log(`\ntoplam bulgu: ${BULGU.length}, otomatik yakalanan: ${yakalanan}, kacan: ${BULGU.length - yakalanan}`);
console.log(`otomatik denetimin bulma orani: ${((100 * yakalanan) / BULGU.length).toFixed(1)}%`);

console.log("\notomatik denetimin kacirdiklari:");
for (const [b, o, y] of BULGU) {
  if (!y) console.log(`  ${o.padEnd(7)} ${b}`);
}

// Kacanlarin hangi elle sinamayla bulundugu
const SINAMA = {
  "veri tablosunda role=presentation": "isaretleme okumasi",
  "odak sirasi gorsel sirayla uyusmuyor": "klavye turu",
  "pencereden klavyeyle cikilamiyor": "klavye turu",
  "pencere kapaninca odak donmuyor": "klavye turu",
  "ayni metin iki farkli hedefe gidiyor": "duzlem incelemesi",
  "atilgan duyuru bekleyeni atiyor": "gorev denemesi",
  "alan kenarligi 3:1 esiginin altinda": "durum dizgesi denetimi",
  "nitelik yalniz renkle bildiriliyor": "renksiz goruntu sinamasi",
};
const sinamaSayim = {};
for (const [b, , y] of BULGU) {
  if (y) continue;
  const s = SINAMA[b];
  sinamaSayim[s] = (sinamaSayim[s] ?? 0) + 1;
}
console.log("\nkacanlari bulan elle sinamalar:");
for (const [s, n] of Object.entries(sinamaSayim).sort((a, b) => b[1] - a[1])) {
  console.log(`  ${s.padEnd(26)} ${n} bulgu`);
}
```

```
olcut sinifi  sayi  oran
otomatik         2  8.7%
kismi           13  56.5%
elle             8  34.8%
toplam          23

toplam bulgu: 18, otomatik yakalanan: 10, kacan: 8
otomatik denetimin bulma orani: 55.6%

otomatik denetimin kacirdiklari:
  1.3.1   veri tablosunda role=presentation
  2.4.3   odak sirasi gorsel sirayla uyusmuyor
  2.1.2   pencereden klavyeyle cikilamiyor
  2.4.3   pencere kapaninca odak donmuyor
  2.4.4   ayni metin iki farkli hedefe gidiyor
  4.1.3   atilgan duyuru bekleyeni atiyor
  1.4.11  alan kenarligi 3:1 esiginin altinda
  1.4.1   nitelik yalniz renkle bildiriliyor

kacanlari bulan elle sinamalar:
  klavye turu                3 bulgu
  isaretleme okumasi         1 bulgu
  duzlem incelemesi          1 bulgu
  gorev denemesi             1 bulgu
  durum dizgesi denetimi     1 bulgu
  renksiz goruntu sinamasi   1 bulgu
```

İlk tablo ölçüt tarafını veriyor: yirmi üç ölçütün yalnızca ikisi baştan sona makinece
karar verilebiliyor. Çoğunluk **kısmi** sınıftadır ve bu sınıfın anlamı yanlış okunmaya
açıktır. Kısmi olan bir ölçütte otomatik denetim yalnızca **başarısızlık** bildirebilir;
başarı bildiremez. Alternatif metin özniteliğinin bulunması 1.1.1 ölçütünün karşılandığını
göstermez, yalnızca özniteliğin var olduğunu gösterir.

İkinci tablo daha somut olanıdır: bu kurstaki altı derste bulunan on sekiz gerçek kusurdan
onu otomatik denetimle yakalanıyor, sekizi kaçıyor. Oran arayüze göre değişir; sabit olan,
kaçanların **hangi türden** olduğudur.

#### Kaçanların Ortak Özelliği

Kaçan sekiz bulgu üç kümeye ayrılıyor ve üçü de yapısal olarak otomatikleştirilemez.

**Anlam kararı gerektirenler.** Bir tablonun veri mi taşıdığı yoksa düzen amaçlı mı olduğu
belgeden okunamaz; sunumsal rol yazılmış bir tabloda denetleyici yazarın niyetini
bilemez. Aynı metnin iki farklı hedefe gitmesi de böyledir: iki bağlantı ayrı şeylere
gidiyorsa kusur, aynı şeyin iki kopyasına gidiyorsa değil.

**Zaman içinde bir dizi gerektirenler.** Klavye tuzağı, odak sırasının görsel sırayla uyuşmaması,
pencere kapandığında odağın dönmemesi ve atılgan duyurunun bekleyeni atması tek bir belge durumuna
bakılarak görülemez; bir etkileşim dizisi çalıştırılmadan ortaya çıkmaz. Bunlar denetlenebilir —
Klavye Erişimi dersindeki durum makinesi tam olarak bunu yaptı— ama denetlenmeleri için bileşenin
davranışının modellenmesi ya da gerçek bir akışın yürütülmesi gerekir.

**Çizim sonucuna bağlı olanlar.** Kenarlığın karşıtlığı, gölge ve saydamlık katmanları
altında ölçüldüğünde iki düz rengin oranı olmaktan çıkar; geçiş dolgusu ya da görsel
üzerindeki metinde tek bir kontrast değeri yoktur. Rengin tek kanal olup olmadığı ise
bilginin hangi kanallarla verildiği sorusudur ve belgede bir bildirim olarak durmaz.

#### Elle Doğrulamanın Sırası

Elle doğrulama, sınırsız bir gözden geçirme değildir; maliyete göre sıralanmış beş
adımdır. Sıralama, önce ucuz olanın çok bulgu üretmesine dayanır.

**Klavye turu.** Sekme tuşuyla sayfanın başından sonuna gidilir. Dört şeye bakılır: her
durakta odak görünüyor mu, sıra ekrandaki sırayla uyuşuyor mu, açılan her bileşenden
klavyeyle çıkılabiliyor mu, kapanışta odak nereye dönüyor. Kaçan sekiz bulgunun üçünü tek
başına bu adım buluyor ve hiçbir araç gerektirmiyor.

**Ölçekleme sınaması.** Metin iki katına büyütülür ve arayüz aynı görevle kullanılmaya
çalışılır. Kırpılan metin, üst üste binen kutu ve kaybolan denetim bu adımda görünür.

**İşaretleme okuması.** Bileşenin ürettiği işaretleme okunur: rol doğru mu, durum
güncelleniyor mu, ad hangi kaynaktan geliyor. Bu adım anlam kararı gerektiren kusurları
bulur.

**Düzlem incelemesi.** Yer imi listesi, başlık ana hattı ve bağlantı listesi ayrı ayrı
çıkarılıp okunur. Liste bağlamından koparılmış hâliyle anlaşılıyor mu?

**Görev denemesi.** Arayüz görmeden ve fare kullanmadan bir görev uçtan uca tamamlanır —
ölçüm listesini süzmek, bir düzeltme bildirmek, sonucu doğrulamak. Bu en pahalı adımdır ve
diğer dördünün bulamadığını bulur: bilginin **yeterli** olup olmadığını.

#### Testi Sürece Bağlamak

Tek seferlik bir denetim, bir sonraki değişiklikte geçersizleşir. Denetimin sürece bağlanma
yolu üç katmanlıdır.

Bileşen ölçeğinde, bir bileşenin ürettiği rol ve erişilebilir ad bir sınamayla sabitlenir:
bileşen çizildiğinde ağaçta düğme rolü ve beklenen ad bulunmalıdır. Bu sınamalar
işaretleme yapısına değil, ağaçtaki karşılığa bakar; bileşenin içi yeniden yazıldığında da
geçerli kalır. Aynı yaklaşım Test ve İzleme konusundaki kullanıcı merkezli sorguların
temelidir.

Sayfa ölçeğinde, otomatik denetleyici yayın öncesi çalıştırılır ve bulduğu her başarısızlık
yayını durdurur. Erişilebilirlik Standartları dersindeki bütçe biçimi burada işler:
karşılanmamış A ve AA ölçüt sayısı sıfır olmalıdır, bilinen istisnalar ölçüt numarası ve
tarihle açık bir listede durur.

Sürüm ölçeğinde, elle doğrulamanın beş adımı her yayında değil, arayüzün değişen
bölümlerinde uygulanır. Yeni bir bileşen eklendiğinde klavye turu ve işaretleme okuması
zorunludur; görev denemesi daha seyrek yapılır.

#### Sıfır Hatanın Anlamı

Otomatik denetimin hiçbir bulgu üretmemesi, arayüzün erişilebilir olduğunu göstermez. Bu
cümle bu dersin sonucudur ve iki yönü vardır.

Birincisi, hesaplanan oran: bu arayüzde bulguların yüzde 55.6'sı yakalandı, kalanı
yakalanmadı. İkincisi, denetleyicinin **yanlış alarm** de üretebilmesidir. Bir geçiş
dolgusu üzerindeki metin, tek bir zemin rengi varsayılarak eşiğin altında bildirilebilir;
doğru kurulmuş bir bileşen, tanımadığı bir örüntü nedeniyle işaretlenebilir. Her bulgunun
insan tarafından doğrulanması gerekir.

Otomatik denetimin değeri, gerileme yakalamasındadır. Bir kez düzeltilmiş bir kusurun geri
gelmesini ucuza engeller ve elle doğrulamanın zamanını, yalnızca elle bulunabilecek
kusurlara ayırır.

#### Özet

- Otomatik denetleyici dört sınıf kural uygular: varlık, biçim geçerliliği, hesaplanabilir
  değer ve teklik–tutarlılık; hepsinde karar belgeden okunabilir olmalıdır.
- Ölçütlerin çoğunluğu kısmi sınıftadır ve kısmi ölçütlerde otomatik denetim yalnızca
  başarısızlık bildirebilir, başarı bildiremez.
- Bu kursta bulunan on sekiz gerçek kusurun onu otomatik yakalanıyor, sekizi kaçıyor;
  kaçanlar anlam kararı, zaman içinde bir dizi ya da çizim sonucu gerektirenlerdir.
- Elle doğrulama maliyete göre sıralanır: klavye turu, ölçekleme sınaması, işaretleme
  okuması, düzlem incelemesi ve görev denemesi.
- Denetim üç ölçekte sürece bağlanır: bileşende rol ve ad sınaması, sayfada yayını durduran
  otomatik denetim, sürümde elle doğrulamanın değişen bölümlere uygulanması.
- Otomatik denetimin sıfır bulgu üretmesi uygunluk kanıtı değildir; denetleyici yanlış
  bildirim de üretir ve asıl değeri gerileme yakalamaktır.

#### Sonraki Adım

Kalitenin iki ekseni bu noktada ölçülebilir hâle geldi. Başarım bir bütçeye, erişilebilirlik
bir ölçüt kümesine bağlandı; ikisi de sayılıyor ve ikisi de gerileme tanımı taşıyor. Üçüncü
bir eksen kaldı ve öncekilerden farklı bir soru soruyor.

Erişilebilirlik, arayüzün taşıdığı bilginin kullanıcıya ulaşmasını sağlar. Sonraki konu ters
yöndeki akışı ele alır: kullanıcıdan gelen metnin arayüze girmesini. Ölçüm giriş formundaki
açıklama alanına yazılan metin, ölçüm listesinde başkalarına gösterilir. O metin sayfaya
yazıldığında **veri olarak mı** yoksa **kod olarak mı** işlenir? Ayrımın kaybolduğu yerde
kullanıcıyı arayüzün kendisinden korumak gerekir. İstemci Tarafı Güvenlik konusunun ilk
dersi, Siteler Arası Betik Çalıştırma, bu ayrımın nasıl kaybolduğunu ve çıktı kaçırmanın
onu nasıl geri kurduğunu ele alır.

## İstemci Tarafı Güvenlik

Kullanıcıyı arayüzün kendisinden koruyan katmanlar; çıktı kaçırma, içerik güvenlik politikası, kaynaklar arası paylaşım kuralları, istek sahteciliğine karşı belirteç ve çerez nitelikleri, güvenlik başlıkları, üçüncü taraf betik riski ve yaygın açık sınıflarının istemci tarafı izdüşümü.

### Siteler Arası Betik Çalıştırma

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/siteler-arasi-betik-calistirma

Sayfaya giren metnin kod olarak çalışması sorunu; yansıyan, depolanmış ve DOM tabanlı türlerin oluşum yolu, bağlama duyarlı çıktı kaçırma ve bağ hedeflerinde şema izin listesi.

Bu kursun ilk iki konusu kaliteyi ölçülebilir iki eksende ele aldı. Başarım konusu
sayfanın ne kadar hızlı kullanılabilir olduğunu bütçeyle bağladı; erişilebilirlik konusu,
Erişilebilirlik Testi dersinde otomatik denetimin nereye kadar gidebildiğini ve elle
doğrulamanın nerede zorunlu olduğunu gösterdi. İki eksen de kullanıcının arayüzle
kurduğu ilişkiyi iyileştirmeyi hedefliyordu.

Üçüncü eksen ters yönde durur: kullanıcıyı arayüzün kendisinden korumak. Kuzey Yamaç
Ölçüm İstasyonu uygulamasında her ölçüm kaydına serbest metin bir **ölçüm notu** girilir.
Bu not veritabanına yazılır, başka kullanıcıların ekranında görünür, dışa aktarılan
raporda yer alır. Notu yazan kişi kötü niyetli olmasa bile, yazdığı metnin sayfaya nasıl
girdiği bir güvenlik kararıdır. Bu ders, o metnin veri olarak kalmasını sağlayan katmanı
kurar.

#### Metnin Koda Dönüştüğü An

Tarayıcı bir belgeyi ayrıştırırken metni bağlamına göre yorumlar. Web'in Temelleri ve HTML
kursunda kurulan ayrım burada belirleyici olur: `<` karakteri ayrıştırıcı için bir etiketin
başlangıcıdır, sıradan bir harf değildir. Uygulama, kullanıcıdan gelen bir dizgiyi belgeye
olduğu gibi yerleştirdiğinde ayrıştırıcıya "burada veri var" demez; ayrıştırıcı gördüğü
karakterleri kendi kurallarına göre okur ve dizginin içindeki işaretlemeyi yapı olarak
kabul eder.

**Siteler arası betik çalıştırma (cross-site scripting)**, bu sınır kaymasının adıdır: bir
kaynağın (origin) belgesi içinde, o kaynağın yetkileriyle, uygulamanın yazmadığı kod
çalışır. Yetki sözcüğü kritiktir. Çalışan kod, belgeyle aynı kaynakta olduğu için o
kaynağın yerel depolamasını okuyabilir, `HttpOnly` işaretli olmayan çerezlerine erişebilir,
kullanıcının oturumuyla istek gönderebilir. Aynı kaynak politikası (same-origin policy) bu
kodu yabancı saymaz, çünkü kod belgenin içindedir.

Sorunun kökeni bir ayrıştırıcı hatası değildir. Kökeni, uygulamanın veriyi kod olarak
yorumlanabilecek bir yere, yorumlanmasını engelleyen bir dönüşüm yapmadan koymasıdır.

#### Üç Oluşum Yolu

Aynı sonuca üç farklı yoldan varılır ve savunmanın nereye konacağı bu yola bağlıdır.

**Yansıyan (reflected)** biçimde, girdi isteğin kendisinden gelir ve aynı yanıtta geri
yazılır. Ölçüm listesindeki arama alanı sorgu dizgisine (query string) yazılır, sunucu
"aradığınız terim: …" başlığını üretirken bu değeri sayfaya koyar. Zarar tek bir yanıtta
oluşur ve kurbanın hazırlanmış bir adresi açması gerekir.

**Depolanmış (stored)** biçimde, girdi bir kez kaydedilir ve sonraki her okumada yeniden
sunulur. Ölçüm notu tam olarak bu sınıftadır: notu kaydeden kullanıcı ile etkilenen
kullanıcı farklı kişilerdir ve etkilenen kişi sıradan bir gezinti yapar. Kalıcılığı
nedeniyle etkisi en geniş türdür.

**DOM tabanlı** biçimde, veri sunucuya hiç uğramaz. İstemci kodu, adresin parçasını
(fragment), yerel depolamadaki bir kaydı veya bir ileti olayının içeriğini okur ve belgeye
yazar. Sunucu günlüklerinde iz kalmaz; savunma yalnızca istemci kodunda kurulabilir.
Tarayıcı ve Web Platformu kursunda tanıtılan depolama API'leri bu yüzden güvenilir kaynak
sayılmaz: yerel depolamaya yazan da aynı sayfada çalışan koddur.

#### Savunmanın Yeri: Girişte Değil Çıkışta

Sezgisel çözüm girdiyi süzmektir: kayıt sırasında tehlikeli görünen karakterleri atmak.
Bu yaklaşım iki nedenle yetersizdir. Birincisi, hangi karakterin tehlikeli olduğu girdinin
nereye yazılacağına bağlıdır ve kayıt anında bu bilinmez; aynı not hem HTML gövdesinde hem
bir öznitelikte hem de dışa aktarılan bir adreste görünebilir. İkincisi, süzme veriyi bozar:
"12'lik anemometre &lt; 3 m/sn okudu" cümlesi geçerli bir ölçüm notudur ve olduğu gibi
saklanmalıdır.

Doğru yer çıkıştır. Veri deposunda ham metin durur; belgeye yazılacağı anda, yazılacağı
bağlamın kurallarına göre dönüştürülür. Bu dönüşüme **kaçırma (escaping)** denir: anlamlı
karakterleri, o bağlamda anlamını yitiren ama görüntüsü korunan gösterimlerle değiştirmek.

Kaçırma bağlamdan bağımsız değildir. Dört ayrı bağlamın dört ayrı kuralı vardır ve birinin
kuralını başka bir bağlamda uygulamak koruma sağlamaz.

#### Dört Bağlam, Dört Kural

Aşağıdaki dosya aynı ölçüm notunu dört bağlam için ayrı ayrı kaçırır. Not, dört bağlamın
da anlamlı karakterlerini taşıyan zararsız bir işaret metnidir.

```js
// kacirma.mjs — ayni olcum notunu dort baglamda kaciran islevler
const GOVDE = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
const OZNITELIK = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };

function govdeKacir(metin) {
  return String(metin).replace(/[&<>]/g, (c) => GOVDE[c]);
}

function oznitelikKacir(metin) {
  return String(metin).replace(/[&<>"']/g, (c) => OZNITELIK[c]);
}

function adresKacir(metin) {
  return encodeURIComponent(String(metin));
}

function betikKacir(metin) {
  return JSON.stringify(String(metin))
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026');
}

const not = `12'lik <isaret> & "kuzey" yamaci`;

const baglamlar = [
  ['HTML govdesi', govdeKacir(not)],
  ['Oznitelik degeri', oznitelikKacir(not)],
  ['Adres bileseni', adresKacir(not)],
  ['Betik dizgisi', betikKacir(not)],
];

console.log('girdi:', not);
for (const [ad, sonuc] of baglamlar) {
  console.log(ad.padEnd(18), sonuc);
}
```

```
$ node kacirma.mjs
girdi: 12'lik <isaret> & "kuzey" yamaci
HTML govdesi       12'lik &lt;isaret&gt; &amp; "kuzey" yamaci
Oznitelik degeri   12&#39;lik &lt;isaret&gt; &amp; &quot;kuzey&quot; yamaci
Adres bileseni     12'lik%20%3Cisaret%3E%20%26%20%22kuzey%22%20yamaci
Betik dizgisi      "12'lik \u003cisaret\u003e \u0026 \"kuzey\" yamaci"
```

Dört satır dört farklı çıktı verir ve farkların her biri bir kurala karşılık gelir.

Gövde bağlamında yalnız üç karakterin dönüştürülmesi yeter; tırnak işaretleri metin
düğümü içinde anlam taşımaz. Öznitelik bağlamında tırnaklar da dönüştürülür, çünkü tırnak
öznitelik değerinin sınırını belirler. Bu iki kuralın karışması sık görülen bir kusurdur:
gövde kuralıyla kaçırılmış bir değeri tırnaklı bir özniteliğe yazmak, değerin özniteliği
erken kapatmasına izin verir.

Adres bağlamında kural tümüyle başkadır. `&` karakteri HTML varlığına değil, yüzde
kodlamasına çevrilir; çünkü buradaki ayrıştırıcı HTML ayrıştırıcısı değil adres
çözümleyicisidir. Betik bağlamında ise dizgi sınırlayıcısı ve ters bölü karakterleri
dönüştürülür; `<` karakterinin `\u003c` biçimine çevrilmesi, dizginin içindeki metnin
kuşatan betik ögesini erken sonlandırmasını engeller.

Bu iç içelik bir kuralı doğurur: bir değer birden çok bağlamdan geçiyorsa kaçırma da iç
içe uygulanır. Betik içinde yazılan bir adres için önce adres kodlaması, sonra betik
kaçırması gerekir; sıra ters çevrilirse ikinci dönüşüm birincinin ürettiği karakterleri
bozar.

#### Kaçırmanın Yetmediği Yer: Şema Denetimi

Bazı öznitelikler değeri metin olarak değil, adres olarak yorumlar. Bir bağın hedefi
kaçırılmış olsa bile bir çalıştırılabilir şemaya işaret ediyorsa, kaçırma hiçbir şey
değiştirmez: karakterlerin hiçbiri HTML açısından anlamlı değildir. Ölçüm notundaki
kullanıcı bağlarını sayfaya koyan kod, bu yüzden şemayı ayrıca denetler.

```js
// sema.mjs — bag hedefinin semasini izin listesine gore dogrular
const IZINLI_SEMA = new Set(['http:', 'https:', 'mailto:']);

function bagHedefi(ham, taban) {
  let adres;
  try {
    adres = new URL(ham, taban);
  } catch {
    return { izinli: false, neden: 'cozumlenemedi' };
  }
  if (!IZINLI_SEMA.has(adres.protocol)) {
    return { izinli: false, neden: `sema listede yok: ${adres.protocol}` };
  }
  return { izinli: true, hedef: adres.href };
}

const taban = 'https://olcum.ornek.test/notlar/';
const adaylar = [
  '/istasyon/kuzey-yamac',
  'https://veri.ornek.test/rapor.csv',
  'mailto:kayit@ornek.test',
  'veri-semasi:text/html;base64,SSBhbSB0ZXh0',
  'olcum://kuzey',
  '::bozuk::',
];

for (const aday of adaylar) {
  const sonuc = bagHedefi(aday, taban);
  console.log(String(sonuc.izinli).padEnd(6), aday.padEnd(44), sonuc.hedef ?? sonuc.neden);
}
```

```
$ node sema.mjs
true   /istasyon/kuzey-yamac                        https://olcum.ornek.test/istasyon/kuzey-yamac
true   https://veri.ornek.test/rapor.csv            https://veri.ornek.test/rapor.csv
true   mailto:kayit@ornek.test                      mailto:kayit@ornek.test
false  veri-semasi:text/html;base64,SSBhbSB0ZXh0    sema listede yok: veri-semasi:
false  olcum://kuzey                                sema listede yok: olcum:
true   ::bozuk::                                    https://olcum.ornek.test/notlar/::bozuk::
```

Son satır kuralın nasıl yazılması gerektiğini gösterir. `::bozuk::` geçerli bir mutlak
adres değildir, ama bir taban adresle çözümlendiğinde göreli yol sayılır ve sayfanın kendi
şemasını devralır. Denetim ham dizgi üzerinde değil, **çözümlenmiş adres** üzerinde
yapılır; izin listesi de reddedilecekleri saymak yerine kabul edilecekleri sayar. Reddedilenleri
saymak, listeye girmemiş her şemayı sessizce geçirir.

#### Kaçırmayı Elle Yapmamak

Yukarıdaki işlevler kuralı göstermek için yazıldı; üretim kodunda her yazma noktasında
elle çağrılmaları beklenmez. Bileşen Tabanlı Arayüz Geliştirme kursunda tanıtılan
bildirimsel oluşturma modelinin güvenlik açısından en önemli özelliği budur: ifade yerine
konan değerler öntanımlı olarak metin düğümü sayılır ve bağlamına uygun biçimde kaçırılır.
Kaçırma öntanımlı, ham işaretleme ekleme ise açıkça istenen bir kaçış deliğidir.

Bu kapıların hepsinin ortak yanı, adlarının veya belgelerinin tehlikeyi bildirmesidir: ham
HTML atayan özellik, işaretleme ayrıştırıp ekleyen yöntem, dizgiden bileşen üreten
yardımcılar. Denetim kuralı sadedir: bu kapıların geçtiği yerler sayılabilir olmalı ve her
biri için "buraya gelen veri nereden geliyor" sorusunun yazılı bir yanıtı bulunmalıdır.
Metin yazmak yeterliyse `textContent` kullanılır; `innerHTML` bir ihtiyaç değil, bir
karardır.

Kullanıcının biçimlendirilmiş metin girmesi gerçekten gerekiyorsa sorun kaçırma sorunu
olmaktan çıkar ve **arındırma (sanitization)** sorununa dönüşür: işaretlemeyi ayrıştırıp
yalnız izin listesindeki ögeleri, öznitelikleri ve şemaları bırakan bir katman gerekir.
Arındırma kaçırmadan belirgin biçimde zordur, çünkü ayrıştırma davranışlarındaki
farklılıklara duyarlıdır. Kural olarak arındırıcı elle yazılmaz; bakımı sürdürülen bir
uygulaması kullanılır ve çıktısı, bu dersin sonraki katmanı olan politika denetimiyle
birlikte savunulur.

#### Özet

- Siteler arası betik çalıştırma, verinin kod olarak yorumlanabildiği bir yere dönüşümsüz
  yerleştirilmesinden doğar; çalışan kod belgenin kaynağının yetkilerini devralır.
- Üç oluşum yolu vardır: yansıyan, depolanmış ve DOM tabanlı. Sonuncusunda veri sunucuya
  hiç uğramadığı için savunma yalnız istemci kodunda kurulabilir.
- Savunma girişte süzmek değil, çıkışta bağlama uygun kaçırmaktır; HTML gövdesi, öznitelik,
  adres ve betik bağlamlarının kuralları birbirinin yerine geçmez.
- Adres yorumlayan öznitelikler için kaçırma yetmez; çözümlenmiş adresin şeması bir izin
  listesine karşı denetlenir.
- Bildirimsel oluşturma kaçırmayı öntanımlı yapar; ham işaretleme ekleyen kaçış kapıları
  sayılabilir ve gerekçelendirilebilir olmalıdır.

#### Sonraki Adım

Kaçırma, uygulamanın kendi yazdığı her yazma noktasında doğru davranmasına bağlıdır. Tek
bir noktanın atlanması savunmanın tamamını geçersiz kılar ve büyük bir arayüzde bunun
gözden kaçmayacağını varsaymak gerçekçi değildir. Sonraki ders, kaçırmanın altına ikinci
bir katman koyar: tarayıcıya hangi kaynaktan gelen betiğin çalıştırılabileceğini bildiren
ve bu kararı belgenin kendisinden bağımsız hale getiren içerik güvenlik politikası.

### İçerik Güvenlik Politikası

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/icerik-guvenlik-politikasi

Tarayıcıya hangi kaynağın yüklenebileceğini bildiren politikanın yazımı; yönergeler ve kaynak ifadeleri, im ve karma tabanlı izin, rapor kipi ve ihlal kaydı, politikanın kapsamadığı alanlar.

Önceki ders savunmayı çıkışa yerleştirdi: her yazma noktasında bağlama uygun kaçırma.
Bu savunmanın zayıf yanı kapsamıdır. Doğru olması için uygulamadaki her yolun doğru olması
gerekir; tek bir ham işaretleme noktası, tek bir üçüncü taraf bileşen, tek bir eski sayfa
şablonu yeter. Büyük bir arayüzde bu kapsamın sürekli korunacağını varsaymak, denetlenmesi
zor bir varsayımdır.

Bu ders ikinci bir katman kurar. Katmanın niteliği önemlidir: uygulamanın kendi kodunun
doğruluğuna bağlı değildir. Karar tarayıcıya bildirilir ve tarayıcı, belge ne söylerse
söylesin, yükleme anında bu kararı uygular. Kuzey Yamaç Ölçüm İstasyonu arayüzü boyunca
kurulacak politika, ölçüm notunu gösteren sayfanın hangi betikleri çalıştırabileceğini ve
gömülü harita çerçevesine izin verilip verilmeyeceğini bu yolla sabitler.

#### Politikanın Yaptığı İş

**İçerik güvenlik politikası (content security policy)**, bir yanıtın
`Content-Security-Policy` başlığıyla taşınan bildirimidir. Bildirim, belgenin hangi
türden kaynağı nereden yükleyebileceğini ve satır içi kodun hangi koşulla
çalıştırılabileceğini sayar. Tarayıcı bir alt kaynağı (subresource) getirmeden veya bir
betiği çalıştırmadan önce politikaya bakar; ifade listesinde eşleşme yoksa işlemi
gerçekleştirmez.

Bu, kaçırmanın yerine geçen bir savunma değildir. Kaçırma verinin koda dönüşmesini
engeller; politika, engelin aşıldığı durumda çalışacak kodun ne yapabileceğini daraltır.
İkisi bağımsız katmanlardır ve bağımsız oldukları için birlikte çalışırlar.

Politika HTTP başlığı olarak gönderilir. Belge içindeki `meta` ögesiyle de bildirilebilir,
ancak bu yol bazı yönergeleri desteklemez ve belge ayrıştırılmaya başladıktan sonra devreye
girer. Sunucu yapılandırmasında başlık olarak tanımlamak, kapsamı geniş olan yoldur.

#### Yönergeler ve Kaynak İfadeleri

Politika, noktalı virgülle ayrılmış **yönergelerden (directive)** oluşur. Her yönerge bir
kaynak türünü adlandırır ve arkasından o tür için geçerli **kaynak ifadelerini** listeler.

`default-src` diğer yönergelerin yedeğidir: politika belirli bir tür için yönerge
içermiyorsa tarayıcı bu listeye bakar. `script-src` betikleri, `style-src` biçem
dosyalarını, `img-src` görselleri, `connect-src` betikten açılan ağ bağlantılarını,
`font-src` yazı tiplerini kapsar. Bunların yanında yükleme dışı kararlar veren yönergeler
vardır: `frame-ancestors` belgeyi kimin çerçeveleyebileceğini, `form-action` form
gönderiminin nereye gidebileceğini, `base-uri` belge taban adresinin değiştirilip
değiştirilemeyeceğini belirler.

Kaynak ifadeleri birkaç biçimdedir. `'none'` hiçbir şeye izin vermez. `'self'` belgenin kendi
kaynağını gösterir. Şema ifadesi (`https:`, `data:`) o şemadan gelen her adresi kapsar. Konak
ifadesi bir alan adını, önüne yıldız konarak alt alanlarını da içerir. `'nonce-…'` ve `'sha256-…'`
satır içi kod için tek tek izin verir. `'unsafe-inline'` satır içi kodun tamamına izin verir ve
adındaki uyarı yerindedir: bu ifade, politikanın betik çalıştırmaya karşı sağladığı korumayı büyük
ölçüde geçersiz kılar. Aynı yönergede bir im ya da karma ifadesi varsa tarayıcı
`'unsafe-inline'`'ı yok sayar; bu istisna, ifadenin yalnız eski gerçekleştirimlere yedek olarak
yazılmasını mümkün kılar.

#### Politikayı Karara Bağlamak

Politikanın nasıl okunduğunu görmenin en doğrudan yolu, okuyan kodu yazmaktır. Aşağıdaki
çözümleyici bir politika dizgesini yönergelere ayırır, verilen isteği ilgili listeye karşı
sınar ve yedek yönergeye düşme davranışını uygular.

```js
// csp.mjs — politika dizgesini cozumler ve bir kaynagin izinli olup olmadigini karara baglar
function politikayiCozumle(dizge) {
  const yonergeler = new Map();
  for (const parca of dizge.split(';')) {
    const sozcukler = parca.trim().split(/\s+/).filter(Boolean);
    if (sozcukler.length === 0) continue;
    yonergeler.set(sozcukler[0].toLowerCase(), sozcukler.slice(1));
  }
  return yonergeler;
}

function konakEsler(ifade, adres) {
  const [semaKismi, kalan] = ifade.includes('://') ? ifade.split('://') : [null, ifade];
  const [konak, ...yol] = kalan.split('/');
  if (semaKismi && `${semaKismi}:` !== adres.protocol) return false;
  if (konak.startsWith('*.')) {
    if (!adres.hostname.endsWith(konak.slice(1))) return false;
  } else if (konak !== adres.host && konak !== adres.hostname) {
    return false;
  }
  if (yol.length > 0 && yol[0] !== '') return adres.pathname.startsWith('/' + yol.join('/'));
  return true;
}

function ifadeEsler(ifade, istek, belgeKaynagi) {
  if (ifade === "'none'") return false;
  if (ifade === "'self'") return istek.adres !== null && new URL(istek.adres).origin === belgeKaynagi;
  if (ifade === "'unsafe-inline'") return istek.satirIci === true && !istek.nonce;
  if (ifade.startsWith("'nonce-")) return istek.nonce === ifade.slice(7, -1);
  if (ifade.startsWith("'sha256-")) return istek.karma === ifade.slice(1, -1);
  if (ifade.endsWith(':')) return istek.adres !== null && new URL(istek.adres).protocol === ifade;
  return istek.adres !== null && konakEsler(ifade, new URL(istek.adres));
}

function karar(politika, istek, belgeKaynagi) {
  const yonergeler = politikayiCozumle(politika);
  const ad = yonergeler.has(istek.yonerge) ? istek.yonerge : 'default-src';
  const liste = yonergeler.get(ad);
  if (!liste) return { izinli: true, yonerge: null };
  const eslesen = liste.find((ifade) => ifadeEsler(ifade, istek, belgeKaynagi));
  return { izinli: Boolean(eslesen), yonerge: ad, eslesen: eslesen ?? null };
}

function raporKaydi(istek, sonuc, politika, kip) {
  return {
    'document-uri': 'https://olcum.ornek.test/istasyon/kuzey-yamac',
    'violated-directive': sonuc.yonerge,
    'blocked-uri': istek.adres ?? 'inline',
    'disposition': kip,
    'original-policy': politika,
  };
}

const POLITIKA =
  "default-src 'none'; script-src 'self' 'nonce-r4k2' https://olcum-cdn.ornek.test; " +
  "img-src 'self' data:; connect-src 'self' https://*.veri.ornek.test; frame-ancestors 'none'";
const KAYNAK = 'https://olcum.ornek.test';

const istekler = [
  { ad: 'kendi betigi', yonerge: 'script-src', adres: 'https://olcum.ornek.test/js/olcum.js' },
  { ad: 'dagitim betigi', yonerge: 'script-src', adres: 'https://olcum-cdn.ornek.test/grafik.js' },
  { ad: 'yabanci betik', yonerge: 'script-src', adres: 'https://baska.ornek.test/izleme.js' },
  { ad: 'imli satir ici', yonerge: 'script-src', adres: null, satirIci: true, nonce: 'r4k2' },
  { ad: 'imsiz satir ici', yonerge: 'script-src', adres: null, satirIci: true },
  { ad: 'gomulu veri gorseli', yonerge: 'img-src', adres: 'data:image/png;base64,iVBOR' },
  { ad: 'alt alan cagrisi', yonerge: 'connect-src', adres: 'https://olcum1.veri.ornek.test/kayit' },
  { ad: 'yazi tipi', yonerge: 'font-src', adres: 'https://olcum.ornek.test/f/a.woff2' },
];

for (const istek of istekler) {
  const sonuc = karar(POLITIKA, istek, KAYNAK);
  const satir = [
    (sonuc.izinli ? 'izin' : 'engel').padEnd(6),
    istek.ad.padEnd(20),
    (sonuc.yonerge ?? '-').padEnd(14),
    sonuc.eslesen ?? '',
  ].join(' ');
  console.log(satir.trimEnd());
}

console.log('\nrapor kaydi:');
const engellenen = istekler[2];
console.log(JSON.stringify(raporKaydi(engellenen, karar(POLITIKA, engellenen, KAYNAK), POLITIKA, 'enforce'), null, 2));
```

```
$ node csp.mjs
izin   kendi betigi         script-src     'self'
izin   dagitim betigi       script-src     https://olcum-cdn.ornek.test
engel  yabanci betik        script-src
izin   imli satir ici       script-src     'nonce-r4k2'
engel  imsiz satir ici      script-src
izin   gomulu veri gorseli  img-src        data:
izin   alt alan cagrisi     connect-src    https://*.veri.ornek.test
engel  yazi tipi            default-src

rapor kaydi:
{
  "document-uri": "https://olcum.ornek.test/istasyon/kuzey-yamac",
  "violated-directive": "script-src",
  "blocked-uri": "https://baska.ornek.test/izleme.js",
  "disposition": "enforce",
  "original-policy": "default-src 'none'; script-src 'self' 'nonce-r4k2' https://olcum-cdn.ornek.test; img-src 'self' data:; connect-src 'self' https://*.veri.ornek.test; frame-ancestors 'none'"
}
```

Son satır politikanın en öğretici davranışını gösterir. Yazı tipi isteği `font-src`
yönergesi bulunmadığı için `default-src 'none'` listesine düşer ve engellenir. Bu, sıkı
politika yazmanın önerilen biçimidir: `default-src 'none'` ile başlanır, uygulamanın
gerçekten kullandığı her tür için yönerge açıkça eklenir. Eksik yönerge sessizce izin
vermek yerine görünür bir hataya dönüşür.

#### İm ve Karma ile Satır İçi Kod

Satır içi betik gerçekten gerekiyorsa iki yol vardır ve ikisi de `'unsafe-inline'`
kullanmaz.

**İm (nonce)**, sunucunun her yanıt için ürettiği tahmin edilemez bir değerdir. Politikada
`'nonce-r4k2'` biçiminde bildirilir, betik ögesinde aynı değer `nonce` özniteliği olarak
yazılır. Tarayıcı yalnız eşleşen ögeyi çalıştırır. Üç kural bağlayıcıdır: değer kriptografik
kalitede rastgele üretilir, her yanıtta yenilenir ve önbelleğe alınan bir sayfayla birlikte
yeniden kullanılmaz. Sabit bir im, politikayı `'unsafe-inline'` düzeyine indirir.

**Karma tabanlı izin**, içeriği değişmeyen satır içi betikler için uygundur. Betiğin
gövdesinin özeti alınır ve politikaya `'sha256-…'` biçiminde yazılır. Yanıt başına üretim
gerektirmez, ama betik metni bir karakter değiştiğinde politika da güncellenmelidir; bu da
karmanın derleme aşamasında hesaplanmasını gerektirir.

Konak listeleri büyüdükçe politikanın koruma değeri düşer, çünkü listedeki her alan adı
güvenilen bir yükleme noktası hâline gelir. Bu sorunu azaltmak için tanımlanmış
`'strict-dynamic'` ifadesi, imle çalıştırılmış bir betiğin yüklediği alt betiklere güveni
devreder ve konak listesini gereksizleştirir. Bu ifadeyi anlamayan bir gerçekleştirimde
politikanın çalışmaya devam etmesi için, ifade konak listesiyle birlikte yazılır: eski
davranış listeyi okur, yeni davranış listeyi yok sayar.

#### Rapor Kipi ve Yayına Alma

Sıkı bir politikayı hazır bir uygulamaya doğrudan uygulamak, çalışan işlevleri kırar.
`Content-Security-Policy-Report-Only` başlığı bu yüzden vardır: tarayıcı politikayı
değerlendirir, ihlalleri bildirir, ama hiçbir şeyi engellemez. Yukarıdaki rapor kaydında
`disposition` alanı bu ayrımı taşır — zorlama kipinde `enforce`, rapor kipinde `report`
değerini alır.

İhlal kayıtları, politikada bildirilen bir toplama ucuna gönderilir. Kaydın alanları
ihlalin hangi belgede oluştuğunu, hangi yönergenin ihlal edildiğini ve neyin engellendiğini
söyler. Satır içi kod için `blocked-uri` alanı bir adres yerine `inline` değerini taşır.

Yayına alma sırası şudur: rapor kipiyle başlanır, gelen kayıtlar bir süre toplanır, meşru
yükleme noktaları politikaya eklenir, kalan ihlaller kod düzeltmesiyle giderilir; ancak
kayıt akışı durulduktan sonra zorlama kipine geçilir. Toplama ucunun kendisi de
gürültüye dayanıklı olmalıdır: kullanıcı tarafında sayfaya kod ekleyen eklentiler,
uygulamayla ilgisi olmayan ihlaller üretir ve bu kayıtlar ayıklanmadığında gerçek
bulgular görünmez olur.

#### Politikanın Kapsamadığı Alan

Politika yükleme ve çalıştırma kararlarını denetler; belge içinde çalışan meşru kodun ne
yaptığını denetlemez. Önceki derste tanıtılan DOM tabanlı sınıf bu boşluğa düşer:
uygulamanın kendi betiği, kendi kaynağından gelen bir değeri ham işaretleme olarak yazarsa
politika bunu bir dış yükleme saymaz.

Bu boşluğu daraltmak için tanımlanmış olan mekanizma, ham işaretleme kabul eden yerleştirme
noktalarını yalnız denetlenmiş bir tür üzerinden yazılabilir kılar ve dizgi atamasını hata
hâline getirir. Politikaya bir yönergeyle bağlanır ve rapor kipiyle ölçülebilir. Desteği
her ortamda bulunmayabileceği için, yetenek saptamayla sınanır ve bulunmadığında uygulama
önceki derste kurulan kaçırma katmanına dayanır.

#### Özet

- İçerik güvenlik politikası, hangi kaynağın yüklenip hangi kodun çalıştırılabileceğini
  tarayıcıya bildirir ve kararı uygulamanın kendi kodundan bağımsız kılar.
- Politika yönergelerden oluşur; `default-src` diğerlerinin yedeğidir, bu yüzden
  `default-src 'none'` ile başlayıp gerekli türleri tek tek eklemek eksikleri görünür kılar.
- Satır içi kod için `'unsafe-inline'` yerine yanıt başına yenilenen im veya derleme
  aşamasında hesaplanan karma kullanılır.
- Rapor kipi politikayı engellemeden değerlendirir; yayına alma rapor kipiyle başlar,
  kayıt akışı durulduktan sonra zorlama kipine geçilir.
- Politika dış yükleme kararlarını denetler; belgenin kendi kodunun ham işaretleme yazması
  ayrı bir mekanizmayla sınırlandırılır.

#### Sonraki Adım

Politika, betiğin nereden yüklenebileceğini söyler. Betik yüklendikten sonra veri okumak
için başka bir kaynağa istek attığında ise başka bir kural kümesi devreye girer: aynı kaynak
politikası, bu isteğin yanıtının okunmasına öntanımlı olarak izin vermez. Ölçüm arayüzünün
ayrı bir alan adındaki veri ucundan okuma yapabilmesi için sunucunun açık bir izin
bildirmesi gerekir. Sonraki ders bu izin mekanizmasını, tarayıcının uyguladığı kısıtları ve
doğru sunucu yapılandırmasını kurar.

### Kaynaklar Arası Paylaşım

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/kaynaklar-arasi-paylasim

Aynı kaynak politikasının neyi engellediği, basit istek ile ön denetim ayrımı, paylaşım başlıklarının anlamı, kimlik bilgili isteklerin kuralları ve sık görülen yanlış yapılandırmalar.

Önceki ders, içerik güvenlik politikasının hangi kaynaktan betik yüklenebileceğini
belirlediğini kurdu. Betik yüklendikten sonra ayrı bir sınır devreye girer. Kuzey Yamaç
Ölçüm İstasyonu arayüzü `https://olcum.ornek.test` adresinden sunuluyor, ölçüm verisi ise
ayrı bir alan adındaki bir uçtan geliyor. Arayüzün betiği bu uca istek attığında yanıtı
okuyup okuyamayacağına politika değil, aynı kaynak politikası karar verir.

Bu ders o kararı ve kararı gevşetmenin denetimli yolunu kurar.

#### Aynı Kaynak Politikası Neyi Engeller

Tarayıcı ve Web Platformu kursunda tanımlandığı gibi bir **kaynak (origin)** şema, ana
bilgisayar adı ve bağlantı noktasının oluşturduğu üçlüdür. Üçlünün herhangi bir bileşeni
farklıysa kaynak farklıdır: `https://olcum.ornek.test` ile `https://veri.ornek.test` ayrı
kaynaklardır, `http://olcum.ornek.test` de ayrıdır.

Aynı kaynak politikasının ne yaptığı konusundaki en sık yanılgı, isteğin gönderilmesini
engellediği sanısıdır. Engellenen şey **yanıtın okunmasıdır**. Betik başka bir kaynağa istek
gönderebilir; istek sunucuya ulaşır, sunucu isteği işler ve yanıt verir; tarayıcı yanıtı betiğe
teslim etmez. İstek yan etki üretmişse o yan etki gerçekleşmiştir.

Bu ayrım sonraki dersin konusunu doğurur ve burada not edilmelidir: politika okumayı
kısıtlar, yazmayı değil.

Politika yalnız betikten açılan bağlantılar için geçerlidir. Görsel, biçem dosyası, yazı tipi ve
betik ögeleri başka kaynaklardan yükleme yapabilir; belge bunları gömer ama içeriklerini okuyamaz.
Tarayıcı ve Web Platformu kursundaki Servis Çalışanları dersinde tanıtılan **saydam olmayan
yanıt** kavramı bu durumun adıdır: yanıt kullanılabilir, ama gövdesi ve başlıkları program
tarafından görülemez.

#### Basit İstek ve Ön Denetim

Kaynaklar arası isteklerin bir bölümü doğrudan gönderilir, bir bölümü gönderilmeden önce
izin sorar.

Doğrudan gönderilen istekler, formların uzun süredir kaynaklar arası yapabildiği
isteklerdir: `GET`, `HEAD` veya `POST` yöntemi, sınırlı bir içerik türü kümesi ve
uygulamanın eklemediği başlıklar. Bu istekler için ek bir tur yoktur; tarayıcı isteği
gönderir ve yanıtın okunup okunamayacağına dönen başlıklara bakarak karar verir.

Bunun dışındaki her istek **ön denetim (preflight)** gerektirir. Yöntem `PUT` veya `DELETE`
ise, içerik türü `application/json` ise, uygulama `X-` ile başlayan bir başlık ekliyorsa
tarayıcı önce `OPTIONS` yöntemiyle ayrı bir istek gönderir. Bu istekte
`Access-Control-Request-Method` ve `Access-Control-Request-Headers` başlıkları, asıl
isteğin ne yapacağını sunucuya bildirir. Sunucu izin verirse asıl istek gönderilir;
vermezse asıl istek hiç oluşmaz.

Ön denetimin varlık nedeni geriye dönük uyumluluktur. Tarayıcının doğrudan
gönderebileceği istekler zaten formlarla gönderilebilen isteklerdir; yeni yetenekler
sunucunun haberi olmadan kullanıma açılmaz.

#### Sunucu Tarafındaki Yapılandırma

Aşağıdaki sunucu ölçüm verisini sunar ve paylaşım kararını tek bir izin listesi üzerinden
verir.

```js
// cors-sunucu.mjs — olcum ucunun kaynaklar arasi paylasim yapilandirmasi
import { createServer } from 'node:http';

const IZINLI_KAYNAKLAR = new Set(['https://olcum.ornek.test']);
const IZINLI_YONTEMLER = 'GET, POST, OPTIONS';
const IZINLI_BASLIKLAR = 'content-type, x-olcum-anahtari';

const sunucu = createServer((istek, yanit) => {
  const kaynak = istek.headers.origin;
  const izinli = kaynak !== undefined && IZINLI_KAYNAKLAR.has(kaynak);

  yanit.setHeader('Vary', 'Origin');
  if (izinli) {
    yanit.setHeader('Access-Control-Allow-Origin', kaynak);
    yanit.setHeader('Access-Control-Allow-Credentials', 'true');
  }

  if (istek.method === 'OPTIONS') {
    if (izinli) {
      yanit.setHeader('Access-Control-Allow-Methods', IZINLI_YONTEMLER);
      yanit.setHeader('Access-Control-Allow-Headers', IZINLI_BASLIKLAR);
      yanit.setHeader('Access-Control-Max-Age', '600');
    }
    yanit.writeHead(204).end();
    return;
  }

  yanit.setHeader('Content-Type', 'application/json');
  yanit.end(JSON.stringify({ istasyon: 'kuzey-yamac', sicaklik: -3.4 }));
});

sunucu.listen(8791, '127.0.0.1', () => {
  console.log('dinliyor: http://127.0.0.1:8791');
});
```

Sunucu arka planda başlatılır, üç istek gönderilir ve sonunda durdurulur. Bayrak
`-D -` yanıt başlıklarını standart çıktıya yazar; `tr -d '\r'` satır sonlarındaki taşıma
karakterini ayıklar, `grep` ise her çalıştırmada değişecek olan tarih satırını eler.

```bash
node cors-sunucu.mjs &
sunucu=$!
sleep 1

echo '--- 1. izinli kaynaktan on denetim ---'
curl -s -o /dev/null -D - -X OPTIONS http://127.0.0.1:8791/olcum \
  -H 'Origin: https://olcum.ornek.test' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type' | tr -d '\r' | grep -v '^Date:'

echo '--- 2. listede olmayan kaynaktan on denetim ---'
curl -s -o /dev/null -D - -X OPTIONS http://127.0.0.1:8791/olcum \
  -H 'Origin: https://baska.ornek.test' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type' | tr -d '\r' | grep -v '^Date:'

echo '--- 3. izinli kaynaktan gercek istek ---'
curl -s -D - http://127.0.0.1:8791/olcum \
  -H 'Origin: https://olcum.ornek.test' | tr -d '\r' | grep -v '^Date:'

kill $sunucu
```

```
dinliyor: http://127.0.0.1:8791
--- 1. izinli kaynaktan on denetim ---
HTTP/1.1 204 No Content
Vary: Origin
Access-Control-Allow-Origin: https://olcum.ornek.test
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: content-type, x-olcum-anahtari
Access-Control-Max-Age: 600
Connection: keep-alive
Keep-Alive: timeout=5

--- 2. listede olmayan kaynaktan on denetim ---
HTTP/1.1 204 No Content
Vary: Origin
Connection: keep-alive
Keep-Alive: timeout=5

--- 3. izinli kaynaktan gercek istek ---
HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Origin: https://olcum.ornek.test
Access-Control-Allow-Credentials: true
Content-Type: application/json
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 42

{"istasyon":"kuzey-yamac","sicaklik":-3.4}
```

İkinci durum, reddin nasıl göründüğünü gösterir. Sunucu bir hata kodu döndürmez; yalnızca
izin başlıklarını yazmaz. Karar tarayıcıya aittir ve tarayıcı, izin başlığı bulunmayan bir
ön denetimden sonra asıl isteği hiç göndermez. Bu, ret durumunun neden sunucu günlüğünde
bir hata olarak görünmediğini de açıklar.

#### Başlıkların Anlamı

`Access-Control-Allow-Origin`, yanıtı okumasına izin verilen tek kaynağı bildirir. Değeri
ya tam bir kaynak dizgisidir ya da tüm kaynakları gösteren yıldızdır.

`Access-Control-Allow-Methods` ve `Access-Control-Allow-Headers` yalnız ön denetim
yanıtında anlamlıdır; asıl isteğin hangi yöntemi ve hangi başlıkları kullanabileceğini
sayar. `Access-Control-Max-Age`, ön denetim sonucunun kaç saniye önbelleğe alınabileceğini
söyler; her istekten önce bir tur daha atılmasını engeller.

`Access-Control-Expose-Headers`, betiğin yanıt üzerinde hangi başlıkları okuyabileceğini
genişletir. Öntanımlı olarak yalnızca küçük bir başlık kümesi okunabilir; sayfalama
bilgisini özel bir başlıkta taşıyan bir uç, o başlığı burada saymazsa betik başlığı
göremez.

`Vary: Origin` bir izin başlığı değildir ama eksikliği gerçek bir kusurdur. Yanıtın içeriği
istekteki `Origin` başlığına göre değiştiği için, aradaki her önbellek bu başlığı önbellek
anahtarına katmalıdır. Katmazsa, izinli bir kaynak için üretilmiş izin başlıkları başka bir
kaynağa sunulabilir.

#### Kimlik Bilgili İstekler

Çerezlerin ve kimlik başlıklarının kaynaklar arası isteğe eklenmesi ayrıca istenmelidir.
İstemci tarafında bu, isteğin kimlik bilgisi taşıyacağının belirtilmesiyle olur; sunucu
tarafında iki kural birden geçerlidir.

Birincisi, `Access-Control-Allow-Credentials` başlığı `true` değerini taşımalıdır.
İkincisi, `Access-Control-Allow-Origin` yıldız olamaz; tam kaynak dizgisi yazılmalıdır.
Bu kural, "herkese açık" bir yapılandırmanın yanlışlıkla kimlik bilgisi taşıyan istekleri
kapsamasını engeller.

Çerezin gerçekten gönderilmesi ayrı bir koşula daha bağlıdır: çerezin `SameSite` niteliği
kaynaklar arası gönderime izin vermelidir. Bu nitelik sonraki dersin konusudur ve iki
mekanizma birlikte değerlendirilmelidir; paylaşım başlıkları doğru olsa bile çerez
niteliği engelliyorsa istek kimliksiz gider.

#### Sık Görülen Yanlış Yapılandırmalar

**Gelen kaynağı olduğu gibi yansıtmak.** İstekteki `Origin` başlığını doğrulamadan yanıta
yazan bir sunucu, izin listesi tutuyor gibi görünür ama her kaynağa izin vermiş olur.
Yukarıdaki örnekte yansıtma yapılıyor, ancak yalnız listede bulunan bir değer için.

**Yıldızı kimlik bilgisiyle birlikte kullanmaya çalışmak.** Tarayıcı bunu reddeder; bu
reddi aşmak için kaynağı yansıtmaya geçmek, ilk kusuru üretir.

**Alan adı eşleşmesini gevşek yazmak.** `ornek.test` ile biten her adı kabul eden bir
denetim, `ornek.test.baska.test` adını da kabul eder. Eşleşme dizgi sonu üzerinden değil,
tam kaynak karşılaştırmasıyla yapılır.

**`null` kaynağına izin vermek.** Bazı bağlamlar `Origin` başlığını `null` olarak gönderir;
bu değeri izin listesine koymak, o bağlamların hepsine izin vermek demektir.

#### Paylaşım Bir Yetkilendirme Değildir

Kaynaklar arası paylaşım, kullanıcının tarayıcısını başka bir sitenin betiğinin veri
okumasına karşı korur. Sunucuyu korumaz. Tarayıcı olmayan bir istemci bu başlıkları hiç
dikkate almaz; izin listesinde bulunmayan bir kaynak adı yazmak da bir engel oluşturmaz,
çünkü `Origin` başlığını tarayıcı dışında hiçbir şey zorunlu kılmaz.

Bundan çıkan kural nettir: bir uç, kimlik doğrulaması ve yetkilendirmesi olmadan
korunmuş sayılmaz. Paylaşım yapılandırması bu denetimlerin yerine geçmez, üzerine eklenir.

#### Özet

- Aynı kaynak politikası kaynaklar arası isteğin gönderilmesini değil, yanıtının
  okunmasını engeller; yan etki üreten bir istek yine de gerçekleşir.
- Basit koşulları sağlamayan istekler `OPTIONS` yöntemiyle bir ön denetim turu geçirir;
  sunucu izin vermezse asıl istek hiç oluşmaz.
- Ret, hata kodu olarak değil, izin başlıklarının yokluğu olarak görünür.
- Kimlik bilgili isteklerde `Access-Control-Allow-Origin` yıldız olamaz ve
  `Access-Control-Allow-Credentials` gerekir; çerezin `SameSite` niteliği ayrıca izin
  vermelidir.
- Paylaşım yapılandırması tarayıcı içindir; sunucunun kimlik doğrulaması ve
  yetkilendirmesi bundan bağımsız olarak gereklidir.

#### Sonraki Adım

Bu dersin başındaki ayrım açık bir soru bıraktı: aynı kaynak politikası okumayı engelliyorsa
ve yazma isteği yine de sunucuya ulaşıyorsa, başka bir sitenin sayfası kullanıcının oturumu
üzerinden bir ölçüm kaydı silebilir mi? Yanıtın okunamaması, işlemin yapılmadığı anlamına
gelmez. Sonraki ders bu saldırı sınıfını, çerez niteliklerinin ve istek belirteçlerinin
oluşturduğu savunmayı ele alır.

### İstek Sahteciliği

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/istek-sahteciligi

Çerezlerin kendiliğinden eklenmesinden doğan saldırı sınıfı; SameSite ve Secure niteliklerinin karar tablosu, oturuma bağlanmış istek belirteci, kaynak başlığı denetimi ve yöntem disiplini.

Önceki ders bir soruyla bitti: aynı kaynak politikası yanıtın okunmasını engelliyorsa,
yazma isteği ne oluyor? Yanıt, engellenmediğidir. Kaynaklar arası bir istek sunucuya
ulaşır, sunucu isteği işler, yan etki gerçekleşir; yalnız yanıt gövdesi isteği başlatan
sayfaya teslim edilmez.

Bu boşluk tek başına sorun değildir. Sorun, isteğe kullanıcının kimliğinin kendiliğinden
eklenmesiyle oluşur. Kuzey Yamaç Ölçüm İstasyonu arayüzünde oturum bir çerezle taşınıyorsa,
tarayıcı o çerezi isteğin nereden başlatıldığına bakmadan ekler. Bu ders o davranışın
sonucunu ve iki bağımsız savunma katmanını kurar.

#### Ortam Yetkisi

Çerez, İnternet Nasıl Çalışır kursunda tanımlandığı gibi istekle birlikte kendiliğinden
gider. Bu, oturumun sürmesini sağlayan davranıştır: kullanıcı her sayfada kimliğini
yeniden bildirmez. Aynı davranış bir yan sonuç üretir. İsteği kimin başlattığı çerezin
eklenmesini etkilemez; tarayıcı hedef adrese bakar, kaynağa değil.

Buna **ortam yetkisi (ambient authority)** denir: yetki, isteği yapan koda değil, isteğin
gittiği yere iliştirilmiştir. **Siteler arası istek sahteciliği (cross-site request
forgery)** bu yetkinin başka bir sitenin sayfasından tetiklenmesidir. Kullanıcı ölçüm
istasyonuna giriş yapmış durumdayken başka bir sekmede bir sayfa açar; o sayfa, ölçüm
istasyonunun bir uç adresine yazma isteği başlatır. İstek kullanıcının çerezini taşıdığı
için sunucu tarafında meşru görünür.

Saldırıya betik gerekmez. Bir form ögesi gönderilebilir, bir görsel ögesi bir adresi
çağırabilir. Yanıtın okunamaması saldırganın umurunda değildir; amaç okumak değil,
yaptırmaktır.

#### Birinci Katman: Çerez Nitelikleri

İlk savunma çerezin kendisindedir. `SameSite` niteliği, çerezin kaynaklar arası isteklere
eklenip eklenmeyeceğini belirler ve üç değer alır. Aşağıdaki kod bu değerlerin karar
tablosunu hesaplar.

```js
// samesite.mjs — cerez nitelikleri istek turlerine gore karar tablosu uretir
const GUVENLI_YONTEMLER = new Set(['GET', 'HEAD']);

function gonderilirMi(cerez, istek) {
  if (cerez.secure && istek.sema !== 'https') return ['hayir', 'Secure nitelikli cerez sifresiz semada gonderilmez'];
  if (istek.ayniSite) return ['evet', 'ayni site istegi'];
  switch (cerez.sameSite) {
    case 'Strict':
      return ['hayir', 'Strict kaynaklar arasi her istegi diser'];
    case 'Lax':
      return istek.ustDuzeyGezinme && GUVENLI_YONTEMLER.has(istek.yontem)
        ? ['evet', 'Lax ust duzey gezinmede guvenli yontemi gecirir']
        : ['hayir', 'Lax yalniz ust duzey guvenli gezinmeyi gecirir'];
    case 'None':
      return cerez.secure ? ['evet', 'None kaynaklar arasi gonderir'] : ['hayir', 'None icin Secure zorunlu'];
    default:
      return ['hayir', 'taninmayan deger'];
  }
}

const istekler = [
  { ad: 'ayni site cagri', ayniSite: true, ustDuzeyGezinme: false, yontem: 'GET', sema: 'https' },
  { ad: 'disaridan bag (GET)', ayniSite: false, ustDuzeyGezinme: true, yontem: 'GET', sema: 'https' },
  { ad: 'disaridan form (POST)', ayniSite: false, ustDuzeyGezinme: true, yontem: 'POST', sema: 'https' },
  { ad: 'disaridan cerceve', ayniSite: false, ustDuzeyGezinme: false, yontem: 'GET', sema: 'https' },
  { ad: 'disaridan betik cagrisi', ayniSite: false, ustDuzeyGezinme: false, yontem: 'POST', sema: 'https' },
];

const cerezler = [
  { ad: 'Strict+Secure', sameSite: 'Strict', secure: true },
  { ad: 'Lax+Secure', sameSite: 'Lax', secure: true },
  { ad: 'None+Secure', sameSite: 'None', secure: true },
  { ad: 'None, Secure yok', sameSite: 'None', secure: false },
];

const basliklar = ['istek turu'.padEnd(24), ...cerezler.map((c) => c.ad.padEnd(16))].join('').trimEnd();
console.log(basliklar);
for (const istek of istekler) {
  const hucreler = cerezler.map((cerez) => gonderilirMi(cerez, istek)[0].padEnd(16));
  console.log(istek.ad.padEnd(24) + hucreler.join('').trimEnd());
}

console.log('\ngerekce ornegi (Lax+Secure):');
for (const istek of istekler) {
  const [karar, neden] = gonderilirMi(cerezler[1], istek);
  console.log(' ', istek.ad.padEnd(24), karar.padEnd(6), neden);
}
```

```
$ node samesite.mjs
istek turu              Strict+Secure   Lax+Secure      None+Secure     None, Secure yok
ayni site cagri         evet            evet            evet            evet
disaridan bag (GET)     hayir           evet            evet            hayir
disaridan form (POST)   hayir           hayir           evet            hayir
disaridan cerceve       hayir           hayir           evet            hayir
disaridan betik cagrisi hayir           hayir           evet            hayir

gerekce ornegi (Lax+Secure):
  ayni site cagri          evet   ayni site istegi
  disaridan bag (GET)      evet   Lax ust duzey gezinmede guvenli yontemi gecirir
  disaridan form (POST)    hayir  Lax yalniz ust duzey guvenli gezinmeyi gecirir
  disaridan cerceve        hayir  Lax yalniz ust duzey guvenli gezinmeyi gecirir
  disaridan betik cagrisi  hayir  Lax yalniz ust duzey guvenli gezinmeyi gecirir
```

Tablo üç kuralı görünür kılar. `Strict`, kaynaklar arası her isteği diser; koruması en
geniştir ama başka bir siteden gelen bir bağla giren kullanıcı oturumsuz karşılanır.
`Lax`, yalnız üst düzey gezinmede ve yalnız güvenli yöntemle çerezi geçirir; dışarıdan
gelen bir formun `POST` isteği ile bir çerçeve yüklemesi eler. `None` her durumda gönderir
ve bu yüzden `Secure` niteliğini zorunlu kılar; nitelik yoksa çerez hiç gönderilmez.

Bu tabloyla birlikte iki nitelik daha oturum çerezinin ayrılmaz parçasıdır. `HttpOnly`,
çerezin betikten okunmasını engeller; ilk derste kurulan betik çalıştırma savunması
aşıldığında oturumun doğrudan çalınmasını zorlaştırır. `Secure`, çerezin şifresiz bir
bağlantıda gönderilmesini önler.

#### SameSite'ın Yetmediği Yerler

Nitelik güçlü bir azaltmadır, ama tek savunma olarak yeterli sayılmaz.

Birincisi, "aynı site" ile "aynı kaynak" farklı ölçütlerdir. Aynı site ölçütü kayıt
edilebilir alan adı üzerinden hesaplanır; `rapor.ornek.test` ile `olcum.ornek.test` ayrı
kaynaklardır ama aynı sitedir. Alt alanlardan birinin denetimini kaybetmek, o alan
üzerinden gönderilen isteklerin aynı site sayılması demektir. Bu riski daraltmak için
çerez adına `__Host-` öneki konur: bu önek, çerezin yalnız tam olarak belirlenen konağa
ait olmasını ve alan adı kapsamının genişletilememesini zorunlu kılar.

İkincisi, niteliğin bildirilmediği durumda uygulanacak davranış gerçekleştirime bağlıdır.
Bazı gerçekleştirimler bildirilmemiş çerezi kısıtlayıcı bir değer gibi ele alır, bazıları
almaz. Bu yüzden nitelik varsayılana bırakılmaz, açıkça yazılır.

Üçüncüsü, `Lax` değeri üst düzey `GET` gezinmesini geçirir. Durum değiştiren bir işlem
`GET` ile yapılıyorsa bu savunma o işlemi kapsamaz. Yöntem disiplini bu yüzden savunmanın
parçasıdır.

#### İkinci Katman: Oturuma Bağlı Belirteç

İkinci savunma, isteğin gerçekten uygulamanın kendi sayfasından başlatıldığını kanıtlar.
Sunucu her oturum için tahmin edilemez bir **istek belirteci** üretir, sayfaya gömer ve
gönderimde geri bekler. Belirtecin oturuma bağlanması kritiktir: bağlanmazsa saldırgan
kendi oturumu için aldığı geçerli bir belirteci başka bir kullanıcının isteğine
yerleştirebilir.

```js
// belirtec.mjs — oturuma baglanmis istek belirteci uretimi ve dogrulanmasi
import { createHmac, timingSafeEqual } from 'node:crypto';

const SUNUCU_GIZLISI = Buffer.from('7b2a0f5c9d1e4a8b6c3f0d2e5a7b9c1d', 'utf8');

function belirtecUret(oturumKimligi, tuz) {
  const imza = createHmac('sha256', SUNUCU_GIZLISI).update(`${oturumKimligi}.${tuz}`).digest('base64url');
  return `${tuz}.${imza}`;
}

function belirtecDogrula(oturumKimligi, belirtec) {
  if (typeof belirtec !== 'string' || !belirtec.includes('.')) return false;
  const [tuz] = belirtec.split('.');
  const beklenen = Buffer.from(belirtecUret(oturumKimligi, tuz));
  const gelen = Buffer.from(belirtec);
  return beklenen.length === gelen.length && timingSafeEqual(beklenen, gelen);
}

function kaynakDenetimi(basliklar, beklenen) {
  if (basliklar['sec-fetch-site'] === 'same-origin') return true;
  if (basliklar.origin) return basliklar.origin === beklenen;
  return false;
}

const OTURUM = 'oturum-4417';
const gecerli = belirtecUret(OTURUM, 'a1b2c3');
console.log('uretilen belirtec:', gecerli);

const denemeler = [
  ['dogru oturum, dogru belirtec', OTURUM, gecerli],
  ['baska oturumun belirteci', 'oturum-9002', gecerli],
  ['imzasi degistirilmis', OTURUM, `${gecerli.slice(0, -1)}X`],
  ['belirtec yok', OTURUM, undefined],
];

console.log('\nbelirtec dogrulama:');
for (const [ad, oturum, deger] of denemeler) {
  console.log(' ', ad.padEnd(30), belirtecDogrula(oturum, deger) ? 'gecerli' : 'reddedildi');
}

const BEKLENEN_KAYNAK = 'https://olcum.ornek.test';
const baslikKumeleri = [
  ['kendi sayfasindan', { origin: BEKLENEN_KAYNAK, 'sec-fetch-site': 'same-origin' }],
  ['baslik yok, isaret var', { 'sec-fetch-site': 'same-origin' }],
  ['disaridan gonderim', { origin: 'https://baska.ornek.test', 'sec-fetch-site': 'cross-site' }],
  ['ikisi de yok', {}],
];

console.log('\nkaynak denetimi:');
for (const [ad, basliklar] of baslikKumeleri) {
  console.log(' ', ad.padEnd(30), kaynakDenetimi(basliklar, BEKLENEN_KAYNAK) ? 'kabul' : 'reddedildi');
}
```

```
$ node belirtec.mjs
uretilen belirtec: a1b2c3.F5jj3rvIaTbTm94oBnF_AttfP9Oumrx8um4rO-DYY64

belirtec dogrulama:
  dogru oturum, dogru belirtec   gecerli
  baska oturumun belirteci       reddedildi
  imzasi degistirilmis           reddedildi
  belirtec yok                   reddedildi

kaynak denetimi:
  kendi sayfasindan              kabul
  baslik yok, isaret var         kabul
  disaridan gonderim             reddedildi
  ikisi de yok                   reddedildi
```

Belirteç, sunucu gizlisiyle üretilen bir imza taşır ve imza oturum kimliğini kapsar; başka
bir oturumun belirteci sunulduğunda imza uyuşmaz. Karşılaştırma `timingSafeEqual` ile
yapılır: karakter karakter kısa devre yapan bir karşılaştırma, doğru önek uzunluğunu
ölçülebilir bir zaman farkına dönüştürür.

Belirtecin taşınma yolu da kuralın parçasıdır. Yalnız bir çerezde taşınırsa saldırı
karşısında değeri kalmaz, çünkü çerez de kendiliğinden gider. Belirteç, çerez dışında bir
yerde bulunmalıdır: gizli bir form alanı veya uygulamanın açıkça eklediği bir istek
başlığı. Uygulama Mimarisi kursundaki Tarayıcıda Kimlik Doğrulama dersi bu ayrımın öteki
yüzünü kurmuştu; oturumu bir çerez yerine bellekteki bir belirteçle taşıyan ve isteğe
açıkça ekleyen bir tasarımda ortam yetkisi zaten oluşmaz, dolayısıyla bu saldırı sınıfı
kendiliğinden ortadan kalkar. Bedeli, oturumun sayfa yenilemesinde yeniden kurulmasıdır.

#### Üçüncü Katman: Kaynak Denetimi

Çıktının son bölümü üçüncü katmanı gösterir. Sunucu, durum değiştiren her istekte isteğin
kaynağını denetler. `Origin` başlığı bulunuyorsa beklenen kaynakla karşılaştırılır. Bu
başlığın bulunmadığı durumlar için tarayıcının eklediği getirme meta bilgisi kullanılır:
`Sec-Fetch-Site` başlığı isteğin aynı kaynaktan mı, aynı siteden mi, yoksa dışarıdan mı
başlatıldığını bildirir.

Denetimin son satırı önemlidir: iki başlığın da bulunmadığı istek kabul edilmez.
Eksikliği "sorun yok" saymak, denetimi bir başlığın silinmesiyle atlatılabilir hâle
getirir. Bu başlıklar uygulama kodunun değil tarayıcının yazdığı başlıklardır ve tarayıcı
tarafından değiştirilemez; yokluklarını güvenli yorumlamak için bir neden yoktur.

#### Yöntem Disiplini

Bütün bu katmanların altında bir varsayım vardır: durum değiştiren işlemler güvenli
yöntemlerle yapılmaz. İnternet Nasıl Çalışır kursunda tanımlanan **güvenli yöntem**
kavramı burada bir güvenlik gereğine dönüşür. Ölçüm kaydını silen bir uç `GET` ile
çağrılabiliyorsa, bir görsel ögesi bile o ucu tetikler; `Lax` çerez davranışı ve pek çok
kaynak denetimi bu durumu kapsamaz.

Kural olarak yazma işlemleri `POST`, `PUT`, `PATCH` veya `DELETE` yöntemleriyle yapılır ve
sunucu, yazma uçlarında yöntem denetimini açıkça uygular.

#### Özet

- İstek sahteciliği, çerezin isteği kimin başlattığına bakılmadan eklenmesinden doğar;
  yanıtın okunamaması yan etkinin gerçekleşmesini engellemez.
- `SameSite` niteliği ilk katmandır: `Strict` kaynaklar arası her isteği, `Lax` üst düzey
  güvenli gezinme dışındaki her isteği eler; `None` için `Secure` zorunludur.
- Aynı site ölçütü aynı kaynak ölçütünden geniştir; alt alan riski `__Host-` öneki ve ikinci
  bir katmanla daraltılır.
- İstek belirteci oturuma bağlanır, çerez dışında bir yerde taşınır ve sabit zamanlı
  karşılaştırmayla doğrulanır.
- `Origin` ve `Sec-Fetch-Site` başlıkları üçüncü katmandır; ikisinin de bulunmadığı istek
  reddedilir ve durum değiştiren işlemler güvenli yöntemlere açılmaz.

#### Sonraki Adım

Bu dersin savunmaları tek tek başlıklar ve nitelikler üzerinden kuruldu. Aynı yanıt
başlığı yüzeyinde, saldırı sınıfına özgü olmayan ama tümünün etkisini azaltan bir küme
daha vardır: bağlantının şifresiz kalmasını engelleyen, belgenin başka bir sayfa
tarafından çerçevelenmesini sınırlayan ve dışarı sızan adres bilgisini daraltan başlıklar.
Sonraki ders bu kümeyi ve eksikliklerini bulan bir yapılandırma denetimini ele alır.

### Güvenlik Başlıkları

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/guvenlik-basliklari

Taşıma güvenliğinin zorlanması, çerçevelemenin sınırlanması, referans bilgisinin daraltılması, tür tahmininin kapatılması ve kaynak yalıtımı; eksikleri bulan bir yapılandırma denetleyicisi.

Önceki üç ders belirli saldırı sınıflarına karşı belirli katmanlar kurdu. Bu ders, tek bir
sınıfa bağlı olmayan ama hepsinin etkisini azaltan bir kümeyi ele alır: yanıt başlıklarıyla
bildirilen ve tarayıcının uyguladığı kısıtlar.

Bu başlıkların ortak özelliği, uygulama kodunda değil sunucu yapılandırmasında
yaşamalarıdır. Bir sürüm çıkarken gözden kaçmaları kolaydır, çünkü hiçbir işlevi
bozmazlar; yokluklarının gözlenebilir sonucu yalnız bir şeyler ters gittiğinde ortaya
çıkar. Dersin sonundaki denetleyici bu görünmezliği giderir.

#### Taşıma Güvenliğinin Zorlanması

İnternet Nasıl Çalışır kursundaki HTTPS'in Rolü dersi şifreli taşımanın gizlilik, bütünlük
ve kimlik doğrulama sağladığını kurmuştu. Uygulama tümüyle şifreli sunuluyor olsa bile bir
boşluk kalır: kullanıcı adresi şemasız yazdığında ilk istek şifresiz gidebilir ve
yönlendirmeden önce araya girilebilir.

`Strict-Transport-Security` başlığı bu boşluğu kapatır. Tarayıcıya, bildirilen süre boyunca bu
konağa yalnız şifreli bağlanmasını söyler; şemasız yazılan adres ağa çıkmadan dönüştürülür. Üç
bileşeni vardır. `max-age` sürenin saniye cinsinden uzunluğudur ve anlamlı bir koruma için uzun
tutulur. `includeSubDomains` kuralı alt alanlara yayar; bu olmadan bir alt alan şifresiz kalabilir
ve çerez kapsamı üzerinden uygulamayı etkiler. Üçüncü bileşen, konağı tarayıcılara gömülü listeye
kaydeden ön yükleme bildirimidir; ilk isteği de kapsar, ama geri alınması yavaştır ve tüm alt
alanların şifreli sunulmasını kalıcı olarak taahhüt eder. Bu üçüncüsü, ilk ikisinin aksine
başlığın spesifikasyonunda tanımlı değildir: tarayıcı üreticilerinin işlettiği ayrı bir listedir.

Başlığın şifresiz bir yanıtta gönderilmesinin bir etkisi yoktur ve olmamalıdır: aksi
takdirde araya giren biri, konağı erişilemez kılan bir kayıt yazdırabilirdi.

#### Çerçevelemenin Sınırlanması

Kuzey Yamaç Ölçüm İstasyonu arayüzü, istasyon konumunu gösteren bir harita çerçevesi gömer.
Bu, çerçevelemenin bir yönüdür. Öteki yön, uygulamanın kendisinin başka bir sayfa
tarafından çerçevelenmesidir ve savunma gerektiren yön budur.

Görünmez kılınmış bir çerçeve içine alınan bir sayfa, kullanıcının gördüğü başka bir
arayüzün altında durur. Kullanıcı gördüğü düğmeye tıkladığını sanırken çerçeve içindeki
sayfanın bir denetimine tıklar; oturum kendisine ait olduğu için işlem gerçekleşir. Bu
sınıfın adı **tıklama hırsızlığıdır (clickjacking)** ve savunması, belgenin kimler
tarafından çerçevelenebileceğini bildirmektir.

İki mekanizma vardır. `X-Frame-Options` başlığı `DENY` veya `SAMEORIGIN` değerlerini alır
ve tek bir karar bildirir. İçerik güvenlik politikasının `frame-ancestors` yönergesi ise
kaynak listesi kabul eder ve daha ayrıntılıdır: yalnız belirli kaynakların çerçevelemesine
izin verilebilir. Yönergeyi anlayan bir gerçekleştirimde yönerge kazanır; ikisini birlikte
yazmak, yalnız birini anlayan gerçekleştirimlerde de koruma sağlar.

#### Referans Bilgisinin Daraltılması

Bir bağa tıklandığında veya bir alt kaynak yüklendiğinde tarayıcı, isteğe hangi sayfadan
gelindiğini bildiren bir başlık ekler. Tarayıcıların bir zamanlar öntanımlı saydığı davranış, tam
adresi başka kaynaklara da göndermekti. Ölçüm istasyonunda bu,
`.../istasyon/kuzey-yamac?olcum=4417` biçimindeki bir adresin yol ve sorgu bileşenlerinin üçüncü
bir tarafa sızması demekti.

`Referrer-Policy` başlığı bu davranışı daraltır. Değerler bir eksen üzerinde durur: hiçbir şey
göndermeyen uçtan, tam adresi gönderen uca kadar. Dengeli seçim, aynı kaynağa tam adresi, başka
kaynaklara yalnız kaynak dizgisini gönderen ve şifreliden şifresize geçişte hiçbir şey göndermeyen
değerdir — güncel tarayıcılarda başlık hiç yazılmadığında geçerli olan öntanım da budur. Başlık,
bundan daha dar bir davranış istendiğinde ya da öntanıma güvenilmediğinde açıkça yazılır. Analiz
ihtiyacı bu bilgiyi gerektiriyorsa, bilgi başlıktan değil uygulamanın kendi ölçümünden alınır.

#### Tür Tahmininin Kapatılması

Bir yanıtın içerik türü eksik veya yanlışsa, tarayıcı gövdeye bakıp türü tahmin edebilir.
Kullanıcıların dosya yüklediği bir uygulamada bu tahmin bir açığa dönüşür: metin olarak
sunulmak istenen bir yükleme, işaretleme gibi yorumlanırsa aynı kaynakta belge olarak
çalışır.

`X-Content-Type-Options: nosniff` başlığı tahmini kapatır ve tarayıcıyı bildirilen türe
uymaya zorlar. Başlık tek başına yetmez; bildirilen türün doğru olması, kullanıcı
yüklemelerinin ayrı bir kaynaktan sunulması ve indirme olarak işaretlenmesi aynı savunmanın
parçalarıdır.

#### Kaynak Yalıtımı ve Yetenek İzni

`Cross-Origin-Resource-Policy` başlığı, bir kaynağın başka kaynaklarca gömülüp
gömülemeyeceğini bildirir. Değeri `same-origin` olduğunda, kaynak yalnız kendi kaynağının
belgelerinde kullanılabilir. Bu, verinin başka bir belgenin bellek alanına çekilmesini
sınırlayan bir kısıttır.

Aynı ailede iki başlık daha bulunur: belgenin açtığı ve kendisini açan pencerelerle
tarayıcı bağlamı paylaşımını kesen başlık ve gömülen kaynakların açık izin bildirmesini
zorunlu kılan başlık. İkisi birlikte **kaynak yalıtımı (cross-origin isolation)** adı
verilen durumu kurar. Bu durum bazı yeteneklerin ön koşuludur ve yeteneklerin
kullanılmadığı bir uygulamada zorunlu değildir; gereksiz yere açıldığında meşru gömme
işlemlerini kırar.

Web'in Temelleri ve HTML kursunda tanıtılan **yetenek izni (permission policy)** ise ayrı
bir eksendir: belgenin ve gömdüğü çerçevelerin hangi aygıt yeteneklerini kullanabileceğini
sayar. Harita çerçevesinin konum yeteneğine ihtiyacı yoksa, çerçeveye o yetenek verilmez.

#### Yapılandırmayı Denetlemek

Bu başlıkların hepsi tek tek doğru yazılabilir ve bir sürüm sonra biri düşebilir. Denetimi
kural kümesine dönüştürmek, düşen başlığı görünür kılar.

```js
// basliklar.mjs — yanit basliklarini kural kumesine gore denetler
const BIR_YIL = 31536000;

const KURALLAR = [
  {
    ad: 'tasima guvenligi',
    denetle: (b) => {
      const deger = b['strict-transport-security'];
      if (!deger) return 'Strict-Transport-Security yok';
      const sure = Number(/max-age=(\d+)/.exec(deger)?.[1] ?? 0);
      if (sure < BIR_YIL) return `max-age ${sure} saniye, bir yildan kisa`;
      if (!/includeSubDomains/i.test(deger)) return 'includeSubDomains yok, alt alanlar disarida';
      return null;
    },
  },
  {
    ad: 'icerik guvenlik politikasi',
    denetle: (b) => {
      const deger = b['content-security-policy'];
      if (!deger) return 'Content-Security-Policy yok';
      if (/script-src[^;]*'unsafe-inline'/.test(deger)) return "script-src icinde 'unsafe-inline' var";
      if (!/frame-ancestors/.test(deger)) return 'frame-ancestors yonergesi yok';
      return null;
    },
  },
  {
    ad: 'cerceveleme',
    denetle: (b) =>
      b['x-frame-options'] || /frame-ancestors/.test(b['content-security-policy'] ?? '')
        ? null
        : 'cerceveleme sinirlanmamis',
  },
  {
    ad: 'tur tahmini',
    denetle: (b) => (b['x-content-type-options'] === 'nosniff' ? null : 'X-Content-Type-Options: nosniff yok'),
  },
  {
    ad: 'referans politikasi',
    denetle: (b) => {
      const deger = b['referrer-policy'];
      if (!deger) return 'Referrer-Policy yok';
      const gevsek = ['unsafe-url', 'no-referrer-when-downgrade', 'origin-when-cross-origin'];
      return gevsek.includes(deger) ? `gevsek deger: ${deger}` : null;
    },
  },
  {
    ad: 'kaynak paylasim politikasi',
    denetle: (b) => (b['cross-origin-resource-policy'] ? null : 'Cross-Origin-Resource-Policy yok'),
  },
  {
    ad: 'oturum cerezi',
    denetle: (b) => {
      const deger = b['set-cookie'];
      if (!deger) return null;
      const eksik = ['HttpOnly', 'Secure', 'SameSite'].filter((n) => !new RegExp(n, 'i').test(deger));
      return eksik.length ? `cerez nitelikleri eksik: ${eksik.join(', ')}` : null;
    },
  },
];

function denetle(ad, basliklar) {
  const bulgular = KURALLAR.map((k) => [k.ad, k.denetle(basliklar)]).filter(([, b]) => b !== null);
  console.log(`\n${ad}: ${bulgular.length} bulgu`);
  for (const [kural, bulgu] of bulgular) console.log('  -', `${kural}:`, bulgu);
  if (bulgular.length === 0) console.log('  - bulgu yok');
}

const ilkYayin = {
  'content-type': 'text/html; charset=utf-8',
  'strict-transport-security': 'max-age=86400',
  'content-security-policy': "default-src 'self'; script-src 'self' 'unsafe-inline'",
  'referrer-policy': 'unsafe-url',
  'set-cookie': 'olcum_oturum=abc; Path=/',
};

const duzeltilmis = {
  'content-type': 'text/html; charset=utf-8',
  'strict-transport-security': 'max-age=31536000; includeSubDomains',
  'content-security-policy': "default-src 'self'; script-src 'self' 'nonce-r4k2'; frame-ancestors 'none'",
  'x-frame-options': 'DENY',
  'x-content-type-options': 'nosniff',
  'referrer-policy': 'strict-origin-when-cross-origin',
  'cross-origin-resource-policy': 'same-origin',
  'set-cookie': 'olcum_oturum=abc; Path=/; HttpOnly; Secure; SameSite=Lax',
};

denetle('ilk yayin', ilkYayin);
denetle('duzeltilmis yapilandirma', duzeltilmis);
```

```
$ node basliklar.mjs

ilk yayin: 7 bulgu
  - tasima guvenligi: max-age 86400 saniye, bir yildan kisa
  - icerik guvenlik politikasi: script-src icinde 'unsafe-inline' var
  - cerceveleme: cerceveleme sinirlanmamis
  - tur tahmini: X-Content-Type-Options: nosniff yok
  - referans politikasi: gevsek deger: unsafe-url
  - kaynak paylasim politikasi: Cross-Origin-Resource-Policy yok
  - oturum cerezi: cerez nitelikleri eksik: HttpOnly, Secure, SameSite

duzeltilmis yapilandirma: 0 bulgu
  - bulgu yok
```

İlk yapılandırmanın kusurları tek tek küçüktür ve hiçbiri uygulamayı bozmaz: taşıma
kuralının süresi kısadır, politika satır içi koda izin verir, çerçeveleme sınırlanmamıştır,
oturum çerezi üç niteliğini de taşımaz. Bulgu listesi bunları tek bir çalıştırmada görünür
kılar.

Bu denetimin değeri, sürekli çalıştırılmasındadır. Yayın öncesi bir adım olarak
çalıştırıldığında, başlık düşüren bir yapılandırma değişikliği kendini bir sonraki güvenlik
gözden geçirmesinde değil, aynı gün gösterir. Kural kümesi de zamanla değişir; yeni bir
kural eklendiğinde bulgu listesinin uzaması beklenen davranıştır.

#### Özet

- Güvenlik başlıkları sunucu yapılandırmasında yaşar ve eksiklikleri hiçbir işlevi
  bozmadığı için sessizce oluşur.
- `Strict-Transport-Security` ilk istek boşluğunu kapatır; `includeSubDomains` olmadan alt
  alanlar korumasız kalır.
- Çerçeveleme `X-Frame-Options` veya `frame-ancestors` yönergesiyle sınırlanır; ikincisi
  kaynak listesi kabul ettiği için daha ayrıntılıdır.
- `Referrer-Policy` yol ve sorgu bilgisinin dışarı sızmasını daraltır;
  `X-Content-Type-Options: nosniff` tür tahminini kapatır.
- Başlık kümesini denetleyen bir kural çözümleyicisi, düşen bir başlığı aynı gün görünür
  kılar; denetim yayın akışının bir adımı olarak çalıştırılır.

#### Sonraki Adım

Buraya kadarki savunmalar uygulamanın kendi kodu ve kendi sunucusu üzerinde kuruldu.
Ölçüm arayüzü ise kendi yazmadığı kodu da çalıştırır: harita çerçevesi, ölçüm grafiğini
çizen dış betik, bağımlılık ağacındaki yüzlerce paket. Bu kodun her satırı, uygulamanın
kaynağının yetkileriyle çalışır. Sonraki ders üçüncü taraf kodun oluşturduğu riski, tedarik
zinciri boyunca nerede oluştuğunu ve alt kaynak bütünlüğü ile nasıl daraltılabileceğini
ele alır.

### Üçüncü Taraf Betik Riski

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/ucuncu-taraf-betik-riski

Kendi yazılmayan kodun sayfa kaynağının yetkileriyle çalışması; tedarik zincirinin halkaları, alt kaynak bütünlüğü karmasının üretimi ve doğrulanması, bütünlüğün kapsamı ve çalışma zamanı yalıtımı.

Önceki dersler uygulamanın kendi kodunu ve kendi sunucusunun yapılandırmasını konu aldı.
Kuzey Yamaç Ölçüm İstasyonu arayüzü ise kendi yazmadığı kodu da çalıştırır: ölçüm
grafiğini çizen dış betik, sayfaya gömülü harita çerçevesi ve derleme sırasında paketlenen
bağımlılık ağacı.

Bu ders o kodun taşıdığı riski ve riski daraltan katmanları kurar. Çıkış noktası tek bir
gözlemdir: sayfada çalışan üçüncü taraf betik, uygulamanın kendi betiğinden daha az
yetkili değildir.

#### Devralınan Yetki

İlk derste kurulan yetki kavramı burada yeniden geçerlidir. Bir betik ögesiyle sayfaya
alınan kod, belgenin kaynağı içinde çalışır. Aynı kaynak politikası onu yabancı saymaz:
belge ağacının tamamını okuyabilir ve değiştirebilir, `HttpOnly` işaretli olmayan çerezlere
erişebilir, yerel depolamayı okuyabilir, kullanıcının oturumuyla istek gönderebilir. Ölçüm
notu alanına yazılan metni, kullanıcı gönderme düğmesine basmadan da görebilir.

Bu yetki devri bir kusur değil, betik ögesinin tanımıdır. Kusur, yetkinin farkında
olmadan devredilmesidir. "Grafik çizen bir betik" cümlesi, betiğin yalnız grafik çizeceğini
söylemez; yalnız o an öyle davrandığını söyler.

Ölçüm grafiği betiği kendi başına iyi niyetli olabilir. Risk, betiğin bugünkü içeriğinin
yarın da aynı kalacağı varsayımındadır ve bu varsayım zincirin her halkasında ayrı ayrı
sınanmalıdır.

#### Tedarik Zincirinin Halkaları

Sayfada çalışan kodun kaynağına doğru geriye gidildiğinde birkaç ayrı güven noktası
görünür.

**Doğrudan bağımlılıklar**, geliştiricinin bilerek eklediği paketlerdir; sayıları
gözden geçirilebilir düzeydedir. **Dolaylı bağımlılıklar**, o paketlerin kendi
bağımlılıklarıdır ve sayıları hızla büyür. Üretim paketine giren kodun büyük bölümü
genellikle bu ikinci kümeden gelir ve hiçbir zaman tek tek okunmamıştır.

**Paket kayıt defteri**, sürümlerin dağıtıldığı yerdir. Bir sürümün yayımlanması, o paketin
bakımcısının hesabına erişimi olan herkesin yapabildiği bir işlemdir. **Derleme araçları**
kendi eklentileriyle birlikte gelir ve derleme aşamasında çalıştırılan kod, üretim
paketinin içeriğini belirler.

**İçerik dağıtım ağı** üzerinden çalışma zamanında yüklenen betikler, zincirin en kırılgan
halkasıdır: kod uygulamanın deposunda hiç bulunmaz, sürüm sabitlenmemişse her sayfa
yüklemesinde farklı olabilir ve değişiklik hiçbir derleme kaydına düşmez.

Zincirin uzunluğu bir kural doğurur: her halka için "bu içerik değiştiğinde bunu ne haber
verir" sorusunun bir yanıtı olmalıdır. Aşağıdaki mekanizma bu soruyu çalışma zamanı halkası
için yanıtlar.

#### Alt Kaynak Bütünlüğü

**Alt kaynak bütünlüğü (subresource integrity)**, bir betik veya biçem ögesine, yüklenecek
içeriğin özetini önceden bildirme olanağı verir. Tarayıcı içeriği getirir, özetini
hesaplar ve bildirilen değerle karşılaştırır; uyuşmazsa kaynağı kullanmaz.

```js
// butunluk.mjs — alt kaynak butunlugu karmasi uretir ve dogrular
import { createHash } from 'node:crypto';

const BETIK_ILK = 'export function olcumGrafigi(veri) { return veri.map((n) => n.sicaklik); }\n';
const BETIK_DEGISMIS = 'export function olcumGrafigi(veri) { return veri.map((n) => n.sicaklik ?? 0); }\n';

function butunlukDegeri(icerik, algoritma = 'sha384') {
  const karma = createHash(algoritma).update(icerik, 'utf8').digest('base64');
  return `${algoritma}-${karma}`;
}

function dogrula(icerik, beklenenler) {
  const liste = beklenenler.trim().split(/\s+/);
  for (const beklenen of liste) {
    const [algoritma] = beklenen.split('-');
    if (butunlukDegeri(icerik, algoritma) === beklenen) return { gecerli: true, eslesen: beklenen };
  }
  return { gecerli: false, eslesen: null };
}

const bildirilen = `${butunlukDegeri(BETIK_ILK)} ${butunlukDegeri(BETIK_ILK, 'sha512')}`;
console.log('bildirilen integrity degeri:');
console.log(' ', bildirilen.split(' ')[0]);
console.log(' ', bildirilen.split(' ')[1]);

console.log('\nteslim edilen icerik degerlendirmesi:');
for (const [ad, icerik] of [['yayindaki surum', BETIK_ILK], ['degistirilmis surum', BETIK_DEGISMIS]]) {
  const sonuc = dogrula(icerik, bildirilen);
  console.log(' ', ad.padEnd(22), sonuc.gecerli ? 'yuklenir' : 'reddedilir');
}

console.log('\nkarma karsilastirmasi (sha384):');
console.log(' ', 'yayindaki'.padEnd(14), butunlukDegeri(BETIK_ILK).slice(0, 34) + '...');
console.log(' ', 'degistirilmis'.padEnd(14), butunlukDegeri(BETIK_DEGISMIS).slice(0, 34) + '...');
```

```
$ node butunluk.mjs
bildirilen integrity degeri:
  sha384-ZkRpwsatJjtybxkzgNKSmNTOnRaRUzSzw9F6JovPGdvv5p8LDezsXzf1oQJwgSSa
  sha512-csD06cIQYqXhn6nHO3g1ZVsnr7JYYDUqLYC/8fr2S3YiuOXbxS71oHDXxSCYlr2vntrI5NJPZrw9djvCNy5Zww==

teslim edilen icerik degerlendirmesi:
  yayindaki surum        yuklenir
  degistirilmis surum    reddedilir

karma karsilastirmasi (sha384):
  yayindaki      sha384-ZkRpwsatJjtybxkzgNKSmNTOnRa...
  degistirilmis  sha384-FVN8BPXsL5tcc6wAQtU46Q0KIEA...
```

İki sürüm arasındaki fark tek bir işleçtir; karmaların başlangıcı bile ortak değildir.
Özet işlevinin bu davranışı mekanizmanın tamamını taşır: içerikte tek karakter değişirse
karma tanınmaz hâle gelir ve doğrulama kalır.

Bildirilen değer birden çok karma taşıyabilir. Tarayıcı desteklediği en güçlü algoritmayı
seçer; bir algoritmanın gelecekte güvenilmez sayılması durumunda liste, ögeyi
değiştirmeden geçişe olanak verir.

İki gerçekleştirme kuralı bağlayıcıdır. Birincisi, kaynaklar arası yüklenen bir öge için
`crossorigin` özniteliği gerekir; bütünlük denetimi, içeriği okunamayan saydam bir yanıt
üzerinde yapılamaz. İkincisi, karmanın bir anlamı olması için adresin sürümlenmiş olması
gerekir. "En son sürüm" anlamına gelen bir adres, sağlayıcı içeriği güncellediğinde
doğrulamayı kırar; bu kırılma bir kusur değil, mekanizmanın çalıştığının kanıtıdır — ama
uygulamanın çalışmayı sürdürmesi isteniyorsa adresin sabit olması gerekir.

#### Bütünlüğün Kapsamadığı Alan

Bütünlük denetimi, yüklenen baytların beklenen baytlar olduğunu doğrular. O baytların ne
yaptığını doğrulamaz. Sağlayıcı yeni bir sürüm yayımlar, uygulama yeni karmayı bildirir ve
yeni sürüm ilk sürümün yapmadığı bir şey yapıyorsa denetim hiçbir uyarı üretmez.

Denetim ayrıca yalnız bildirilen ögeyi kapsar. Yüklenen betiğin kendisi çalışma zamanında başka
betikler getiriyorsa, o getirmeler bu ögeye bağlı değildir. İçerik güvenlik politikasının kaynak
listeleri bu boşluğun bir bölümünü kapatır: bütünlük "bu adresten ne geleceğini" sabitler,
politika ise "hangi adreslerden yükleme yapılabileceğini" sınırlar. İki mekanizma birbirinin
yerine geçmez.

#### Çalışma Zamanında Eklenen Kod

Bir sınıf araç, sayfaya betik eklemeyi kod değişikliğinden ayırır: bir yönetim arayüzünden
tanımlanan kod parçaları, uygulamanın yükleyicisi tarafından çalışma zamanında sayfaya
alınır. Ölçüm istasyonunda bu yolla eklenen bir kullanım ölçüm betiği, uygulamanın
deposunda hiç görünmez.

Bu düzenlemenin güvenlik açısından iki sonucu vardır. Birincisi, sayfada çalışan kodun
kümesi artık depodan okunamaz; envanter ancak çalışan sayfadan çıkarılabilir. İkincisi,
sayfaya kod ekleme yetkisi, kod gözden geçirmesinden geçmeyen bir gruba açılmıştır ve o
grubun hesap güvenliği uygulamanın güvenliğinin parçası hâline gelir.

Daraltma yolları üçtür. İçerik güvenlik politikasının kaynak listeleri yükleyicinin
getirebileceği adresleri sınırlar; bu, aracı bir yükleme noktası olmaktan çıkarmaz ama
sınırsız olmaktan çıkarır. Politikanın rapor kipiyle toplanan ihlal kayıtları, beklenmeyen
bir yükleme noktasını görünür kılar. Üçüncüsü yönetimseldir: sayfada çalışan üçüncü taraf
kodun listesi yazılı tutulur, her kalem bir sahiple ve bir gerekçeyle eşleşir, gerekçesi
kalmayan kalem kaldırılır. Envanteri olmayan bir sayfada bu dersteki hiçbir önlem
tamamlanmış sayılmaz.

#### Yalıtım ve Derleme Zamanı Önlemleri

Bir üçüncü taraf bileşenin sayfada arayüz göstermesi gerekiyorsa, betik ögesi yerine ayrı
bir kaynaktan yüklenen bir çerçeve kullanmak yetki devrini keser. Web'in Temelleri ve HTML
kursunda tanıtılan **yalıtım (sandboxing)** öznitelikleri, çerçevenin hangi yetenekleri
kullanabileceğini sayar; yetenek izni başlığı aygıt erişimini daraltır. İletişim, ana
belgeyle çerçeve arasında dar bir ileti arayüzü üzerinden kurulur ve gelen iletiler
gönderen kaynağına göre doğrulanır. Harita çerçevesi bu yapının tipik örneğidir: haritanın
ölçüm notlarını okuması için hiçbir neden yoktur ve çerçeve bunu yapısal olarak
engeller.

Derleme zamanı tarafında dört önlem birlikte çalışır. **Kilit dosyası**, çözümlenmiş
sürümleri ve içerik özetlerini deponun içine yazar; kilit dosyasını yok sayan bir kurulum,
bu savunmanın tamamını devre dışı bırakır. **Sürüm sabitleme**, bir sonraki kurulumun aynı
kodu getirmesini sağlar. **Bilinen açık taraması**, bağımlılıklardaki yayımlanmış açık
kayıtlarını kurulumla eşler. **Paket içeriğinin ölçülmesi**, üretim paketine giren kodun
boyutunu ve bileşimini görünür kılar; bir bağımlılığın beklenmedik biçimde büyümesi çoğu
zaman ilk sinyaldir.

Son bir önlem yönetimseldir: yeni bir bağımlılık eklemek bir kod değişikliğidir ve aynı
gözden geçirmeden geçer. Bir işlevi kendi yazmakla bir paket eklemek arasındaki seçim, satır
sayısı karşılaştırması değil, güven yüzeyi kararıdır.

#### Özet

- Sayfaya betik ögesiyle alınan üçüncü taraf kod, uygulamanın kaynağının bütün yetkilerini
  devralır; belge ağacına, çerezlere ve ağa erişimi kendi kodundan farksızdır.
- Tedarik zinciri doğrudan ve geçişli bağımlılıklar, kayıt defteri, derleme araçları ve
  dağıtım ağından oluşur; her halka için değişikliği haber veren bir mekanizma gerekir.
- Alt kaynak bütünlüğü, yüklenecek içeriğin özetini önceden bildirir; tek karakterlik bir
  değişiklik karmayı tanınmaz kılar ve kaynak kullanılmaz.
- Bütünlük baytları doğrular, davranışı doğrulamaz ve yalnız bildirilen ögeyi kapsar;
  politikanın kaynak listeleriyle birlikte kullanılır.
- Arayüz gösteren üçüncü taraf bileşen ayrı kaynaktan yüklenen yalıtılmış bir çerçeveye
  taşınır; derleme tarafında kilit dosyası, sürüm sabitleme, açık taraması ve paket
  ölçümü birlikte çalışır.

#### Sonraki Adım

Bu konudaki altı ders belirli mekanizmalara odaklandı: kaçırma, politika, paylaşım
başlıkları, istek belirteci, güvenlik başlıkları ve bütünlük. Uygulamada karşılaşılan
açıkların çoğu bu mekanizmaların kapsadığı sınıflara girer, ama hepsi girmez. Sonraki ders
sık görülen açık sınıflarını istemci tarafındaki izdüşümleriyle ele alır: arayüzde verilen
yetki kararları, adres yönlendirmeleri ve istemciye ulaşan veriye duyulan güven.

### Yaygın Web Riskleri

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/yaygin-web-riskleri

Sık görülen açık sınıflarının istemci tarafındaki karşılıkları; arayüzde verilen yetki kararları, açık yönlendirme, istemciye ulaşan veriye duyulan güven ve pakete sızan sırlar.

Bu konudaki altı ders belirli mekanizmalar kurdu: çıkışta kaçırma, içerik güvenlik
politikası, kaynaklar arası paylaşım kuralları, istek belirteçleri, güvenlik başlıkları ve
alt kaynak bütünlüğü. Her biri adı konmuş bir saldırı sınıfına karşılık geliyordu.

Uygulamada karşılaşılan sorunların bir bölümü bu sınıfların dışında kalır ve ortak bir
kökene sahiptir: istemcinin bir güvenlik sınırı sanılması. Bu ders, sık görülen açık
sınıflarını istemci tarafındaki izdüşümleriyle ele alır ve her biri için ölçülebilir bir
kural verir.

#### Arayüzde Gizlemek Yetki Vermemekle Aynı Şey Değil

Kuzey Yamaç Ölçüm İstasyonu arayüzünde ölçüm kaydını düzenleme düğmesi yalnız düzenleyici
rolündeki kullanıcılara gösteriliyor. Bu doğru bir arayüz kararıdır ve yanlış bir güvenlik
kararıdır. Düğmenin görünmemesi, düğmenin tetiklediği isteğin gönderilemeyeceği anlamına
gelmez; istek bir adres ve bir gövdeden ibarettir.

```js
// yetki.mjs — istemci gizlemesi ile sunucu yetkilendirmesinin bagimsizligi
function arayuzGosterir(kullanici) {
  return kullanici.roller.includes('duzenleyici');
}

function sunucuIzinVerir(oturum, kayit) {
  if (!oturum.dogrulandi) return false;
  if (!oturum.roller.includes('duzenleyici')) return false;
  return kayit.istasyon === oturum.istasyon;
}

const kayit = { istasyon: 'kuzey-yamac', olcumNo: 4417 };
const durumlar = [
  ['duzenleyici, kendi istasyonu', { dogrulandi: true, roller: ['duzenleyici'], istasyon: 'kuzey-yamac' }],
  ['duzenleyici, baska istasyon', { dogrulandi: true, roller: ['duzenleyici'], istasyon: 'guney-sirt' }],
  ['yalniz okuyucu', { dogrulandi: true, roller: ['okuyucu'], istasyon: 'kuzey-yamac' }],
  ['dogrulanmamis', { dogrulandi: false, roller: ['duzenleyici'], istasyon: 'kuzey-yamac' }],
];

console.log('durum'.padEnd(30), 'dugme gorunur', 'istek kabul');
for (const [ad, oturum] of durumlar) {
  const gorunur = arayuzGosterir({ roller: oturum.roller }) ? 'evet' : 'hayir';
  const kabul = sunucuIzinVerir(oturum, kayit) ? 'evet' : 'hayir';
  console.log(ad.padEnd(30), gorunur.padEnd(14), kabul);
}
```

```
$ node yetki.mjs
durum                          dugme gorunur istek kabul
duzenleyici, kendi istasyonu   evet           evet
duzenleyici, baska istasyon    evet           hayir
yalniz okuyucu                 hayir          hayir
dogrulanmamis                  evet           hayir
```

İki sütunun ayrı ayrı hesaplanması bilerek yapıldı. İkinci ve dördüncü satırlar, arayüzün "evet"
dediği ama sunucunun "hayır" dediği durumlardır; arayüz tarafındaki karar yalnız rolü görür;
sunucu tarafındaki karar ise kaydın hangi istasyona ait olduğunu ve oturumun doğrulanıp
doğrulanmadığını da görür.

Bu ayrım iki alt sınıfın kökenidir. **Bozuk nesne düzeyinde erişim denetimi**, bir kaydın
tanımlayıcısı değiştirildiğinde sunucunun sahiplik denetimi yapmamasıdır; arayüz o kaydı
hiç göstermediği için sorun geç fark edilir. **Dikey yetki yükselmesi**, yalnız arayüzde
gizlenmiş bir yönetim işleminin doğrudan çağrılabilmesidir. İkisinin de savunması aynı
cümledir: her istek, isteği yapanın kim olduğuna ve neye eriştiğine bakılarak sunucuda
yeniden değerlendirilir.

Aynı kural doğrulamaya da uygulanır. Formların istemci tarafındaki kısıt doğrulaması bir
kullanılabilirlik özelliğidir; hatayı kullanıcıya erken gösterir. Veri bütünlüğünü sağlayan
denetim sunucudakidir ve istemci tarafındaki kuralın varlığı onu gereksizleştirmez.

#### Açık Yönlendirme

Giriş akışı, kullanıcıyı doğrulamadan sonra geldiği sayfaya geri gönderir. Hedef adres
genellikle sorgu dizgisinde taşınır ve doğrulanmazsa uygulama, herhangi bir adrese
yönlendirme yapan bir araca dönüşür. Zararı doğrudan değildir: uygulamanın alan adı,
kullanıcıyı başka bir yere göndermek için kullanılır.

```js
// yonlendirme.mjs — gerisdonus adresini izin listesine gore dogrular
const KENDI_KAYNAGI = 'https://olcum.ornek.test';
const IZINLI_KAYNAKLAR = new Set([KENDI_KAYNAGI, 'https://rapor.ornek.test']);
const VARSAYILAN = KENDI_KAYNAGI + '/istasyon';

function gerisdonusHedefi(ham) {
  if (typeof ham !== 'string' || ham === '') return VARSAYILAN;
  let adres;
  try {
    adres = new URL(ham, KENDI_KAYNAGI);
  } catch {
    return VARSAYILAN;
  }
  if (!['http:', 'https:'].includes(adres.protocol)) return VARSAYILAN;
  if (!IZINLI_KAYNAKLAR.has(adres.origin)) return VARSAYILAN;
  return adres.href;
}

const adaylar = [
  '/istasyon/kuzey-yamac?gun=12',
  'https://rapor.ornek.test/ozet',
  'https://baska.ornek.test/giris',
  '//baska.ornek.test/giris',
  'https://olcum.ornek.test.baska.test/giris',
  'olcum-uygulamasi://ac',
  '',
];

for (const aday of adaylar) {
  console.log(gerisdonusHedefi(aday).padEnd(52), '<-', aday === '' ? '(bos)' : aday);
}
```

```
$ node yonlendirme.mjs
https://olcum.ornek.test/istasyon/kuzey-yamac?gun=12 <- /istasyon/kuzey-yamac?gun=12
https://rapor.ornek.test/ozet                        <- https://rapor.ornek.test/ozet
https://olcum.ornek.test/istasyon                    <- https://baska.ornek.test/giris
https://olcum.ornek.test/istasyon                    <- //baska.ornek.test/giris
https://olcum.ornek.test/istasyon                    <- https://olcum.ornek.test.baska.test/giris
https://olcum.ornek.test/istasyon                    <- olcum-uygulamasi://ac
https://olcum.ornek.test/istasyon                    <- (bos)
```

Dördüncü ve beşinci satırlar denetimin neden dizgi karşılaştırmasıyla yazılamayacağını
gösterir. Şema olmadan iki bölü işaretiyle başlayan bir adres, sayfanın şemasını devralarak
başka bir konağa gider; "kendi alan adımızla başlıyor" ölçütü ise `ornek.test.baska.test`
adını geçirir. Karşılaştırma çözümlenmiş adresin `origin` bileşeni üzerinde yapılır ve
doğrulanamayan her girdi izin listesine değil, sabit bir varsayılana düşer.

#### İstemciye Ulaşan Veriye Duyulan Güven

Yerel depolamadan okunan bir tercih kaydı, adres parçasından çözülen bir durum veya bir
uçtan gelen yapılandırma nesnesi, uygulamanın kendi ürettiği veri değildir. Bu veriyi
uygulama nesnelerine birleştiren kod, anahtar adlarının da veri olduğunu varsaymalıdır.

```js
// birlestirme.mjs — disaridan gelen ayarlari prototip zincirine dokunmadan birlestirir
const TEHLIKELI_ANAHTARLAR = new Set(['__proto__', 'prototype', 'constructor']);

function guvenliBirlestir(taban, gelen) {
  const sonuc = Object.create(null);
  for (const [anahtar, deger] of Object.entries(taban)) sonuc[anahtar] = deger;
  for (const [anahtar, deger] of Object.entries(gelen)) {
    if (TEHLIKELI_ANAHTARLAR.has(anahtar)) {
      console.log('  atlanan anahtar:', anahtar);
      continue;
    }
    sonuc[anahtar] = deger;
  }
  return sonuc;
}

const varsayilan = { birim: 'santigrat', esik: -5, otomatikYenile: true };
const kayittanGelen = JSON.parse('{"esik": -12, "__proto__": {"yonetici": true}, "birim": "kelvin"}');

console.log('birlestirme:');
const ayarlar = guvenliBirlestir(varsayilan, kayittanGelen);

console.log('sonuc:', JSON.stringify(ayarlar));
console.log('prototip zinciri:', Object.getPrototypeOf(ayarlar));
console.log('sirada olmayan nesnede yonetici alani:', ({}).yonetici);
```

```
$ node birlestirme.mjs
birlestirme:
  atlanan anahtar: __proto__
sonuc: {"birim":"kelvin","esik":-12,"otomatikYenile":true}
prototip zinciri: null
sirada olmayan nesnede yonetici alani: undefined
```

JavaScript'te Nesneler ve Fonksiyonlar kursunda tanıtılan prototip zinciri burada **prototip
kirlenmesi** adıyla bilinen bir saldırı yüzeyine dönüşür: ortak bir prototipe yazılan bir alan, o
prototipten türeyen bütün nesnelerde görünür. Savunma iki parçalıdır. Birleştirme sırasında
zincire ulaşan anahtarlar atlanır ve hedef nesne **prototipsiz nesne** olarak oluşturulur; son
satır, ilgisiz bir nesnenin etkilenmediğini doğrular.

Aynı ilkenin başka bir uygulaması, dış veriyi bir şemaya karşı doğrulamaktır. Uygulama
Mimarisi kursundaki doğrulama şemaları dersinde kurulan yaklaşım burada güvenlik değeri
kazanır: beklenen alanları sayan bir şema, beklenmeyen anahtarları yapısal olarak eler.

#### Pakete Sızan Sırlar

İstemciye gönderilen her şey herkese açıktır. Bu cümle bir uyarı değil, bir tanımdır: paket
dosyası indirilebilir, ağ paneli açılabilir, kaynak haritası okunabilir.

Buradan üç kural çıkar. Bir dış hizmetin gizli anahtarı istemci paketine konmaz; istemcinin
gerektirdiği çağrılar kendi sunucusu üzerinden yapılır. Yalnız istemci tarafında anlamlı
olan yapılandırma ile sunucu tarafındaki sır ayrı yerlerde tutulur ve derleme aracının
istemci paketine hangi değişkenleri gömdüğü açıkça denetlenir. Kaynak haritalarının üretim
ortamında herkese açık sunulup sunulmayacağı bilinçli bir karardır; hata izleme için
gerekiyorsa, tarayıcıya değil hata toplama katmanına verilir.

Aynı kural hata yüzeyine de uygulanır. İstemciye dönen hata iletileri iç yapı bilgisi
taşımaz; istemciden toplanan hata kayıtları ise kullanıcı verisi taşıyabilir ve toplanmadan
önce ayıklanmalıdır.

#### Sınıfları Bir Arada Okumak

Bu konudaki yedi dersin ortak dokusu tek bir ölçütte toplanır: bir denetimin nerede
çalıştığı, o denetimin ne kadar güvenilebilir olduğunu belirler. İstemcide çalışan her
denetim kullanıcı deneyimine hizmet eder ve kullanıcının denetimindedir. Tarayıcının
uyguladığı denetimler (aynı kaynak politikası, içerik güvenlik politikası, çerez
nitelikleri, bütünlük doğrulaması) sunucunun bildirdiği kurallara dayanır ve kullanıcının
tarayıcısını korur. Sunucuda çalışan denetimler ise uygulamanın verisini korur ve
diğerlerinin hiçbiri onların yerine geçmez.

Üç katmanın karıştırılması, bu konuda anlatılan açık sınıflarının çoğunun ortak nedenidir.

#### Özet

- Arayüzde bir denetimi gizlemek yetkilendirme değildir; her istek sunucuda, isteği yapanın
  kimliğine ve eriştiği kayda bakılarak yeniden değerlendirilir.
- İstemci tarafındaki kısıt doğrulaması kullanılabilirlik içindir; veri bütünlüğünü
  sunucudaki doğrulama sağlar.
- Yönlendirme hedefi çözümlenmiş adresin kaynağı üzerinden izin listesine karşı sınanır;
  doğrulanamayan girdi sabit bir varsayılana düşer.
- Dış kaynaktan gelen nesnelerde anahtar adları da veridir; birleştirme prototip zincirine
  ulaşan anahtarları eler ve hedefi prototipsiz nesne olarak kurar.
- İstemciye gönderilen her şey herkese açıktır; gizli anahtarlar pakete girmez, kaynak
  haritalarının yayımlanması bilinçli bir karardır.
- Bir denetimin güvenilirliği nerede çalıştığına bağlıdır: istemci, tarayıcı ve sunucu
  katmanları birbirinin yerine geçmez.

#### Sonraki Adım

Bu konu boyunca kurulan savunmaların ortak bir zayıflığı vardır: hepsi sessizce bozulabilir.
Bir kaçırma noktası atlanır, bir politika yönergesi gevşetilir, bir çerez niteliği düşer, bir
bütünlük değeri güncellenmeden bırakılır — ve arayüz her durumda çalışmayı sürdürür. Kalitenin
son ekseni bu yüzden ölçmedir. Sonraki konu, davranışın beklendiği gibi kaldığını doğrulayan
sınamalarla başlar: Birim ve Bileşen Testleri dersi, bileşenlerin gerçekten ne yaptığını
davranış üzerinden sınamanın kurallarını kurar.

## Test ve İzleme

Davranış odaklı bileşen sınama, erişilebilir rollere göre eleman seçimi, uçtan uca akış doğrulama, görsel gerileme karşılaştırması, kırılgan testlerin nedenleri ve üretimden dönen alan verisiyle hata kayıtlarının okunması.

### Birim ve Bileşen Testleri

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/birim-ve-bilesen-testleri

Bileşen testinin birim testinden ayrıldığı sınır, davranış sözleşmesi ile uygulama ayrıntısının ayrılması ve aynı yeniden düzenlemede iki test biçiminin nasıl farklı davrandığı.

Güvenlik konusu bir savunma katmanı dizisiyle kapandı: çıktı kaçırma, içerik güvenlik
politikası, kaynak kısıtları, istek sahteciliğine karşı belirteçler, güvenlik başlıkları ve
üçüncü taraf betik denetimi. Başarım konusu bir bütçe bıraktı, erişilebilirlik konusu bir
ölçüt kümesi. Bu kararların hepsi aynı soruyu sorar: kurulan şey bozulduğunda bunu ne haber
verecek?

Elle denetim bu soruya yanıt vermez. Bir savunmanın kaldırıldığını, bir rolün düştüğünü ya
da bir bütçenin aşıldığını fark etmek için o değişikliği gören bir mekanizma gerekir. Bu
konu, o mekanizmayı iki yönden kurar: değişiklikten önce çalışan testler ve yayımdan sonra
çalışan izleme. Konunun ilk sorusu en dar olanıdır — bir bileşenin doğru çalıştığı nasıl
sınanır?

#### Sınanan Sınır

Test türleri ne sınadıklarıyla değil, **nereye kadar uzandıklarıyla** ayrılır.

**Birim testi (unit test)** tek bir fonksiyonu ya da modülü, dış dünyaya dokunmadan sınar.
Süzgeç ölçütünü ölçüm listesine uygulayan saf fonksiyon bu kapsama girer. Node.js Çalışma
Zamanı kursundaki Test dersinde kurulan biçim aynen geçerlidir.

**Bileşen testi (component test)** bir bileşeni özellikleriyle kurar, ona kullanıcı olayı
gönderir ve çizilen sonucu inceler. Sınırı bileşenin kendi ağacıdır: dışarıdaki ağ, adres
çubuğu ve depolama bu sınırın ötesindedir.

**Tümleşim testi (integration test)** birkaç bileşeni ya da bir bileşenle veri katmanını
birlikte sınar. **Uçtan uca test (end-to-end test)** ise uygulamayı gerçek bir tarayıcıda
çalıştırır; üçüncü ders onu ele alır.

Bileşen testinin ayırt edici yanı, girdisinin de çıktısının da kullanıcı sözlüğünde
olmasıdır: girdi bir olay, çıktı ekranda görünen şeydir. Testin bileşenin içine bakması
gerekmez — ve bakmaması, bu dersin asıl konusudur.

#### Küçük Bir Bileşen Modeli

Bileşen sınamanın mantığını görmek için bir çatıya gerek yoktur. Aşağıdaki yirmi beş satır
bir bileşen modelinin özünü verir: durum, o durumdan üretilen ağaç ve durumu ilerleten olay.

```js
// bilesen-modeli.mjs — kucuk bir bilesen modeli: durum, cizim, olay
export function o(tur, ozellikler = {}, ...cocuklar) {
  return { tur, ozellikler, cocuklar: cocuklar.flat() };
}

export function bagla(bilesen) {
  let durum = bilesen.baslangic();
  const ornek = {
    durum: () => durum,
    agac: () => bilesen.ciz(durum),
    gonder(olay) {
      durum = bilesen.indirge(durum, olay);
      return ornek;
    },
  };
  return ornek;
}

export function* dolas(dugum) {
  if (dugum === null || typeof dugum !== 'object') return;
  yield dugum;
  for (const cocuk of dugum.cocuklar ?? []) yield* dolas(cocuk);
}

export function metin(dugum) {
  if (typeof dugum === 'string') return dugum;
  return (dugum.cocuklar ?? []).map(metin).join(' ').trim();
}
```

`bagla` bir bileşeni sınama için ayağa kaldırır: durumu tutar, olay geldiğinde indirgeyiciyi
çağırır ve her istendiğinde güncel ağacı üretir. Bileşen Tabanlı Arayüz Geliştirme kursundaki
bildirimsel model burada en yalın hâlindedir; ağacın gerçekten belgeye yazılması sınama için
gerekli değildir.

Sınanacak bileşen, Kuzey Yamaç arayüzünün ölçüm tablosudur: bir süzgeç alanı ve eşleşen
ölçümleri gösteren bir tablo.

```js
// olcum-tablosu.mjs — surum 1
import { o } from './bilesen-modeli.mjs';

const OLCUMLER = [
  { istasyon: 'kuzey-yamac-01', deger: 21.4 },
  { istasyon: 'kuzey-yamac-02', deger: 19.8 },
  { istasyon: 'dogu-sirt-01', deger: 24.1 },
  { istasyon: 'vadi-tabani-03', deger: 17.2 },
];

export const OlcumTablosu = {
  baslangic: () => ({ suzgecMetni: '', olcumler: OLCUMLER }),

  indirge(durum, olay) {
    if (olay.tur === 'suzgec-degisti') return { ...durum, suzgecMetni: olay.deger };
    return durum;
  },

  ciz(durum) {
    const gorunen = durum.olcumler.filter((x) => x.istasyon.includes(durum.suzgecMetni));
    return o('section', { class: 'olcum-tablosu' },
      o('label', { for: 'suzgec' }, 'Istasyon suzgeci'),
      o('input', { id: 'suzgec', type: 'search', value: durum.suzgecMetni }),
      o('table', { class: 'tablo' },
        ...gorunen.map((x) =>
          o('tr', { class: 'satir' },
            o('td', {}, x.istasyon),
            o('td', {}, x.deger.toFixed(1)))),
      ),
      o('p', { class: 'ozet' }, `${gorunen.length} olcum`),
    );
  },
};
```

#### İki Test Biçimi

Aynı bileşen iki farklı biçimde sınanabilir. Birinci biçim bileşenin içine bakar: durum
nesnesinin alan adına ve ağaçtaki çocuk sırasına dayanır. İkinci biçim yalnız dışarıdan
görüneni doğrular.

```js
// olcum-tablosu.test.mjs
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { bagla, dolas, metin } from './bilesen-modeli.mjs';
import { OlcumTablosu } from './olcum-tablosu.mjs';

function satirMetinleri(agac) {
  return [...dolas(agac)].filter((d) => d.tur === 'tr').map(metin);
}

describe('uygulama ayrintisina bagli', () => {
  test('suzgec durumu yazilan degeri tutar', () => {
    const ornek = bagla(OlcumTablosu);
    ornek.gonder({ tur: 'suzgec-degisti', deger: 'kuzey' });
    assert.equal(ornek.durum().suzgecMetni, 'kuzey');
  });

  test('ikinci cocuk girdi ogesidir', () => {
    const ornek = bagla(OlcumTablosu);
    assert.equal(ornek.agac().cocuklar[1].tur, 'input');
  });
});

describe('davranisa bagli', () => {
  test('suzgec yazildiginda yalniz eslesen istasyonlar kalir', () => {
    const ornek = bagla(OlcumTablosu);
    ornek.gonder({ tur: 'suzgec-degisti', deger: 'kuzey' });
    assert.deepEqual(satirMetinleri(ornek.agac()), [
      'kuzey-yamac-01 21.4',
      'kuzey-yamac-02 19.8',
    ]);
  });

  test('eslesme yoksa tablo bosalir ve ozet sifir der', () => {
    const ornek = bagla(OlcumTablosu);
    ornek.gonder({ tur: 'suzgec-degisti', deger: 'zirve' });
    assert.deepEqual(satirMetinleri(ornek.agac()), []);
    assert.match(metin(ornek.agac()), /0 olcum/);
  });
});
```

Üç dosya aynı dizinde durur ve test koşucusu çalışma zamanının kendi parçasıdır:

```sh
node --test olcum-tablosu.test.mjs
```

```
▶ uygulama ayrintisina bagli
  ✔ suzgec durumu yazilan degeri tutar (0.260292ms)
  ✔ ikinci cocuk girdi ogesidir (0.096083ms)
✔ uygulama ayrintisina bagli (0.96575ms)
▶ davranisa bagli
  ✔ suzgec yazildiginda yalniz eslesen istasyonlar kalir (0.350458ms)
  ✔ eslesme yoksa tablo bosalir ve ozet sifir der (0.087291ms)
✔ davranisa bagli (0.50475ms)
ℹ tests 4
ℹ suites 2
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.797459
```

Süre alanları ortama bağlıdır ve her koşumda değişir; anlamlı olan sayaçlardır. Dördü de
geçiyor. Bu noktada iki biçim arasında bir fark görünmüyor.

#### Yeniden Düzenleme Sınavı

Fark, kod değiştiğinde ortaya çıkar. Aşağıdaki sürüm iki değişiklik yapıyor: durum alanının
adı `suzgecMetni` yerine `sorgu` oldu ve süzgeç alanı ile etiketi bir kapsayıcının içine
alındı. Süzme mantığı ayrı bir fonksiyona taşındı. Kullanıcının gördüğü hiçbir şey
değişmedi.

```js
// olcum-tablosu.mjs — surum 2: alan adi degisti, suzgec paneli sarmalandi
import { o } from './bilesen-modeli.mjs';

const OLCUMLER = [
  { istasyon: 'kuzey-yamac-01', deger: 21.4 },
  { istasyon: 'kuzey-yamac-02', deger: 19.8 },
  { istasyon: 'dogu-sirt-01', deger: 24.1 },
  { istasyon: 'vadi-tabani-03', deger: 17.2 },
];

function suz(olcumler, sorgu) {
  return olcumler.filter((x) => x.istasyon.includes(sorgu));
}

export const OlcumTablosu = {
  baslangic: () => ({ sorgu: '', olcumler: OLCUMLER }),

  indirge(durum, olay) {
    if (olay.tur === 'suzgec-degisti') return { ...durum, sorgu: olay.deger };
    return durum;
  },

  ciz(durum) {
    const gorunen = suz(durum.olcumler, durum.sorgu);
    return o('section', { class: 'olcum-tablosu' },
      o('div', { class: 'suzgec-paneli' },
        o('label', { for: 'suzgec' }, 'Istasyon suzgeci'),
        o('input', { id: 'suzgec', type: 'search', value: durum.sorgu })),
      o('table', { class: 'tablo' },
        ...gorunen.map((x) =>
          o('tr', { class: 'satir' },
            o('td', {}, x.istasyon),
            o('td', {}, x.deger.toFixed(1)))),
      ),
      o('p', { class: 'ozet' }, `${gorunen.length} olcum`),
    );
  },
};
```

Test dosyası değişmeden aynı komutla yeniden çalıştırılır. Aşağıdaki çıktıdan sayaç
satırlarının bir bölümü ve başarısızlık gövdelerindeki yığıt satırları çıkarılmıştır.

```
▶ uygulama ayrintisina bagli
  ✖ suzgec durumu yazilan degeri tutar (0.727333ms)
  ✖ ikinci cocuk girdi ogesidir (0.515ms)
✖ uygulama ayrintisina bagli (1.638333ms)
▶ davranisa bagli
  ✔ suzgec yazildiginda yalniz eslesen istasyonlar kalir (0.3995ms)
  ✔ eslesme yoksa tablo bosalir ve ozet sifir der (0.103625ms)
✔ davranisa bagli (0.589375ms)
ℹ tests 4
ℹ pass 2
ℹ fail 2
✖ suzgec durumu yazilan degeri tutar
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  + actual - expected
  + undefined
  - 'kuzey'
✖ ikinci cocuk girdi ogesidir
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
  'table' !== 'input'
```

İki test düştü ve ikisi de gerçek bir kusuru bildirmiyor. Bildirdikleri şey, kodun
yeniden düzenlendiğidir. Bu, testin verdiği en pahalı yanlış işarettir: değişiklik güvenli
olduğu hâlde kırmızı, dolayısıyla her yeniden düzenleme bir de test bakımı getirir. Bir
süre sonra ekip testleri düzeltmek yerine yeniden düzenlemekten vazgeçer.

Davranışa bağlı iki test, aynı değişiklikte sessiz kaldı — çünkü sınadıkları şey
değişmemişti. Ayrım şudur: bileşenin **davranış sözleşmesi** kullanıcının gözlemleyebildiği
şeydir. Ekranda görünen metinler, erişilebilirlik ağacındaki roller, tetiklenen dış çağrılar.
**Uygulama ayrıntısı** ise sözleşmeyi yerine getirmek için seçilen yoldur: durum alanının
adı, ağaçtaki sarmalayıcılar, yardımcı fonksiyonların bölünmesi.

Bir kural olarak: test, bileşeni yazan kişinin değiştirmekte özgür olması gereken hiçbir
şeye dayanmamalıdır.

#### Sınırın Ötesini Değiştirmek

Bileşen testinin dışarıya açılan yerleri de vardır: ağ isteği, saat, rastgelelik, depolama.
Bunlar sınanan sınırın ötesindedir ve testte yerlerine **sahte bağımlılık (test double)** konur.
İki biçimi ayırmak yeterlidir: **güdük (stub)** sabit bir yanıt döndürür, **sahte nesne
(mock)** ek olarak kendisine yapılan çağrıları kaydeder ve test bu kayıtları doğrular.

Node.js kursundaki Test dersinde kurulan yöntem burada da geçerlidir: bağımlılık dışarıdan
verilir. Bileşen saatı kendi içinden okumak yerine özellik olarak alırsa, test sabit bir saat
verir ve çıktı belirlenimci olur. Aynı kural ağ için de işler — ölçüm listesini getiren
fonksiyon dışarıdan verildiğinde bileşen testi ağa hiç dokunmaz.

Sahte nesnenin bir maliyeti vardır: doğruladığı şey gerçek bağımlılığın davranışı değil, o
davranışa dair varsayımdır. Varsayım kayarsa test yeşil kalırken uygulama bozulur. Bu yüzden
sahte nesne sayısı azaldıkça testin değeri artar; sınırı olabildiğince dışarı itmek,
tümleşim testinin varlık nedenidir.

#### Özet

- Test türleri sınadıkları şeyle değil, uzandıkları sınırla ayrılır; bileşen testinin sınırı
  bileşenin kendi ağacıdır.
- Bileşen testinin girdisi bir olay, çıktısı ekranda görünendir; ikisi de kullanıcı
  sözlüğündedir.
- Davranış sözleşmesi gözlemlenebilir olandır; uygulama ayrıntısı onu yerine getiren yoldur.
  Test yalnız birincisine dayanmalıdır.
- Alan adı ve ağaç yapısı değiştiren bir yeniden düzenlemede uygulama ayrıntısına bağlı iki
  test düştü, davranışa bağlı iki test geçti; düşenler gerçek bir kusur bildirmiyordu.
- Ağ, saat ve rastgelelik gibi sınır ötesi bağımlılıklar dışarıdan verilir; güdük sabit yanıt
  döndürür, sahte nesne çağrıları da kaydeder.

#### Sonraki Adım

Davranışa bağlı test yazmak bir soruyu açıkta bırakıyor: sınanacak eleman testin içinde nasıl
bulunacak? Bu dersteki yardımcı fonksiyon düğüm türüne göre arama yaptı, yani hâlâ bir
uygulama ayrıntısına — ağacın etiket seçimine — dayanıyordu. Sınıf adına dayanan bir seçici
de aynı sorunu taşır. Kullanıcının elemanı tanıma biçimi bunların hiçbiri değildir:
kullanıcı düğmeyi düğme olduğu ve üstünde "Kaydet" yazdığı için bulur. Sonraki ders bu iki
bilgiyi — erişilebilir rolü ve erişilebilir adı — sorgu ölçütüne dönüştürür.

### Kullanıcı Merkezli Sorgular

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/kullanici-merkezli-sorgular

Erişilebilir rol ve erişilebilir adın sorgu ölçütüne dönüştürülmesi, aynı belgenin rol sorgusuyla ve sınıf seçicisiyle sorgulanması, iki ölçütün yeniden düzenleme ve erişilebilirlik gerilemesi karşısındaki davranışı.

Önceki ders davranışa bağlı testin uygulama ayrıntısına bağlı testten daha dayanıklı
olduğunu gösterdi, ama sınanacak ögeyi bulmak için hâlâ ağacın etiket seçimine baktı.
Sınıf adına dayanan bir seçici de aynı türden bir bağımlılıktır: her ikisi de yazarın
serbestçe değiştirebilmesi gereken şeylere dayanır.

Kullanıcı ögeyi böyle bulmaz. Fareyle çalışan kullanıcı "Kaydet" yazan bir düğme arar; ekran
okuyucuyla çalışan kullanıcı "Kaydet, düğme" diye duyurulan ögeye ulaşır. İkisinin ortak
noktası iki bilgidir: ögenin **rolü** ve **erişilebilir adı**. Bu ders o iki bilgiyi bir
sorgu ölçütüne dönüştürür.

#### Ölçütün Kaynağı

Web'in Temelleri ve HTML kursunda erişilebilirlik ağacı tanımlanmıştı: tarayıcının belge
ağacından türettiği, yardımcı teknolojiye sunulan ikinci ağaç. Bu ağaçtaki her düğümün bir
rolü, çoğunun bir adı vardır. Rol ögenin ne olduğunu söyler (düğme, bağlantı, arama kutusu,
satır), ad ise hangisi olduğunu.

Testin bu iki bilgiye göre sorgu yapmasının iki sonucu vardır. Birincisi dayanıklılıktır:
sınıf adları, sarmalayıcı kaplar ve biçem mimarisi kararları rolü değiştirmez. İkincisi ve
daha önemlisi, sorgunun kendisi bir erişilebilirlik doğrulaması hâline gelir. Rolü olmayan
bir ögeyi rol sorgusu bulamaz; test kırmızıya döner ve bildirdiği şey gerçek bir kusurdur.

#### Rol ve Ad Nasıl Hesaplanır

Aşağıdaki modül, hesabın öğretici bir alt kümesini kurar: yaygın ögelerin örtük rolleri, açık
`role` özniteliğinin önceliği ve ad hesabının sırası.

```js
// sorgu.mjs — erisilebilir rol ve ada gore oge secimi
export function o(tur, ozellikler = {}, ...cocuklar) {
  return { tur, ozellikler, cocuklar: cocuklar.flat() };
}

export function* dolas(dugum) {
  if (dugum === null || typeof dugum !== 'object') return;
  yield dugum;
  for (const cocuk of dugum.cocuklar ?? []) yield* dolas(cocuk);
}

function metin(dugum) {
  if (typeof dugum === 'string') return dugum;
  return (dugum.cocuklar ?? []).map(metin).join(' ').replace(/\s+/g, ' ').trim();
}

const ORTUK_ROL = {
  button: () => 'button',
  a: (oz) => (oz.href === undefined ? null : 'link'),
  table: () => 'table',
  tr: () => 'row',
  td: () => 'cell',
  th: (oz) => (oz.scope === 'row' ? 'rowheader' : 'columnheader'),
  ul: () => 'list',
  li: () => 'listitem',
  nav: () => 'navigation',
  main: () => 'main',
  h1: () => 'heading',
  h2: () => 'heading',
  h3: () => 'heading',
  form: (oz) => (oz['aria-label'] === undefined ? null : 'form'),
  input: (oz) => ({
    search: 'searchbox',
    text: 'textbox',
    number: 'spinbutton',
    checkbox: 'checkbox',
    submit: 'button',
  })[oz.type ?? 'text'] ?? null,
};

export function rol(dugum) {
  if (dugum.ozellikler?.role) return dugum.ozellikler.role;
  const kural = ORTUK_ROL[dugum.tur];
  return kural ? kural(dugum.ozellikler ?? {}) : null;
}

export function erisilebilirAd(agac, dugum) {
  const oz = dugum.ozellikler ?? {};
  if (oz['aria-labelledby']) {
    const hedef = [...dolas(agac)].find((d) => d.ozellikler?.id === oz['aria-labelledby']);
    if (hedef) return metin(hedef);
  }
  if (oz['aria-label']) return oz['aria-label'];
  if (oz.id) {
    const etiket = [...dolas(agac)].find((d) => d.tur === 'label' && d.ozellikler?.for === oz.id);
    if (etiket) return metin(etiket);
  }
  if (['button', 'a', 'h1', 'h2', 'h3', 'td', 'th'].includes(dugum.tur)) return metin(dugum);
  if (oz.title) return oz.title;
  if (oz.value !== undefined && dugum.tur === 'input' && oz.type === 'submit') return oz.value;
  return '';
}

export function roleGore(agac, aranan, { ad } = {}) {
  return [...dolas(agac)].filter(
    (d) => rol(d) === aranan && (ad === undefined || erisilebilirAd(agac, d) === ad),
  );
}

export function sinifaGore(agac, sinif) {
  return [...dolas(agac)].filter((d) => (d.ozellikler?.class ?? '').split(' ').includes(sinif));
}
```

Üç ayrıntı gerçek hesapla aynıdır. Açık `role` özniteliği örtük rolü ezer. Ad hesabı bir öncelik
dizisidir ve ilk bulunan kazanır: başka bir ögeye yapılan başvuru, doğrudan verilen etiket
dizgisi, yerel etiket ögesi, ögenin kendi içeriği, en sonda `title`. Bazı ögeler niteliğe bağlı
rol alır — bağlantı yalnız `href` taşıdığında bağlantıdır, bir form ögesi ancak erişilebilir adı
olduğunda yer imi sayılır.

Gerçek hesap bundan geniştir: gizli ögelerin dışlanması, ad hesabının özyinelemesi ve rol
eşlemesinin tamamı standartta tanımlıdır. Buradaki alt küme, sorgunun neye dayandığını
göstermeye yeter.

#### Aynı Belgeyi İki Ölçütle Sorgulamak

Sınanacak belge ölçüm giriş formudur. Üç sürüm var: başlangıç işaretlemesi, yalnız sınıf
adları değişmiş hâli ve kaydet düğmesinin rolsüz bir kaba dönüştüğü hâli.

```js
// giris-formu.mjs — olcum giris formunun uc surumu
import { o } from './sorgu.mjs';

// Surum 1: baslangic isaretlemesi
export const surum1 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'form__baslik' }, 'Yeni olcum'),
    o('label', { for: 'istasyon', class: 'form__etiket' }, 'Istasyon'),
    o('input', { id: 'istasyon', type: 'text', class: 'form__girdi' }),
    o('label', { for: 'deger', class: 'form__etiket' }, 'Sicaklik'),
    o('input', { id: 'deger', type: 'number', class: 'form__girdi' }),
    o('button', { type: 'submit', class: 'dugme dugme--birincil' }, 'Kaydet'));

// Surum 2: yalniz sinif adlari ve sarmalayicilar degisti
export const surum2 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'yigin__baslik' }, 'Yeni olcum'),
    o('div', { class: 'alan' },
      o('label', { for: 'istasyon', class: 'alan__ad' }, 'Istasyon'),
      o('input', { id: 'istasyon', type: 'text', class: 'alan__deger' })),
    o('div', { class: 'alan' },
      o('label', { for: 'deger', class: 'alan__ad' }, 'Sicaklik'),
      o('input', { id: 'deger', type: 'number', class: 'alan__deger' })),
    o('button', { type: 'submit', class: 'eylem eylem--birincil' }, 'Kaydet'));

// Surum 3: kaydet dugmesi rolsuz bir kaba donustu
export const surum3 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'yigin__baslik' }, 'Yeni olcum'),
    o('div', { class: 'alan' },
      o('label', { for: 'istasyon', class: 'alan__ad' }, 'Istasyon'),
      o('input', { id: 'istasyon', type: 'text', class: 'alan__deger' })),
    o('div', { class: 'alan' },
      o('label', { for: 'deger', class: 'alan__ad' }, 'Sicaklik'),
      o('input', { id: 'deger', type: 'number', class: 'alan__deger' })),
    o('div', { class: 'eylem eylem--birincil', onclick: 'gonder()' }, 'Kaydet'));
```

```js
// sorgu.test.mjs
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { roleGore, sinifaGore, erisilebilirAd } from './sorgu.mjs';
import { surum1, surum2, surum3 } from './giris-formu.mjs';

describe('surum 1', () => {
  test('rol sorgusu kaydet dugmesini bulur', () => {
    const agac = surum1();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 1);
  });

  test('sinif sorgusu ayni dugmeyi bulur', () => {
    assert.equal(sinifaGore(surum1(), 'dugme').length, 1);
  });

  test('etiketli girdinin erisilebilir adi etiket metnidir', () => {
    const agac = surum1();
    const [girdi] = roleGore(agac, 'spinbutton');
    assert.equal(erisilebilirAd(agac, girdi), 'Sicaklik');
  });
});

describe('surum 2 — sinif adlari degisti', () => {
  test('rol sorgusu hala bulur', () => {
    const agac = surum2();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 1);
  });

  test('sinif sorgusu artik bulamaz', () => {
    assert.equal(sinifaGore(surum2(), 'dugme').length, 0);
  });
});

describe('surum 3 — dugme rolsuz kaba dondu', () => {
  test('sinif sorgusu bulmayi surdurur', () => {
    assert.equal(sinifaGore(surum3(), 'eylem').length, 1);
  });

  test('rol sorgusu bulamaz', () => {
    const agac = surum3();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 0);
  });
});
```

```sh
node --test sorgu.test.mjs
```

```
▶ surum 1
  ✔ rol sorgusu kaydet dugmesini bulur (0.631ms)
  ✔ sinif sorgusu ayni dugmeyi bulur (0.071459ms)
  ✔ etiketli girdinin erisilebilir adi etiket metnidir (0.069417ms)
✔ surum 1 (1.141792ms)
▶ surum 2 — sinif adlari degisti
  ✔ rol sorgusu hala bulur (0.075042ms)
  ✔ sinif sorgusu artik bulamaz (0.049541ms)
✔ surum 2 — sinif adlari degisti (0.184916ms)
▶ surum 3 — dugme rolsuz kaba dondu
  ✔ sinif sorgusu bulmayi surdurur (0.077958ms)
  ✔ rol sorgusu bulamaz (0.061166ms)
✔ surum 3 — dugme rolsuz kaba dondu (0.214625ms)
ℹ tests 7
ℹ suites 3
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.906584
```

İki ölçütün üç sürümdeki davranışı bir tabloda toplanabilir.

| Değişiklik | Rol ve ad sorgusu | Sınıf seçicisi |
|---|---|---|
| Sınıf adları ve sarmalayıcılar değişti | bulur | bulamaz |
| Düğme rolsüz bir kaba dönüştü | bulamaz | bulur |

İki satır iki farklı türde bilgi verir. Birinci satırda sınıf seçicisi **yanlış alarm**
üretir: davranış değişmemişken test kırmızıya döner. İkinci satırda rol sorgusu bulamaz,
çünkü **bulunacak bir düğme kalmamıştır** — o kap klavyeyle odaklanamaz, boşluk tuşuyla
çalıştırılamaz ve ekran okuyucuya düğme diye duyurulmaz. Sınıf seçicisinin bu sürümde hâlâ
"bulması" iyi bir haber değildir; testin gerçek bir gerilemeyi görmediğini gösterir.

#### Ölçütlerin Önceliği

Pratikte bir sıra kurulabilir. En üstte **rol ve erişilebilir ad** vardır: her etkileşimli
öge için birincil ölçüt budur. Sonra **etiket metni** gelir; form alanlarında rolle birlikte
zaten aynı bilgiyi verir. Üçüncü sırada, etkileşimli olmayan içerik için **görünen metin**
bulunur — bir uyarı bölgesinin duyurduğu ileti gibi.

En sonda **test özniteliği** durur: yalnız sınama için eklenen bir veri özniteliği. Bunun
maliyeti, kullanıcının hiç görmediği bir sözleşme yaratmasıdır; öge erişilebilirlik ağacından
tümüyle düşse bile test yeşil kalır. Gerekçesi olan iki durum vardır: erişilebilir adı doğal
olarak bulunmayan yapısal bir kap ve aynı rol ile aynı adı taşıyan çok sayıda öge arasından
belirli birini ayırma ihtiyacı.

Sorgunun birden çok öge bulması da bir bilgidir. Aynı sayfada "Kaydet" adlı iki düğme varsa,
testin belirsizliği kullanıcının belirsizliğidir; çözüm sorguyu daraltmak değil, adları
ayırmaktır.

#### Özet

- Kullanıcı ögeyi rolüyle ve adıyla tanır; sorgunun ölçütü de bu ikisi olduğunda test
  kullanıcının bakış açısını taşır.
- Örtük rol öge türünden ve niteliklerinden türer; açık `role` özniteliği onu ezer. Ad hesabı
  bir öncelik dizisidir ve ilk bulunan kazanır.
- Sınıf adları ve sarmalayıcılar değiştiğinde rol sorgusu ögeyi bulmayı sürdürdü, sınıf
  seçicisi yanlış alarm üretti.
- Düğme rolsüz bir kaba dönüştüğünde rol sorgusu bulamadı; bu, gerçek bir erişilebilirlik
  gerilemesinin testte görünmesidir.
- Ölçüt sırası rol ve ad, etiket metni, görünen metin, en sonda test özniteliğidir; test
  özniteliği kullanıcının görmediği bir sözleşme yarattığı için son çaredir.

#### Sonraki Adım

Bileşen testi bileşenin kendi ağacıyla sınırlıdır. Ölçüm listesinin gerçekten sunucudan
gelmesi, adres çubuğunun güncellenmesi, formun gönderilmesi ve listenin yenilenmesi bu
sınırın dışındadır. Kullanıcının yaşadığı şey ise tam olarak bu zincirdir: parçalar tek tek
doğru çalışırken akış bir yerde kopabilir. Sonraki ders uygulamayı gerçek bir tarayıcıda
baştan sona çalıştıran testleri, akışın bir durum makinesi olarak modellenmesini ve bu
testlerin neden az sayıda tutulduğunu ele alır.

### Uçtan Uca Testler

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/uctan-uca-testler

Gerçek tarayıcıda çalışan testlerin kapsadığı risk, akışın durum makinesi olarak modellenmesi ve doğrulanması, bekleme ile veri yalıtımı kararları ve bu testlerin neden az sayıda tutulduğu.

Bileşen testi bileşenin kendi ağacıyla sınırlıdır. Ölçüm listesinin sunucudan gelmesi,
adresin güncellenmesi, formun gönderilmesi ve listenin yenilenmesi o sınırın dışında kalır.
Parçalar tek tek doğru çalışırken akışın bir yerinde kopması olağandır: yapı çıktısında bir
modül düşmüştür, yönlendirme kuralı değişmiştir, sunucunun döndürdüğü alan adı istemcinin
beklediğinden farklıdır.

Uçtan uca test bu boşluğu kapatır. Uygulamayı gerçek bir tarayıcıda başlatır, gerçek girdi
olayları gönderir ve kullanıcının gördüğünü doğrular.

#### Kapsanan Risk

Testi bir **tarayıcı sürücüsü** yürütür: tarayıcıyı denetim altında başlatan, adrese giden,
tıklama ve tuş olayı üreten, belgeyi ve erişilebilirlik ağacını okuyabilen bir program.
Sürücünün ürettiği olaylar uygulamanın gördüğü açıdan gerçek kullanıcı olaylarıdır.

Bu düzenek, başka hiçbir test türünün göremediği bir risk kümesini kapsar. Yapı çıktısının
gerçekten çalışır olması — geliştirme kipinde geçen bir kod, üretim yapısında ağaç
sarsmayla düşmüş bir dışa aktarım yüzünden kırılabilir. Yönlendirme ve derin bağlantı.
Sunucu ile istemci arasındaki veri sözleşmesi. Oturum ve çerez davranışı. Güvenlik
başlıklarının gerçekten uygulanması: İçerik Güvenlik Politikası dersinde yazılan politika,
ancak gerçek bir tarayıcı betiği reddettiğinde sınanmış olur.

Bedeli de aynı ölçüde büyüktür. Bir uçtan uca test, bir bileşen testinin yüzlerce katı
sürer. Başarısız olduğunda hangi katmanın kusurlu olduğunu söylemez; tanı için ayrı bir
çalışma gerekir. Ve dış dünyaya bağlı olduğu için kırılganlığa açıktır — beşinci ders bunun
nedenlerini sayacaktır.

#### Akışı Önce Modellemek

Uçtan uca test yazmanın verimli yolu, akışı adım listesi olarak değil **durum makinesi**
olarak düşünmektir. Web'in Temelleri ve HTML kursunda ayrıştırıcı için kullanılan aynı
kavram burada arayüz akışına uygulanır: sonlu sayıda durum, durumlar arası olaylarla tanımlı
geçişler.

Kuzey Yamaç arayüzünün ölçüm girişi akışı şu durumları taşır: sayfa boşta, liste yükleniyor,
liste hazır, form açık, gönderiliyor, liste hatası.

```js
// akis.mjs — olcum girisi akisinin durum makinesi
export const GECISLER = {
  bosta: { 'sayfa-acildi': 'listeYukleniyor' },
  listeYukleniyor: { 'liste-geldi': 'listeHazir', 'istek-basarisiz': 'listeHatasi' },
  listeHazir: { 'yeni-olcum': 'formAcik', 'suzgec-degisti': 'listeYukleniyor' },
  formAcik: { 'gonder': 'gonderiliyor', 'iptal': 'listeHazir' },
  gonderiliyor: { 'kayit-alindi': 'listeYukleniyor', 'dogrulama-hatasi': 'formAcik' },
  listeHatasi: {},
};

export function senaryoyuYurut(gecisler, baslangic, adimlar) {
  let durum = baslangic;
  const izleme = [durum];
  for (const adim of adimlar) {
    const sonraki = gecisler[durum]?.[adim];
    if (sonraki === undefined) {
      throw new Error(`'${durum}' durumunda '${adim}' adimi tanimli degil`);
    }
    durum = sonraki;
    izleme.push(durum);
  }
  return { durum, izleme };
}

export function ulasilabilir(gecisler, baslangic) {
  const gorulen = new Set([baslangic]);
  const kuyruk = [baslangic];
  while (kuyruk.length > 0) {
    const su = kuyruk.shift();
    for (const hedef of Object.values(gecisler[su] ?? {})) {
      if (gorulen.has(hedef)) continue;
      gorulen.add(hedef);
      kuyruk.push(hedef);
    }
  }
  return gorulen;
}

export function kurtarilamayanlar(gecisler, guvenliDurum) {
  return Object.keys(gecisler).filter(
    (d) => d !== guvenliDurum && !ulasilabilir(gecisler, d).has(guvenliDurum),
  );
}
```

`ulasilabilir` fonksiyonu, Veri Yapıları kursundaki enine aramanın doğrudan uygulanmasıdır:
geçiş tablosu bir çizgedir, durumlar düğüm, olaylar kenardır.

Model kurulduğunda iki tür doğrulama yazılabilir. Birincisi senaryo doğrulamasıdır: belirli
bir adım dizisi beklenen durum izini üretmeli. İkincisi model doğrulamasıdır: tablonun
kendisi hakkında her senaryoda doğru kalması gereken önermeler.

```js
// akis.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { GECISLER, senaryoyuYurut, ulasilabilir, kurtarilamayanlar } from './akis.mjs';

test('mutlu yol: olcum girilir ve liste yenilenir', () => {
  const { izleme } = senaryoyuYurut(GECISLER, 'bosta', [
    'sayfa-acildi', 'liste-geldi', 'yeni-olcum', 'gonder', 'kayit-alindi', 'liste-geldi',
  ]);
  assert.deepEqual(izleme, [
    'bosta', 'listeYukleniyor', 'listeHazir', 'formAcik',
    'gonderiliyor', 'listeYukleniyor', 'listeHazir',
  ]);
});

test('gecersiz olcum formda kalir', () => {
  const { durum } = senaryoyuYurut(GECISLER, 'bosta', [
    'sayfa-acildi', 'liste-geldi', 'yeni-olcum', 'gonder', 'dogrulama-hatasi',
  ]);
  assert.equal(durum, 'formAcik');
});

test('tanimsiz adim acik hatayla durur', () => {
  assert.throws(
    () => senaryoyuYurut(GECISLER, 'bosta', ['sayfa-acildi', 'gonder']),
    /'listeYukleniyor' durumunda 'gonder' adimi tanimli degil/,
  );
});

test('her durum baslangictan ulasilabilir', () => {
  const gorulen = ulasilabilir(GECISLER, 'bosta');
  assert.deepEqual([...gorulen].sort(), Object.keys(GECISLER).sort());
});

test('her durumdan listeHazir durumuna donulebilir', () => {
  assert.deepEqual(kurtarilamayanlar(GECISLER, 'listeHazir'), []);
});
```

```sh
node --test akis.test.mjs
```

Aşağıdaki çıktıdan sayaç satırlarının bir bölümü ve başarısızlık gövdesindeki yığıt
satırları çıkarılmıştır.

```
✔ mutlu yol: olcum girilir ve liste yenilenir (0.529625ms)
✔ gecersiz olcum formda kalir (0.05425ms)
✔ tanimsiz adim acik hatayla durur (0.130791ms)
✔ her durum baslangictan ulasilabilir (0.068625ms)
✖ her durumdan listeHazir durumuna donulebilir (0.719375ms)
ℹ tests 5
ℹ pass 4
ℹ fail 1
✖ her durumdan listeHazir durumuna donulebilir
  AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
  + actual - expected
  + [
  +   'listeHatasi'
  + ]
  - []
```

Model, henüz tek satır arayüz kodu çalıştırılmadan bir tasarım boşluğu buldu: liste isteği
başarısız olduğunda ulaşılan durumdan geri dönüş yoktur. Kullanıcı hata ekranında sıkışır.
Düzeltme geçiş tablosuna bir kenar eklemektir:

```js
// akis.mjs — GECISLER tablosunun duzeltilmis hali; yalnizca son satir degisti
export const GECISLER = {
  bosta: { 'sayfa-acildi': 'listeYukleniyor' },
  listeYukleniyor: { 'liste-geldi': 'listeHazir', 'istek-basarisiz': 'listeHatasi' },
  listeHazir: { 'yeni-olcum': 'formAcik', 'suzgec-degisti': 'listeYukleniyor' },
  formAcik: { 'gonder': 'gonderiliyor', 'iptal': 'listeHazir' },
  gonderiliyor: { 'kayit-alindi': 'listeYukleniyor', 'dogrulama-hatasi': 'formAcik' },
  listeHatasi: { 'yeniden-dene': 'listeYukleniyor' },
};
```

```
✔ mutlu yol: olcum girilir ve liste yenilenir (0.539167ms)
✔ gecersiz olcum formda kalir (0.060417ms)
✔ tanimsiz adim acik hatayla durur (0.140833ms)
✔ her durum baslangictan ulasilabilir (0.073041ms)
✔ her durumdan listeHazir durumuna donulebilir (0.3595ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.845583
```

Modelin değeri iki yönlüdür. Test yazarken hangi senaryoların gerçekten farklı olduğunu gösterir —
mutlu yol ile doğrulama hatası yolu farklı geçişler kullanır, dolayısıyla iki ayrı testtir. Ve
tabloda eksik kalan geçiş, arayüzde eksik kalan bir düğmedir.

#### Adımlar, Bekleme ve Veri

Tarayıcıda çalışan testin her adımı iki parçadan oluşur: bir kullanıcı eylemi ve o eylemin
gözlemlenebilir sonucu. Sonuç doğrulanmadan bir sonraki eyleme geçilirse test, uygulamadan
hızlı çalışmaya başlar ve zamanlamaya bağımlı hâle gelir.

Bekleme bu yüzden sabit süreyle yapılmaz. Sabit bekleme iki türlü yanlıştır: yavaş bir koşumda
yetmez; hızlı bir koşumda boşa harcanır. Doğru biçim **koşula göre beklemedir** — "ölçüm
tablosunda en az bir satır görünene kadar, en çok şu kadar süre bekle". Koşulun kendisi önceki
dersteki ölçütle kurulur: role ve ada göre bulunan bir öge.

Test verisinin yalıtımı ikinci kuraldır. Paylaşılan bir hesabın üzerinde çalışan iki test
birbirinin verisini görür; biri ölçüm eklerken diğerinin sayım doğrulaması bozulur. Her
koşum kendi istasyonunu ve kendi ölçümlerini kurar, sonunda temizler.

Giriş akışı ayrı bir karardır. Kimlik doğrulamayı her testte arayüzden geçirmek her teste
birkaç saniye ekler ve giriş ekranı değiştiğinde tüm paketi düşürür. Oturumu doğrudan
kurmak — belirteci ya da çerezi arayüzü kullanmadan yerleştirmek — bu maliyeti kaldırır.
Giriş akışının kendisi ayrıca ve bir kez sınanır.

Ağ tarafında iki seçenek vardır. Gerçek arka uca bağlanmak sözleşme uyuşmazlığını yakalar
ama yavaştır ve arka ucun durumuna bağımlıdır. Yanıtları sabitlemek testi hızlı ve
belirlenimci yapar ama sözleşme kaymasını göremez. Yaygın çözüm ikisini bölmektir: birkaç
akış gerçek arka uçla, geri kalanı sabitlenmiş yanıtlarla.

#### Ne Kadar Uçtan Uca

Test paketinin şekli bir bütçe sorunudur. Birim ve bileşen testleri saniyeler içinde geri
bildirim verir ve kusurun yerini gösterir; uçtan uca testler dakikalar sürer ve yalnız
"bir yerde bozuldu" der. Bu yüzden çoğunluk alta, azınlık üste konur.

Seçim ölçütü kapsam yüzdesi değil, **başarısızlığın bedelidir**. Kuzey Yamaç arayüzünde üç
akış bu ölçütü karşılar: ölçüm listesinin yüklenip süzülmesi, yeni ölçümün girilip
kaydedilmesi ve liste isteği başarısız olduğunda kurtarma. Kalan her şey daha ucuz bir
katmanda sınanabiliyorsa orada sınanır.

#### Özet

- Uçtan uca test yapı çıktısını, yönlendirmeyi, veri sözleşmesini, oturumu ve güvenlik
  başlıklarını birlikte kapsar; hiçbir alt katman bu kümeyi göremez.
- Akışı durum makinesi olarak modellemek hangi senaryoların farklı olduğunu gösterir; geçiş
  tablosu üzerinde enine aramayla ulaşılabilirlik ve kurtarılabilirlik doğrulanabilir.
- Model, hata durumundan geri dönüş bulunmadığını arayüz kodu çalıştırılmadan bildirdi;
  eksik geçiş arayüzde eksik bir düğmeye karşılık gelir.
- Her adım bir eylem ve gözlemlenebilir bir sonuçtan oluşur; bekleme sabit süreyle değil
  koşulla yapılır.
- Her koşum kendi verisini kurar, oturumu arayüzden geçmeden kurar ve akışların bir bölümünü
  gerçek arka uçla, kalanını sabitlenmiş yanıtlarla sınar.

#### Sonraki Adım

Uçtan uca test akışın işlediğini doğrular: satır göründü, kayıt alındı, hata ekranından
dönüldü. Doğrulamadığı şey, o satırların nasıl göründüğüdür. Bir biçem kuralı yanlış
kapsayıcıya uygulandığında, bir aralık değeri iki katına çıktığında ya da bir düğme
görünmez hâle geldiğinde akış testleri geçmeyi sürdürür — çünkü öge ağaçta durur ve
tıklanabilir. Sonraki ders bu boşluğu kapatan yöntemi, iki görüntüyü piksel piksel
karşılaştıran gerileme testini ele alır.

### Görsel Gerileme Testi

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/gorsel-gerileme-testi

Referans görüntüyle aday görüntünün piksel düzeyinde karşılaştırılması, piksel toleransı ile oran eşiğinin ayrı işlevleri, maskeleme kararı ve karşılaştırmayı anlamlı kılan belirlenimcilik koşulları.

Akış testi ölçüm satırının göründüğünü doğrular; nasıl göründüğünü doğrulamaz. Bir biçem
kuralı yanlış kapsayıcıya uygulandığında, bir aralık değeri iki katına çıktığında ya da
kaydet düğmesi arka planla aynı renge geldiğinde önceki iki dersin testleri geçmeyi sürdürür.
Öge ağaçtadır, rolü yerindedir, tıklanabilir — ama ekranda yanlıştır.

Görsel gerileme testi bu boşluğu doldurur: bir ekran görüntüsünü kabul edilmiş bir referansla
karşılaştırır ve farkı sayar.

#### Karşılaştırılan Şey

Üç görüntü vardır. **Referans görüntü**, daha önce incelenip onaylanmış hâldir ve depoda
saklanır. **Aday görüntü**, o anki koddan üretilen hâldir. **Fark görüntüsü** ise ikisinin
uyuşmadığı pikselleri işaretler.

Karşılaştırma üç ölçekte yapılabilir. Tam sayfa karşılaştırması en geniş kapsamı verir ama
en gürültülü olanıdır: sayfanın herhangi bir yerindeki değişiklik testi düşürür. Bileşen
karşılaştırması yalnız ilgili bileşenin çerçevesini alır ve kusuru daha dar bir alana
bağlar. Öge düzeyinde karşılaştırma en dar kapsamdır.

Buradan ayrı bir yöntem, **anlık görüntü testidir (snapshot test)**: çizilen ağacın metin
biçimine dökülüp kaydedilmesi. Yapıdaki değişikliği yakalar, görünüşteki değişikliği
yakalamaz — renk, aralık ve yazı tipi kararları ağaçta görünmez. İkisi farklı riskleri
kapsar.

#### Fark Hesabı

Karşılaştırmanın çekirdeği kısa bir hesaptır: iki ızgaranın karşılıklı piksellerini gez,
farkı bir toleransla kıyasla, uyuşmayanları say.

```js
// gorsel-fark.mjs — iki gri tonlamali izgarayi karsilastirir
export function fark(referans, aday, { tolerans = 0, maske = [] } = {}) {
  if (referans.genislik !== aday.genislik || referans.yukseklik !== aday.yukseklik) {
    return { boyutFarki: true, farkliPiksel: NaN, oran: NaN };
  }
  const maskeli = (x, y) =>
    maske.some((m) => x >= m.x && x < m.x + m.en && y >= m.y && y < m.y + m.boy);
  let farkli = 0;
  let sayilan = 0;
  for (let y = 0; y < referans.yukseklik; y += 1) {
    for (let x = 0; x < referans.genislik; x += 1) {
      if (maskeli(x, y)) continue;
      sayilan += 1;
      const i = y * referans.genislik + x;
      if (Math.abs(referans.pikseller[i] - aday.pikseller[i]) > tolerans) farkli += 1;
    }
  }
  return { boyutFarki: false, farkliPiksel: farkli, oran: farkli / sayilan };
}

export function karar(sonuc, esik) {
  if (sonuc.boyutFarki) return 'kaldi (boyut degisti)';
  return sonuc.oran > esik ? 'kaldi' : 'gecti';
}
```

Denemek için gerçek bir ekran görüntüsü gerekmez; ölçüm tablosu ekranını belirlenimci olarak
çizen kırk sütunluk bir ızgara yeter. Aşağıdaki modül başlık şeridini, saat alanını, dört
ölçüm satırını ve kaydet düğmesini gri tonlarla yerleştirir.

```js
// goruntu.mjs — olcum tablosu ekraninin belirlenimci gri tonlamali cizimi
const GENISLIK = 40;
const YUKSEKLIK = 20;

function bos() {
  return { genislik: GENISLIK, yukseklik: YUKSEKLIK, pikseller: new Uint8Array(GENISLIK * YUKSEKLIK).fill(255) };
}

function kutu(g, x, y, en, boy, ton) {
  for (let sy = y; sy < y + boy; sy += 1) {
    for (let sx = x; sx < x + en; sx += 1) {
      if (sx >= 0 && sx < g.genislik && sy >= 0 && sy < g.yukseklik) g.pikseller[sy * g.genislik + sx] = ton;
    }
  }
}

export function ekran({ dugmeTonu = 60, saatDeseni = 0, kaydir = 0 } = {}) {
  const g = bos();
  kutu(g, 0, 0 + kaydir, 40, 2, 200);              // baslik seridi
  kutu(g, 30, 0 + kaydir, 8, 1, 120 + saatDeseni); // saat alani
  for (let s = 0; s < 4; s += 1) {                 // olcum satirlari
    kutu(g, 2, 4 + s * 3 + kaydir, 34, 2, 230);
    kutu(g, 3, 5 + s * 3 + kaydir, 10, 1, 90);
  }
  kutu(g, 26, 17 + kaydir, 12, 2, dugmeTonu);      // kaydet dugmesi
  return g;
}

// cizim motorunun kenar yumusatma benzeri kucuk sapmasi
export function gurultuEkle(g, tohum = 12345, genlik = 3) {
  const kopya = { ...g, pikseller: Uint8Array.from(g.pikseller) };
  let s = tohum;
  for (let i = 0; i < kopya.pikseller.length; i += 1) {
    s = (s * 1103515245 + 12345) % 2147483648;
    const sapma = (s % (2 * genlik + 1)) - genlik;
    kopya.pikseller[i] = Math.min(255, Math.max(0, kopya.pikseller[i] + sapma));
  }
  return kopya;
}
```

```js
// rapor.mjs — adaylarin referansla karsilastirilmasi
import { fark, karar } from './gorsel-fark.mjs';
import { ekran, gurultuEkle } from './goruntu.mjs';

const referans = ekran();
const ESIK = 0.005;
const SAAT_MASKESI = [{ x: 30, y: 0, en: 8, boy: 1 }];

const adaylar = [
  ['ayni cizim', ekran(), {}],
  ['kenar yumusatma sapmasi', gurultuEkle(ekran()), {}],
  ['ayni sapma, tolerans 5', gurultuEkle(ekran()), { tolerans: 5 }],
  ['saat alani degisti', ekran({ saatDeseni: 40 }), {}],
  ['saat alani maskeli', ekran({ saatDeseni: 40 }), { maske: SAAT_MASKESI }],
  ['dugme rengi degisti', ekran({ dugmeTonu: 30 }), {}],
  ['icerik 1 piksel kaydi', ekran({ kaydir: 1 }), {}],
];

console.log('durum                     farkli   oran    karar');
for (const [ad, aday, secenek] of adaylar) {
  const s = fark(referans, aday, secenek);
  const oran = (s.oran * 100).toFixed(2).padStart(6);
  console.log(`${ad.padEnd(25)} ${String(s.farkliPiksel).padStart(6)} ${oran}%  ${karar(s, ESIK)}`);
}
```

```sh
node rapor.mjs
```

```
durum                     farkli   oran    karar
ayni cizim                     0   0.00%  gecti
kenar yumusatma sapmasi      502  62.75%  kaldi
ayni sapma, tolerans 5         0   0.00%  gecti
saat alani degisti             8   1.00%  kaldi
saat alani maskeli             0   0.00%  gecti
dugme rengi degisti           24   3.00%  kaldi
icerik 1 piksel kaydi        424  53.00%  kaldi
```

Yedi satır, yöntemin bütün karar noktalarını içeriyor.

#### Tolerans, Eşik ve Maske

İkinci satır, sıfır toleransın neden kullanılamadığını gösteriyor. Her piksele en çok üç
birimlik bir sapma eklendiğinde sekiz yüz pikselin beş yüz ikisi "farklı" sayıldı: yüzde
62,75. Gözle görülmeyen bir sapma, karşılaştırmanın tamamını anlamsız kıldı. Üçüncü satırda
aynı görüntü beş birimlik toleransla karşılaştırıldı ve fark sıfıra indi.

Buradan iki ayrı kelepçe çıkar ve ikisi karıştırılmamalıdır. **Piksel toleransı** tek bir
pikselin ne kadar sapabileceğini söyler; çizim motorunun kenar yumuşatma gürültüsünü
soğurur. **Oran eşiği** kaç pikselin sapabileceğini söyler; kabul edilen toplam değişim
miktarıdır. Tolerans gürültüye, eşik küçük ve önemsiz kaymalara karşıdır.

Dördüncü ve beşinci satırlar dinamik içeriği gösteriyor. Başlıktaki saat alanı her koşumda
farklı bir değer taşır ve sekiz pikseli değiştirir: yüzde bir. Bu, kodla ilgisi olmayan bir
farktır ve maskeyle karşılaştırma dışında bırakılır. Maske dikdörtgeni sayımdan tümüyle
düşer; oran, kalan piksellere göre hesaplanır.

Altıncı satır gerçek bir gerilemedir: kaydet düğmesinin tonu değişti, yirmi dört piksel
farklı, yüzde üç. Eşiğin üstünde olduğu için test kaldı.

Yedinci satır yöntemin en önemli sınırını gösteriyor. İçerik bir piksel aşağı kaydığında
dört yüz yirmi dört piksel değişti — yüzde elli üç. Görsel karşılaştırma **yerel değildir**:
bir kapsayıcının dış boşluğundaki tek birimlik değişiklik, altındaki her şeyi kaydırır ve
farkı sayfanın tamamına yayar. Bu yüzden fark oranı kusurun büyüklüğünü ölçmez; yalnız
"bir şey değişti" der. Kusurun yerini bulmak fark görüntüsüne bakmayı gerektirir.

Sonuncu ayrıntı boyut denetimidir. Görüntülerin ölçüleri farklıysa piksel karşılaştırması
tanımsızdır; karşılaştırma başlamadan geçersiz sayılır.

#### Testin İçine Koymak

Karar ölçütü belirlendiğinde bu bir teste dönüşür.

```js
// gorsel.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { fark, karar } from './gorsel-fark.mjs';
import { ekran, gurultuEkle } from './goruntu.mjs';

const ESIK = 0.005;
const OLCUT = { tolerans: 5, maske: [{ x: 30, y: 0, en: 8, boy: 1 }] };

test('cizim sapmasi ve saat alani gerileme sayilmaz', () => {
  const aday = gurultuEkle(ekran({ saatDeseni: 40 }));
  assert.equal(karar(fark(ekran(), aday, OLCUT), ESIK), 'gecti');
});

test('dugme tonundaki degisiklik gerileme olarak bildirilir', () => {
  const sonuc = fark(ekran(), ekran({ dugmeTonu: 30 }), OLCUT);
  assert.equal(karar(sonuc, ESIK), 'kaldi');
  assert.equal(sonuc.farkliPiksel, 24);
});

test('boyut degisikligi karsilastirmayi gecersiz kilar', () => {
  const dar = { genislik: 39, yukseklik: 20, pikseller: new Uint8Array(39 * 20) };
  assert.equal(karar(fark(ekran(), dar, OLCUT), ESIK), 'kaldi (boyut degisti)');
});
```

```sh
node --test gorsel.test.mjs
```

```
✔ cizim sapmasi ve saat alani gerileme sayilmaz (0.923334ms)
✔ dugme tonundaki degisiklik gerileme olarak bildirilir (0.115667ms)
✔ boyut degisikligi karsilastirmayi gecersiz kilar (0.053917ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 32.929375
```

#### Belirlenimcilik Koşulları

Görsel karşılaştırma ancak aday görüntü belirlenimci üretildiğinde işe yarar. Beş koşul
sağlanmalıdır.

Görüntü alanı ve piksel oranı sabitlenir; farklı genişlik farklı bir düzen üretir. Animasyon ve
geçiş durdurulur, yoksa görüntü hangi karede alındığına bağlı olur — Düzen Sistemleri ve Duyarlı
Tasarım kursundaki hareket azaltma tercihi bu iş için de kullanılabilir. Yazı tipi yüklenene kadar
beklenir; yedek yazı tipiyle çizilmiş bir kare her satırın genişliğini değiştirir. Saat, rastgele
kimlik ve canlı veri sabitlenir ya da maskelenir. Kaydırma konumu sıfırlanır.

Bir koşul daha vardır ve dışarıdan gelir: çizim motoru ile işletim sistemi. Aynı kod, farklı
bir ortamda kenar yumuşatmayı farklı yapar. Bu yüzden referans görüntüler üretildikleri
ortama bağlıdır ve tek bir ortamda üretilip orada karşılaştırılır.

Referansların kendisi de yönetilmesi gereken bir varlıktır. Depoda ikili dosya olarak
dururlar ve Sürüm Kontrolüne Giriş kursunda anlatıldığı gibi ikili dosyaların farkı
okunamaz. Her kabul edilen değişiklik depoyu büyütür. Bu yüzden referans güncellemesi
kendiliğinden yapılmaz: değişikliği bir kişi görür, onaylar ve yeni referans o onayla
kaydedilir. Onaysız güncelleme, gerileme testini kendi kendini haklı çıkaran bir düzeneğe
çevirir.

#### Özet

- Görsel gerileme testi referans ve aday görüntüyü piksel düzeyinde karşılaştırır; anlık
  görüntü testi ise yapıyı yakalar, görünüşü yakalamaz.
- Piksel toleransı tek pikselin sapmasını, oran eşiği toplam değişim miktarını sınırlar;
  ikisi farklı sorunlara karşıdır ve karıştırılmamalıdır.
- Dinamik alanlar maskelenir; maskelenen bölge sayımın tamamen dışında kalır.
- Fark oranı yerel değildir: bir piksellik kayma sayfanın yarısını farklı gösterdi, bu yüzden
  oran kusurun büyüklüğünü ölçmez.
- Karşılaştırma ancak görüntü alanı, animasyon, yazı tipi, saat ve kaydırma sabitlendiğinde
  anlamlıdır; referanslar üretildikleri ortama bağlıdır ve güncellemeleri onaya bağlanır.

#### Sonraki Adım

Dört ders boyunca kurulan testlerin hepsi bir varsayıma dayanıyordu: aynı kod aynı sonucu
verir. Uygulamada bu varsayım sık sık bozulur. Bir test yirmi koşumun on dokuzunda geçer,
birinde düşer; kimse bir şey değiştirmemiştir. Ekip önce yeniden çalıştırmayı dener, sonra
o testi görmezden gelmeye başlar ve paketin verdiği işaret değersizleşir. Sonraki ders bu
davranışın nedenlerini — zamanlama, sıraya bağımlılık ve paylaşılan durum — sayılabilir hâle
getirir ve her birinin çözümünü gösterir.

### Test Kararlılığı

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/test-kararliligi

Kırılgan testin tanımı ve maliyeti, zamanlama, sıraya bağımlılık ve paylaşılan durumun belirlenimci bir benzetimle sayılması, her nedenin çözümü ve yeniden deneme ile karantinanın yeri.

Önceki dört dersin testleri bir varsayıma dayanıyordu: aynı kod aynı sonucu verir. Bu
varsayım sık sık bozulur. Bir test yirmi koşumun on dokuzunda geçer, birinde düşer; kimse
bir şey değiştirmemiştir. İkinci çalıştırmada yeşile döner ve iş devam eder.

Bu davranış zararsız görünür ama paketin verdiği işareti aşındırır. Kırmızı bir sonucun
anlamı belirsizleştiğinde ekip önce yeniden çalıştırmayı dener, sonra bakmadan yeniden
çalıştırır, en sonunda gerçek bir kusuru da kırılganlık sanıp geçer. Test paketinin bütün
değeri, kırmızının bir şey ifade etmesindedir.

#### Kırılgan Test ve Ölçüsü

**Kırılgan test (flaky test)**, sınadığı kod değişmediği hâlde koşumdan koşuma farklı sonuç
veren testtir. Kusur testin kendisindedir: testin sonucu, sınadığı davranışın dışındaki bir
şeye bağlıdır.

Bu bir izlenim olarak değil, sayı olarak ele alınmalıdır. **Düşme oranı**, bir testin
belirli sayıda koşumda kaç kez düştüğüdür. Bir test bilinmeyen aralıklarla düşüyorsa, onu
aynı koşullarda çok kez çalıştırıp oranı ölçmek hem sorunun varlığını hem de düzeltmenin
işe yaradığını kanıtlar.

#### Üç Nedeni Saymak

Aşağıdaki benzetim üç kırılganlık nedenini ayrı ayrı modelliyor ve sözde rastgele ama
belirlenimci bir üreteçle iki yüz koşum boyunca kaç kez düştüklerini sayıyor. Üreteç
doğrusal eşlenik yöntemidir: aynı tohum her zaman aynı diziyi verir, dolayısıyla sonuçlar
yeniden üretilebilir.

```js
// kirilganlik.mjs — uc kirilganlik nedeninin belirlenimci benzetimi
export function uretec(tohum) {
  let s = tohum >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

function karistir(dizi, rastgele) {
  const k = [...dizi];
  for (let i = k.length - 1; i > 0; i -= 1) {
    const j = Math.floor(rastgele() * (i + 1));
    [k[i], k[j]] = [k[j], k[i]];
  }
  return k;
}

export function paketiKosur({ tohum = 7, kosum = 200, duzeltilmis = false, karistirma = true }) {
  const rastgele = uretec(tohum);
  const adlar = ['tabloYuklenir', 'suzgecDaraltir', 'formGonderilir'];
  const dusen = { tabloYuklenir: 0, suzgecDaraltir: 0, formGonderilir: 0 };
  let taslak = null; // modul duzeyinde yasayan paylasilan durum

  for (let n = 0; n < kosum; n += 1) {
    const sira = karistirma ? karistir(adlar, rastgele) : adlar;
    const kurulan = new Set();
    if (duzeltilmis) taslak = null; // her kosum kendi durumuyla baslar

    for (const ad of sira) {
      let gecti;
      if (ad === 'tabloYuklenir') {
        const yanitSuresi = 40 + 80 * rastgele();
        gecti = duzeltilmis ? yanitSuresi <= 500 : yanitSuresi <= 100;
        kurulan.add('tablo');
      } else if (ad === 'suzgecDaraltir') {
        if (duzeltilmis) kurulan.add('tablo'); // test kendi verisini kurar
        gecti = kurulan.has('tablo');
      } else {
        gecti = taslak === null; // form bos taslakla acilmali
        const gonderimBasarili = rastgele() < 0.5;
        taslak = gonderimBasarili ? null : { istasyon: 'kuzey-yamac-01' };
      }
      if (gecti === false) dusen[ad] += 1;
    }
  }
  return dusen;
}
```

```js
// rapor.mjs — 200 kosumda hangi test kac kez dustu
import { paketiKosur } from './kirilganlik.mjs';

const senaryolar = [
  ['sabit sira, duzeltilmemis', { karistirma: false }],
  ['karisik sira, duzeltilmemis', { karistirma: true }],
  ['karisik sira, duzeltilmis', { karistirma: true, duzeltilmis: true }],
];

console.log('senaryo                       tabloYuklenir  suzgecDaraltir  formGonderilir');
for (const [ad, secenek] of senaryolar) {
  const d = paketiKosur({ tohum: 7, kosum: 200, ...secenek });
  console.log(
    ad.padEnd(30) +
      String(d.tabloYuklenir).padStart(9) +
      String(d.suzgecDaraltir).padStart(16) +
      String(d.formGonderilir).padStart(16),
  );
}
```

```sh
node rapor.mjs
```

```
senaryo                       tabloYuklenir  suzgecDaraltir  formGonderilir
sabit sira, duzeltilmemis            50               0              95
karisik sira, duzeltilmemis          48             102             104
karisik sira, duzeltilmis             0               0               0
```

Üç satır, üç ayrı ders veriyor.

#### Zamanlama

`tabloYuklenir` testi listenin gelmesi için sabit yüz milisaniye bekliyor. Benzetimde yanıt
süresi kırk ile yüz yirmi milisaniye arasında değişiyor; süre beklemeyi aştığında test
düşüyor. İki yüz koşumun ellisinde ve kırk sekizinde düştü — dörtte bire yakın bir oran,
koşum sırasından bağımsız.

Sabit bekleme iki yönden yanlıştır. Yavaş bir koşumda yetmez; hızlı bir koşumda boşa
harcanır. Yüz test için eklenen yüzer milisaniye, paketin süresine on saniye ekler ve hiçbir
şeyi güvence altına almaz.

Doğru biçim **koşula göre beklemedir**: bir gözlemlenebilir sonuç oluşana kadar kısa
aralıklarla denetleyip beklemek, cömert bir üst sınırla. Düzeltilmiş sürümde üst sınır beş
yüz milisaniyedir ve iki yüz koşumun hiçbirinde aşılmadı. Üst sınırın cömert olması testi
yavaşlatmaz — koşul erken sağlandığında bekleme erken biter. Üst sınırın büsbütün
kaldırılması ise kabul edilmez: gerçekten donmuş bir uygulamada test sonsuza kadar bekler.

Eşzamansız JavaScript ve Çalışma Zamanı kursundaki mikro görev sırası burada da geçerlidir. Bir
sözün sonuçlanması ile ekranın güncellenmesi aynı tur içinde olmayabilir; beklenecek şey sözün
kendisi değil, kullanıcının göreceği sonuçtur.

#### Sıraya Bağımlılık

`suzgecDaraltir` testi, tablonun daha önce yüklenmiş olmasını bekliyor — veriyi kuran başka
bir testtir. Birinci satırda bu test hiç düşmedi: sabit sırada `tabloYuklenir` her zaman
önce çalışıyor. İkinci satırda sıra karıştırıldı ve iki yüz koşumun yüz ikisinde düştü.

Buradaki asıl bulgu şudur: **sabit sıra kusuru gizler.** Test paketi aylarca yeşil kalabilir,
sonra bir dosya adı değiştiğinde ya da koşucu paralelleştiğinde birden kırmızıya döner.
Sırayı bilerek karıştırmak bir tanı aracıdır; karıştırmada kullanılan tohum çıktıya
yazıldığında, düşen sıralama birebir yeniden üretilebilir.

Çözüm sırayı düzeltmek değil, bağımlılığı kaldırmaktır. Her test ihtiyaç duyduğu veriyi
kendisi kurar. Düzeltilmiş sürümde `suzgecDaraltir` kendi ölçümünü ekliyor ve sıradan
bağımsız hâle geliyor.

#### Paylaşılan Durum

`formGonderilir` testi formun boş bir taslakla açılmasını bekliyor. Taslak modül düzeyinde
yaşıyor ve yalnız gönderim başarılı olduğunda temizleniyor; başarısız gönderimde bir sonraki
koşuma kalıyor. İki yüz koşumun doksan beşinde ve yüz dördünde düştü.

Bu neden diğer ikisinden bir yönüyle ayrılır: test kendi başına, temiz bir ortamda her zaman
geçer. Düşmesi için önce bir koşumun ortamı kirletmiş olması gerekir. Geliştirici makinesinde
tek başına çalıştırıldığında geçen, paket içinde düşen test tam olarak budur — ve tanıyı en
çok zorlaştıran davranış budur, çünkü düşen testin kendisinde bir kusur yoktur.

Paylaşılan durumun kaynakları sayılabilir: modül düzeyinde tutulan değerler, yerel depolama
ve oturum depolaması, çerezler, program denetimli önbellek, arka uçtaki veri ve dosya
sistemi. Çözüm her testten önce kurulan ve sonra yıkılan bir düzenektir. Düzeltilmiş sürümde
taslak her koşumun başında sıfırlanıyor ve düşme sıfıra iniyor.

Paralel koşum bu sorunu büyütür. Aynı arka uç kaydını ya da aynı bağlantı noktasını
paylaşan iki koşucu birbirinin ortamını bozar; yalıtım, koşum başına ayrı veri kümesiyle
sağlanır.

#### Kırılganlıkla Ne Yapılır

Yeniden deneme akla ilk gelen çözümdür ve tek başına yanlıştır. Düşen testi sessizce yeniden
çalıştırmak kırılganlığı gizler; gizlenen kırılganlık zamanla artar ve arasına karışan
gerçek kusurlar da gizlenir. Kabul edilebilir tek biçim, yeniden denemenin **kaydedilmesi**
ve düşme oranının bir ölçü olarak izlenmesidir.

Bir test düzeltilemeyecek kadar karmaşıksa **karantinaya** alınır: paketten ayrılır, sonucu
yayımı engellemez, ama listede bir sahibi ve bir son tarihi vardır. Sahipsiz karantina,
testin sessizce silinmesinin uzun yoludur.

Belirlenimciliği artıran genel önlemler de vardır. Saat sabitlenir, yerel ayar ve saat
dilimi açıkça verilir, rastgelelik bu dersteki gibi tohumlanır ve tohum çıktıya yazılır.
Amaç aynıdır: testin sonucunu, sınadığı davranışın dışındaki hiçbir şeye bağlı bırakmamak.

#### Özet

- Kırılgan test, sınadığı kod değişmediği hâlde koşumdan koşuma farklı sonuç veren testtir;
  maliyeti kırmızı sonucun anlamını yitirmesidir.
- Sabit bekleme yanıt süresi dağılımının kuyruğunda düşer; koşula göre bekleme cömert bir
  üst sınırla bu nedeni sıfırladı.
- Sabit koşum sırası sıraya bağımlılığı gizler: aynı test sabit sırada hiç düşmezken karışık
  sırada iki yüz koşumun yüz ikisinde düştü.
- Paylaşılan durumun imzası, testin yalnız başına geçip paket içinde düşmesidir; her testin
  kendi düzeneğini kurup yıkması bu nedeni sıfırladı.
- Yeniden deneme kırılganlığı gizler; kabul edilebilir biçimi kaydedilen ve düşme oranı
  olarak izlenen yeniden denemedir, karantina ise sahipli ve süreli olmalıdır.

#### Sonraki Adım

Kararlı bir test paketi, denetim altındaki bir ortamda beklenen davranışın korunduğunu
söyler. Söylemediği şey, uygulamanın gerçek kullanıcıların elinde nasıl davrandığıdır. Test
makinesi hızlıdır, ağı düzgündür, önbelleği bilinen bir durumdadır; alandaki bir gözlemci
zayıf bir bağlantıda, yaşlı bir aygıtla ve dolu bir ekranla çalışır. Sonraki ders bu iki
veri kaynağını — denetimli koşumdan gelen laboratuvar verisiyle gerçek oturumlardan gelen
alan verisini — yan yana koyar ve aradaki farkı yüzdelik dilim hesabıyla ölçer.

### Gerçek Kullanıcı İzleme

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/gercek-kullanici-izleme

Laboratuvar ölçümüyle alan verisinin yanıtladığı farklı sorular, alanda ölçüt toplama ve gönderme biçimi, yüzdelik dilim hesabıyla dağılımın okunması, bölütleme ve örneklem büyüklüğünün etkisi.

Kararlı bir test paketi, denetim altındaki bir ortamda beklenen davranışın korunduğunu
söyler. Söylemediği şey, uygulamanın gerçek kullanıcıların elinde nasıl davrandığıdır. Test
makinesi hızlıdır, ağı düzgündür, önbelleği bilinen bir durumdadır. Kuzey Yamaç arayüzünü
alanda kullanan gözlemci ise zayıf bir bağlantıda, yaşlı bir aygıtla ve arka planda başka
sekmelerle çalışır.

Bu ders iki veri kaynağını yan yana koyar ve aradaki farkı ölçülebilir hâle getirir.

#### İki Kaynak, İki Soru

**Laboratuvar verisi**, denetimli bir koşumdan gelir: sabit aygıt profili, sabit ağ
koşulları, temiz önbellek. Başarım konusundaki ölçümler bu türdendir. Değerli yanı
belirlenimci olmasıdır — iki koşum arasındaki fark, koda yapılan değişikliğe bağlanabilir.
Yanıtladığı soru şudur: bu değişiklik neyi bozdu?

**Alan verisi**, gerçek oturumlardan toplanan ölçümlerdir; toplama yöntemine **gerçek
kullanıcı izleme (real user monitoring)** denir. Tek bir sayı değil, bir dağılım verir.
Yanıtladığı soru farklıdır: kullanıcılar gerçekte ne yaşıyor?

İkisi birbirinin yerine geçmez. Laboratuvar ölçümü gerileme tespiti için, alan verisi hedef
belirlemek için kullanılır.

#### Alanda Ne Toplanır

Tarayıcı, ölçütleri kendi başına biriktirir ve bir gözlemciyle sunar. Aşağıdaki kod boyama
kayıtlarını ve eşiği aşan etkileşim kayıtlarını toplar, sayfa gizlendiğinde tek bir demet
hâlinde gönderir.

```js
// olcut-toplayici.js — alan olcutlerini toplar ve tek demet halinde gonderir
const demet = { yol: location.pathname, surum: '2.4.1', olcutler: {} };

const gozlemci = new PerformanceObserver((liste) => {
  for (const kayit of liste.getEntries()) {
    demet.olcutler[kayit.name] = Math.round(kayit.startTime ?? kayit.duration);
  }
});
gozlemci.observe({ type: 'paint', buffered: true });
gozlemci.observe({ type: 'event', buffered: true, durationThreshold: 40 });

addEventListener('visibilitychange', () => {
  if (document.visibilityState !== 'hidden') return;
  gozlemci.takeRecords();
  navigator.sendBeacon('/olcut', JSON.stringify(demet));
}, { once: true });
```

Üç karar bu kodun içindedir. `buffered` seçeneği, gözlemci kurulmadan önce oluşmuş kayıtları
da getirir; ölçüm betiği geç yüklendiğinde ilk boyama kaybolmasın diye. Gönderim sayfa
kapanışında değil, sayfa **gizlendiğinde** yapılır: bir sekme kapanmadan arka plana atılıp
bir daha öne gelmeyebilir ve o durumda kapanış olayı hiç çalışmaz. Gönderim, sayfa
kaldırılırken de tamamlanan bir işaret isteğiyle yapılır.

Demete bağlam alanları eklenir: yol, uygulama sürümü, aygıt sınıfı, bağlantı türü, gezinme
türü. Bölütleme bu alanlarla yapılır ve bir sonraki bölümde görüleceği gibi, tek bir sayının
gizlediği şey burada ortaya çıkar.

Toplama her oturumda yapılmak zorunda değildir. **Örnekleme**, oturumların bir bölümünden
veri almaktır; oranı düşürmek iletim ve saklama maliyetini düşürür. Seçim oturum başına bir
kez yapılmalıdır — sayfa başına yapılırsa aynı oturumun bazı sayfaları örnekleme girer,
bazıları girmez ve oturum düzeyindeki sorular yanıtsız kalır.

#### Dağılımı Okumak

Alan verisi bir dağılımdır ve dağılımı tek sayıya indirmenin birden çok yolu vardır.
Aşağıdaki modül belirlenimci bir alan örneklemi üretir — üç aygıt sınıfı, farklı paylarla ve
farklı süre aralıklarıyla — ve yüzdelik dilim hesabını verir.

```js
// alan-verisi.mjs — alan olcumlerinin belirlenimci benzetimi ve yuzdelik hesabi
export function uretec(tohum) {
  let s = tohum >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

export const SINIFLAR = [
  { ad: 'hizli', pay: 0.60, taban: 300, yayilim: 200 },
  { ad: 'orta', pay: 0.30, taban: 700, yayilim: 600 },
  { ad: 'yavas', pay: 0.10, taban: 1800, yayilim: 2200 },
];

export function alanOrneklemi(sayi, tohum = 42) {
  const rastgele = uretec(tohum);
  const ornekler = [];
  for (let i = 0; i < sayi; i += 1) {
    const secim = rastgele();
    let birikim = 0;
    const sinif = SINIFLAR.find((s) => (birikim += s.pay) >= secim) ?? SINIFLAR.at(-1);
    ornekler.push({ sinif: sinif.ad, sure: Math.round(sinif.taban + sinif.yayilim * rastgele()) });
  }
  return ornekler;
}

// en yakin siralama yontemi: p yuzdeliginin altinda kalan orneklerin orani en az p'dir
export function yuzdelik(degerler, p) {
  if (degerler.length === 0) return NaN;
  const sirali = [...degerler].sort((a, b) => a - b);
  const sira = Math.ceil((p / 100) * sirali.length);
  return sirali[Math.min(sirali.length, Math.max(1, sira)) - 1];
}

export function ortalama(degerler) {
  return Math.round(degerler.reduce((t, d) => t + d, 0) / degerler.length);
}
```

```js
// rapor.mjs — laboratuvar olcumu ile alan dagiliminin karsilastirilmasi
import { alanOrneklemi, yuzdelik, ortalama, SINIFLAR } from './alan-verisi.mjs';

const alan = alanOrneklemi(5000);
const sureler = alan.map((o) => o.sure);

// laboratuvar kosumu: hizli aygit, kararli ag, dagilimin ortasi
const hizli = SINIFLAR[0];
const laboratuvar = Math.round(hizli.taban + hizli.yayilim * 0.5);

console.log(`laboratuvar olcumu   ${String(laboratuvar).padStart(5)} ms`);
console.log(`alan ortalamasi      ${String(ortalama(sureler)).padStart(5)} ms`);
for (const p of [50, 75, 90, 95]) {
  console.log(`alan p${String(p).padEnd(2)}             ${String(yuzdelik(sureler, p)).padStart(5)} ms`);
}

console.log('\naygit sinifina gore p75');
for (const s of SINIFLAR) {
  const alt = alan.filter((o) => o.sinif === s.ad).map((o) => o.sure);
  console.log(`  ${s.ad.padEnd(6)} ${String(alt.length).padStart(4)} oturum   p75 = ${yuzdelik(alt, 75)} ms`);
}

console.log('\norneklem buyuklugu ve p75 kararliligi');
for (const n of [20, 100, 1000, 5000]) {
  const p75ler = [0, 1, 2, 3, 4].map((t) => yuzdelik(alanOrneklemi(n, 100 + t).map((o) => o.sure), 75));
  console.log(`  n=${String(n).padStart(4)}  ${p75ler.join('  ')}`);
}
```

```sh
node rapor.mjs
```

```
laboratuvar olcumu     400 ms
alan ortalamasi        848 ms
alan p50               471 ms
alan p75              1014 ms
alan p90              1853 ms
alan p95              2971 ms

aygit sinifina gore p75
  hizli  2918 oturum   p75 = 452 ms
  orta   1561 oturum   p75 = 1143 ms
  yavas   521 oturum   p75 = 3473 ms

orneklem buyuklugu ve p75 kararliligi
  n=  20  775  930  837  1073  900
  n= 100  1054  1027  967  1082  1049
  n=1000  978  968  1006  1017  986
  n=5000  985  1001  996  1016  992
```

#### Sayıların Söylediği

Laboratuvar ölçümü 400 milisaniye. Alandaki dağılımın ortancası 471, dörtte üçlük dilimi
1014. Yani denetimli koşum, kullanıcıların iyi yarısını temsil ediyor ve kalan yarının
gerçeğini hiç göstermiyor. Bu, laboratuvar ölçümünün yanlış olduğu anlamına gelmez;
kapsamının dar olduğu anlamına gelir.

Ortalama 848 milisaniye ve hiçbir kullanıcı grubunu tarif etmiyor. Dağılımın kuyruğu uzun
olduğunda ortalama, azınlıktaki çok yavaş oturumlar tarafından yukarı çekilir; sonuç ne
çoğunluğun deneyimidir ne de en kötü durumun ölçüsü. Süre dağılımlarında ortalama okunmaz.

Yüzdelik dilim seçimi bir denge sorunudur. Medyan kullanıcıların yarısını gizler. p95 ve
üstü, tek tük uç oturumlarla — arka planda kalmış bir sekme, aşırı yüklü bir aygıt —
oynamaya başlar ve gürültülüdür. **p75** ikisinin arasındadır: dörtte üç oranında kullanıcının
altında kaldığı değeri verir ve kuyruğa yeterince duyarlıdır. Alan ölçütleri bu yüzden
yaygın olarak dörtte üçlük dilimden okunur.

Bölütleme tablosu tek sayının neyi gizlediğini gösteriyor. Aynı uygulamanın p75 değeri hızlı
aygıtlarda 452, orta sınıfta 1143, yavaş sınıfta 3473 milisaniye. Genel değer olan 1014,
bu üç gerçekten hiçbirine karşılık gelmiyor. İyileştirme kararı ancak bölütlerle verilebilir:
yavaş sınıftaki 521 oturum, genel ortalamayı düzeltmeye çalışan her işten daha çok kazanç
taşır.

Son tablo örneklem büyüklüğünün etkisidir. Yirmi oturumluk beş ayrı örneklemin p75 değerleri
775 ile 1073 arasında dolaşıyor — aynı dağılımdan gelmelerine rağmen. Bin oturumda aralık
968 ile 1017 arasına daralıyor. Küçük örneklemle ölçülen bir "iyileşme", çoğu zaman yalnızca
örneklem gürültüsüdür. Bir ölçütü bölütlere ayırmak da örneklem büyüklüğünü böler; her
bölütün kendi başına yeterli oturumu olmalıdır.

#### Bütçeye Bağlamak

Başarım konusunda tanımlanan bütçe, alan verisiyle doğrulandığında anlam kazanır. Bütçe
denetimi bir teste dönüştürülebilir.

```js
// butce.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { alanOrneklemi, yuzdelik } from './alan-verisi.mjs';

const BUTCE = { p75: 1200, yavasSinifP75: 4000 };

test('genel p75 butcenin altinda', () => {
  const sureler = alanOrneklemi(5000).map((o) => o.sure);
  assert.ok(yuzdelik(sureler, 75) <= BUTCE.p75, `p75 = ${yuzdelik(sureler, 75)} ms`);
});

test('yavas aygit sinifi kendi butcesinin altinda', () => {
  const yavas = alanOrneklemi(5000).filter((o) => o.sinif === 'yavas').map((o) => o.sure);
  assert.ok(yavas.length >= 300, 'yeterli ornek yok');
  assert.ok(yuzdelik(yavas, 75) <= BUTCE.yavasSinifP75, `yavas p75 = ${yuzdelik(yavas, 75)} ms`);
});
```

```sh
node --test butce.test.mjs
```

```
✔ genel p75 butcenin altinda (2.985458ms)
✔ yavas aygit sinifi kendi butcesinin altinda (0.718ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 37.70275
```

İki testin ayrı bütçeleri olması bilinçlidir. Tek bir genel eşik, yavaş sınıfın bozulmasını
hızlı sınıfın iyileşmesiyle örtebilir. Bölüt başına bütçe bu takası engeller.

Toplanan verinin kapsamı da bir karardır. Ölçüt demeti kullanıcıyı tanımlayan alan
taşımamalıdır; yol bilgisi kimlik içeren parçalardan arındırılır, serbest metin hiç
gönderilmez. Ölçüm kodunun kendisinin de bir maliyeti vardır — gözlemci geri çağrıları ana
iş parçacığında çalışır ve ölçtüğü şeyi bir miktar yavaşlatır.

#### Özet

- Laboratuvar verisi belirlenimcidir ve gerileme tespitine yarar; alan verisi bir dağılımdır
  ve kullanıcıların gerçekte ne yaşadığını söyler.
- Ölçütler tarayıcının gözlemcisiyle toplanır ve sayfa gizlendiğinde tek demet hâlinde
  gönderilir; örnekleme kararı oturum başına bir kez verilir.
- Süre dağılımlarında ortalama okunmaz; ortanca yarıyı gizler, çok yüksek dilimler
  gürültülüdür, dörtte üçlük dilim ikisinin arasındadır.
- Laboratuvar ölçümü 400 milisaniyeyken alan p75 değeri 1014 çıktı; bölütlere ayrıldığında
  aynı ölçüt 452, 1143 ve 3473 milisaniye oldu.
- Küçük örneklemde p75 geniş bir aralıkta dolaşır; bölüt başına bütçe, bir sınıftaki
  bozulmanın bir başkasındaki iyileşmeyle örtülmesini engeller.

#### Sonraki Adım

Ölçütler uygulamanın ne kadar hızlı olduğunu söyler; çalışıp çalışmadığını söylemez. Bir
gözlemci ölçümü kaydetmeye çalışırken bir istisna fırlarsa, o oturumun süre ölçütleri gayet
iyi görünmeyi sürdürür. Kullanıcı ise boş bir ekrana bakmaktadır ve çoğu zaman kimseye haber
vermez. Son ders bu sessiz başarısızlıkları görünür kılan düzeneği ele alır: istemci
hatalarının yakalanması, gönderilmesi ve binlerce olayın tek tek değil, aynı kökene inen
gruplar hâlinde okunması.

### Hata İzleme

Source: https://academia.sh/tr/kurslar/frontend-kalitesi/hata-izleme

İstemci hatalarının yakalandığı noktalar, kaynak haritasıyla yığıt izinin okunur kılınması, parmak izi üreterek gruplama, bir grubun etkisinin ölçülmesi ve gürültü ile gizlilik kararları.

Ölçütler uygulamanın ne kadar hızlı olduğunu söyler; çalışıp çalışmadığını söylemez. Ölçüm
tablosu çizilirken bir istisna fırlarsa, o oturumun süre ölçütleri iyi görünmeyi sürdürür.
Kullanıcı boş bir ekrana bakar ve çoğu zaman kimseye haber vermez.

Bu ders, o sessiz başarısızlıkları görünür kılan düzeneği kurar: hatanın yakalanması,
gönderilmesi ve binlerce olayın tek tek değil, aynı kökene inen gruplar hâlinde okunması.

#### Yakalama Noktaları

İstemci hataları tek bir yerden gelmez. Dört ayrı kaynak vardır ve her birinin kendi
sınırı bulunur.

Yakalanmayan istisnalar belge düzeyindeki `error` olayıyla bildirilir; olay nesnesi bir
`ErrorEvent` örneğidir ve iletiyi, kaynağı, konumu ve hata nesnesini taşır. Reddedilmiş sözlerin
işlenmemiş olanları `unhandledrejection` olayıyla gelir — Eşzamansız JavaScript ve Çalışma Zamanı
kursunda tanımlanan işlenmeyen reddetme kavramının tarayıcıdaki karşılığı budur. Kaynak yükleme
hataları kabarmaz; yalnız yakalama evresinde dinlenerek görülebilir. Dördüncüsü uygulamanın kendi
bildirdiği hatalardır: yakalanmış ama kurtarılamamış durumlar.

```js
// hata-toplayici.js — istemci hatalarinin toplanmasi
const uc = '/hata';
const ortak = { surum: '2.4.1', yol: location.pathname };

function bildir(tur, ileti, yigitIzi) {
  navigator.sendBeacon(uc, JSON.stringify({ ...ortak, tur, ileti, yigitIzi }));
}

addEventListener('error', (olay) => {
  if (olay.target !== window) {           // kaynak yukleme hatasi
    bildir('kaynak', `${olay.target.tagName} yuklenemedi`, olay.target.src ?? '');
    return;
  }
  bildir('istisna', olay.message, olay.error?.stack ?? '');
}, true);

addEventListener('unhandledrejection', (olay) => {
  const neden = olay.reason;
  bildir('reddetme', String(neden?.message ?? neden), neden?.stack ?? '');
});
```

Bir sınır baştan bilinmelidir. Farklı bir kaynaktan yüklenen bir betikten fırlayan hata,
tarayıcı tarafından ayrıntısız verilir: ileti yalnızca betik hatası olduğunu söyler, yığıt
izi boştur. Bunun nedeni kaynak yalıtımıdır — Kaynaklar Arası Paylaşım dersindeki kurallar
burada da işler. Ayrıntının açılması için betiğin kaynaklar arası olarak istenmesi ve
sunucunun buna izin veren başlığı göndermesi gerekir.

İkinci sınır, bileşen düzeyidir. Bileşen Tabanlı Arayüz Geliştirme kursundaki hata sınırı,
çizim sırasında fırlayan hatayı yakalayıp yedek bir arayüz gösterir; bu hatalar belge
düzeyindeki dinleyiciye ulaşmaz ve ayrıca bildirilmeleri gerekir.

#### Yığıt İzini Okunur Kılmak

Üretim yapısındaki kod küçültülmüştür: fonksiyon adları tek harfe inmiş, satırlar
birleşmiştir. Böyle bir yığıt izi okunamaz. Çözüm Modüller, Araçlar ve Ekosistem kursunda
tanımlanan **kaynak haritasıdır**: küçültülmüş konumu özgün dosya, satır ve ada geri
eşleyen tablo.

Eşleme istemcide değil, hataları toplayan tarafta yapılır. Kaynak haritası yayımlanan
varlıklarla birlikte dağıtılmaz — özgün kaynağı okunur kılar. Yapı sırasında ayrıca
üretilip yalnız toplama tarafına yüklenir.

#### Gruplama

Tek bir kusur binlerce olay üretir. Aynı satırdaki bir hata, listeyi açan her kullanıcıda
bir kez fırlar; okunabilir olması için olayların aynı kökene inenlerinin tek bir grupta
toplanması gerekir. Bunun anahtarına **parmak izi** denir.

Parmak izi, olayın kararlı parçalarından üretilir ve kararsız parçalar dışarıda bırakılır.
Yapı özeti taşıyan dosya adı, satır ve sütun numaraları, sorgu dizgileri, iletideki kimlik
ve sayı değerleri her yayımda ya da her olayda değişir; bunlar anahtara girerse aynı kusur
onlarca gruba dağılır.

```js
// parmak-izi.mjs — yigit izinden gruplama anahtari uretir
const CERCEVE = /^\s*at\s+(?:(.+?)\s+\()?([^()]+?)\)?$/;

export function cerceveler(yigitIzi) {
  return yigitIzi
    .split('\n')
    .slice(1)
    .map((satir) => satir.match(CERCEVE))
    .filter(Boolean)
    .map((e) => ({ islev: e[1] ?? '<anonim>', kaynak: e[2] }));
}

export function kaynagiNormallestir(kaynak) {
  if (kaynak === '<anonymous>' || kaynak.startsWith('node:')) return kaynak;
  return kaynak
    .replace(/^[a-z]+:\/\/[^/]+/, '')           // koken
    .replace(/[?#][^:]*$/, '')                  // sorgu ve parca
    .replace(/:\d+:\d+$/, '')                   // satir ve sutun
    .replace(/\.[0-9a-f]{6,}\.(m?js)$/, '.$1'); // yapi ozeti
}

export function iletiyiNormallestir(ileti) {
  return ileti
    .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, '<kimlik>')
    .replace(/\b\d+\b/g, '<sayi>');
}

function ozet(metin) {
  let h = 0x811c9dc5;
  for (const kod of metin) {
    h ^= kod.codePointAt(0);
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h.toString(16).padStart(8, '0');
}

export function parmakIzi(olay, { derinlik = 3 } = {}) {
  const [ilkSatir] = olay.yigitIzi.split('\n');
  const [tur, ...kalan] = ilkSatir.split(': ');
  const anahtar = [
    tur,
    iletiyiNormallestir(kalan.join(': ')),
    ...cerceveler(olay.yigitIzi)
      .slice(0, derinlik)
      .map((c) => `${c.islev}@${kaynagiNormallestir(c.kaynak)}`),
  ].join('|');
  return { anahtar, ozet: ozet(anahtar) };
}

export function grupla(olaylar) {
  const gruplar = new Map();
  for (const olay of olaylar) {
    const { anahtar, ozet: kimlik } = parmakIzi(olay);
    const grup = gruplar.get(kimlik) ?? { kimlik, anahtar, sayi: 0, oturumlar: new Set(), surumler: new Set() };
    grup.sayi += 1;
    grup.oturumlar.add(olay.oturum);
    grup.surumler.add(olay.surum);
    gruplar.set(kimlik, grup);
  }
  return [...gruplar.values()].sort((a, b) => b.oturumlar.size - a.oturumlar.size);
}
```

Aşağıdaki altı olay, aynı kusurun farklı yayımlardan ve farklı oturumlardan gelen
örneklerini içeriyor.

```js
// rapor.mjs — alti hata olayinin gruplanmasi
import { grupla } from './parmak-izi.mjs';

const OLAYLAR = [
  { oturum: 's-1', surum: '2.4.0', yigitIzi:
`TypeError: Cannot read properties of undefined (reading 'deger')
    at satirCiz (https://kuzey-yamac.ornek/varlik/tablo.4f2a9c.js:12:31)
    at Array.map (<anonymous>)
    at OlcumTablosu (https://kuzey-yamac.ornek/varlik/tablo.4f2a9c.js:40:22)` },
  { oturum: 's-2', surum: '2.4.1', yigitIzi:
`TypeError: Cannot read properties of undefined (reading 'deger')
    at satirCiz (https://kuzey-yamac.ornek/varlik/tablo.9b1d70.js:12:47)
    at Array.map (<anonymous>)
    at OlcumTablosu (https://kuzey-yamac.ornek/varlik/tablo.9b1d70.js:41:22)` },
  { oturum: 's-3', surum: '2.4.1', yigitIzi:
`TypeError: Cannot read properties of undefined (reading 'deger')
    at satirCiz (https://kuzey-yamac.ornek/varlik/tablo.9b1d70.js:12:47?v=3)
    at Array.map (<anonymous>)
    at OlcumTablosu (https://kuzey-yamac.ornek/varlik/tablo.9b1d70.js:41:22)` },
  { oturum: 's-2', surum: '2.4.1', yigitIzi:
`Error: olcum 84213 kaydedilemedi (400)
    at gonder (https://kuzey-yamac.ornek/varlik/form.1c8e55.js:88:11)` },
  { oturum: 's-4', surum: '2.4.1', yigitIzi:
`Error: olcum 91007 kaydedilemedi (400)
    at gonder (https://kuzey-yamac.ornek/varlik/form.1c8e55.js:88:11)` },
  { oturum: 's-5', surum: '2.4.1', yigitIzi:
`TypeError: sozlesme.gonder is not a function
    at kaydet (https://kuzey-yamac.ornek/varlik/form.1c8e55.js:120:8)` },
];

for (const g of grupla(OLAYLAR)) {
  console.log(`${g.kimlik}  olay=${g.sayi}  oturum=${g.oturumlar.size}  surum=${[...g.surumler].join(',')}`);
  console.log(`          ${g.anahtar}`);
}
```

```sh
node rapor.mjs
```

```
1c081b4b  olay=3  oturum=3  surum=2.4.0,2.4.1
          TypeError|Cannot read properties of undefined (reading 'deger')|satirCiz@/varlik/tablo.js|Array.map@<anonymous>|OlcumTablosu@/varlik/tablo.js
bb0e50c8  olay=2  oturum=2  surum=2.4.1
          Error|olcum <sayi> kaydedilemedi (<sayi>)|gonder@/varlik/form.js
8a4701e6  olay=1  oturum=1  surum=2.4.1
          TypeError|sozlesme.gonder is not a function|kaydet@/varlik/form.js
```

Altı olay üç gruba indi. Birinci grubun üç örneği farklı yapı özetleri, farklı satır ve
sütun numaraları ve birinde bir sorgu dizgisi taşıyordu; normalleştirme bunları eledi.
İkinci grubun iki örneği farklı ölçüm numaraları içeriyordu; sayılar yer tutucuya çevrildiği
için tek kusur olarak göründüler.

Gruplama iki yönde yanlış ayarlanabilir. **Aşırı gruplama**, birbirinden bağımsız kusurları
tek satırda toplar: iletiyi bütünüyle atıp yalnız hata türüne bakmak buna yol açar. **Yetersiz
gruplama** ise tek kusuru yayım başına ayrı gruplara böler ve hiçbirinin etkisi görünmez
olur. Ayarın ölçüsü kullanışlılıktır: bir grup, tek bir düzeltmeyle kapanabiliyorsa doğru
çizilmiştir.

#### Bir Grubun Okunması

Grubun taşıdığı sayılar önceliklendirmeyi belirler. **Olay sayısı** kusurun ne sıklıkta
fırladığını, **etkilenen oturum sayısı** kaç kişiye dokunduğunu söyler. İkisi ayrılmalıdır:
tek bir oturumda döngü içinde binlerce kez fırlayan bir hata, olay sayısında listenin
başına çıkar ama bir kullanıcıyı ilgilendirir. Sıralama oturum sayısına göre yapılır.

Sürüm dağılımı ikinci bilgidir. Bir grup yalnız son sürümde görünüyorsa, kusur o yayımla
gelmiştir ve geri alma bir seçenektir. İlk görülme ve son görülme zamanları aynı soruyu
zaman ekseninde yanıtlar.

Gürültü ayrı bir iştir. Tarayıcı eklentilerinden ve dış betiklerden gelen hatalar
uygulamanın kodunda değildir; yığıt izinin ilk çerçevesi uygulamanın alan adına ait değilse
süzülebilir. Kullanıcı sayfadan ayrılırken iptal edilen isteklerin ürettiği hatalar da
gerçek kusur değildir.

Hacim denetlenmelidir. Bir döngü içinde fırlayan hata tek oturumdan binlerce olay
gönderebilir; oturum başına hız sınırı ve örnekleme bunu keser. Örnekleme yapılırken
gönderilmeyen olayların sayısı da iletilir, yoksa etki ölçüsü bozulur.

Gizlilik son karardır. Hata iletisi kullanıcının yazdığı metni içerebilir, adres kimlik
taşıyabilir. Toplayıcı, bilinen kimlik biçimlerini ve serbest metni gönderimden önce
maskeler. Kişisel veri, toplandıktan sonra silinmesi zor olan veridir.

#### Özet

- İstemci hataları dört noktadan gelir: yakalanmayan istisnalar, işlenmeyen söz reddetmeleri,
  kaynak yükleme hataları ve uygulamanın kendi bildirdikleri.
- Farklı kaynaktan gelen betiğin hatası ayrıntısız verilir; ayrıntı için kaynaklar arası
  istek ve izin başlığı gerekir. Yığıt izi kaynak haritasıyla toplama tarafında çözülür.
- Parmak izi, hata türü ile normalleştirilmiş ileti ve ilk birkaç çerçeveden üretilir; yapı
  özeti, satır ve sütun numarası, sorgu dizgisi ve iletideki sayılar dışarıda bırakılır.
- Altı olay üç gruba indi: farklı yayımlardan ve farklı ölçüm numaralarından gelen örnekler
  tek kusur olarak göründü.
- Önceliklendirme olay sayısına değil etkilenen oturum sayısına göre yapılır; gürültü süzülür,
  hacim sınırlanır ve kişisel veri gönderimden önce maskelenir.

#### Kurs Kapanışı

Frontend Kalitesi kursu dört konuyu aynı ilkeye bağladı: **ölçülmeyen kalite iddiası
yoktur.** Başarım konusu kullanıcı merkezli ölçütleri tanımladı ve bunları bir bütçeye
bağladı. Erişilebilirlik konusu ölçütleri standart düzeyinde ele aldı ve otomatik denetimin
nerede bitip elle doğrulamanın nerede başladığını gösterdi. Güvenlik konusu istemci
tarafındaki riskleri savunma katmanlarına dönüştürdü. Bu son konu ise üçünün de ortak
sorusunu yanıtladı: kurulan şey bozulduğunda bunu ne haber verir? Yanıt iki yönlüydü —
değişiklikten önce çalışan testler ve yayımdan sonra çalışan izleme. İkisi arasında bir
döngü vardır: alanda görülen bir kusur, onu bir daha geçirmeyecek bir teste dönüşür;
testin gözden kaçırdığı bir durum, alanda bir hata grubu olarak geri gelir.

Bu kursla birlikte Frontend Geliştirme müfredatı da kapanıyor. Yol belgeden başladı — HTML
ile yapının kurulması ve tarayıcının o yapıyı nasıl işlediği. Sonra biçeme geçti: basamaklama,
kutu modeli ve görsel özellikler. Düzen sistemleri ve duyarlı tasarım, yapının farklı ekran
ve girdi koşullarına uyarlanmasını ele aldı. Tarayıcı ve web platformu, sayfayı bir belge
olmaktan çıkarıp davranışı olan bir uygulamaya çevirdi. Bileşen tabanlı geliştirme bu
davranışı yeniden kullanılabilir parçalara böldü; uygulama mimarisi o parçaları yönlendirme,
durum ve veri erişimiyle bir bütün hâline getirdi. Oluşturma stratejileri, aynı uygulamanın
sunucuda mı istemcide mi, ne zaman ve hangi maliyetle üretileceğini sordu. Ve bu son kurs,
bütün bu kararların doğru kaldığının nasıl bilineceğini gösterdi: belgeden biçeme, düzenden
davranışa, bileşenden mimariye, oluşturmadan kaliteye.

Müfredat boyunca bir soru bilerek dışarıda bırakıldı. Bir düğmenin nasıl çalışacağı, hangi
rolü taşıyacağı, hangi durumlarda hangi ölçüte uyacağı anlatıldı; o düğmenin **neden o
boyutta, o renkte ve orada** durduğu anlatılmadı. Aralık ölçeğinin, tipografik ölçeğin, renk
rollerinin ve durum tasarımının gerekçeleri kendi başına bir alandır. Arayüz Tasarımı ve
Tasarım Sistemleri müfredatı bu soruyu ele alır: görsel ve etkileşimsel kararları
gerekçelendirilebilir ilkelere bağlar ve bu kararları ölçeklenen bir sisteme dönüştürür. İlk
kursu olan Arayüz Tasarımının Temelleri, bu müfredatla birlikte okunmaya uygundur — burada
kurulan bileşenlerin görünüşüne dair kararlar orada gerekçesini bulur.

