---
title: 'İşlevsel Olmayan Test'
source: 'https://academia.sh/tr/kurslar/islevsel-olmayan-test'
language: tr
updated: '2026-08-17T04:42:57+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Yük, dayanıklılık ve ani yük testlerini hedefe göre tasarlar'
    - 'Güvenlik testi türlerini kapsamlarıyla ayırır'
    - 'Erişilebilirlik ve uyumluluk testini süreçlere yerleştirir'
---

# İşlevsel Olmayan Test

İşlevin doğruluğu dışındaki soruların sınanması: yük, stres, dayanıklılık ve ani yük testlerinin hedefe göre ayrılması, gerçekçi kullanıcı davranışının modellenmesi, yüzdelik gecikme ile verimin ölçüt olarak seçilmesi, sonucun uygulama, veritabanı ve ağ katmanlarına dağıtılması, yanıltıcı ölçümden kaçınma disiplini, statik ve dinamik güvenlik testi ile bağımlılık taramasının kapsamlarının ayrılması, sızma testinin otomasyonun bittiği yerde konumlanması, erişilebilirlik ile uyumluluk testinin sürece yerleştirilmesi ve arıza senaryolarının bilinçli tetiklenerek kurtarmanın doğrulanması.

## Başarım Testi

Yük altındaki davranışın sınanması: yük, stres, dayanıklılık ve ani yük testlerinin sordukları soruyla ayrılması, gerçekçi kullanıcı davranışının düşünme süresi ve karışım oranlarıyla modellenmesi, ortalamanın gizlediğini gösteren yüzdelik gecikme ile verim ölçütlerinin seçilmesi, tek bir sonucun uygulama, veritabanı ve ağ katmanlarına dağıtılması ve ısınma, gürültü ile tek koşum yanılgısından kaçınan karşılaştırma disiplini.

### Başarım Testi Türleri

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/basarim-testi-turleri

Yük altındaki davranışın dört ayrı soruya bölünmesi: yük, stres, dayanıklılık ve ani yük testlerinin aynı ölçütü farklı bir karar penceresinde okuması, aynı iki yapı üzerinde dördünün dört ayrı karar vermesi, eşiğin bir bütçe paylaştırmasından türetilmesi ve eşik taramasıyla hangi türün hangi eşik aralığında yanlış geçme ürettiğinin sayılması.

Tümleşim, Sözleşme ve Uçtan Uca Test kursunun kapanışı üç soru bıraktı: sistemin yük
altında nasıl davrandığı, saldırı altında ne verdiği ve arıza altında nasıl bozulduğu
hiçbir derste sınanmadı; oradaki her testin sorusu **işlev doğru mu** idi. Bu kurs
birinciden başlar ve ilk kararı sorar: hangi test koşacak. Yük altında sınamanın dört
biçimi vardır ve aralarındaki fark süre ya da şiddet değil, **sordukları sorudur**.

#### Dört Soru, Dört Karar Penceresi

**Yük testi** (load test) hedeflenen yükte eşiğin karşılanıp karşılanmadığını,
**dayanıklılık testi** (soak test) aynı yükün uzun sürede bozulma üretip üretmediğini,
**ani yük testi** (spike test) sıçramadan sonra toparlanmanın ne kadar sürdüğünü sorar.
Üçü Başarım Karşıt Kalıpları ve İzleme kursunda kurulmuş ve ölçülmüştü; orada sonuç bir
kapasite kararına çevrildi. Burada testin kendisi tasarlanır ve dördüncü tür eklenir:
**stres testi** (stress test) yükü eşik kırılana kadar artırır ve **kırılma noktasını**
(breaking point) arar.

Dördü de tek bir ölçüt okur — ödünç isteğinin p95 gecikmesi — ama her biri onu başka bir
**karar penceresinde** okur: yük testi bütün koşumdan, stres testi hedef hızın iki katı
basamağından, dayanıklılık testi son çeyrekten, ani yük testi sıçramadan sonraki ilk
saniyeden.

#### Eşik ve Eşiğin Kaynağı

Bir başarım testinin çıktısı bir dağılımdır; "geçti" demek bir eşik seçmektir. Bu kursun
kuralı: eşik kaynağıyla yazılır.

**IO1 (varsayım + hesap) — gişe akışının sunucu tarafı bütçesi 60 ms'tir; akış üç servis
çağrısı yapar ve bütçe eşit paylaştırılır. Ödünç servisinin payı: p95 ≤ 20 ms.** Bütçe bir
varsayımdır, pay bir hesaptır; eşik hiçbir ölçümden okunmadı.
**IO2 (varsayım) — tepe saatte gişe işlemi hızı 1.000 istek/s.**

Kararın sahibi de yazılır: kırmızı sonuç yayımı durdurur; stres testinin verdiği kırılma
noktası durdurmaz, kapasite planına girdi olur.

#### Düzenek

Sınanan sistem kütüphane ödünç servisidir: üyenin açık ödünç sayısı doğrulanır, sonra
kayıtlar yazılır. Bir gişe işlemi üyeye en çok beş kitap verir — Birim Testi ve Test Odaklı
Geliştirme kursundan gelen ödünç sınırı — ve her kitap bir satır ekler. Üç yapı koşturulur:
dizinsiz katalog, dizinli katalog ve dizinli olduğu hâlde ödünç sayısını yanlış hesaplayan
bir üçüncü yapı.

```js
// kutuphane/sunucu.mjs — odunc servisi: uyenin acik odunc sayisini node:sqlite katalogunda
// dogrular ve yeni kayitlari yazar. Argumanlar: <baglanti-noktasi> <yapi 0|1|2>
// 0 = dizinsiz (tam tarama), 1 = dizinli, 2 = dizinli ama sayim yanlis.
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 18000, KITAP = 5;  // uye basina 36 kayit; gise islemi 5 kitap yazar
const port = Number(process.argv[2]), yapi = process.argv[3];

if (Number.isInteger(port) === false || ["0", "1", "2"].includes(yapi) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <yapi 0|1|2>");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  if (yapi !== "0") db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const say = db.prepare(yapi === "2"
    ? "SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ? AND gun = 0"   // kusurlu sayim
    : "SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const ekle = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  let istek = 0, kayit = BASLANGIC, tarama = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    yanit.writeHead(200, { "content-type": "application/json" });
    if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ istek, kayit, tarama })); return; }
    istek += 1;
    tarama += yapi === "0" ? kayit : Math.ceil(Math.log2(kayit));   // dizinsiz yapida tam tarama
    const uye = u.searchParams.get("uye") ?? "U-1";
    const n = say.get(uye).n;
    for (let j = 0; j < KITAP; j += 1) ekle.run(uye, `K-${(istek + j) % 9000}`, istek % 400);
    kayit += KITAP;
    yanit.end(JSON.stringify({ uye, odunc: n + 1 }));
  }).listen(port);
}
```

Yük üreteci **açık döngülüdür**: istekleri hedef hızda gönderir, sunucunun yetişmesini
beklemez. Yük testi koşumunda bir de **test kâhini** çalışır — Kalite ve Test Temelleri
kursunun terimiyle beklenen sonucun kaynağı. Kuralı şudur: her istek üyeye beş kayıt
yazdığı için dönen ödünç sayısı bir önceki yanıttan büyük olmalıdır.

```js
// kutuphane/turler.mjs — acik dongulu uretec ve dort test turu; dordu de ayni olcutu (p95)
// baska bir pencerede okur. Argumanlar: <baglanti-noktasi> <yapi 0|1|2> <tur>
import { Agent, request } from "node:http";

const TABAN = 1000, ESIK = 20;          // IO2: hedef hiz (istek/s), IO1: p95 esigi (ms)
const BASAMAK = [0.5, 1, 2, 3];         // stres merdiveni: hedef hizin katlari, basamak 1 s
const YAPI = { 0: "dizinsiz", 1: "dizinli", 2: "hizli-yanlis" };
const [port, yapi, tur] = [Number(process.argv[2]), process.argv[3], process.argv[4]];
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
const son = new Map();
let bozuk = 0, denetim = 0;

const yuz = (d, p) => { const s = [...d].sort((a, b) => a - b);
  return s.length === 0 ? 0 : s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))]; };
const pencere = (k, a, b) => k.filter((r) => r.sn >= a && r.sn < b).map((r) => r.ms);
const vir = (x) => x.toFixed(1).replace(".", ",");
const olcut = () => new Promise((coz) => {
  request({ port, path: "/olcut", agent }, (y) => { let g = "";
    y.on("data", (p) => { g += p; }); y.on("end", () => coz(JSON.parse(g))); }).end();
});

function birIstek(i, kayit, sn, kahin) {
  const bas = performance.now(), uye = `U-${i % 500}`;
  return new Promise((coz) => {
    const r = request({ port, path: `/odunc?uye=${uye}&kitap=K-${i % 900}`, agent }, (y) => {
      let g = ""; y.on("data", (p) => { g += p; });
      y.on("end", () => {
        if (kahin) {                    // her istek uyeye bes kayit yazar: sayi artmali
          const n = JSON.parse(g).odunc;
          if (son.has(uye)) { denetim += 1; if (n <= son.get(uye)) bozuk += 1; }
          son.set(uye, n);
        }
        kayit.push({ ms: performance.now() - bas, sn }); coz();
      });
    });
    r.on("error", () => { kayit.push({ ms: performance.now() - bas, sn, hata: 1 }); coz(); });
    r.end();
  });
}

async function kosum(sure, hiz, kahin = false) {   // hiz(t): t saniyesindeki hedef hiz
  const kayit = [], ucus = [];
  let borc = 0, i = 0, sonT = 0;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    borc += hiz(t) * (t - sonT);       // gecen gercek sureye gore: zamanlayici sapmasini yutar
    sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(i++, kayit, t, kahin)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  return { kayit, gecen: (performance.now() - bas) / 1000 };
}

if (Number.isInteger(port) === false || ["yuk", "stres", "dayaniklilik", "ani"].includes(tur) === false) {
  console.log("kullanim: node kutuphane/turler.mjs <nokta> <yapi 0|1|2> yuk|stres|dayaniklilik|ani");
} else {
  await birIstek(0, [], 0, false);                 // isinma istegi
  let r, karar, ek;
  if (tur === "yuk") {
    r = await kosum(2, () => TABAN, true);         // kahin yalniz doymamis kosumda anlamli
    karar = yuz(r.kayit.map((k) => k.ms), 95);
    ek = `kahin ${bozuk}/${denetim} bozuk`;
  } else if (tur === "stres") {
    r = await kosum(4, (t) => TABAN * BASAMAK[Math.min(3, Math.floor(t))]);
    const p = BASAMAK.map((_, i) => yuz(pencere(r.kayit, i, i + 1), 95));
    const k = p.findIndex((x) => x > ESIK);
    karar = p[2];                                  // 2 kat basamaginin p95'i
    ek = `kirilma ${k < 0 ? "> 3000" : TABAN * BASAMAK[k]} istek/s`;
  } else if (tur === "dayaniklilik") {
    r = await kosum(10, () => TABAN);
    const c = [0, 1, 2, 3].map((i) => yuz(pencere(r.kayit, i * 2.5, i * 2.5 + 2.5), 95));
    karar = c[3];                                  // son ceyregin p95'i
    ek = `ceyrek p95 ${c.map(vir).join(" / ")}`;
  } else {
    r = await kosum(5, (t) => (t >= 1.5 && t < 3 ? TABAN * 3 : TABAN));
    karar = yuz(pencere(r.kayit, 3, 4), 95);       // sicramadan sonraki ilk saniye
    const d = [3, 4].find((sn) => yuz(pencere(r.kayit, sn, sn + 1), 95) <= ESIK);
    ek = `donus ${d === undefined ? "2,0+" : vir(d - 2)} s`;
  }
  const o = await olcut();
  console.log(JSON.stringify({ yapi: YAPI[yapi], tur, karar, ek,
    hiz: r.kayit.length / r.gecen, tarama: o.tarama }));
}
agent.destroy();
```

Rapor dokuz koşumu okur; eşiği IO1'in 20 milisaniyesinden uzaklaştırır ve her
değerde kaç türün yanlış karar verdiğini sayar.

```js
// kutuphane/rapor.mjs — dokuz kosumu tabloya cevirir ve p95 esigini tarar. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const ESIK = 20, TUR = ["yuk", "stres", "dayaniklilik", "ani"];
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const bul = (y, t) => s.find((r) => r.yapi === y && r.tur === t);
const v = (x) => x.toFixed(2).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const kar = (x) => (x <= ESIK ? "gecti" : "kaldi");

console.log(`esik p95 <= ${ESIK} ms (IO1), hedef hiz 1000 istek/s (IO2)`);
console.log("tur          yapi          ulasilan/s  karar p95  karar   tarama adimi  tur ozel sayi");
for (const t of TUR) for (const y of ["dizinsiz", "dizinli", "hizli-yanlis"]) {
  const r = bul(y, t);
  if (r !== undefined) console.log(t.padEnd(13) + y.padEnd(14) + g(v(r.hiz), 10) +
    g(v(r.karar), 11) + g(kar(r.karar), 7) + g(r.tarama.toLocaleString("tr-TR"), 15) + "  " + r.ek);
}

console.log("\nesik taramasi: dort turun kaci yanlis karar veriyor (dizinsiz kusurlu, dizinli saglam)");
console.log("p95 esigi (ms)  yanlis gecme /4  yanlis kalma /4  ayiran tur /4  ayiran turler");
for (const e of [1, 3, 5, 20, 50]) {
  const gec = TUR.filter((t) => bul("dizinsiz", t).karar <= e);
  const kal = TUR.filter((t) => bul("dizinli", t).karar > e);
  const ayr = TUR.filter((t) => bul("dizinli", t).karar <= e && bul("dizinsiz", t).karar > e);
  console.log(String(e).padEnd(16) + g(gec.length, 15) + g(kal.length, 17) + g(ayr.length, 15) +
    "  " + (ayr.join(", ") || "-"));
}
```

{/* run: skip */}
```bash
# olc.sh — dokuz kosum: dort tur x iki yapi, arti ucuncu yapiya bir yuk testi.
# Her kosum yeni baslatilmis bir sunucuyla calisir; katalog hep ayni boydan baslar.
rm -f sonuc.jsonl
for Y in 0 1 2; do
  for T in yuk stres dayaniklilik ani; do
    if [ "$Y" = 2 ] && [ "$T" != yuk ]; then continue; fi
    node kutuphane/sunucu.mjs 8851 $Y & SUNUCU=$!
    sleep 0.7
    node kutuphane/turler.mjs 8851 $Y $T >> sonuc.jsonl
    kill $SUNUCU
    wait $SUNUCU 2>/dev/null
  done
done
node kutuphane/rapor.mjs sonuc.jsonl
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
esik p95 <= 20 ms (IO1), hedef hiz 1000 istek/s (IO2)
tur          yapi          ulasilan/s  karar p95  karar   tarama adimi  tur ozel sayi
yuk          dizinsiz          997,12       3,99  gecti     45.911.030  kahin 0/1496 bozuk
yuk          dizinli           996,94       2,75  gecti         29.970  kahin 0/1497 bozuk
yuk          hizli-yanlis      997,22       3,22  gecti         29.985  kahin 1495/1498 bozuk
stres        dizinsiz         1113,76    1430,86  kaldi    222.558.255  kirilma 2000 istek/s
stres        dizinli          1622,37       3,37  gecti        100.934  kirilma > 3000 istek/s
dayaniklilik dizinsiz          942,36    1229,72  kaldi    429.703.050  ceyrek p95 4,1 / 4,2 / 6,9 / 1229,7
dayaniklilik dizinli           999,23       2,28  gecti        157.487  ceyrek p95 3,0 / 2,3 / 2,4 / 2,3
ani          dizinsiz          901,25      40,29  kaldi    303.342.330  donus 2,0+ s
ani          dizinli          1596,89       2,26  gecti        124.934  donus 1,0 s

esik taramasi: dort turun kaci yanlis karar veriyor (dizinsiz kusurlu, dizinli saglam)
p95 esigi (ms)  yanlis gecme /4  yanlis kalma /4  ayiran tur /4  ayiran turler
1                             0                4              0  -
3                             0                1              3  yuk, dayaniklilik, ani
5                             1                0              3  stres, dayaniklilik, ani
20                            1                0              3  stres, dayaniklilik, ani
50                            2                0              2  stres, dayaniklilik
```

Süre sütunları bu makineye bağlıdır; **tarama adımı** değildir: tam taramanın adımı o anki
kayıt sayısı, dizinli aramanınki onun ikili logaritmasıdır. Gönderilen istek de hesaptır —
hedef hız ile sürenin çarpımı, dokuz koşumda 55.000. Savlar bu kümeye dayanır; tek koşumdan
okunan değerler *bu koşumda* diye işaretlenir.

#### Dört Tür, Dört Karar

Dizinsiz yapı dört türden **birinde geçti, üçünde kaldı**. Yük testi hedef hızda p95'i bu
koşumda 3,99 milisaniyede buldu — eşiğin beşte biri. Aynı yapı stres testinde hedefin iki
katında kırıldı, dayanıklılık testinde son çeyrekte 1.229,72 milisaniyeye çıktı ve ani yük
testinde sıçramadan sonra iki saniye içinde eşiğin altına dönemedi. Aynı ölçüt, aynı eşik,
dört ayrı karar; fark penceredir.

Dayanıklılık testinin çeyrek dizisi nedeni de veriyor. Yük sabit, kod aynı; değişen tek şey
katalogdaki kayıt sayısıdır. Tam tarama her istekte tablonun tamamına dokunur, tablo her
istekte beş satır büyür, hizmet süresi artar ve doluluk bir yerde tavana dayanır. Tarama
adımı bunu koşumdan bağımsız gösterir: dizinsiz yapı 429.703.050 satır adımı harcarken
dizinli yapı aynı işi 157.487 adımla yaptı — **2.729 kat**. Kusur sınıfının adı
**veri büyümesine bağlı bozulmadır**; Başarım Karşıt Kalıpları ve İzleme kursundaki tanıların girdisi budur.

Stres testinin verdiği sayı başka türdendir. Merdiven kısa tutulur; uzununda veri büyümesi
hıza karışır. Dizinsiz yapı 2.000 istek/s'te kırıldı, dizinli yapı 3.000 istek/s'te hâlâ
eşiğin altındaydı. Bu bir geçti-kaldı değil bir mesafedir: hedef hızın kaç katına kadar
dayanıldığı.

#### Eşik Taraması

Bir eşik ancak iki sayıyla savunulabilir: kaç gerçek kusuru kaçırdığı ve kaç sağlam koşumu
kırmızıya döndürdüğü.

IO1'in verdiği 20 milisaniyede **yanlış geçme 1, yanlış kalma 0**. Kaçan tür yük testidir:
iki saniyelik bir koşumda tablo yalnız 10.000 satır büyüdüğü için kusurlu yapı eşiğin çok
altında kalıyor. Eşiği 5 milisaniyeye indirmek bunu değiştirmiyor; 3 milisaniye yük testini
ayırt eder hâle getiriyor ama sağlam yapı bu kez stres testinde kalıyor. 50 milisaniyede
yanlış geçme ikiye çıkıyor.

İki sonuç çıkar. **Yük testi bu kusur sınıfını IO1'in eşiğinde göremez**, çünkü penceresi
kusurun büyümesine zaman tanımıyor. Ve 3 milisaniyelik eşik hiçbir bütçeden
türetilmemiştir, bu koşumun verisine uydurulmuştur — uydurulduğu hâlde yanlış kalmayı
sıfırda tutamıyor. Eşiği kaynağından koparmak, taramayı düzeltmeden kararı boşaltır.

#### Yakalanmayan Sınıf ve Bedel

Üçüncü yapı dizinlidir, hızlıdır ve yanlıştır: ödünç sayısını yalnız tek bir günün
kayıtları üzerinden hesaplar. Yük testinin p95'i bu koşumda 3,22 milisaniye — sağlam
yapının 2,75'inden ayırt edilemez ve dört tür de bu yapıyı geçirir. Kâhin çalıştırıldığında
1.498 denetimin 1.495'i bozuk çıkıyor. Kaçan kusur sınıfının adı **yanıt doğruluğu
kusurudur**: başarım testleri süre okur, gövde okumaz. Kör kalmalarının nedeni pencere
değil ölçüttür.

Bedelin ortama bağlı yanı: dokuz koşum bu makinede bir dakikanın altında
sürdü. Koşumdan bağımsız yanı sayılır — dokuz sunucu süreci, 55.000 istek, dizinsiz yapıda
1,00 milyar satır adımı. En pahalı tür dayanıklılık testidir: yük testinin beş katı istek,
9,4 katı tarama adımı.

#### Özet

- Dört tür aynı ölçütü (p95) farklı bir karar penceresinde okur: bütün koşum, iki kat
  basamağı, son çeyrek, sıçrama sonrası ilk saniye.
- Eşik kaynağıyla yazılır: IO1'in 20 milisaniyesi 60 milisaniyelik bir bütçe varsayımının
  üç servise eşit paylaştırılmasından gelir, hiçbir ölçümden okunmadı.
- Aynı kusur dört ayrı karar aldı: yük testi geçti, stres testi 2.000 istek/s'te kırıldı,
  dayanıklılık testi son çeyrekte 1.229,72 milisaniye buldu, ani yük testi toparlanamadı.
- Eşik taraması IO1'de yanlış geçme 1, yanlış kalma 0 veriyor; 3 milisaniye yük testini
  ayırır ama yanlış kalmayı bire çıkarır ve eşiği kaynağından koparır.
- Yakalanan sınıf veri büyümesine bağlı bozulmadır (tarama adımı 2.729 kat); yakalanmayan
  sınıf yanıt doğruluğu kusurudur: hızlı ve yanlış bir yapı dört türü de geçer.

#### Sonraki Adım

Bu dersin bütün koşumları tek bir uca, sabit hızda ve aralıksız istek gönderdi. Gerçek bir gişe
günü böyle değildir: görevli arama yapar, ödünç verir ve işlemler arasında düşünür. Sonraki ders
senaryoyu ele alır: duraklarla işlem oranlarının aynı hedef hızda başka bir sonuç üretmesini,
gerçekçi olmayan senaryonun gerçek kusuru geçirmesini ve kusurun karışımın hangi payında
göründüğünü.

### Test Senaryosu Modelleme

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/test-senaryosu-modelleme

Yükün sayısı değil biçimi: iş karışımının hangi payında kusurun görünür olduğunun ölçülmesi, arama ağırlıklı gerçekçi olmayan bir senaryonun aynı hedef hızda yanlış geçme üretmesi, kapalı döngülü üretecin sanal görevli sayısı ile düşünme süresine göre hedef hıza hiç ulaşamaması ve aynı nominal hızın üç ayrı p95 değeri vermesi.

Önceki dersin bütün koşumları tek bir uca, sabit hızda ve aralıksız istek gönderdi. Gerçek
bir gişe günü böyle değildir: görevli önce arar, sonra ödünç verir ve iki işlem arasında
durur. Bu ders yükün **sayısını** değil **biçimini** ele alır ve şunu ölçer: aynı hedef
hız, senaryo değiştiğinde aynı sistemde başka bir karar üretir.

#### Senaryonun İki Parametresi

**İş karışımı** (workload mix), toplam yükün istek türlerine dağılımıdır. Bir sistemin
uçları aynı maliyette değildir; karışım, yükün hangi yola düşeceğini belirler ve böylece
hangi kusur sınıfının doyuma yaklaşacağını da belirler.

**Düşünme süresi** (think time), sanal bir kullanıcının iki işlemi arasındaki duraktır ve
yalnız kapalı döngülü üreteçte anlam taşır. Kapalı döngüde $N$ sanal görevli vardır, her
biri yanıtı bekler, $Z$ kadar durur ve yeni istek gönderir; ulaşılan hız $N / (Z + R)$
olur. Buradaki $R$ sistemin kendi yanıt süresidir — yani **yük, sınanan sistemin bir
işlevidir**. Açık döngülü üreteç bu geri beslemeyi taşımaz.

#### Eşik ve Varsayımlar

Eşik önceki dersten aynen gelir. **IO1 (varsayım + hesap) — gişe akışının 60 ms'lik sunucu
bütçesinin üç servise eşit paylaştırılması: p95 ≤ 20 ms.** **IO2 (varsayım) — tepe saat
hedefi 1.000 istek/s.** Bu derste iki varsayım daha yazılır.

**IO3 (varsayım) — tepe saatte gişe iş karışımı: isteklerin yarısı ödünç, yarısı arama.**
Gerekçe: gişe tezgâhına gelen kişi ödünç almak için gelir; katalog araması ayrı uçlardan da
yapılır ama tezgâh trafiğinin yarısını aşmaz. **IO4 (varsayım) — kapalı döngü modelinde
sanal görevli sayısı hedef hızdan türetilir: $N = 1000 \cdot (Z + R_0)$, $R_0 \approx 2$
ms.** İki çift sınanır: 22 görevli / 20 ms ve 202 görevli / 200 ms. İkisinin de nominal
hızı 1.000 istek/s'tir.

Kararın sahibi bu derste yayım değil, testin kendisidir: senaryo seçimi hangi kusur
sınıfının ölçüleceğini belirler ve yanlış seçilmiş bir senaryo yeşil rapor eder.

#### Düzenek

Servisin iki ucu var. `/ara` katalogda kitap numarasına göre arar ve her iki yapıda da
dizinlidir; `/odunc` üyenin açık ödünç sayısını doğrulayıp beş kayıt yazar ve dizin yalnız
sağlam yapıda vardır. Katalog 110.000 kayıtla başlar, yani tam tarama önceki derstekinden
altı kat pahalıdır.

```js
// kutuphane/sunucu.mjs — gise servisi: /ara katalogda kitap arar, /odunc uyenin acik odunc
// sayisini dogrulayip bes kayit yazar. Argumanlar: <baglanti-noktasi> <yapi 0|1>
// 0 = odunc yolunda uye dizini yok (tam tarama), 1 = dizinli.
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 110000, KITAP = 5;  // uye basina 220 kayit; gise islemi 5 kitap
const port = Number(process.argv[2]), yapi = process.argv[3];

if (Number.isInteger(port) === false || ["0", "1"].includes(yapi) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <yapi 0|1>");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  db.exec("CREATE INDEX odunc_kitap ON odunc(kitap_no)");      // arama yolu her iki yapida dizinli
  if (yapi === "1") db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const ara = db.prepare("SELECT no, uye_no, gun FROM odunc WHERE kitap_no = ? LIMIT 5");
  const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const ekle = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  let arama = 0, odunc = 0, kayit = BASLANGIC, tarama = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    yanit.writeHead(200, { "content-type": "application/json" });
    if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ arama, odunc, kayit, tarama })); return; }
    if (u.pathname === "/ara") {
      arama += 1;
      yanit.end(JSON.stringify({ bulunan: ara.all(u.searchParams.get("kitap") ?? "K-1").length }));
      return;
    }
    odunc += 1;
    tarama += yapi === "0" ? kayit : Math.ceil(Math.log2(kayit));   // dizinsiz yapida tam tarama
    const uye = u.searchParams.get("uye") ?? "U-1";
    const n = say.get(uye).n;
    for (let j = 0; j < KITAP; j += 1) ekle.run(uye, `K-${(odunc + j) % 9000}`, odunc % 400);
    kayit += KITAP;
    yanit.end(JSON.stringify({ uye, odunc: n + 1 }));
  }).listen(port);
}
```

Üreteç iki döngü biçimini de taşır. Senaryo yalnız üç sayıdan oluşur: ödünç payı, sanal
görevli sayısı ve düşünme süresi.

```js
// kutuphane/senaryo.mjs — acik ve kapali dongulu uretec; senaryo istek karisimini ve
// dusunme suresini belirler. Argumanlar: <baglanti-noktasi> <yapi 0|1> <senaryo>
import { Agent, request } from "node:http";

const HEDEF = 1000, ESIK = 20, SURE = 3;   // IO2: hedef hiz, IO1: p95 esigi (ms), kosum suresi
const SENARYO = {                          // pay: odunc istegi orani, n/z: kapali dongu
  kolay: { pay: 0.10 }, "pay-25": { pay: 0.25 }, gercekci: { pay: 0.50 }, "pay-100": { pay: 1.00 },
  "kapali-22": { pay: 0.50, n: 22, z: 20 }, "kapali-202": { pay: 0.50, n: 202, z: 200 },
};
const [port, yapi, ad] = [Number(process.argv[2]), process.argv[3], process.argv[4]];
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
const yuz = (d, p) => { const s = [...d].sort((a, b) => a - b);
  return s.length === 0 ? 0 : s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))]; };
let sira = 0;

function birIstek(kayit, pay) {            // pay orani kadar istek odunc yoluna gider
  const i = sira++;
  const yol = (i * 9973) % 1000 < pay * 1000 ? `/odunc?uye=U-${i % 500}` : `/ara?kitap=K-${i % 9000}`;
  const bas = performance.now();
  return new Promise((coz) => {
    const r = request({ port, path: yol, agent }, (y) => {
      y.resume(); y.on("end", () => { kayit.push(performance.now() - bas); coz(); });
    });
    r.on("error", () => { kayit.push(performance.now() - bas); coz(); });
    r.end();
  });
}

async function acik(sure, hiz, pay) {      // hedef hizda gonderir, yanit beklemez
  const kayit = [], ucus = [];
  let borc = 0, sonT = 0;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    borc += hiz * (t - sonT); sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(kayit, pay)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  return { kayit, gecen: (performance.now() - bas) / 1000 };
}

async function kapali(sure, n, z, pay) {   // n sanal gorevli; her istekten sonra z ms durur
  const kayit = [];
  const bas = performance.now();
  const gorevli = async () => {
    while (performance.now() - bas < sure * 1000) {
      await birIstek(kayit, pay);
      await new Promise((c) => setTimeout(c, z));
    }
  };
  await Promise.all(Array.from({ length: n }, gorevli));
  return { kayit, gecen: (performance.now() - bas) / 1000 };
}

const olcut = () => new Promise((coz) => {
  request({ port, path: "/olcut", agent }, (y) => { let g = "";
    y.on("data", (p) => { g += p; }); y.on("end", () => coz(JSON.parse(g))); }).end();
});

const s = SENARYO[ad];
if (Number.isInteger(port) === false || s === undefined) {
  console.log(`kullanim: node kutuphane/senaryo.mjs <nokta> <yapi 0|1> ${Object.keys(SENARYO).join("|")}`);
} else {
  await birIstek([], 1);                   // isinma istegi
  const r = s.n === undefined ? await acik(SURE, HEDEF, s.pay) : await kapali(SURE, s.n, s.z, s.pay);
  const o = await olcut();
  console.log(JSON.stringify({ ad, yapi: yapi === "0" ? "dizinsiz" : "dizinli", pay: s.pay,
    dongu: s.n === undefined ? "acik" : `kapali N=${s.n} Z=${s.z}`, p95: yuz(r.kayit, 95),
    hiz: r.kayit.length / r.gecen, odunc: o.odunc, tarama: o.tarama }));
}
agent.destroy();
```

```js
// kutuphane/rapor.mjs — on kosumu tabloya cevirir ve p95 esigini tarar. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const ESIK = 20, SEN = ["kolay", "gercekci", "kapali-22", "kapali-202"];
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const bul = (y, a) => s.find((r) => r.yapi === y && r.ad === a);
const v = (x) => x.toFixed(2).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const kar = (x) => (x <= ESIK ? "gecti" : "kaldi");

console.log(`esik p95 <= ${ESIK} ms (IO1), hedef hiz 1000 istek/s (IO2), kosum 3 s`);
console.log("senaryo      dongu              odunc payi  yapi      ulasilan/s  p95(ms)  karar");
for (const a of SEN) for (const y of ["dizinsiz", "dizinli"]) {
  const r = bul(y, a);
  console.log(a.padEnd(13) + r.dongu.padEnd(19) + g(r.pay.toFixed(2).replace(".", ","), 10) +
    "  " + y.padEnd(10) + g(v(r.hiz), 10) + g(v(r.p95), 9) + g(kar(r.p95), 7));
}

console.log("\nodunc payi taramasi (dizinsiz yapi, acik dongu, ayni 1000 istek/s hedefi)");
console.log("odunc payi  odunc istegi/s  p95(ms)  karar   tarama adimi");
for (const a of ["kolay", "pay-25", "gercekci", "pay-100"]) {
  const r = bul("dizinsiz", a);
  console.log(g(r.pay.toFixed(2).replace(".", ","), 10) + g(v(r.odunc / 3), 16) + g(v(r.p95), 9) +
    g(kar(r.p95), 7) + g(r.tarama.toLocaleString("tr-TR"), 15));
}

console.log("\nesik taramasi: dort senaryonun kaci yanlis karar veriyor");
console.log("p95 esigi (ms)  yanlis gecme /4  yanlis kalma /4  ayiran senaryo /4  ayiranlar");
for (const e of [2, 5, 15, 20, 30, 100]) {
  const gec = SEN.filter((a) => bul("dizinsiz", a).p95 <= e);
  const kal = SEN.filter((a) => bul("dizinli", a).p95 > e);
  const ayr = SEN.filter((a) => bul("dizinli", a).p95 <= e && bul("dizinsiz", a).p95 > e);
  console.log(String(e).padEnd(16) + g(gec.length, 15) + g(kal.length, 17) + g(ayr.length, 19) +
    "  " + (ayr.join(", ") || "-"));
}
```

{/* run: skip */}
```bash
# olc.sh — on kosum: dort senaryo x iki yapi, arti dizinsiz yapiya iki ek odunc payi.
# Her kosum yeni baslatilmis bir sunucuyla calisir; katalog hep 110.000 kayitla baslar.
rm -f sonuc.jsonl
for Y in 0 1; do
  for S in kolay gercekci kapali-22 kapali-202; do
    node kutuphane/sunucu.mjs 8852 $Y & SUNUCU=$!
    sleep 1.2
    node kutuphane/senaryo.mjs 8852 $Y $S >> sonuc.jsonl
    kill $SUNUCU
    wait $SUNUCU 2>/dev/null
  done
done
for S in pay-25 pay-100; do
  node kutuphane/sunucu.mjs 8852 0 & SUNUCU=$!
  sleep 1.2
  node kutuphane/senaryo.mjs 8852 0 $S >> sonuc.jsonl
  kill $SUNUCU
  wait $SUNUCU 2>/dev/null
done
node kutuphane/rapor.mjs sonuc.jsonl
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
esik p95 <= 20 ms (IO1), hedef hiz 1000 istek/s (IO2), kosum 3 s
senaryo      dongu              odunc payi  yapi      ulasilan/s  p95(ms)  karar
kolay        acik                     0,10  dizinsiz      997,90     8,24  gecti
kolay        acik                     0,10  dizinli       997,83     2,84  gecti
gercekci     acik                     0,50  dizinsiz      924,03   568,66  kaldi
gercekci     acik                     0,50  dizinli       997,86     2,85  gecti
kapali-22    kapali N=22 Z=20         0,50  dizinsiz      693,17    28,90  kaldi
kapali-22    kapali N=22 Z=20         0,50  dizinli       977,58     4,17  gecti
kapali-202   kapali N=202 Z=200       0,50  dizinsiz      857,23    77,31  kaldi
kapali-202   kapali N=202 Z=200       0,50  dizinli       969,12    15,92  gecti

odunc payi taramasi (dizinsiz yapi, acik dongu, ayni 1000 istek/s hedefi)
odunc payi  odunc istegi/s  p95(ms)  karar   tarama adimi
      0,10           99,00     8,24  gecti     32.889.780
      0,25          249,67    13,02  gecti     83.790.630
      0,50          498,33   568,66  kaldi    170.033.825
      1,00          999,67  6104,57  kaldi    352.367.505

esik taramasi: dort senaryonun kaci yanlis karar veriyor
p95 esigi (ms)  yanlis gecme /4  yanlis kalma /4  ayiran senaryo /4  ayiranlar
2                             0                4                  0  -
5                             0                1                  3  kolay, gercekci, kapali-22
15                            1                1                  2  gercekci, kapali-22
20                            1                0                  3  gercekci, kapali-22, kapali-202
30                            2                0                  2  gercekci, kapali-202
100                           3                0                  1  gercekci
```

#### Karışım Kararı Değiştiriyor

Hedef hız dört koşumda da 1.000 istek/s. Değişen tek şey bu hızın uçlara dağılımı. Ödünç
payı 0,10 iken kusurlu yapı 8,24 milisaniyede kalıyor ve **geçiyor**; 0,25'te 13,02 ile hâlâ
geçiyor; 0,50'de 568,66 milisaniyeye fırlıyor. Aradaki fark sistemde değil senaryodadır.

Nedeni ikinci sütunda: ödünç isteği hızı 99'dan 498'e çıkıyor. Kusurlu yapıda bu uç tam
tarama yapar ve kendi tavanı yaklaşık 400 istek/s'tir; pay 0,25'te tavanın altında, 0,50'de
üstündedir. Tarama adımı bunu koşumdan bağımsız gösterir: 32.889.780'den 170.033.825'e,
**5,2 kat**. Yakalanan kusur sınıfının adı **tek yolun doyumudur**: sistem bütün olarak
hedef hızı karşılarken tek bir uç tavana dayanır ve bütün isteklerin gecikmesini bozar.

Kolay senaryo bunun kaçtığı yerdir. Arama isteği yazması en ucuz olan istektir, bu yüzden
senaryolar arama ağırlıklı yazılmaya eğilimlidir. IO3 gerçekçi karışımı 0,50 diye
yazıyorsa, 0,10 ile koşulan bir test **yanlış geçme** üretir ve kaçırdığı sınıfın adı
bellidir.

#### Kapalı Döngü Kendi Yükünü Kısıyor

Alt iki senaryo aynı karışımı (0,50) kapalı döngüyle koşuyor ve ikisinin de nominal hızı
1.000 istek/s. Kusurlu yapıda ulaşılan hız 693,17 ve 857,23 istek/s — yani hedefin yüzde
69,3'ü ve yüzde 85,7'si. **Üreteç, sınadığı sistem yavaşladığı için yükü kendiliğinden
azalttı.** IO2'nin istediği yük hiç uygulanmadı; ölçülen p95 başka bir hızın p95'idir.

Bunun sonucu bir küçültmedir. Açık döngü aynı karışımda 568,66 milisaniye raporladı, kapalı
döngü 28,90 ve 77,31. Aynı sistem, aynı kusur, **yirmi kat ve yedi kat** küçük bir sayı. Bu
koşumda üçü de 20 milisaniyelik eşiği aştığı için karar aynı kaldı; eşik 30 milisaniye
olsaydı kapalı-22 geçer, açık döngü kalırdı.

Sağlam yapı sütunu düşünme süresinin kendi etkisini ayrı gösteriyor. Kusur yokken bile aynı
nominal hız üç ayrı p95 veriyor: açık döngüde 2,85, 22 görevli / 20 ms ile 4,17, 202
görevli / 200 ms ile 15,92 milisaniye — **5,6 kat yayılım**. Sayı sistemin değil, üreteç
parametrelerinin sayısıdır.

#### Eşik Taraması ve Bedel

Tarama iki yönlü bir sonuç veriyor. IO1'in 20 milisaniyesinde yanlış geçme 1 (kolay
senaryo), yanlış kalma 0. Eşiği 15 milisaniyeye çekmek yanlış kalmayı bire çıkarıyor ve
kırmızıya dönen koşum **sağlam yapının kapali-202 senaryosudur** — yani eşiği sıkmak,
kusuru değil senaryonun kendi gürültüsünü yakalıyor. 100 milisaniyede yanlış geçme üçe
çıkıyor; yalnız açık döngülü gerçekçi senaryo ayırt ediyor.

Kaçan sınıf da sayılabilir: kapalı döngülü iki senaryo IO2'nin hızına hiç ulaşmadığı için
**hedef hızda doğan doyum** onlarda ölçülmemiştir; kusurlu yapıda eksik kalan yük saniyede
307 ve 143 istektir. Bedelin koşuma bağlı yanı ortama bağlıdır — on koşum bu makinede bir
dakikanın altında sürdü. Koşumdan bağımsız yanı: açık döngülü her koşum 3.000 istek gönderir
ve ödünç isteği sayısı doğrudan paydan gelir (300, 750, 1.500, 3.000); dizinsiz yapıdaki
tarama adımı 32.889.780 ile 352.367.505 arasında, **10,7 kat**.

#### Özet

- Senaryonun iki parametresi vardır: iş karışımı yükün hangi uca düşeceğini, düşünme süresi
  kapalı döngüde ulaşılan hızı belirler.
- Aynı 1.000 istek/s hedefi ödünç payı 0,10'da geçti (8,24 ms), 0,25'te geçti (13,02 ms),
  0,50'de kaldı (568,66 ms); kusur sistemde değil, senaryoda görünür oldu.
- Yakalanan sınıf tek yolun doyumudur: ödünç isteği hızı 99'dan 498'e çıkarken uç kendi
  tavanını aşıyor, tarama adımı 5,2 kat artıyor.
- Kapalı döngü hedef hıza ulaşamadı (693,17 ve 857,23 istek/s) ve bozulmayı 20 ile 7 kat
  küçük raporladı; ölçülen p95 istenen yükün p95'i değildir.
- Sağlam yapıda bile üreteç parametreleri p95'i 2,85'ten 15,92 milisaniyeye taşıyor; eşik
  15 milisaniyeye çekilirse yanlış kalma bu gürültüden doğar.

#### Sonraki Adım

Buraya kadar her karar tek bir sayıya bakılarak verildi: p95. Bu seçim tartışılmadı. Ortalama
alınsaydı 568,66 milisaniyelik koşum bambaşka görünürdü; p99 seçilseydi karar yine değişirdi
ve üç saniyelik bir koşumda p99'un kaç örneğe dayandığı hiç sorulmadı. Verim ile gecikmenin
birlikte okunması gerektiği de yalnız ima edildi. Sonraki ders ölçüt seçimini ele alır:
ortalamanın neyi gizlediğini, bir yüzdeliğin anlamlı olması için kaç örnek gerektiğini ve
gecikmenin verimden ayrı okunduğunda niçin yanıltıcı olduğunu ölçer.

### Ölçüt Seçimi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/olcut-secimi

Aynı koşumdan yedi ayrı sonuç çıkarılması: ortalama ile ortancanın eşiği geçerken kuyruk yüzdeliklerinin kalması, bir yüzdeliğin okunabilmesi için gereken en az örnek sayısının alt örnek taramasıyla sayılması ve ulaşılan hız raporlanmadan bir gecikme sayısının okunamayacağının hedef hız merdiveniyle gösterilmesi.

Önceki iki ders her kararı tek bir sayıya bağladı: p95. Bu seçim hiç tartışılmadı. Ortalama
alınsaydı aynı koşumlar bambaşka görünürdü, p99 seçilseydi karar yine değişirdi ve üç
saniyelik bir koşumda p99'un kaç örneğe dayandığı sorulmadı. Bu ders ölçütün kendisini bir
karar olarak ele alır.

#### Ölçüt Bir Seçimdir

Bir koşumun ürettiği şey tek bir sayı değil, binlerce gecikme değeridir. Ölçüt, bu
dağılımdan tek bir sayı çıkaran işlevdir ve seçim eşiğin anlamını değiştirir. **Ortalama**
bütün örnekleri eşit ağırlıkla toplar; bir tek çok yavaş istek onu az, bin tane orta yavaş
istek çok oynatır. **Yüzdelik gecikme** ise sıralı örnekte bir konumu okur: p99, isteklerin
yüzde birinin daha kötü olduğu değerdir ve kaç kat daha kötü olduklarını hiç söylemez.

Eşik önceki derslerden aynen gelir: **IO1 (varsayım + hesap) — p95 ≤ 20 ms.** Bu derste
eşiğin bir de örnek koşulu yazılır.

**IO5 (hesap) — bir yüzdeliğin karar taşıyabilmesi için koşumun en az $1/(1-p)$ örnek
içermesi gerekir: p95 için 20, p99 için 100, p99,9 için 1.000.** Gerekçe: yüzdeliğin
dışladığı pay en az bir örneğe düşmezse, okunan değer o payın değil, dağılımın daha
alçak bir yerinin değeridir. Bu sayı bir varsayım değil, örnek sayısından çıkan bir alt
sınırdır ve aşağıda taramayla sınanır.

Kararın sahibi ölçüt seçimidir: p99'a bağlanan bir eşik, koşum süresine de bir alt sınır
koyar. Ölçüt seçmek aynı zamanda testin ne kadar süreceğini seçmektir.

#### Düzenek

Servis önceki dersten aynen gelir: `/ara` her iki yapıda dizinlidir, `/odunc` yalnız sağlam
yapıda. Üreteç bu kez bütün gecikmeleri biriktirip dosyaya yazar; analiz sunucusuz çalışır.

```js
// kutuphane/sunucu.mjs — gise servisi: /ara katalogda kitap arar, /odunc uyenin acik odunc
// sayisini dogrulayip bes kayit yazar. Argumanlar: <baglanti-noktasi> <yapi 0|1>
// 0 = odunc yolunda uye dizini yok (tam tarama), 1 = dizinli.
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 110000, KITAP = 5;  // uye basina 220 kayit; gise islemi 5 kitap
const port = Number(process.argv[2]), yapi = process.argv[3];

if (Number.isInteger(port) === false || ["0", "1"].includes(yapi) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <yapi 0|1>");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  db.exec("CREATE INDEX odunc_kitap ON odunc(kitap_no)");      // arama yolu her iki yapida dizinli
  if (yapi === "1") db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const ara = db.prepare("SELECT no, uye_no, gun FROM odunc WHERE kitap_no = ? LIMIT 5");
  const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const ekle = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  let arama = 0, odunc = 0, kayit = BASLANGIC, tarama = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    yanit.writeHead(200, { "content-type": "application/json" });
    if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ arama, odunc, kayit, tarama })); return; }
    if (u.pathname === "/ara") {
      arama += 1;
      yanit.end(JSON.stringify({ bulunan: ara.all(u.searchParams.get("kitap") ?? "K-1").length }));
      return;
    }
    odunc += 1;
    tarama += yapi === "0" ? kayit : Math.ceil(Math.log2(kayit));   // dizinsiz yapida tam tarama
    const uye = u.searchParams.get("uye") ?? "U-1";
    const n = say.get(uye).n;
    for (let j = 0; j < KITAP; j += 1) ekle.run(uye, `K-${(odunc + j) % 9000}`, odunc % 400);
    kayit += KITAP;
    yanit.end(JSON.stringify({ uye, odunc: n + 1 }));
  }).listen(port);
}
```

```js
// kutuphane/olc.mjs — acik dongulu uretec; bir kosumun butun gecikmelerini biriktirip
// JSON satiri olarak basar. Argumanlar: <nokta> <yapi 0|1> <hedef hiz> <odunc payi> <sure s>
import { Agent, request } from "node:http";

const [port, yapi, hiz, pay, sure] = process.argv.slice(2).map((x, i) => (i === 1 ? x : Number(x)));
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
let sira = 0;

function birIstek(kayit) {
  const i = sira++;
  const yol = (i * 9973) % 1000 < pay * 1000 ? `/odunc?uye=U-${i % 500}` : `/ara?kitap=K-${i % 9000}`;
  const bas = performance.now();
  return new Promise((coz) => {
    const r = request({ port, path: yol, agent }, (y) => {
      y.resume(); y.on("end", () => { kayit.push(performance.now() - bas); coz(); });
    });
    r.on("error", () => { kayit.push(performance.now() - bas); coz(); });
    r.end();
  });
}

if (Number.isInteger(port) === false || ["0", "1"].includes(yapi) === false) {
  console.log("kullanim: node kutuphane/olc.mjs <nokta> <yapi 0|1> <hedef hiz> <odunc payi> <sure>");
} else {
  await birIstek([]);                              // isinma istegi
  const kayit = [], ucus = [];
  let borc = 0, sonT = 0;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    borc += hiz * (t - sonT); sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(kayit)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  const gecen = (performance.now() - bas) / 1000;
  console.log(JSON.stringify({ yapi: yapi === "0" ? "dizinsiz" : "dizinli", hiz, pay,
    ulasilan: kayit.length / gecen, ms: kayit.map((x) => Math.round(x * 1000) / 1000) }));
}
agent.destroy();
```

Alt örnek taraması, kaydedilen gecikmelerden yerine koyarak örnek çeker; üreteç doğrusal
eşleniktir ve tohumu görünürdür.

```js
// kutuphane/analiz.mjs — kayitli gecikmeler uzerinde olcut secimi, ornek sayisi taramasi ve
// verim-gecikme okumasi. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const ESIK = 20, TOHUM = 20260731, ALT = 200;      // IO1 esigi, gorunur tohum, alt ornek sayisi
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const bul = (y, h, p) => s.find((r) => r.yapi === y && r.hiz === h && r.pay === p);
const v = (x, n = 2) => x.toFixed(n).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const ort = (d) => d.reduce((a, b) => a + b, 0) / d.length;
const yuz = (d, p) => { const t = [...d].sort((a, b) => a - b);
  return t[Math.min(t.length - 1, Math.floor((p / 100) * t.length))]; };

const A = bul("dizinsiz", 1700, 0.25), B = bul("dizinli", 1700, 0.25);
const OLCUT = [["ortalama", (d) => ort(d)], ["p50", (d) => yuz(d, 50)], ["p90", (d) => yuz(d, 90)],
  ["p95", (d) => yuz(d, 95)], ["p99", (d) => yuz(d, 99)], ["p99,9", (d) => yuz(d, 99.9)],
  ["en buyuk", (d) => Math.max(...d)]];
console.log(`ayni iki kosum, yedi olcut; esik ${ESIK} ms (IO1), ornek ${A.ms.length} / ${B.ms.length}`);
console.log("olcut     dizinsiz(ms)  karar   dizinli(ms)  karar   dizinsiz/dizinli");
for (const [ad, f] of OLCUT) {
  const a = f(A.ms), b = f(B.ms);
  console.log(ad.padEnd(10) + g(v(a), 12) + g(a <= ESIK ? "gecti" : "kaldi", 7) +
    g(v(b), 13) + g(b <= ESIK ? "gecti" : "kaldi", 7) + g(v(a / b), 18));
}

let t = TOHUM;
const rast = () => { t = (1103515245 * t + 12345) % 2147483648; return t / 2147483648; };
const altOrnek = (d, n) => Array.from({ length: n }, () => d[Math.floor(rast() * d.length)]);
console.log(`\nornek sayisi taramasi: ${ALT} alt ornek, tohum ${TOHUM}; tam ornekte p99 ` +
  `dizinsiz ${v(yuz(A.ms, 99))} ms, dizinli ${v(yuz(B.ms, 99))} ms`);
console.log("ornek  p99 en kucuk  p99 en buyuk  yayilim kat  yanlis gecme/200  yanlis kalma/200");
for (const n of [20, 50, 100, 500, 2000]) {
  const a = Array.from({ length: ALT }, () => yuz(altOrnek(A.ms, n), 99));
  const b = Array.from({ length: ALT }, () => yuz(altOrnek(B.ms, n), 99));
  console.log(g(n, 5) + g(v(Math.min(...a)), 13) + g(v(Math.max(...a)), 14) +
    g(v(Math.max(...a) / Math.min(...a)), 13) + g(a.filter((x) => x <= ESIK).length, 18) +
    g(b.filter((x) => x > ESIK).length, 18));
}

console.log("\nverim-gecikme: dizinsiz yapi, ayni karisim, dort hedef hiz");
console.log("hedef hiz  ulasilan/s  ulasilan/hedef  p95(ms)  karar");
for (const h of [900, 1300, 1700, 2100]) {
  const r = bul("dizinsiz", h, 0.25);
  console.log(g(h, 9) + g(v(r.ulasilan), 12) + g(v(r.ulasilan / h, 3), 16) +
    g(v(yuz(r.ms, 95)), 9) + g(yuz(r.ms, 95) <= ESIK ? "gecti" : "kaldi", 7));
}
```

{/* run: skip */}
```bash
# olc.sh — bes kosum: ayni senaryo iki yapida (5 s), arti dizinsiz yapida uc ek hedef hiz (2 s).
rm -f sonuc.jsonl
for Y in 0 1; do
  node kutuphane/sunucu.mjs 8853 $Y & SUNUCU=$!
  sleep 1.2
  node kutuphane/olc.mjs 8853 $Y 1700 0.25 5 >> sonuc.jsonl
  kill $SUNUCU
  wait $SUNUCU 2>/dev/null
done
for H in 900 1300 2100; do
  node kutuphane/sunucu.mjs 8853 0 & SUNUCU=$!
  sleep 1.2
  node kutuphane/olc.mjs 8853 0 $H 0.25 2 >> sonuc.jsonl
  kill $SUNUCU
  wait $SUNUCU 2>/dev/null
done
node kutuphane/analiz.mjs sonuc.jsonl
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
ayni iki kosum, yedi olcut; esik 20 ms (IO1), ornek 8494 / 8492
olcut     dizinsiz(ms)  karar   dizinli(ms)  karar   dizinsiz/dizinli
ortalama          9,04  gecti         1,87  gecti              4,84
p50               8,66  gecti         1,64  gecti              5,28
p90              16,09  gecti         3,02  gecti              5,33
p95              18,33  gecti         3,54  gecti              5,18
p99              25,35  kaldi         7,77  gecti              3,26
p99,9            98,38  kaldi        15,58  gecti              6,31
en buyuk        103,88  kaldi        16,05  gecti              6,47

ornek sayisi taramasi: 200 alt ornek, tohum 20260731; tam ornekte p99 dizinsiz 25,35 ms, dizinli 7,77 ms
ornek  p99 en kucuk  p99 en buyuk  yayilim kat  yanlis gecme/200  yanlis kalma/200
   20        13,03         38,18         2,93                86                 0
   50        17,62         38,18         2,17                15                 0
  100        21,68         38,18         1,76                 0                 0
  500        27,54         34,50         1,25                 0                 0
 2000        27,54         27,54         1,00                 0                 0

verim-gecikme: dizinsiz yapi, ayni karisim, dort hedef hiz
hedef hiz  ulasilan/s  ulasilan/hedef  p95(ms)  karar
      900      896,85           0,996    11,31  gecti
     1300     1291,61           0,994    13,89  gecti
     1700     1695,88           0,998    18,33  gecti
     2100     1470,45           0,700  1381,94  kaldi
```

#### Ortalamanın Gizlediği

Birinci tablo tek bir koşumdan yedi sayı çıkarıyor ve yedisi aynı yapı için **iki ayrı
karar** veriyor. Bu koşumda ortalama 9,04, ortanca 8,66, p90 16,09 ve p95 18,33 eşiğin
altında; p99 25,35, p99,9 98,38 ve en büyük 103,88 üstünde. Kod aynı, koşum aynı, örnek aynı;
değişen tek şey dağılımdan hangi noktanın okunduğu. Sayılar ölçüm sınıfındadır ve bu makineye
bağlıdır; **kesme noktasının hangi ölçüt çiftinin arasına düştüğü koşumdan koşuma oynar** —
başka bir koşumda p95 de eşiğin üstüne çıkabilir. Koşumdan bağımsız olan, sıranın kendisidir:
kuyruğa doğru gidildikçe okunan değer büyür ve dizinsiz yapının sağlam yapıya oranı artar.

Ortalamanın niçin geçtiği son sütunda görünüyor. Bu koşumda isteklerin dörtte biri ödünç
yoluna gidiyor ve tam tarama yapıyor; kalan dörtte üçü ucuz aramalar. Ortalama iki kümeyi
birbirine karıştırır ve pahalı olanın ağırlığını dörtte bire indirir. Kuyruk yüzdelikleri
karıştırmaz: p99 tanımı gereği yalnız en yavaş yüzde birde durur ve orada dizinsiz yapı
sağlam yapının bu koşumda 3,26 katıdır, p99,9'da 6,31 katı. Katın tam değeri koşuma bağlıdır;
koşumdan bağımsız olan, oranın kuyruğa doğru gidildikçe büyümesidir.

Yakalanan kusur sınıfının adı **kuyruk gecikmesidir**. Sağlam yapı yedi ölçütün yedisinde
de geçiyor, dolayısıyla bu koşumda p99'un yanlış kalma üretmediği de görülüyor: ölçütü
sıkılaştırmak sağlam yapıyı kırmızıya döndürmedi.

#### Bir Yüzdelik Kaç Örneğe Dayanır

İkinci tablo aynı gecikmelerden alt örnekler çekiyor ve her boyda p99'u yeniden hesaplıyor.
Tam örnekte p99 25,35 milisaniye; yirmi örneklik alt örneklerde aynı sayı 13,03 ile 38,18
arasında geziniyor, yani **2,93 kat yayılım**. Kararı bozan yay budur: iki yüz alt örneğin
86'sında p99 eşiğin altında çıkıyor ve koşum **yanlış geçme** veriyor. Elli örnekte bu sayı
15'e, yüz örnekte sıfıra iniyor.

Sıfırın yüz örnekte gelmesi rastlantı değil, IO5'in kendisidir: p99'un dışladığı pay yüzde
bir olduğu için yüz örnekten azında o paya tek bir istek bile düşmeyebilir ve okunan değer
dağılımın daha alçak bir yerinden gelir. Beş yüz ve iki bin örnekte yayılım 1,25 ve 1,00
kata iniyor. Yanlış kalma sütunu bütün boylarda sıfır: sağlam yapının p99'u eşikten o kadar
uzak ki alt örnekleme onu eşiğin üstüne çıkaramıyor. **Örnek sayısı yalnız yanlış geçme
tarafını bozuyor** — az örnek, hep iyimser bir sonuç üretir.

Buradan çıkan kural koşum süresine bakar: hedef hız 1.700 istek/s ise p99 için gereken yüz
örnek 0,06 saniyede toplanır, ama tepe saatin dışında 10 istek/s ile koşan bir ortamda aynı
sayı 10 saniye ister. p99,9 için bu süre on katına çıkar.

#### Verim Okunmadan Gecikme Okunmaz

Üçüncü tablo aynı yapıyı dört hedef hızda koşturuyor. İlk üç satırda ulaşılan hız hedefin
0,994 ile 0,998'i arasında; p95 sırasıyla 11,31, 13,89 ve 18,33 milisaniye. Bu üç sayı
karşılaştırılabilir, çünkü üçü de istenen yükün altında ölçüldü.

Dördüncü satır karşılaştırılabilir değildir. Hedef 2.100 istek/s, ulaşılan 1.470,45 — yani
**hedefin 0,700'ü**. p95 1.381,94 milisaniye görünüyor ama bu sayı 2.100 istek/s'in p95'i
değil, sistemin kabul edebildiği 1.470 istek/s'in p95'idir; saniyede 630 istek hiç
karşılanmadı. Yalnız gecikme sütununu taşıyan bir rapor bu satırı "yavaş" diye okur, oysa
söylediği şey başkadır: **sistem istenen yüke ulaşamadı.** Gecikme ile verim tek bir çift
olarak okunur; ulaşılan hız raporlanmayan bir gecikme sayısı hangi yüke ait olduğunu
söylemez.

#### Bedel ve Kaçan Sınıf

Bedelin koşuma bağlı yanı ortama bağlıdır: beş koşum bu makinede yarım dakikanın altında sürdü.
Koşumdan bağımsız yanı sayılır — iki analiz koşumu 8.494 ve 8.492 gecikme değeri biriktirdi,
alt örnek taraması beş boyda iki yüzer kez, iki yapı için de yürütüldü; 2.000 yeniden hesap
yaptı ve toplam 536.000 örnek çekti. Kaçan sınıf da ölçüldü: ortalama ve ortanca bu kusuru yedi
ölçütün dördünde geçirdi, yani bir ölçüt kümesinin sınırı da yanlış geçme sayısıyla yazılır.

#### Özet

- Ölçüt bir seçimdir: aynı koşumda ortalama 9,04 ve p50 8,66 eşiği geçerken p99 25,35 ve
  p99,9 98,38 kalıyor.
- Ortalama pahalı ile ucuz isteği karıştırır; kuyruk yüzdeliği karıştırmaz ve iki yapıyı
  p99'da 3,26, p99,9'da 6,31 kat ayırır.
- Yüzdelik örnek sayısına duyarlıdır: yirmi örnekte p99 tahmini 2,93 kat yayılıyor ve 200
  alt örneğin 86'sı yanlış geçme veriyor; yüz örnekte bu sayı sıfırlanıyor.
- IO5 bunu bir alt sınıra bağlar: bir yüzdelik en az $1/(1-p)$ örnek ister, yani p99 için
  100, p99,9 için 1.000. Az örnek yalnız iyimser yönde yanılır.
- Gecikme verimle birlikte okunur: 2.100 istek/s hedefinde ulaşılan hız 0,700'e düştüğü
  için 1.381,94 milisaniyelik p95 istenen yükün değil, kabul edilen yükün sayısıdır.

#### Sonraki Adım

Ölçüt seçildi ve eşik kırmızıya döndü. Rapor artık şunu söylüyor: p99 25,35 milisaniye,
eşik 20, kaldı. Bu cümle bir kusuru bildirir ama **nerede** olduğunu söylemez. Aynı sayı
uygulama katmanındaki bir hesaptan, veritabanındaki bir taramadan ya da yanıtın
büyüklüğünden doğmuş olabilir ve üçünün düzeltmesi birbirinden başkadır. Sonraki ders bu
atfetmeyi ele alır: bir yük testi sonucu hangi ölçümle bir katmana bağlanır, aynı belirtiyi
veren iki katman nasıl ayrılır ve atfetme ne zaman yanlıştır.

### Darboğaz Analizi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/darbogaz-analizi

Bir yük testi sonucunun bir katmana bağlanması: uygulama, veritabanı ve aktarım sürelerinin istek içinde ayrı ölçülmesi, aynı istemci belirtisini veren üç kipin üç ayrı katmana atfedilmesi, atfetme kuralının pay eşiğiyle taranması ve zamanın doğru katmanda geçtiği hâlde nedenin başka bir kararda olduğu durumun sayıyla gösterilmesi.

Önceki ders raporu şu cümleye indirdi: p99 25,35 milisaniye, eşik 20, kaldı. Cümle bir
kusuru bildirir ama **nerede** olduğunu söylemez. Aynı sayı uygulama katmanındaki bir
hesaptan, veritabanındaki bir sorgudan ya da yanıtın büyüklüğünden doğmuş olabilir ve
üçünün düzeltmesi birbirinden başkadır. Bu ders sonucu bir katmana bağlamayı ele alır.

#### Katman Payı

Bir isteğin toplam süresi istemcide ölçülür ve tek bir sayıdır. **Darboğaz** (bottleneck)
o sürenin çoğunu tüketen yerdir; bulunması için sürenin istek **içinde** bölünmesi gerekir.
Sunucu her istekte üç saati okur: sorgu öncesi, sorgu sonrası, hesap sonrası ve yanıt
yazıldıktan sonra. Aradaki farklar **veritabanı**, **uygulama** ve **aktarım** katmanlarının
süreleridir.

Bu üç sayı tek başına yeterli değildir, çünkü her istekte her katman bir miktar iş yapar.
Anlamlı olan **fark**tır: aynı düzenekte hiçbir kusur taşımayan bir **taban** koşumu
alınır ve her katmanın süresi tabana göre farkıyla okunur. **Katman payı**, bir katmanın
farkının istemci farkına bölümüdür.

**IO6 (varsayım) — bir sonucu bir katmana atfetmek için o katmanın payı en az 0,50
olmalıdır; değilse sonuç atfedilemez sayılır.** Gerekçe: pay 0,50'nin altındayken o
katmanın tamamen giderilmesi bile belirtinin yarısından azını götürür ve düzeltme kararı
savunulamaz. Eşik aşağıda taranır.

Kararın sahibi bir bütçedir: atıf, düzeltmenin hangi ekibe ve hangi çalışmaya gideceğini
belirler. Yanlış atıf yalnız zaman kaybettirmez, kusuru yerinde bırakır. Başarım Karşıt
Kalıpları ve İzleme kursu belirtiden nedene giden on tanıyı kurmuştu; burada sorulan soru
ondan öncedir: sonuç hangi katmana **aittir**.

#### Düzenek

Ödünç ucu beş kiple koşar. **taban** dizinli sorgu ve küçük yanıt; **db** aynı sorguyu
dizinsiz yapar; **uyg** uygulama katmanında bir hesap turu döner; **ag** açılışta bir kez
çekilmiş 4.000 satırlık bir sayfayı her yanıtta serileştirip yazar; **karma** aynı sayfayı
her istekte veritabanından yeniden çeker. Dördü aynı belirtiyi vermek üzere ayarlanmıştır.

```js
// kutuphane/sunucu.mjs — odunc ucu, bes kiple. Her istekte uc katmanin suresi ayri olculur:
// veritabani (sorgu), uygulama (hesap), aktarim (serilestirme + yaniti yazma).
// Argumanlar: <baglanti-noktasi> <kip taban|db|uyg|ag|karma>
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 55000, TUR = 29000, SATIR = 4000;   // katalog boyu, hesap turu, sayfa satiri
const port = Number(process.argv[2]), kip = process.argv[3];

if (Number.isInteger(port) === false || ["taban", "db", "uyg", "ag", "karma"].includes(kip) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> taban|db|uyg|ag|karma");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  if (kip !== "db") db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const cek = db.prepare("SELECT no, kitap_no, gun FROM odunc LIMIT ?");
  const onbellek = kip === "ag" ? cek.all(SATIR) : [];   // ag kipinde sayfa acilista bir kez cekilir
  let istek = 0, sdb = 0, suyg = 0, sag = 0, bayt = 0, tarama = 0, hesap = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    if (u.pathname === "/olcut") {
      yanit.writeHead(200, { "content-type": "application/json" });
      yanit.end(JSON.stringify({ istek, sdb, suyg, sag, bayt, tarama, hesap })); return;
    }
    istek += 1;
    const uye = u.searchParams.get("uye") ?? "U-1";
    const t0 = performance.now();
    const n = say.get(uye).n;                        // veritabani katmani
    const sayfa = kip === "karma" ? cek.all(SATIR) : onbellek;
    tarama += kip === "db" ? BASLANGIC : Math.ceil(Math.log2(BASLANGIC));
    const t1 = performance.now();
    let ozet = "";                                   // uygulama katmani
    if (kip === "uyg") for (let k = 0; k < TUR; k += 1) { ozet = `${uye}/${(n + k) % 97}`; hesap += 1; }
    const t2 = performance.now();
    const govde = JSON.stringify({ uye, odunc: n, ozet, sayfa });   // aktarim katmani
    sdb += t1 - t0; suyg += t2 - t1; bayt += govde.length;
    yanit.writeHead(200, { "content-type": "application/json" });
    yanit.on("finish", () => { sag += performance.now() - t2; });
    yanit.end(govde);
  }).listen(port);
}
```

Üreteç ilk saniyeyi ölçüme katmaz ve sunucu sayaçlarını o pencerenin başında bir kez daha
okur; böylece hem istemci hem sunucu tarafı aynı aralığı anlatır.

```js
// kutuphane/olc.mjs — acik dongulu uretec; istemci suresini ve sunucunun katman sayaclarini
// okur. Argumanlar: <baglanti-noktasi> <kip> <hedef hiz> <sure s>
import { Agent, request } from "node:http";

const ISINMA = 1;   // ilk saniye olcume katilmaz: JIT ve onbellek isinmasi
const [port, kip, hiz, sure] = process.argv.slice(2).map((x, i) => (i === 1 ? x : Number(x)));
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
const yuz = (d, p) => { const t = [...d].sort((a, b) => a - b);
  return t[Math.min(t.length - 1, Math.floor((p / 100) * t.length))]; };
let sira = 0;

function birIstek(kayit, sn) {
  const bas = performance.now(), i = sira++;
  return new Promise((coz) => {
    const r = request({ port, path: `/odunc?uye=U-${i % 500}`, agent }, (y) => {
      y.resume(); y.on("end", () => { kayit.push({ ms: performance.now() - bas, sn }); coz(); });
    });
    r.on("error", () => { kayit.push({ ms: performance.now() - bas, sn }); coz(); });
    r.end();
  });
}

const olcut = () => new Promise((coz) => {
  request({ port, path: "/olcut", agent }, (y) => { let g = "";
    y.on("data", (p) => { g += p; }); y.on("end", () => coz(JSON.parse(g))); }).end();
});

if (Number.isInteger(port) === false) {
  console.log("kullanim: node kutuphane/olc.mjs <baglanti-noktasi> <kip> <hedef hiz> <sure>");
} else {
  const kayit = [], ucus = [];
  let borc = 0, sonT = 0, o1 = null;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    if (o1 === null && t >= ISINMA) o1 = await olcut();     // isinma penceresi burada kapanir
    borc += hiz * (t - sonT); sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(kayit, t)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  const o2 = await olcut(), n = o2.istek - o1.istek;
  const d = kayit.filter((r) => r.sn >= ISINMA).map((r) => r.ms);
  console.log(JSON.stringify({ kip, hiz, ornek: d.length,
    istemci: d.reduce((a, b) => a + b, 0) / d.length, p95: yuz(d, 95),
    vt: (o2.sdb - o1.sdb) / n, uyg: (o2.suyg - o1.suyg) / n, ag: (o2.sag - o1.sag) / n,
    bayt: (o2.bayt - o1.bayt) / n, tarama: (o2.tarama - o1.tarama) / n }));
}
agent.destroy();
```

```js
// kutuphane/atif.mjs — katman paylarini tabana gore hesaplar, atfi kurala baglar ve
// pay esigini tarar. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const PAY = 0.5;                                   // IO6: bir katmana atfetmek icin en az pay
const GERCEK = { db: "veritabani", uyg: "uygulama", ag: "aktarim", karma: "aktarim" };
const KAT = ["vt", "uyg", "ag"], AD = { vt: "veritabani", uyg: "uygulama", ag: "aktarim" };
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const bul = (k) => s.find((r) => r.kip === k);
const T = bul("taban"), KIP = ["db", "uyg", "ag", "karma"];
const v = (x, n = 3) => x.toFixed(n).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const fark = (r) => ({ istemci: r.istemci - T.istemci, vt: r.vt - T.vt, uyg: r.uyg - T.uyg, ag: r.ag - T.ag });
const atif = (r, esik) => {                        // en buyuk pay esigi asiyorsa o katman
  const f = fark(r);
  const en = KAT.reduce((a, k) => (f[k] > f[a] ? k : a), "vt");
  return f[en] / f.istemci >= esik ? AD[en] : "atfedilemez";
};

console.log(`taban: istemci ${v(T.istemci)} ms, vt ${v(T.vt)}, uyg ${v(T.uyg)}, aktarim ${v(T.ag)}, ` +
  `bayt ${Math.round(T.bayt)}`);
console.log("kip    istemci  p95   fark   vt payi  uyg payi  aktarim payi  bayt     atif");
for (const k of KIP) {
  const r = bul(k), f = fark(r);
  console.log(k.padEnd(7) + g(v(r.istemci, 2), 7) + g(v(r.p95, 2), 6) + g(v(f.istemci, 2), 7) +
    g(v(f.vt / f.istemci, 2), 9) + g(v(f.uyg / f.istemci, 2), 10) + g(v(f.ag / f.istemci, 2), 14) +
    g(Math.round(r.bayt), 9) + "  " + atif(r, PAY));
}

console.log("\npay esigi taramasi: dort kipin atfi (gercek katman: db=veritabani, uyg=uygulama, " +
  "ag ve karma=aktarim)");
console.log("pay esigi  dogru atif /4  yanlis atif /4  atfedilemez /4");
for (const e of [0.3, 0.5, 0.7, 0.8, 1.0]) {
  const a = KIP.map((k) => atif(bul(k), e));
  const dogru = a.filter((x, i) => x === GERCEK[KIP[i]]).length;
  const yok = a.filter((x) => x === "atfedilemez").length;
  console.log(v(e, 1).padEnd(11) + g(dogru, 13) + g(4 - dogru - yok, 16) + g(yok, 16));
}
```

{/* run: skip */}
```bash
# olc.sh — bes kosum: taban ve dort kip, ayni hedef hizda, her biri yeni bir sunucuyla.
rm -f sonuc.jsonl
for K in taban db uyg ag karma; do
  node kutuphane/sunucu.mjs 8854 $K & SUNUCU=$!
  sleep 1.2
  node kutuphane/olc.mjs 8854 $K 250 6 >> sonuc.jsonl
  kill $SUNUCU
  wait $SUNUCU 2>/dev/null
done
node kutuphane/atif.mjs sonuc.jsonl
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
taban: istemci 1,040 ms, vt 0,043, uyg 0,000, aktarim 0,177, bayt 48
kip    istemci  p95   fark   vt payi  uyg payi  aktarim payi  bayt     atif
db        2,99  4,95   1,95     1,09     -0,00         -0,08       48  veritabani
uyg       2,80  4,90   1,76     0,00      0,78          0,02       55  uygulama
ag        2,93  4,87   1,89    -0,01     -0,00          0,84   164730  aktarim
karma     3,14  6,80   2,10     0,56     -0,00          0,33   164730  veritabani

pay esigi taramasi: dort kipin atfi (gercek katman: db=veritabani, uyg=uygulama, ag ve karma=aktarim)
pay esigi  dogru atif /4  yanlis atif /4  atfedilemez /4
0,3                    3               1               0
0,5                    3               1               0
0,7                    3               0               1
0,8                    2               0               2
1,0                    1               0               3
```

#### Aynı Belirti, Üç Ayrı Katman

Birinci sütun kararın kendisidir ve dört kipte de aynıdır. İstemci ortalaması bu koşumda
2,80 ile 3,14 milisaniye arasında, yani en yüksek ile en düşük arasında yüzde on ikilik bir
fark var; p95 üçünde 4,87–4,95, birinde 6,80. Yalnız istemci sayısına bakan bir rapor bu
dört yapıyı **birbirinden ayıramaz**.

Pay sütunları ayırıyor. `db` kipinde veritabanı payı 1,09, `uyg` kipinde uygulama payı
0,78, `ag` kipinde aktarım payı 0,84. Üç kip aynı belirtiyi veriyor ve üç ayrı katmana
atfediliyor; düzeltmeleri de birbirinden başkadır — dizin eklemek, hesabı seyreltmek, yanıtı
küçültmek.

Bir payın 1'i aşması ölçümün sınırını gösterir. `db` kipinde veritabanı farkı istemci
farkından büyük çıktı; iki koşumun taban gürültüsü aynı değildir ve fark küçüldükçe bu
gürültü paya oranla büyür. Kural yalnız en büyük paya baktığı için sonuç değişmiyor, ama
pay 1'i aştığında okunacak şey "veritabanı belirtinin tamamını açıklıyor"dur, daha fazlası
değil.

#### Atfetme Yanlış Olduğunda

Dördüncü satır kuralın kırıldığı yerdir. `karma` kipinde veritabanı payı 0,56, aktarım payı
0,33; kural veritabanını suçluyor. Ölçüm doğrudur: zaman gerçekten sorguda geçiyor, çünkü
sorgu 4.000 satır okuyor. **Sonuç yine de yanlıştır.**

Kanıt son sütunda ve koşumdan bağımsızdır: `karma` ile `ag`, yanıt başına **aynı 164.730
baytı** gönderiyor. `ag` kipinde o sayfa açılışta bir kez çekilmiş ve önbelleğe alınmıştır,
bu yüzden veritabanı payı sıfırdır ve kural aktarımı suçlar. İki kipin farkı sorguda değil,
**yanıtın büyüklüğünde alınan karardadır** — ve o karar ikisinde de aynıdır. `karma` kipinde
veritabanına dizin eklemek hiçbir şey değiştirmez; sorgu zaten dizin gerektirmiyor, 4.000
satır okuyor.

Kural şudur: **katman payı zamanın nerede geçtiğini söyler, onu hangi kararın ürettiğini
söylemez.** İkincisini söyleyen ölçüm koşumdan bağımsız niceliktir — gönderilen bayt,
okunan satır, taranan kayıt. Bu koşumda `db` kipi istek başına 55.000 satır tarıyor,
ötekiler 16; `ag` ve `karma` istek başına 164.730 bayt gönderiyor, ötekiler 48 ve 55. İki
katmanın aynı belirtiyi verdiği her yerde ayırt eden ölçüm budur.

#### Pay Eşiği Taraması

İkinci tablo IO6'nın 0,50'sini kaydırıyor. Tablonun tek tek sayıları koşuma bağlıdır — pay,
ölçülen süre farklarından çıkar ve bu makinede koşumdan koşuma oynar — ama **yönü koşumdan
bağımsızdır** ve karar o yönde verilir. Düşük eşikte dört kipin dördü de bir katmana bağlanır
ve `karma` **yanlış atıf** alır; eşik yükseldikçe önce yanlış atıf sıfırlanır, sonra doğru
atıflar da eşiğin altında kalıp **atfedilemez** olur. Yukarıdaki koşumda dönüş 0,70'te
başlıyor; başka bir koşumda bir basamak önce ya da sonra başlayabilir, çünkü eşiği geçen şey
payın kendisidir.

İki hata biçimi arasındaki takas budur. Düşük eşik suçsuz bir katmanı suçlar; yüksek eşik
kusuru kimseye bağlayamaz ve bulgu bir kayda dönüşmeden kapanır. IO6'nın 0,50'si yanlış
atfı sıfırlamaz — onu sıfırlayan şey eşik değil, koşumdan bağımsız niceliğin okunmasıdır.

#### Bedel ve Kaçan Sınıf

Bedelin koşuma bağlı yanı ortama bağlıdır: beş koşum bu makinede kırk saniyenin altında
sürdü. Koşumdan bağımsız yanı sayılır — beş sunucu süreci, koşum başına altı saniyenin beş
saniyesi ölçülüyor (ilk saniye ısınmaya ayrıldı) ve her koşumda istek başına dört saat
okuması yapılıyor, yani enstrümantasyon istek başına dört ek ölçüm ekliyor.

Kaçan sınıf da sayılabilir: bu düzenek yalnız **sunucu içindeki** süreyi bölüyor. İstemci
farkı ile ölçülen katmanların toplamı arasındaki artık — `uyg` kipinde farkın yüzde
yirmisi — hiçbir katmana yazılmıyor. Ağ turu, olay döngüsünde bekleme ve işletim sisteminin
soket işi orada durur. Doygunluğa yaklaşan bir sistemde bu artık büyür ve pay tabanlı
atfetme kullanılamaz hâle gelir; o durumda okunacak ölçüt katman payı değil, önceki dersin
verim–gecikme çiftidir.

#### Özet

- Katman payı, bir katmanın taban koşumuna göre farkının istemci farkına bölümüdür; atıf
  kuralı en büyük payın IO6'nın 0,50'sini aşmasını ister.
- Aynı istemci belirtisi (ortalama 2,80–3,14 ms) üç ayrı katmana atfedildi: veritabanı 1,09,
  uygulama 0,78, aktarım 0,84 payla.
- Atfetme `karma` kipinde yanlış: zaman gerçekten sorguda geçiyor ama `ag` ile aynı 164.730
  baytı gönderiyor; nedeni sorgu değil, yanıtın büyüklüğü kararıdır.
- Pay eşiği taraması takası gösteriyor: eşik yükseldikçe önce yanlış atıf, sonra doğru atıf
  düşer ve atfedilemez koşum artar; dönüşün hangi basamakta olduğu koşuma bağlıdır.
- Katman payı zamanın nerede geçtiğini söyler; hangi kararın onu ürettiğini koşumdan
  bağımsız nicelik söyler (55.000 taranan satır, 164.730 gönderilen bayt).

#### Sonraki Adım

Buraya kadarki her sayı **tek bir koşumdan** okundu. Bu dersin taban koşumu bir kez alındı
ve bütün paylar ona bölündü; bir katman payının 1'i aşması da, aynı düzeneğin iki
koşumunun aynı sayıyı vermediğini gösteriyor. Üstelik düzenek ilk saniyeyi ölçüme katmıyor
— yani bir ısınma etkisinin varlığı kabul edildi ama büyüklüğü hiç ölçülmedi. Sonraki ders
ölçümün kendisini sınar: ısınmanın kaç koşum sürdüğünü, aynı yapının koşumları arasındaki
değişkenliği ve iki yapı arasındaki bir farkın anlamlı sayılabilmesi için kaç koşum
gerektiğini sayar.

### Karşılaştırmalı Ölçüm Disiplini

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/karsilastirmali-olcum-disiplini

Bir başarım farkının anlamlı sayılabilmesinin koşulu: ısınma penceresinin istek bloklarıyla ölçülmesi, aynı kodun iki kol olarak koşturulmasıyla düzeneğin kendi gürültüsünün sayılması, tek tura bakan kararın gerilemeyi tersine okuduğu turun gösterilmesi ve eşik ile tur sayısının birlikte taranarak yanlış geçme–yanlış kalma dengesinin kurulması.

Önceki dersin payları tek bir taban koşumuna bölündü ve bir pay 1'i aştı. Nedeni bir kusur
değildi: aynı düzenek iki kez koşturulunca iki ayrı sayı verir. Düzenek ilk saniyeyi de
ölçüme katmıyordu — bir ısınma etkisinin varlığı kabul edilmiş, büyüklüğü hiç ölçülmemişti.
Bu ders ölçümün kendisini sınar.

Soru bir sürüm kararıdır. Ödünç servisinin **aday** sürümü gişe ekranına üyenin gecikmiş
kitap sayısını ekliyor ve bunun için istek başına bir sorgu daha çalıştırıyor. Yayım kapısı
şunu sormak zorunda: aday sürümün ölçümü yüksek çıktığında fark sürümden mi, ölçümden mi?

#### Bir Farkın Anlamlı Sayılması

Aynı düzeneğin iki koşumunun aynı sayıyı vermemesine **gürültü** (noise), sayıların koşumdan
koşuma yayılmasına **koşumlar arası değişkenlik** denir. Bir sürüm farkı ancak bu yayılımdan
ayrılabildiği ölçüde okunabilir.

**IO9 (varsayım) — aday sürümün ölçümü tabanın ölçümünü yüzde beşten fazla aşarsa gerileme
sayılır ve yayım durur.** Kaynağı bir gereksinim ya da ölçüm değil, kapı duyarlılığı
için seçilmiş bir sayıdır; aşağıda taranır. Kararın sahibi yayım kapısıdır: kırmızıya
döndüğünde aday sürüm çıkmaz.

Eşiğin iki hata biçimini sayabilmek için düzenek üç kolla koşar. **taban** ile **denetim**
kolları **aynı kodu** çalıştırır, **aday** kolu yeni sürümü. Denetim kolunun gerçek farkı
sıfırdır; kapı onu kırmızıya döndürürse bu bir **yanlış kalmadır**. Aday kolunun farkı
gerçektir; kapı onu geçirirse bu bir **yanlış geçmedir**. İki özdeş kolu
koşturmak, düzeneğin kendi hatasını okumanın en ucuz yoludur.

#### Düzenek

Ölçüm on **tur** yapar; her tur üç koşumdur ve her koşum kendi sunucu süreciyle başlar. Üç kolun
art arda koşması, makine yükündeki bir kaymanın tek bir kola yüklenmesini engeller.

```js
// kutuphane/sunucu.mjs — odunc ucu. Aday surum (b) yanita gecikmis kitap sayisini da ekler,
// yani istek basina bir sorgu daha calistirir. Argumanlar: <baglanti-noktasi> <surum a|b>
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const UYE = 500, BASLANGIC = 80000, SON = 40, GEC = 30;   // katalog boyu, liste boyu, gecikme gunu
const port = Number(process.argv[2]), surum = process.argv[3];

if (Number.isInteger(port) === false || ["a", "b"].includes(surum) === false) {
  console.log("kullanim: node kutuphane/sunucu.mjs <baglanti-noktasi> <surum a|b>");
} else {
  const db = new DatabaseSync(":memory:");
  db.exec("CREATE TABLE odunc(no INTEGER PRIMARY KEY, uye_no TEXT, kitap_no TEXT, gun INT)");
  db.exec("CREATE INDEX odunc_uye ON odunc(uye_no)");
  db.exec("BEGIN");
  const doldur = db.prepare("INSERT INTO odunc(uye_no, kitap_no, gun) VALUES(?,?,?)");
  for (let i = 0; i < BASLANGIC; i += 1) doldur.run(`U-${i % UYE}`, `K-${i % 9000}`, i % 400);
  db.exec("COMMIT");

  const say = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ?");
  const son = db.prepare("SELECT no, kitap_no, gun FROM odunc WHERE uye_no = ? LIMIT ?");
  const gec = db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE uye_no = ? AND gun > ?");
  let istek = 0, sorgu = 0;

  createServer((gelen, yanit) => {
    const u = new URL(gelen.url, "http://yerel");
    yanit.writeHead(200, { "content-type": "application/json" });
    if (u.pathname === "/olcut") { yanit.end(JSON.stringify({ istek, sorgu })); return; }
    istek += 1;
    const uye = u.searchParams.get("uye") ?? "U-1";
    const n = say.get(uye).n, liste = son.all(uye, SON); sorgu += 2;
    let geciken = -1;
    if (surum === "b") { geciken = gec.get(uye, GEC).n; sorgu += 1; }   // aday surumun ek alani
    yanit.end(JSON.stringify({ uye, odunc: n, geciken, liste }));
  }).listen(port);
}
```

Üreteç önceki derslerden gelir, iki farkla: ısınmayı **kendisi atmaz** ve hatalı istekleri
sayar. İkincisi zorunludur — bağlantısı reddedilen istek çok hızlı bitmiş görünür, yani
sayılmazsa ölü bir sunucu iyi bir ölçüme benzer.

```js
// kutuphane/olc.mjs — acik dongulu uretec. Gecikmeleri gonderim sirasina gore yazar, isinmayi
// disarida birakmaz. Argumanlar: <kol adi> <baglanti-noktasi> <tur> <hedef hiz> <sure s>
import { Agent, request } from "node:http";

const [kol, port, tur, hiz, sure] = process.argv.slice(2).map((x, i) => (i === 0 ? x : Number(x)));
const agent = new Agent({ keepAlive: true, maxSockets: 8192 });
let hata = 0;

function birIstek(ms, i) {
  const bas = performance.now();
  return new Promise((coz) => {
    const bit = () => { ms[i] = Math.round((performance.now() - bas) * 1000) / 1000; coz(); };
    const r = request({ port, path: `/odunc?uye=U-${i % 500}`, agent }, (y) => { y.resume(); y.on("end", bit); });
    r.on("error", () => { hata += 1; bit(); });   // hatali istek hizli gorunur, sayilir
    r.end();
  });
}

const olcut = () => new Promise((coz) => {
  request({ port, path: "/olcut", agent }, (y) => { let govde = "";
    y.on("data", (p) => { govde += p; }); y.on("end", () => coz(JSON.parse(govde))); }).end();
});

if (Number.isInteger(port) === false) {
  console.log("kullanim: node kutuphane/olc.mjs <kol> <baglanti-noktasi> <tur> <hedef hiz> <sure>");
} else {
  const ms = [], ucus = [];
  let borc = 0, sonT = 0, sira = 0;
  const bas = performance.now();
  while (performance.now() - bas < sure * 1000) {
    const t = (performance.now() - bas) / 1000;
    borc += hiz * (t - sonT); sonT = t;
    while (borc >= 1) { borc -= 1; ucus.push(birIstek(ms, sira++)); }
    await new Promise((c) => setTimeout(c, 5));
  }
  await Promise.all(ucus);
  const o = await olcut(), gecen = (performance.now() - bas) / 1000;
  console.log(JSON.stringify({ kol, tur, ulasilan: ms.length / gecen, hata, sorgu: o.sorgu / o.istek, ms }));
}
agent.destroy();
```

Tarama on turdan rastgele kümeler seçip kararı yeniden verir; üreteç doğrusal
eşleniktir ve tohumu görünürdür.

```js
// kutuphane/karsilastir.mjs — isinma, degiskenlik ve karar taramasi. Arguman: <jsonl>
import { readFileSync } from "node:fs";

const ESIK = 0.05, TOHUM = 20260731, DENEME = 500, ISINMA = 100, BLOK = 100;   // IO9, gorunur tohum
const s = readFileSync(process.argv[2], "utf8").trim().split("\n").map((x) => JSON.parse(x));
const v = (x, n = 2) => x.toFixed(n).replace(".", ",");
const g = (x, n) => String(x).padStart(n);
const yuz = (d, p) => { const t = [...d].sort((a, b) => a - b);
  return t[Math.min(t.length - 1, Math.floor((p / 100) * t.length))]; };
const TUR = [...new Set(s.map((r) => r.tur))];
const bul = (t, k) => s.find((r) => r.tur === t && r.kol === k);
const havuz = (k, bas, son) => s.filter((r) => r.kol === k).flatMap((r) => r.ms.slice(bas, son));
const P = (k) => yuz(havuz(k, ISINMA, 1e9), 95);

console.log(`${TUR.length} tur, kol basina tur basina ${Math.round(havuz("taban", 0, 1e9).length / TUR.length)} ` +
  `istek, ${s.reduce((a, r) => a + r.hata, 0)} hatali istek. Havuzda taban p95 ${v(P("taban"))} ms, denetim ` +
  `${v(P("denetim"))}, aday ${v(P("aday"))}; aday/taban ${v(P("aday") / P("taban"))} kat. Sorgu/istek: ` +
  `taban ${bul(TUR[0], "taban").sorgu}, aday ${bul(TUR[0], "aday").sorgu}.`);

console.log(`\nisinma: butun turlerin ayni istek bloklari havuzda, blok ${BLOK} istek`);
console.log("istek blogu  taban ortanca  taban p95(ms)  aday p95(ms)");
for (let i = 0; i < 5; i += 1) {
  const x = i * BLOK, y = i < 4 ? x + BLOK : 1e9, d = havuz("taban", x, y);
  console.log((i < 4 ? `${x}-${x + BLOK - 1}` : `${x}+`).padEnd(13) + g(v(yuz(d, 50), 3), 13) +
    g(v(yuz(d, 95)), 15) + g(v(yuz(havuz("aday", x, y), 95)), 14));
}

const p95 = (t, k) => yuz(bul(t, k).ms.slice(ISINMA), 95);
console.log("\ntur basina okuma: her turdan bir sayi, ilk 100 istek atilarak");
console.log("olcum          en kucuk  ortanca  en buyuk  yayilim kat");
for (const ad of ["taban p95(ms)", "denetim/taban", "aday/taban"]) {
  const d = TUR.map((t) => (ad.includes("/") ? p95(t, ad.split("/")[0]) / p95(t, "taban") : p95(t, "taban")))
    .sort((m, n) => m - n);
  console.log(ad.padEnd(15) + g(v(d[0]), 8) + g(v(yuz(d, 50)), 9) + g(v(d[d.length - 1]), 10) +
    g(v(d[d.length - 1] / d[0]), 13));
}

let t = TOHUM;
const rast = () => { t = (1103515245 * t + 12345) % 2147483648; return t / 2147483648; };
const karistir = (n) => { const a = [...Array(n).keys()];
  for (let i = n - 1; i > 0; i -= 1) { const j = Math.floor(rast() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
  return a; };
const orta = (d, ix) => yuz(ix.map((i) => d[i]), 50);
const tara = (n, esik, x) => {
  const dizi = (k) => TUR.map((tt) => yuz(bul(tt, k).ms.slice(x), 95));
  const T = dizi("taban"), D = dizi("denetim"), A = dizi("aday");
  let kalma = 0, gecme = 0;
  for (let i = 0; i < DENEME; i += 1) {
    const ix = karistir(TUR.length).slice(0, n), kapi = orta(T, ix) * (1 + esik);
    if (orta(D, ix) > kapi) kalma += 1;            // denetim taban ile ayni kod: gerileme yok
    if (orta(A, ix) <= kapi) gecme += 1;           // aday istek basina bir sorgu daha calistirir
  }
  return `${g(kalma, 3)}/${g(gecme, 3)}`;
};

const ESIKLER = [0.02, ESIK, 0.10, 0.20];
console.log(`\nkarar taramasi: ${DENEME} deneme, tohum ${TOHUM}; hucre = yanlis kalma / yanlis gecme`);
console.log("tur/yan  pencere         " + ESIKLER.map((e) => g(`esik %${Math.round(e * 100)}`, 11)).join(""));
for (const [n, x] of [[1, ISINMA], [2, ISINMA], [3, ISINMA], [5, ISINMA], [8, ISINMA], [1, 0], [8, 0]]) {
  console.log(g(n, 7) + "  " + (x === 0 ? "isinma dahil" : "ilk 100 atildi").padEnd(16) +
    ESIKLER.map((e) => g(tara(n, e, x), 11)).join(""));
}
```

{/* run: skip */}
```bash
# olc.sh — on tur; her turda uc kol art arda kosar: taban, denetim (taban ile ayni kod)
# ve aday. Her kosum kendi sunucu sureciyle baslar.
rm -f sonuc.jsonl
for T in $(seq 1 10); do
  for KOL in "taban a 8855" "denetim a 8856" "aday b 8857"; do
    set -- $KOL
    node kutuphane/sunucu.mjs $3 $2 & SUNUCU=$!
    sleep 0.8
    node kutuphane/olc.mjs $1 $3 $T 500 1.4 >> sonuc.jsonl
    kill $SUNUCU
    wait $SUNUCU 2>/dev/null
  done
done
node kutuphane/karsilastir.mjs sonuc.jsonl
```

Çıktı makineden makineye değişir; örnek bir koşum:

```
10 tur, kol basina tur basina 698 istek, 0 hatali istek. Havuzda taban p95 2,05 ms, denetim 1,84, aday 2,37; aday/taban 1,15 kat. Sorgu/istek: taban 2, aday 3.

isinma: butun turlerin ayni istek bloklari havuzda, blok 100 istek
istek blogu  taban ortanca  taban p95(ms)  aday p95(ms)
0-99                 0,579           2,42          2,06
100-199              1,027           1,79          2,44
200-299              1,050           2,12          2,44
300-399              1,001           2,12          2,22
400+                 0,953           2,13          2,38

tur basina okuma: her turdan bir sayi, ilk 100 istek atilarak
olcum          en kucuk  ortanca  en buyuk  yayilim kat
taban p95(ms)      1,70     1,86      3,43         2,02
denetim/taban      0,52     1,03      1,07         2,05
aday/taban         0,66     1,24      1,56         2,37

karar taramasi: 500 deneme, tohum 20260731; hucre = yanlis kalma / yanlis gecme
tur/yan  pencere             esik %2    esik %5   esik %10   esik %20
      1  ilk 100 atildi      310/ 62    167/ 62      0/194      0/193
      2  ilk 100 atildi      264/107    196/108      0/245      0/252
      3  ilk 100 atildi      175/ 43     62/ 58      0/ 92      0/130
      5  ilk 100 atildi      132/  0     16/ 16      0/ 53      0/114
      8  ilk 100 atildi      141/  0      0/  0      0/  0      0/140
      1  isinma dahil        309/112     86/110      0/113      0/235
      8  isinma dahil        142/  0      0/  0      0/  2      0/140
```

#### Isınma Penceresi

**Isınma** (warm-up), bir sürecin ilk isteklerinin yerleşik durumdan başka bir maliyetle
karşılanmasıdır. Birinci tablo ilk yüz isteğin ortancasını 0,579 milisaniye, dört yüzüncü
istekten sonrasını 0,953 milisaniye okuyor: ilk blok ortancada **1,65 kat iyimser**. Aynı
bloğun p95'i ise 2,42, yerleşik pencerede 2,13 — kuyrukta ters yönde. Isınma iki şey birden
yapıyor: derleme ve önbellek dolumu birkaç isteği çok yavaşlatıyor, üretecin eşzamanlılığı
henüz kurulmadığı için kalan istekler boş kuyruk buluyor.

Asıl önemlisi son sütundur. İlk blokta aday/taban oranı 2,06/2,42, yani **0,85**; yerleşik
pencerede 2,38/2,13, yani 1,12. Isınma gerilemenin işaretini ters çeviriyor: o blokta aday
sürüm tabandan hızlı görünüyor. Pencere bu yüzden ölçümden okunur — düzenek ilk yüz isteği
atar, çünkü tablo ikinci bloktan itibaren yerleşikliği gösteriyor.

#### Gürültü ve Tek Tura Bakan Karar

İkinci tablo aynı sayıyı on kez okuyor. Tabanın p95'i 1,70 ile 3,43 milisaniye arasında
**2,02 kat** yayılıyor. Kodda hiçbir şey değişmedi; değişen tek şey turun koştuğu andır.

İkinci satır düzeneğin kendi hatasını veriyor. Denetim kolu tabanla **aynı kodu** çalıştırır;
oranın her turda 1,00 olması beklenirken 0,52 ile 1,07 arasında geziniyor. Ortanca 1,03,
yani sistematik bir kayma yok; ama yayılım 2,05 kat, yani iki özdeş sürüm arasında tek bir
turda yüzde kırk sekizlik bir "fark" görülebiliyor.

Üçüncü satır gerçek farkı taşıyor: turların aday/taban ortancası 1,24, yayılım 0,66–1,56.
Havuz oranı 1,15'tir; turların ortancası turları eşitler, havuz yavaş turlara ağırlık verir.
**En küçük değer 1'in altındadır:** tabanın 3,43 milisaniye okuduğu turda aday sürüm, istek
başına bir sorgu daha çalıştırdığı hâlde tabandan **yüzde otuz dört hızlı** ölçüldü. Tek
tura bakan karar orada gerilemeyi kaçırmakla kalmaz, tersine okur.

#### Kaç Tur, Hangi Eşik

Üçüncü tablo iki ekseni birlikte tarıyor: yan başına toplanan tur sayısı ve eşik. Her hücre
denetim kolunun yanlış kalmasını, aday kolunun yanlış geçmesini sayıyor.

Yüzde ikilik eşik hiçbir tur sayısında kurtarılamıyor: bir turda 310, sekiz turda hâlâ 141
yanlış kalma. Neden ortanca değil yayılımdır: denetim kolunun 1,07'ye çıkan okumaları
sekiz turun ortancasını da kapının üstüne taşıyor. Bir eşik, aynı kodun kendisiyle
karşılaştırıldığında ürettiği yayılımın altına inemez.

IO9'un yüzde beşi bu alt sınırın üstünde ve savunulabilir — ama tek turda değil. Bir turda
167 yanlış kalma ve 62 yanlış geçme var; üç turda 62/58, beş turda 16/16, sekiz turda
**0/0**. Aranan sayı budur: yüzde beşlik kapı, yan başına sekiz tur toplanmadan karar
taşımaz.

Üst sınırı son sütun veriyor. Yüzde yirmide yanlış kalma sıfır, ama yanlış geçme sekiz
turda bile 140: gerçek fark 1,24 kat olduğu için kapı ona fazla yaklaşıyor ve gerileme
yayımlanıyor. Eşik, ayırması istenen farkın belirgin altında kalmak zorundadır.

Isınma satırları tabloyu ikinci kez veriyor. Sekiz turda ve yüzde beşte iki pencere de
0/0; tek turda ısınmayı içeri almak yanlış geçmeyi 62'den 110'a çıkarıyor. Isınma kararı
ancak tur sayısı azken çeviriyor, hep aynı yönde: gerilemeyi gizler.

#### Bedel ve Kaçan Sınıf

Bedelin koşuma bağlı yanı ortama bağlıdır: otuz koşum bu makinede yetmiş saniyenin altında
sürdü. Koşumdan bağımsız yanı sayılır — otuz sunucu süreci, süreç başına 80.000 satırlık
dolum (toplam 2,4 milyon satır), sıfır hatalı istek ve tarama
tablosunda 28.000 yeniden verilmiş karar. Sürüm farkı da koşumdan bağımsız bir niceliğe
bağlanır: istek başına iki sorguya karşı üç sorgu.

Bu ölçümün göremediği iki sınıf var. Birincisi, tarama **rastgele** gürültüyü sayar,
**sistematik sapmayı** saymaz. Denetim kolunun ortancası 1,03 çıktığı için burada sapma
görünmedi; görünseydi tur eklemek onu küçültmezdi, çünkü ortalama yalnız rastgele bileşeni
giderir. Nedenini ayıracak olan da bu düzenek değil, kol sırasının turlar arasında
döndürülmesidir.

İkincisi eşiğin biçimidir. Oransal bir kapı, tabanı yavaş olan bir uçta büyük mutlak
gerilemeleri geçirir: yüzde beş burada 0,10 milisaniyedir, taban 200 milisaniye olsaydı
10 milisaniye olurdu. Kapı bu yüzden oranı değil, birinci dersin mutlak eşiğini de taşır.

#### Özet

- Gürültü ölçülür, varsayılmaz: aynı kodun p95'i on turda 2,02 kat yayıldı, iki özdeş kolun
  oranı 0,52 ile 1,07 arasında gezindi.
- Isınma penceresi istek bloklarıyla okunur: ilk yüz istek ortancada 1,65 kat iyimser,
  p95'te yüksek; net etkisi aday/taban oranını 1,12'den 0,85'e çevirmektir.
- Tek tura bakan karar yanılır: on turun birinde aday/taban 0,66 ölçüldü ve bir sorgu daha
  çalıştıran sürüm tabandan hızlı göründü.
- Eşiğin alt sınırı düzeneğin kendi yayılımıdır: yüzde ikilik kapı sekiz turda bile 141
  yanlış kalma bıraktı; IO9'un yüzde beşi ancak sekiz turla 0/0'a indi.
- Eşiğin üst sınırı gerçek farktır: yüzde yirmilik kapı 1,24 katlık gerilemeyi sekiz turda
  bile 140 kez geçirdi.

#### Sonraki Adım

Başarım testinin beş dersi bir eşiği kurdu, senaryoyu modelledi, ölçütü seçti, sonucu bir
katmana atfetti ve karşılaştırmayı disipline aldı. Beşinin de altında aynı varsayım duruyor:
istekler geçerliydi, niyet iyiydi. Yük üreteci gerçek bir okuyucunun yapabileceği şeyleri
hızlı yaptı; hiçbir koşum, sistemi bilerek kötüye kullanan bir istemcinin ne bulacağını
sormadı. Sonraki ders bu soruyu en ucuz sorulduğu yerde, programın kaynak metninde ele alır
ve yine sınamanın kendisini ölçer: kaç kusur bulunuyor, kaç uyarı boşa çıkıyor, hangi kusur
hiçbir eşikte görünmüyor.

## Güvenlik ve Erişilebilirlik Testi

Saldırıya ve kullanılabilirliğe karşı sınama: kaynak kod üzerinden taramanın yakaladığı ve yanlış alarm ürettiği sınıflar, çalışan uygulamayı sınayan dinamik testin kapsam sınırı, bilinen açıkları bağımlılık ağacında arayan tarama, otomasyonun bittiği yerde sızma testinin konumu, erişilebilirlikte otomatik denetimin kapsadığı ölçüt payı ile elle doğrulamanın zorunlu kaldığı alan ve tarayıcı, aygıt ile platform matrisinin uyumluluk testine dönüştürülmesi.

### Statik Uygulama Güvenlik Testi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/statik-uygulama-guvenlik-testi

Kaynak kodu koşturmadan tarayan bir denetimin ölçülmesi: üç kurallık bir tarayıcının bilinen kusurlu bir dosya kümesinde ürettiği yanlış alarm ve kaçırma sayıları, eşik taramasının iki maliyet ağırlığı altında farklı eşik seçmesi ve kapı yeşile döndükten sonra kod tabanında duran kusur.

Buraya kadarki bütün ölçümler sistemin kendi kullanıcısını varsaydı. Yük üreteci geçerli
istekler gönderdi, senaryo gerçekçi bir okuyucuyu taklit etti, darboğaz analizi doğru
çalışan bir yolun neresinin yavaşladığını sordu. Hiçbir ölçüm, sistemi bilerek kötüye
kullanan bir istemcinin ne bulacağını sormadı.

Bu sorunun en ucuz sorulduğu yer kaynak kodudur. **Statik uygulama güvenlik testi**
programı çalıştırmadan metnine bakar: hangi çağrılar yapılıyor, sorgular nasıl kuruluyor,
kodun içinde hangi değişmez değerler duruyor. Bu dersin konusu güvenlik açıklarının kendisi
değil — onların istemci tarafındaki karşılıkları Frontend Kalitesi kursunda, birleştirilmiş
sorgu riski İleri SQL kursunda kuruldu — **taramanın kendisinin ölçülmesidir**: kaç kusur
bulunuyor, kaç uyarı boşa çıkıyor, hangi kusur hiçbir eşikte görünmüyor.

#### Kod Tabanı ve Bilinen Kusurlar

Ödünç sisteminin altı dosyası taranacak. Kusurların yeri elle doğrulanmıştır; tarayıcının
sonucu bu listeyle karşılaştırılacak.

**IO7 (varsayım):** elle doğrulanmış kusur listesi eksiksizdir. Listede olmayan bir satır
sağlam sayılır. Gerçek bir kod tabanında bu varsayım tutmaz; ölçülen oranlar bu listeye
göreli oranlardır.

```js
// kaynak.mjs — odunc sisteminin taranacak kod tabani ve elle dogrulanmis kusur listesi
export const KOD = {
  "katalog.mjs": [
    `const ARAMA = "SELECT isbn, baslik FROM kitap WHERE baslik LIKE ?";`,
    `export const ara = (db, q) => db.prepare(ARAMA).all("%" + q + "%");`,
    `export const sirala = (db, alan) =>`,
    `  db.prepare("SELECT isbn FROM kitap ORDER BY " + alan).all();`,
  ],
  "odunc.mjs": [
    `const GUN = 21;`,
    `export function ver(db, uyeNo, isbn) {`,
    `  const s = "INSERT INTO odunc(uyeNo, isbn, gun) VALUES(?, ?, " + GUN + ")";`,
    `  return db.prepare(s).run(uyeNo, isbn);`,
    `}`,
    `export const gecmis = (db, uyeNo) =>`,
    "  db.prepare(`SELECT * FROM odunc WHERE uyeNo = '${uyeNo}'`).all();",
  ],
  "uye.mjs": [
    `const jeton_anahtari = "k9TdQ2vRm7XzP4bH";`,
    `export const imzala = (uyeNo) => uyeNo + ":" + jeton_anahtari.length;`,
    `export const parola_alani = "kullanici-parolasi";`,
  ],
  "bildirim.mjs": [
    `export function bicimle(sablon, veri) {`,
    "  return new Function(\"v\", \"return `\" + sablon + \"`\")(veri);",
    `}`,
    `// eval( ) cagrisi bu dosyadan kaldirildi`,
  ],
  "rapor.mjs": [
    `const kural = { gecikme: "gun > 21", ceza: "gun * 2" };`,
    `export const uygula = (ad, v) => eval(kural[ad]);`,
    `export const basliklar = ["uyeNo", "isbn", "gun"];`,
  ],
  "ayar.mjs": [
    `export const gizli_yol = process.env.ODUNC_ANAHTAR;`,
    `export const kanal_parola = "parola123";`,
    `export const anahtar_adi = "ODUNC_ANAHTAR";`,
    `export const sunucu = "127.0.0.1";`,
  ],
};

// Elle dogrulanmis kusurlar: dosya, satir (1'den), sinif.
export const GERCEK = [
  { dosya: "katalog.mjs", satir: 4, sinif: "birlestirilmis-sorgu" },
  { dosya: "odunc.mjs", satir: 7, sinif: "birlestirilmis-sorgu" },
  { dosya: "uye.mjs", satir: 1, sinif: "sabit-gizli-deger" },
  { dosya: "bildirim.mjs", satir: 2, sinif: "yasak-cagri" },
  { dosya: "rapor.mjs", satir: 2, sinif: "yasak-cagri" },
  { dosya: "ayar.mjs", satir: 2, sinif: "sabit-gizli-deger" },
];

export const satirSayisi = Object.values(KOD).reduce((t, s) => t + s.length, 0);
```

Tarayıcı üç kuraldan oluşuyor. Her kural bir desen ve bir **puan** taşır; puan, bulgunun
gerçek bir kusur olma olasılığına dair tarayıcının kendi tahminidir.

```js
// tarayici.mjs — uc kurallik statik tarayici; her bulgu bir puan tasir
const gizli = /\b(\w*(?:anahtar|parola|jeton|gizli)\w*)\s*=\s*["']([^"']{8,})["']/i;
const sorgu = /(SELECT|INSERT|UPDATE|DELETE)[^"'`]*["'`]\s*\+\s*(\w+)/i;

export const KURAL = [
  {
    ad: "yasak-cagri", desen: /\beval\s*\(|\bnew\s+Function\s*\(/,
    puan: (s) => (s.trimStart().startsWith("//") ? 20 : 90),
  },
  {
    ad: "birlestirilmis-sorgu", desen: sorgu,
    // Buyuk harfli ad bir sabittir; degisken adi disaridan gelen deger olabilir.
    puan: (s) => (/^[A-Z0-9_]+$/.test(s.match(sorgu)[2]) ? 30 : 80),
  },
  {
    ad: "sabit-gizli-deger", desen: gizli,
    // Uzun ve karisik deger uretilmis bir sir, kisa ya da tek bicimli deger genelde ad.
    puan: (s) => {
      const d = s.match(gizli)[2];
      return d.length >= 16 && /[a-z]/.test(d) && /[A-Z]/.test(d) && /\d/.test(d) ? 85 : 40;
    },
  },
];

export function tara(kod) {
  const bulgu = [];
  for (const [dosya, satirlar] of Object.entries(kod)) {
    satirlar.forEach((s, i) => {
      for (const k of KURAL) if (k.desen.test(s)) bulgu.push({ dosya, satir: i + 1, sinif: k.ad, puan: k.puan(s) });
    });
  }
  return bulgu;
}

export const karsilastirma = (kod) =>
  Object.values(kod).reduce((t, s) => t + s.length, 0) * KURAL.length;
```

#### Bulgular

```js
// bulgular.mjs — taramanin bulgulari ve her birinin gercek kusur listesindeki karsiligi
import { KOD, GERCEK, satirSayisi } from "./kaynak.mjs";
import { tara, karsilastirma, KURAL } from "./tarayici.mjs";

const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));
const bulgu = tara(KOD).sort((a, b) => b.puan - a.puan);

console.log(`${Object.keys(KOD).length} dosya, ${satirSayisi} satir, ${KURAL.length} kural -> ${karsilastirma(KOD)} karsilastirma`);
console.log(`${bulgu.length} bulgu, ${GERCEK.length} elle dogrulanmis kusur\n`);
console.log(`${"yer".padEnd(18)}${"sinif".padEnd(23)}puan  gercek`);
for (const b of bulgu) {
  console.log(`${anahtar(b).padEnd(18)}${b.sinif.padEnd(23)}${String(b.puan).padStart(4)}  ${gercekKume.has(anahtar(b)) ? "evet" : "hayir"}`);
}

const bulunan = new Set(bulgu.map(anahtar));
const hic = GERCEK.filter((g) => !bulunan.has(anahtar(g)));
console.log(`\nhicbir puanda gorunmeyen kusur: ${hic.map((g) => `${anahtar(g)} ${g.sinif}`).join(", ")}`);
```

```
6 dosya, 25 satir, 3 kural -> 75 karsilastirma
9 bulgu, 6 elle dogrulanmis kusur

yer               sinif                  puan  gercek
bildirim.mjs:2    yasak-cagri              90  evet
rapor.mjs:2       yasak-cagri              90  evet
uye.mjs:1         sabit-gizli-deger        85  evet
katalog.mjs:4     birlestirilmis-sorgu     80  evet
uye.mjs:3         sabit-gizli-deger        40  hayir
ayar.mjs:2        sabit-gizli-deger        40  evet
ayar.mjs:3        sabit-gizli-deger        40  hayir
odunc.mjs:3       birlestirilmis-sorgu     30  hayir
bildirim.mjs:4    yasak-cagri              20  hayir

hicbir puanda gorunmeyen kusur: odunc.mjs:7 birlestirilmis-sorgu
```

Dokuz bulgunun beşi gerçek, dördü **yanlış alarm**. Yanlış alarmların ortak özelliği,
desenin tuttuğu ama bağlamın masum olmasıdır: bir yorum satırında geçen çağrı adı, bir
sabitle birleştirilen sorgu metni, gizli değer değil alan adı taşıyan iki değişken.
Tarayıcı satırı görür, satırın ne anlama geldiğini görmez.

Altıncı kusur hiçbir puanda görünmedi. `odunc.mjs` içindeki sorgu birleştirme işareti
kullanmıyor; değeri şablon dizgisinin içine yerleştiriyor. Desen `" +` arıyor, orada
öyle bir dizi yok. **Kaçırılan sınıf budur: aynı riski farklı bir sözdizimiyle yazan kod.**
Kuralı genişletmek mümkündür, ama her genişletme yeni yanlış alarm getirir; bir sonraki
bölüm bu takası sayıyor.

#### Eşik Taraması

Puanların hepsi kapıya götürülmez. Bir eşik seçilir ve eşiğin altındaki bulgular
listelenir ama yayını durdurmaz. Eşik nereden gelir?

**IO8 (varsayım):** kaçan bir kusurun bedeli, bir yanlış alarmın incelenmesinin beş
katıdır. Bu sayı bir ölçümden değil, bir kabulden gelir ve ders boyunca ağırlık `w` diye
anılır. Eşik doğrudan bu ağırlıktan türer.

```js
// esik.mjs — esik taramasi: her esikte yanlis alarm, kacirma ve iki agirlikla maliyet
import { KOD, GERCEK } from "./kaynak.mjs";
import { tara } from "./tarayici.mjs";

const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));
const bulgu = tara(KOD);

// IO8: kacan bir kusurun bedeli, bir yanlis alarmin incelenmesinin AGIRLIK kati.
const olc = (esik) => {
  const kalan = bulgu.filter((b) => b.puan >= esik);
  const yanlisAlarm = kalan.filter((b) => !gercekKume.has(anahtar(b))).length;
  const gorulen = new Set(kalan.filter((b) => gercekKume.has(anahtar(b))).map(anahtar)).size;
  return { esik, kalan: kalan.length, yanlisAlarm, kacirma: GERCEK.length - gorulen };
};

const ESIKLER = [20, 30, 40, 50, 60, 70, 80, 90];
const satir = ESIKLER.map(olc);
console.log(`${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=1".padStart(12)}${"maliyet w=5".padStart(12)}`);
for (const s of satir) {
  console.log(`${String(s.esik).padStart(4)}${String(s.kalan).padStart(7)}${String(s.yanlisAlarm).padStart(14)}${String(s.kacirma).padStart(9)}${String(s.yanlisAlarm + s.kacirma).padStart(12)}${String(s.yanlisAlarm + 5 * s.kacirma).padStart(12)}`);
}

for (const w of [1, 5]) {
  const en = satir.reduce((a, b) => (b.yanlisAlarm + w * b.kacirma < a.yanlisAlarm + w * a.kacirma ? b : a));
  console.log(`w=${w} -> en dusuk maliyetli esik ${en.esik} (yanlis alarm ${en.yanlisAlarm}, kacirma ${en.kacirma})`);
}
```

```
esik  kalan  yanlis alarm  kacirma maliyet w=1 maliyet w=5
  20      9             4        1           5           9
  30      8             3        1           4           8
  40      7             2        1           3           7
  50      4             0        2           2          10
  60      4             0        2           2          10
  70      4             0        2           2          10
  80      4             0        2           2          10
  90      2             0        4           4          20
w=1 -> en dusuk maliyetli esik 50 (yanlis alarm 0, kacirma 2)
w=5 -> en dusuk maliyetli esik 40 (yanlis alarm 2, kacirma 1)
```

Tabloda **yanlış alarm** sütunu sağlam bir satırın kapıyı kırmızıya döndürmesidir; **kaçırma** sütunu gerçek bir kusurun kapıdan geçmesidir (başarım derslerinin
`yanlış kalma` / `yanlış geçme` çiftiyle aynı ayrım). İki sütun
ters yönde hareket eder ve aralarında düz bir kazanan yoktur.

Ağırlık kararı değiştiriyor. İki incelemeyi bir kaçırmaya yeğ tutan ekip eşiği 40'ta
tutar; incelemenin pahalı olduğu ekip 50'ye çıkar ve iki kusuru bilerek kapıdan geçirir.
Aynı tarayıcı, aynı kod, aynı bulgu kümesi — farklı eşik. **Eşiğin kaynağı IO8'dir**, bir
ölçüm değil; ölçülen şey eşiğin sonucudur.

#### Kapı ve Düzeltme

Eşik 40 seçilir ve tarama, Kalite ve Test Temelleri kursunda tanımlanan **kabul kapısı**
olarak çalışır. Kapı kırmızı döner, dört kusur düzeltilir, kod yeniden taranır.

```js
// kapi.mjs — duzeltme sonrasi yeniden tarama ve esik 40 ile calisan kabul kapisi
import { KOD, GERCEK } from "./kaynak.mjs";
import { tara } from "./tarayici.mjs";

const ESIK = 40;
const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));

// Dort kusurun duzeltilmis hali; odunc.mjs:7 bilerek dokunulmadan birakildi.
const DUZELTME = {
  "katalog.mjs": { 4: `  db.prepare("SELECT isbn FROM kitap " + SIRA[alan]).all();` },
  "uye.mjs": { 1: `const jeton_anahtari = process.env.ODUNC_ANAHTAR;` },
  "bildirim.mjs": { 2: "  return sablon.replace(/{(\\w+)}/g, (_, a) => veri[a]);" },
  "rapor.mjs": { 2: `export const uygula = (ad, v) => KURALLAR[ad](v);` },
  "ayar.mjs": { 2: `export const kanal_parola = process.env.KANAL_PAROLA;` },
};

const yeni = Object.fromEntries(Object.entries(KOD).map(([d, s]) =>
  [d, s.map((satir, i) => DUZELTME[d]?.[i + 1] ?? satir)]));

const kapi = (kod, bastirilan = []) => {
  const kalan = tara(kod).filter((b) => b.puan >= ESIK && !bastirilan.includes(anahtar(b)));
  return { durum: kalan.length === 0 ? "yesil" : "kirmizi", kalan };
};

for (const [ad, kod] of [["duzeltme oncesi", KOD], ["duzeltme sonrasi", yeni]]) {
  const { durum, kalan } = kapi(kod);
  console.log(`${ad.padEnd(17)} esik ${ESIK} -> ${durum}, ${kalan.length} bulgu` +
    `, ${kalan.filter((b) => !gercekKume.has(anahtar(b))).length} tanesi yanlis alarm`);
  for (const b of kalan) console.log(`  ${anahtar(b).padEnd(16)}${b.sinif.padEnd(23)}${b.puan}`);
}

const BASTIRILAN = ["uye.mjs:3", "ayar.mjs:3"];   // incelendi, alan adi; kayit acildi
console.log(`\nbastirma listesi ${BASTIRILAN.length} satir -> kapi ${kapi(yeni, BASTIRILAN).durum}`);

const kalanGercek = GERCEK.filter((g) =>
  !Object.keys(DUZELTME[g.dosya] ?? {}).includes(String(g.satir)));
console.log(`kapi yesilken kod tabaninda duran gercek kusur: ${kalanGercek.length}` +
  ` (${kalanGercek.map((g) => `${anahtar(g)} ${g.sinif}`).join(", ")})`);
```

```
duzeltme oncesi   esik 40 -> kirmizi, 7 bulgu, 2 tanesi yanlis alarm
  katalog.mjs:4   birlestirilmis-sorgu   80
  uye.mjs:1       sabit-gizli-deger      85
  uye.mjs:3       sabit-gizli-deger      40
  bildirim.mjs:2  yasak-cagri            90
  rapor.mjs:2     yasak-cagri            90
  ayar.mjs:2      sabit-gizli-deger      40
  ayar.mjs:3      sabit-gizli-deger      40
duzeltme sonrasi  esik 40 -> kirmizi, 2 bulgu, 2 tanesi yanlis alarm
  uye.mjs:3       sabit-gizli-deger      40
  ayar.mjs:3      sabit-gizli-deger      40

bastirma listesi 2 satir -> kapi yesil
kapi yesilken kod tabaninda duran gercek kusur: 1 (odunc.mjs:7 birlestirilmis-sorgu)
```

Katalog düzeltmesi sıralama alanını sabit bir tablodan okuyor; kural yine eşleşir ama
işleneni büyük harfli bir sabit olduğu için puan 80'den 30'a düşer ve eşiğin altında kalır.
Düzeltmeden sonra kapı hâlâ kırmızı ve kalan iki bulgunun ikisi de yanlış alarm. Bu, statik
taramanın en çok zaman yiyen yeridir: kapıyı yeşile döndürmek için ya kural daraltılır ya
da bulgular tek tek incelenip **bastırma listesine** yazılır. Liste iki satır uzayınca kapı
yeşile döner — ve kod tabanında hâlâ gerçek bir kusur durur.

**Kararın sahibi:** kırmızı kapı yayımı durdurur, bastırılan her satır bir inceleme kaydı
açar. Bastırma listesi zamanla büyür ve gözden geçirilmediğinde taramanın gördüğü alanı
sessizce daraltır.

#### Taramanın Bedeli

Koşumdan bağımsız bedel üç sayıdadır. Birincisi tarama işinin kendisi: 25 satır, 3 kural,
75 karşılaştırma; sıfır süreç ayağa kalkıyor, sıfır istek gidiyor, sıfır test verisi
hazırlanıyor. Bu yüzden tarama her değişiklikte koşturulabilir.

İkincisi inceleme: eşik 40'ta 7 bulgu insan gözü ister ve bunların 2'si boşa çıkar. Kod
tabanı büyüdükçe bu sayı satır sayısıyla birlikte büyür, kusur sayısıyla değil.

Üçüncüsü bakım: üç kural ve iki satırlık bastırma listesi elle yazılmıştır. Yeni bir
sözdizimi kullanıma girdiğinde kural sessiz kalır — `odunc.mjs:7` bunun kanıtıdır — ve
sessiz kalan bir kural, olmayan bir kuraldan ayırt edilemez.

#### Özet

- Statik uygulama güvenlik testi kaynağı çalıştırmadan tarar; bu derste üç kurallık bir
  tarayıcı 6 dosya ve 25 satır üzerinde 9 bulgu üretti, bunların 5'i gerçek kusurdu.
- Yanlış alarmların ortak nedeni bağlam körlüğüdür: yorum satırı, sabitle birleştirilen
  sorgu ve gizli değer değil alan adı taşıyan değişkenler desene takıldı.
- Eşik taraması iki sütunu birlikte verir: eşik 40'ta 2 yanlış alarm ve 1 kaçırma,
  eşik 50'de 0 yanlış alarm ve 2 kaçırma. Seçim, IO8'in ağırlığından türer.
- Yakalanan sınıf, desenle eşleşen kusurlu satırdır; kaçan sınıf aynı riski farklı
  sözdizimiyle yazan koddur — şablon dizgisiyle kurulan sorgu hiçbir eşikte görünmedi.
- Bedel: 75 karşılaştırma, sıfır süreç, sıfır istek; karşılığında eşik 40'ta 7 bulgunun
  elle incelenmesi ve büyüyen bir bastırma listesinin bakımı.
- Kapı yeşile döndüğünde kod tabanında hâlâ bir gerçek kusur duruyordu; yeşil kapı kusursuz
  kod anlamına gelmez, yalnız kuralların sustuğu anlamına gelir.

#### Sonraki Adım

Bu taramanın gördüğü her şey metinden okundu: hangi çağrı yazılmış, sorgu nasıl kurulmuş,
hangi değer koda gömülmüş. Metin ise programın ne yaptığını değil, ne yazdığını söyler.
`rapor.mjs` içindeki çağrı hiç ulaşılmayan bir dalda olabilir; `ayar.mjs` içindeki değer
üretimde ortam değişkeniyle bastırılıyor olabilir. Tersi de doğrudur: iki dosyanın ayrı
ayrı kusursuz olduğu bir sistemde, uçların birleşimi bir isteği hiç beklenmedik bir yanıtla
karşılayabilir. Sonraki ders ödünç servisini gerçekten ayağa kaldırır, uçlarına istek
gönderir ve tek bir soruyu ölçer: çalışan uygulamayı sınayan bir test uçların ne kadarına
erişebiliyor ve bu iki yöntemin gördüğü kusur sınıfları nerede ayrışıyor.

### Dinamik Uygulama Güvenlik Testi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/dinamik-uygulama-guvenlik-testi

Çalışan bir servise istek göndererek yapılan sınamanın ölçülmesi: bağlantı keşfiyle ve rota tablosuyla ulaşılan uç payı, sondaların ürettiği puanlı bulgular üzerinde eşik taraması ve statik taramayla dinamik testin gördüğü kusur sınıflarının kesişimsiz ayrışması.

Statik tarama kodun ne yazdığını okudu, ne yaptığını değil. Bir satırda duran çağrı hiç
ulaşılmayan bir dalda olabilir; koda gömülü bir değer üretimde bir ortam değişkeniyle
bastırılıyor olabilir. Tersi de doğrudur: iki dosyanın ayrı ayrı temiz olduğu bir sistemde,
uçların birleşimi bir isteği hiç beklenmedik bir yanıtla karşılayabilir.

**Dinamik uygulama güvenlik testi** bu yüzden kaynağa değil koşan sürece bakar: uçlara
istek gönderir, yanıtları okur ve yanıtın kendisini kanıt sayar. Bu dersin ölçtüğü üç şey
var: bu testin uçların ne kadarına ulaşabildiği, kanıtı bulguya çeviren eşiğin nerede
durduğu ve iki yöntemin gördüğü kusur sınıflarının nerede ayrıştığı.

#### Sınanacak Servis

Ödünç servisinin sekiz ucu var. Beş kusuru elle doğrulanmıştır; sonuçlar bu listeyle
karşılaştırılacak.

```js
// servis.mjs — odunc servisinin sekiz ucu; baslat() dinleyen bir sunucu dondurur
import { createServer } from "node:http";

const KITAP = { "978-1": "Kum Saati", "978-2": "Rafta Kalan" };
const UYE = { 1: { ad: "A. Yilmaz", ceza: 0 }, 2: { ad: "B. Demir", ceza: 12 } };

export const UCLAR = ["/", "/katalog", "/kitap", "/durum", "/uye", "/odunc", "/rapor", "/yonetim/gunluk"];

// Elle dogrulanmis kusurlar (uc | sinif).
export const GERCEK = [
  "/kitap|girdi-yankisi", "/kitap|bos-girdi-ayrintisi", "/uye|kimliksiz-erisim",
  "/odunc|kimliksiz-kabul", "/yonetim/gunluk|ic-ayrinti",
];

export function baslat() {
  const s = createServer((istek, yanit) => {
    const u = new URL(istek.url, "http://y");
    const gonder = (kod, govde) => yanit.writeHead(kod, { "content-type": "application/json" })
      .end(JSON.stringify(govde));

    if (u.pathname === "/") return gonder(200, { baglanti: ["/katalog", "/durum"] });
    if (u.pathname === "/katalog")
      return gonder(200, { kitap: Object.keys(KITAP).map((i) => `/kitap?isbn=${i}`) });
    if (u.pathname === "/kitap") {
      const isbn = u.searchParams.get("isbn") ?? "";
      if (isbn === "")                                        // yalniz bos girdide acilan dal
        return gonder(400, { hata: "isbn bos", sorgu: "SELECT baslik FROM kitap WHERE isbn = ''" });
      return KITAP[isbn] ? gonder(200, { isbn, baslik: KITAP[isbn] })
        : gonder(404, { hata: `kayit yok: ${isbn}` });          // girdi oldugu gibi yankilanir
    }
    if (u.pathname === "/durum") return gonder(200, { durum: "acik", kuyruk: 0 });
    if (u.pathname.startsWith("/uye/")) {
      const no = u.pathname.slice(5);
      return UYE[no] ? gonder(200, { no, ...UYE[no] }) : gonder(404, { hata: "uye yok" });
    }
    if (u.pathname === "/odunc" && istek.method === "POST") return gonder(202, { alindi: true });
    if (u.pathname === "/rapor") return gonder(200, { satir: Object.keys(UYE).length });
    if (u.pathname === "/yonetim/gunluk")
      return gonder(500, { hata: "gunluk okunamadi", yol: "/srv/odunc/var/log/odunc.log" });
    return gonder(404, { hata: "yol yok" });
  });
  return s;
}

export const dinle = (s) => new Promise((c) => s.listen(0, "127.0.0.1", () => c(s.address().port)));
```

#### Kapsanan Uç Payı

Dinamik testin ilk sınırı ulaşabildiği yüzeydir. Yalnız kök uçtan başlayıp yanıtlarda geçen
bağlantıları izleyen bir keşif ile, kaynaktan çıkarılmış rota tablosu verilen bir koşum
aynı servis üzerinde ayrı sonuçlar verir.

```js
// kesif.mjs — dinamik testin ulasabildigi uc payi: yalniz baglantidan ve rota tablosuyla
import { baslat, dinle, UCLAR, GERCEK } from "./servis.mjs";

const s = baslat();
const p = await dinle(s);
const kok = `http://127.0.0.1:${p}`;
const yol = (u) => (u.split("?")[0].startsWith("/uye/") ? "/uye" : u.split("?")[0]);

// (a) Yalniz "/" ucundan baslayip yanitlarda gecen baglantilari izleyen kesif.
const gorulen = new Set(), sira = ["/"];
let istek = 0;
while (sira.length) {
  const u = sira.shift();
  if (gorulen.has(yol(u))) continue;
  gorulen.add(yol(u));
  const govde = await (await fetch(kok + u)).text();
  istek += 1;
  for (const m of govde.matchAll(/"(\/[^"]*)"/g)) if (!gorulen.has(yol(m[1]))) sira.push(m[1]);
}
console.log(`baglantidan kesif : ${istek} istek, ${gorulen.size}/${UCLAR.length} uc` +
  ` = %${((100 * gorulen.size) / UCLAR.length).toFixed(0)}`);
const disarida = UCLAR.filter((u) => !gorulen.has(u));
console.log(`  ulasilamayan    : ${disarida.join(", ")}`);
console.log(`  bu uclarda duran kusur: ${GERCEK.filter((g) => disarida.includes(g.split("|")[0])).length}/${GERCEK.length}`);

// (b) Kaynaktan cikarilan rota tablosu verilirse ayni kosum butun uclara dokunur.
const rota = new Set();
for (const u of UCLAR) {
  const hedef = u === "/uye" ? "/uye/1" : u;
  const y = await fetch(kok + hedef, u === "/odunc" ? { method: "POST", body: "{}" } : undefined);
  if (y.status !== 404 || u === "/kitap") rota.add(u);
  await y.text();
}
console.log(`rota tablosuyla   : ${UCLAR.length} istek, ${rota.size}/${UCLAR.length} uc` +
  ` = %${((100 * rota.size) / UCLAR.length).toFixed(0)}`);
s.close();
```

```
baglantidan kesif : 4 istek, 4/8 uc = %50
  ulasilamayan    : /uye, /odunc, /rapor, /yonetim/gunluk
  bu uclarda duran kusur: 3/5
rota tablosuyla   : 8 istek, 8/8 uc = %100
```

Bağlantı izleyerek çalışan keşif uçların yarısını gördü ve gördüğü yarıda kusurların yalnız
ikisi vardı. Ulaşılamayan dördü hiçbir yanıtta anılmıyor: biri gövde ister, biri yönetim
tarafında, ikisi bağlantı listelerinde geçmiyor. **Kapsama eşiğinin kaynağı burada bir
ölçümdür:** kapsanmayan uçlarda duran kusur sayısı 3/5 çıktığı için "uçların tamamına
dokunulmadan koşum kabul edilmez" kuralı savunulabilir hâle gelir.

Bu eşiği tutturmanın yolu ilginçtir: rota tablosu **kaynaktan** çıkarılır. Dinamik testin
kapsamasını tavana çıkaran şey statik bir üründür; iki yöntem burada birbirini besler.

#### Sondalar ve Eşik Taraması

Her uca tek bir istek gider. Yanıt üç kurala göre puanlanır: gönderilen zararsız işaretçi
gövdede aynen görünüyor mu, kimlik bildirilmeden kişisel alan dönüyor mu, gövdede iç
ayrıntı geçiyor mu. Puanların anlamı bir önceki dersteki gibidir; ağırlık **IO8**'den
gelir ve `w = 5` olarak sürer.

```js
// sonda.mjs — her uca tek istek gonderen sondalar, puanlanmis bulgular ve esik taramasi
import { baslat, dinle, UCLAR, GERCEK } from "./servis.mjs";

const IZ = "ISBN-KONTROL-7391";                 // zararsiz isaretci: yanitta aranacak
const KISISEL = ["ad", "ceza"], SAYAC = ["kuyruk", "satir"];

const s = baslat();
const kok = `http://127.0.0.1:${await dinle(s)}`;
const bulgu = [];
for (const uc of UCLAR) {
  const post = uc === "/odunc";
  const hedef = uc === "/uye" ? "/uye/2" : `${uc}?isbn=${IZ}`;
  const y = await fetch(kok + hedef, post ? { method: "POST", body: "{}" } : undefined);
  const g = await y.text();
  if (g.includes(IZ)) bulgu.push({ uc, sinif: "girdi-yankisi", puan: 80 });
  if (y.status < 300 && KISISEL.some((a) => g.includes(`"${a}"`))) bulgu.push({ uc, sinif: "kimliksiz-erisim", puan: 90 });
  if (y.status < 300 && post) bulgu.push({ uc, sinif: "kimliksiz-kabul", puan: 50 });
  if (/"\/(srv|var|home)\//.test(g)) bulgu.push({ uc, sinif: "ic-ayrinti", puan: 85 });
  else if (SAYAC.some((a) => g.includes(`"${a}"`))) bulgu.push({ uc, sinif: "ic-sayac", puan: 40 });
}
s.close();

const anahtar = (b) => `${b.uc}|${b.sinif}`;
console.log(`${UCLAR.length} uc, ${UCLAR.length} istek, ${bulgu.length} bulgu, ${GERCEK.length} elle dogrulanmis kusur\n`);
for (const b of bulgu.sort((a, c) => c.puan - a.puan)) {
  console.log(`  ${b.uc.padEnd(17)}${b.sinif.padEnd(20)}${String(b.puan).padStart(3)}  gercek: ${GERCEK.includes(anahtar(b)) ? "evet" : "hayir"}`);
}

console.log(`\n${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=5".padStart(12)}`);
for (const e of [30, 40, 50, 60, 80, 90]) {
  const k = bulgu.filter((b) => b.puan >= e);
  const ya = k.filter((b) => !GERCEK.includes(anahtar(b))).length;
  const kacirma = GERCEK.length - k.filter((b) => GERCEK.includes(anahtar(b))).length;
  console.log(`${String(e).padStart(4)}${String(k.length).padStart(7)}${String(ya).padStart(14)}${String(kacirma).padStart(9)}${String(ya + 5 * kacirma).padStart(12)}`);
}
console.log(`hicbir esikte gorunmeyen: ${GERCEK.filter((g) => !bulgu.some((b) => anahtar(b) === g)).join(", ")}`);
```

```
8 uc, 8 istek, 6 bulgu, 5 elle dogrulanmis kusur

  /uye             kimliksiz-erisim     90  gercek: evet
  /yonetim/gunluk  ic-ayrinti           85  gercek: evet
  /kitap           girdi-yankisi        80  gercek: evet
  /odunc           kimliksiz-kabul      50  gercek: evet
  /durum           ic-sayac             40  gercek: hayir
  /rapor           ic-sayac             40  gercek: hayir

esik  kalan  yanlis alarm  kacirma maliyet w=5
  30      6             2        1           7
  40      6             2        1           7
  50      4             0        1           5
  60      3             0        2          10
  80      3             0        2          10
  90      1             0        4          20
hicbir esikte gorunmeyen: /kitap|bos-girdi-ayrintisi
```

En düşük maliyet eşik 50'de: iki yanlış alarm düşüyor, dört gerçek kusur kalıyor.
Eşiğin bir önceki dersten farkı, bulguların puan ekseninde **ayrık** kümelenmesidir.
Statik taramada gerçek ve masum bulgular aynı bantta duruyordu; burada iki yanlış alarm
40'ta, dört gerçek kusur 50 ve üstünde. Nedeni yöntemin doğasıdır: kanıt yanıtın kendisidir,
niyet tahmini değil.

**Yakalanan sınıflar** kimliksiz erişim, kimliksiz kabul, girdi yankısı ve iç ayrıntı
sızıntısı. Dördü de yalnız koşarken görünür. **Kaçan sınıf** son satırdadır: yalnız boş
girdide açılan dal hiçbir eşikte görünmedi, çünkü sonda o girdiyi hiç göndermedi. Dinamik
test, gönderdiği girdinin açtığı yolları görür; göndermediği girdi kod tabanında sessiz
durur.

#### İki Yöntemin Ayrıştığı Yer

Aynı servise bir önceki dersin üç statik kuralı uygulanırsa ne çıkar?

```js
// ayrisma.mjs — ayni servise statik kurallarin uygulanmasi ve iki yontemin gordugu sinif kumesi
import { readFileSync } from "node:fs";

const gizli = /\b(\w*(?:anahtar|parola|jeton|gizli)\w*)\s*=\s*["'][^"']{8,}["']/i;
const KURAL = [
  ["yasak-cagri", /\beval\s*\(|\bnew\s+Function\s*\(/],
  ["birlestirilmis-sorgu", /(SELECT|INSERT|UPDATE|DELETE)[^"'`]*["'`]\s*\+\s*\w+/i],
  ["sabit-gizli-deger", gizli],
];

const satir = readFileSync(new URL("./servis.mjs", import.meta.url), "utf8").split("\n");
const statikBulgu = satir.flatMap((s, i) =>
  KURAL.filter(([, d]) => d.test(s)).map(([ad]) => `servis.mjs:${i + 1} ${ad}`));
console.log(`servis.mjs: ${satir.length} satir, ${KURAL.length} kural -> ${statikBulgu.length} statik bulgu`);

// IO9: her kusur sinifinin iki yontemce gorulebilirligi elle siniflandirildi.
const SINIF = [                                  // [sinif, statik gorur, dinamik gorur]
  ["birlestirilmis-sorgu", true, false],
  ["sabit-gizli-deger", true, false],
  ["yasak-cagri", true, false],
  ["girdi-yankisi", false, true],
  ["kimliksiz-erisim", false, true],
  ["kimliksiz-kabul", false, true],
  ["ic-ayrinti", false, true],
  ["sablon-dizgisiyle-sorgu", false, false],
  ["bos-girdi-ayrintisi", false, false],
];

const say = (s, d) => SINIF.filter(([, a, b]) => a === s && b === d).length;
console.log(`\n${SINIF.length} kusur sinifi:`);
console.log(`  yalniz statik : ${say(true, false)}  (${SINIF.filter(([, a, b]) => a && !b).map(([n]) => n).join(", ")})`);
console.log(`  yalniz dinamik: ${say(false, true)}  (${SINIF.filter(([, a, b]) => !a && b).map(([n]) => n).join(", ")})`);
console.log(`  ikisi birden  : ${say(true, true)}`);
console.log(`  hicbiri       : ${say(false, false)}  (${SINIF.filter(([, a, b]) => !a && !b).map(([n]) => n).join(", ")})`);
const birlesim = SINIF.filter(([, a, b]) => a || b).length;
console.log(`birlesik kapsama: ${birlesim}/${SINIF.length} = %${((100 * birlesim) / SINIF.length).toFixed(0)}`);
```

```
servis.mjs: 46 satir, 3 kural -> 0 statik bulgu

9 kusur sinifi:
  yalniz statik : 3  (birlestirilmis-sorgu, sabit-gizli-deger, yasak-cagri)
  yalniz dinamik: 4  (girdi-yankisi, kimliksiz-erisim, kimliksiz-kabul, ic-ayrinti)
  ikisi birden  : 0
  hicbiri       : 2  (sablon-dizgisiyle-sorgu, bos-girdi-ayrintisi)
birlesik kapsama: 7/9 = %78
```

Statik kurallar bu dosyada tek bulgu üretmedi; dinamik sondalar aynı dosyada dört gerçek
kusur buldu. Ters yön de aynı derecede keskin: bir önceki dersin kod tabanındaki gömülü
değer ve birleştirilmiş sorgu, buradaki hiçbir istekle görünmez. Bu kurulumda iki yöntemin
**kesişimi boştur** ve birleşik kapsama dokuz sınıfın yedisidir. Kural kümeleri büyüdükçe
kesişim de büyür, ama biri diğerinin yerine geçmez.

**Kararın sahibi:** eşik 50'nin üstünde kalan bulgu yayımı durdurur; kapsama eşiğini
tutturamayan koşum ise sonuç bildirmez, yeniden koşar. Kapsamasını bildirmeyen bir dinamik
test raporu, kaç ucun hiç denenmediğini gizler.

#### Testin Bedeli

Koşumdan bağımsız bedel üç yerdedir. Birincisi istek sayısı: 8 uç, koşum başına 8 istek ve
ayakta duran bir süreç. Statik tarama sıfır süreçle çalışıyordu; dinamik test uygulamanın
kendisini, verisini ve bağımlılıklarını ister.

İkincisi rota tablosunun bakımı: kapsamayı %50'den %100'e çıkaran şey kaynaktan çıkarılan
sekiz satırlık listedir ve yeni bir uç eklendiğinde bu liste elle güncellenmezse kapsama
sessizce düşer.

Üçüncüsü girdi kümesi: sondalar yalnız gönderdikleri değerin açtığı dalları görür. Boş
girdi dalı bir tek değer eksik olduğu için kaçtı. Girdi kümesini büyütmek istek sayısını
uç sayısıyla çarpar; kapsama doğrusal artarken maliyet çarpımsal artar.

#### Özet

- Dinamik uygulama güvenlik testi koşan sürece istek gönderir ve kanıtı yanıtın kendisinden
  alır; bu derste 8 uca 8 istekle 6 bulgu üretildi, 4'ü gerçek kusurdu.
- Bağlantı izleyen keşif uçların %50'sine ulaştı ve elle doğrulanmış 5 kusurun 3'ü
  ulaşılamayan uçlarda kaldı; rota tablosu verildiğinde kapsama %100'e çıktı.
- Eşik taraması en düşük maliyeti 50'de verdi: 0 yanlış alarm, 1 kaçırma. Bulgular
  puan ekseninde ayrık kümelendiği için eşik seçimi statik taramadakinden daha kolaydır.
- Yakalanan sınıflar yalnız koşarken görünür olanlardır; kaçan sınıf, sondanın hiç
  göndermediği girdinin açtığı daldır.
- Aynı servise uygulanan üç statik kural sıfır bulgu verdi; dokuz kusur sınıfında iki
  yöntemin kesişimi boş çıktı ve birleşik kapsama %78'de kaldı.
- Bedel: ayakta bir süreç, koşum başına 8 istek, elle bakılan bir rota tablosu ve girdi
  kümesi büyüdükçe çarpımsal büyüyen istek sayısı.

#### Sonraki Adım

İki tarama da **yazılan** kodu sınadı: biri metnini okudu, öteki koştururken davranışını
gözledi. Oysa ödünç servisinin çalıştırdığı satırların çoğunu bu takım yazmadı. Katalog
şeması bir doğrulayıcıya, bildirim kanalı bir kuyruk istemcisine, günlük yazımı bir
biçimleyiciye dayanıyor ve bunların her biri kendi bağımlılıklarını getiriyor. Bu kodda
kusur aramanın yolu taramak değil, **bilinen kusurları aramaktır**: bir bileşenin hangi
sürümünde hangi kusurun bildirildiği kayıtlıdır. Sonraki ders ödünç sisteminin bağımlılık
ağacını çıkarır, kendi kayıt kümesini bu ağaçla eşleştirir ve üç şeyi ölçer: doğrudan
yazılmamış bağımlılıkların payı, sürüm aralığı eşleşmesinin ürettiği yanlış alarm ve tek
bir düzeltmenin kaç pakete dokunduğu.

### Bağımlılık ve Bileşen Taraması

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/bagimlilik-ve-bilesen-taramasi

Bilinen açıkların bağımlılık ağacında aranması: dolaylı bağımlılıkların payı, sürüm aralığı eşleşmesinin ürettiği yanlış alarm, önem eşiğinin taranması ve tek bir düzeltmenin sürüm aralıkları yüzünden kaç pakete birden dokunduğu.

İki tarama da bu takımın yazdığı kodu sınadı. Oysa ödünç servisinin çalıştırdığı satırların
çoğunu bu takım yazmadı: katalog şeması bir doğrulayıcıya, bildirim kanalı bir kuyruk
istemcisine, günlük yazımı bir biçimleyiciye dayanıyor ve bunların her biri kendi
bağımlılıklarını getiriyor.

Bu kodda kusur aramanın yolu taramak değil, **bilinen açıkları** aramaktır: bir bileşenin
hangi sürüm aralığında hangi kusuru taşıdığı kayıtlıdır ve tarama bu kayıtları kurulu
sürümlerle eşleştirir. Yöntem ucuzdur, ama üç yerde yanılır — ders bu üç yeri sayar.

#### Bağımlılık Ağacı ve Kilit Dosyası

Ödünç sisteminin dört doğrudan bağımlılığı var. Kilit dosyası, kurulu sürümleri tam
sayılarla sabitler; tarama bu sürümlere bakar, bildirilen aralıklara değil.

```js
// agac.mjs — odunc sisteminin bagimlilik agaci, kilit dosyasi ve surum araligi hesabi
export const KOK = {
  "sema-dogrulayici": "^2.1.0", "kuyruk-istemcisi": "^1.4.0",
  "gunluk-bicimleyici": "^3.0.0", "kimlik-imzalayici": "^1.2.0",
};

// Her paketin her surumunun kendi bildirdigi araliklar.
export const DEPO = {
  "sema-dogrulayici": {
    "2.1.4": { "desen-derleyici": "^1.0.0", "metin-normallestirici": "^0.9.0" },
    "2.2.0": { "desen-derleyici": "^2.0.0", "metin-normallestirici": "^0.9.0" },
  },
  "kuyruk-istemcisi": {
    "1.4.2": { "cerceve-cozucu": "^4.2.0", "yeniden-deneme": "^1.1.0" },
    "1.5.0": { "cerceve-cozucu": "^4.3.0", "yeniden-deneme": "^1.2.0" },
  },
  "gunluk-bicimleyici": { "3.0.1": { "metin-normallestirici": "^0.9.0", "zaman-damgasi": "^2.0.0" } },
  "kimlik-imzalayici": { "1.2.0": { "sayisal-ozet": "^3.1.0" } },
  "desen-derleyici": { "1.0.7": {}, "2.0.0": {} },
  "metin-normallestirici": { "0.9.8": {} },
  "cerceve-cozucu": { "4.2.1": {}, "4.3.0": {} },
  "yeniden-deneme": { "1.1.0": {}, "1.2.0": {} },
  "zaman-damgasi": { "2.0.4": {}, "2.1.0": {} },
  "sayisal-ozet": { "3.1.2": {} },
};

// Kilit dosyasi: kurulu olan surumler.
export const KILIT = {
  "sema-dogrulayici": "2.1.4", "kuyruk-istemcisi": "1.4.2", "gunluk-bicimleyici": "3.0.1",
  "kimlik-imzalayici": "1.2.0", "desen-derleyici": "1.0.7", "metin-normallestirici": "0.9.8",
  "cerceve-cozucu": "4.2.1", "yeniden-deneme": "1.1.0", "zaman-damgasi": "2.0.4",
  "sayisal-ozet": "3.1.2",
};

export const parca = (v) => v.split(".").map(Number);
export const kucuk = (a, b) => {
  const [x, y] = [parca(a), parca(b)];
  for (let i = 0; i < 3; i += 1) if (x[i] !== y[i]) return x[i] < y[i];
  return false;
};
// "^a.b.c": ilk sifir olmayan basamak sabit kalir. "<a.b.c": kucuk olan her surum.
export function uyar(surum, aralik) {
  if (aralik.startsWith("<")) return kucuk(surum, aralik.slice(1));
  const t = parca(aralik.slice(1)), s = parca(surum);
  if (kucuk(surum, aralik.slice(1))) return false;
  const i = t.findIndex((x) => x > 0);
  return s.slice(0, i + 1).join(".") === t.slice(0, i + 1).join(".");
}

// Kilitli surumleri izleyerek her paketin derinligini bulur.
export function agac(kok = KOK, kilit = KILIT) {
  const derinlik = {}, sira = Object.keys(kok).map((a) => [a, 1]);
  while (sira.length) {
    const [ad, d] = sira.shift();
    if (derinlik[ad] !== undefined && derinlik[ad] <= d) continue;
    derinlik[ad] = d;
    for (const alt of Object.keys(DEPO[ad][kilit[ad]])) sira.push([alt, d + 1]);
  }
  return derinlik;
}
```

#### Eşleşme ve Yanlış Alarm

Kayıt kümesi kendi kayıtlarımızdır: her kayıt bir paketi, etkilenen bir sürüm aralığını,
bir önem puanını ve düzeltilmiş sürümü taşır. Tarama, kilitli sürümün etkilenen aralığa
düşüp düşmediğine bakar — bu, bir metin karşılaştırmasından biraz karmaşık bir sürüm
karşılaştırmasıdır ve başka hiçbir bilgi kullanmaz.

**IO10 (varsayım):** gerçek risk, kusurlu kodun bu sistemde **çağrılmasıdır**. Bir paket
etkilenen aralıkta olabilir ama kusurlu işlevi bu sistemde hiç çağrılmıyor olabilir; elle
doğrulanmış risk listesi bu ayrımı yapar, tarama yapamaz.

```js
// tarama.mjs — kayit kumesinin kilit dosyasiyla eslesmesi, yanlis alarmlar ve esik taramasi
import { KILIT, uyar, agac } from "./agac.mjs";

const derinlik = agac();
const dogrudan = Object.keys(derinlik).filter((a) => derinlik[a] === 1);
const n = Object.keys(derinlik).length;
console.log(`${n} paket, dogrudan ${dogrudan.length}, dolayli ${n - dogrudan.length}` +
  ` (%${((100 * (n - dogrudan.length)) / n).toFixed(0)}), en buyuk derinlik ${Math.max(...Object.values(derinlik))}`);

// Kendi kayit kumemiz: paket, etkilenen aralik, onem, duzeltilmis surum.
const KAYIT = [
  { no: "K-01", paket: "desen-derleyici", etkilenen: "<2.0.0", onem: 80, duzelten: "2.0.0" },
  { no: "K-02", paket: "metin-normallestirici", etkilenen: "<0.9.8", onem: 60, duzelten: "0.9.8" },
  { no: "K-03", paket: "cerceve-cozucu", etkilenen: "<4.3.0", onem: 70, duzelten: "4.3.0" },
  { no: "K-04", paket: "kuyruk-istemcisi", etkilenen: "<1.5.0", onem: 90, duzelten: "1.5.0" },
  { no: "K-05", paket: "zaman-damgasi", etkilenen: "<2.1.0", onem: 30, duzelten: "2.1.0" },
];

// IO10: elle dogrulanmis gercek risk, kusurlu kodun bu sistemde cagrilmasidir.
const GERCEK = ["desen-derleyici", "kuyruk-istemcisi", "yeniden-deneme"];

const bulgu = KAYIT.filter((k) => uyar(KILIT[k.paket], k.etkilenen));
console.log(`${KAYIT.length} kayit, ${bulgu.length} eslesme, ${GERCEK.length} elle dogrulanmis risk\n`);
console.log(`${"kayit".padEnd(6)}${"paket".padEnd(23)}${"kurulu".padEnd(8)}${"etkilenen".padEnd(11)}onem  derinlik  gercek`);
for (const k of KAYIT) {
  const e = uyar(KILIT[k.paket], k.etkilenen);
  console.log(`${k.no.padEnd(6)}${k.paket.padEnd(23)}${KILIT[k.paket].padEnd(8)}${k.etkilenen.padEnd(11)}` +
    `${String(k.onem).padStart(4)}${String(derinlik[k.paket]).padStart(10)}  ${e ? (GERCEK.includes(k.paket) ? "evet" : "hayir") : "-"}`);
}

console.log(`\n${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=5".padStart(12)}`);
for (const e of [20, 30, 50, 70, 80, 90]) {
  const k = bulgu.filter((b) => b.onem >= e);
  const ya = k.filter((b) => !GERCEK.includes(b.paket)).length;
  const kac = GERCEK.length - k.filter((b) => GERCEK.includes(b.paket)).length;
  console.log(`${String(e).padStart(4)}${String(k.length).padStart(7)}${String(ya).padStart(14)}${String(kac).padStart(9)}${String(ya + 5 * kac).padStart(12)}`);
}
console.log(`kayit kumesinde hic gecmeyen gercek risk: ${GERCEK.filter((g) => !KAYIT.some((k) => k.paket === g)).join(", ")}`);
```

```
10 paket, dogrudan 4, dolayli 6 (%60), en buyuk derinlik 2
5 kayit, 4 eslesme, 3 elle dogrulanmis risk

kayit paket                  kurulu  etkilenen  onem  derinlik  gercek
K-01  desen-derleyici        1.0.7   <2.0.0       80         2  evet
K-02  metin-normallestirici  0.9.8   <0.9.8       60         2  -
K-03  cerceve-cozucu         4.2.1   <4.3.0       70         2  hayir
K-04  kuyruk-istemcisi       1.4.2   <1.5.0       90         1  evet
K-05  zaman-damgasi          2.0.4   <2.1.0       30         2  hayir

esik  kalan  yanlis alarm  kacirma maliyet w=5
  20      4             2        1           7
  30      4             2        1           7
  50      3             1        1           6
  70      3             1        1           6
  80      2             0        1           5
  90      1             0        2          10
kayit kumesinde hic gecmeyen gercek risk: yeniden-deneme
```

Üç sayı okunmalı. Birincisi **dolaylı bağımlılıkların payı**: on paketin altısı hiçbir
yerde bildirilmedi, dört bildirimin altından geldi. Kilit dosyası olmasa bu altı paketin
hangi sürümde olduğu bile bilinmezdi ve tarama sürüm bilmeden çalışamaz.

İkincisi **yanlış alarm**: dört eşleşmenin ikisi gerçek risk değil. İkisi de sürüm
karşılaştırması açısından doğru — kurulu sürüm etkilenen aralıkta — ama kusurlu kod yolu bu
sistemde çalışmıyor. Sürüm aralığı eşleşmesi paketin varlığını ölçer, kullanımını değil.

Üçüncüsü **kaçırma**: `yeniden-deneme` içindeki kusur elle doğrulanmış risk listesinde var,
kayıt kümesinde yok. Bağımlılık taramasının yapısal sınırı budur — yalnız **bildirilmiş**
kusurları bulur ve hiçbir eşik bildirilmemiş bir kusuru göremez.

**Eşiğin kaynağı ölçümdür:** eşik taraması, önem 80'de iki yanlış alarmın da düştüğünü ve
kalan iki bulgunun ikisinin de gerçek çıktığını gösteriyor; maliyet en düşük değerine
orada iniyor. Ağırlık yine IO8'den geliyor.

#### Düzeltmenin Yayılması

Eşiği geçen iki kayıt düzeltilecek. Düzeltme tek bir sürüm numarası değiştirmek gibi
görünür; sürüm aralıkları bunu engeller.

```js
// duzeltme.mjs — bir kaydin duzeltilmesinin kac pakete dokundugu
import { DEPO, KOK, KILIT, uyar, kucuk, agac } from "./agac.mjs";

const surumler = (ad) => Object.keys(DEPO[ad]).sort((a, b) => (kucuk(a, b) ? -1 : 1));
const enDusukUyan = (ad, aralik) => surumler(ad).find((v) => uyar(v, aralik));
const ustler = (ad, kilit) => Object.keys(DEPO).filter((u) => DEPO[u][kilit[u]]?.[ad]);

function yukselt(kilit, paket, hedef) {
  const yeni = { ...kilit }, dokunulan = new Set(), engel = [];
  const sira = [[paket, hedef]];
  while (sira.length) {
    const [ad, s] = sira.shift();
    if (yeni[ad] === s) continue;
    yeni[ad] = s;
    dokunulan.add(ad);
    for (const [alt, aralik] of Object.entries(DEPO[ad][s]))
      if (!uyar(yeni[alt], aralik)) sira.push([alt, enDusukUyan(alt, aralik)]);
    for (const u of ustler(ad, yeni)) {
      if (uyar(s, DEPO[u][yeni[u]][ad])) continue;
      const uygun = surumler(u).find((v) => DEPO[u][v][ad] && uyar(s, DEPO[u][v][ad]));
      uygun ? sira.push([u, uygun]) : engel.push(`${u} icin uygun surum yok`);
    }
    if (KOK[ad] && !uyar(s, KOK[ad])) engel.push(`kok araligi ${KOK[ad]} ${ad}@${s} kabul etmiyor`);
  }
  return { yeni, dokunulan: [...dokunulan], engel };
}

let birlesik = KILIT;
for (const [no, paket, hedef] of [["K-01", "desen-derleyici", "2.0.0"], ["K-04", "kuyruk-istemcisi", "1.5.0"]]) {
  const { yeni, dokunulan, engel } = yukselt(KILIT, paket, hedef);
  console.log(`${no}: ${paket} ${KILIT[paket]} -> ${hedef}; ${dokunulan.length} pakete dokunur`);
  for (const a of dokunulan) console.log(`   ${a.padEnd(23)}${KILIT[a]} -> ${yeni[a]}`);
  if (engel.length) console.log(`   engel: ${engel.join("; ")}`);
  birlesik = yukselt(birlesik, paket, hedef).yeni;
}

const degisen = Object.keys(KILIT).filter((a) => birlesik[a] !== KILIT[a]);
const n = Object.keys(KILIT).length;
console.log(`\niki duzeltme birlikte: ${degisen.length}/${n} paket (%${((100 * degisen.length) / n).toFixed(0)}) yeni surume gecti`);
console.log(`agac derinligi degismedi: ${Math.max(...Object.values(agac(KOK, birlesik)))}`);
console.log(`yan etki: K-03'un isaret ettigi cerceve-cozucu ${KILIT["cerceve-cozucu"]} -> ${birlesik["cerceve-cozucu"]}`);
```

```
K-01: desen-derleyici 1.0.7 -> 2.0.0; 2 pakete dokunur
   desen-derleyici        1.0.7 -> 2.0.0
   sema-dogrulayici       2.1.4 -> 2.2.0
K-04: kuyruk-istemcisi 1.4.2 -> 1.5.0; 3 pakete dokunur
   kuyruk-istemcisi       1.4.2 -> 1.5.0
   cerceve-cozucu         4.2.1 -> 4.3.0
   yeniden-deneme         1.1.0 -> 1.2.0

iki duzeltme birlikte: 5/10 paket (%50) yeni surume gecti
agac derinligi degismedi: 2
yan etki: K-03'un isaret ettigi cerceve-cozucu 4.2.1 -> 4.3.0
```

İlk düzeltme yukarı yayıldı. `desen-derleyici` düzeltilmiş sürümü büyük basamağı
artırdığı için üstündeki paketin bildirdiği aralık onu kabul etmiyor; bu yüzden ara paket
de yükseltilmek zorunda. İkinci düzeltme aşağı yayıldı: yeni sürüm kendi bağımlılıklarından
daha yenisini istiyor. İki düzeltme birlikte on paketin beşini oynattı.

Bu sayı taramanın gerçek bedelidir. Beş paketin yeni sürümü, güvenlik kaydıyla ilgisi
olmayan davranış değişiklikleri de getirir; yükseltmeyi izleyen gerileme testi kaçınılmazdır
ve maliyeti bulgu sayısıyla değil **dokunulan paket sayısıyla** ölçülür. Son satır tersini
de gösteriyor: yanlış alarm sayılan bir kayıt, başka bir düzeltmenin yan etkisiyle bedava
kapandı.

**Kararın sahibi:** önem 80'in üstündeki eşleşme yayımı durdurur ve bir yükseltme işi açar;
altındakiler bir listede birikir. Kayıt kümesi her gün değişebildiği için aynı kilit dosyası
bugün yeşil, yarın kırmızı olabilir — bu tarama, kod değişmeden sonucu değişen tek
denetimdir.

#### Özet

- Bağımlılık taraması kilit dosyasındaki sürümleri kayıt kümesindeki etkilenen aralıklarla
  eşleştirir; bu ağaçta on paketin altısı (%60) hiçbir yerde bildirilmemiş dolaylı
  bağımlılıktır.
- Dört eşleşmenin ikisi yanlış alarm çıktı: sürüm doğru eşleşti ama kusurlu kod yolu bu
  sistemde çağrılmıyor. Sürüm aralığı eşleşmesi varlığı ölçer, kullanımı değil.
- Yakalanan sınıf bildirilmiş bir kusurun kurulu sürümde bulunmasıdır; kaçan sınıf henüz
  bildirilmemiş kusurdur — elle doğrulanmış üç riskten biri hiçbir kayıtta geçmiyordu.
- Eşik taraması önem 80'i verdi: 0 yanlış alarm, 1 kaçırma. Eşiğin kaynağı bu ölçüm,
  ağırlığın kaynağı IO8'dir.
- Bedel dokunulan paket sayısıdır: iki düzeltme sürüm aralıkları üzerinden yukarı ve aşağı
  yayılarak on paketin beşini yeni sürüme taşıdı ve her biri gerileme testi ister.
- Bu tarama, kod hiç değişmeden sonucu değişebilen tek denetimdir; kayıt kümesi büyüdükçe
  aynı kilit dosyası yeniden kırmızıya döner.

#### Sonraki Adım

Üç yöntem de bir kural kümesine bakıyordu: statik tarama desenlere, dinamik test yanıt
imzalarına, bağımlılık taraması kayıtlara. Üçünün ortak varsayımı, aranan şeyin **önceden
adlandırılmış** olmasıdır. Ödünç sisteminin en pahalı kusuru ise hiçbir listede yoktur:
üyelik bitmiş bir okuyucunun ceza kaydını başka bir üyenin üstüne taşıyabilmesi, ayrı ayrı
bakıldığında her adımı yetkili olan bir istek dizisidir. Hiçbir desen bunu adlandırmaz,
çünkü kusur kodda değil kuralların birleşimindedir. Sonraki ders bu sınırı çizer: bir tehdit
modeli üzerinde otomasyonun kapsayabildiği kusur sınıflarını insan muhakemesi gerektiren
sınıflardan ayırır ve ikisinin arasında kalan payı sayar.

### Sızma Testinin Yeri

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/sizma-testinin-yeri

Otomasyonun bittiği noktanın ölçülmesi: bir tehdit modelinde otomatik karar verilebilen girdilerin payı, tek adımlı ve çok adımlı senaryolar arasındaki kapsama farkı ve her adımı tek tek yetkili olan bir işlem zincirinin ancak insan eliyle yazılmış bir değişmezle ihlal sayılabilmesi.

Üç yöntem de bir kural kümesine bakıyordu: statik tarama desenlere, dinamik test yanıt
imzalarına, bağımlılık taraması kayıtlara. Üçünün ortak varsayımı, aranan şeyin **önceden
adlandırılmış** olmasıdır. Ödünç sisteminin en pahalı kusuru ise hiçbir listede yoktur,
çünkü kusur kodun bir satırında değil kuralların birleşimindedir.

**Sızma testi** bu boşluğun adıdır: bir insanın sistemin kurallarını okuyup, tek tek
meşru olan işlemleri sistemin sahibinin hiç düşünmediği bir sırayla birleştirmesi. Bu ders
tekniği değil **yeri** ele alır — otomasyonun kapsayabildiği kusur sınıflarını muhakeme
gerektirenlerden ayırır ve aradaki payı sayar.

#### Tehdit Modeliyle Kapsamı Saymak

Kapsam sorusunun sorulabilmesi için bir liste gerekir. **Tehdit modeli** varlıkları, giriş
noktalarını ve bunlara yönelen tehditleri sayar; buradaki liste ödünç sistemi için çıkarıldı
ve her girdi iki alan taşıyor: kararı hangi kaynağın verebildiği ve senaryonun kaç adım
olduğu.

**IO11 (varsayım):** tehdit modeli eksiksizdir. Listede olmayan bir tehdit yoktur sayılır;
gerçekte bu varsayım tutmaz ve bütün kapsama oranları bu listeye görelidir.

```js
// tehdit.mjs — odunc sisteminin tehdit modeli ve kapsamanin yontemlere dagilimi
// [tehdit, karar kaynagi, senaryo adim sayisi]
// karar kaynagi: desen (statik), yanit (dinamik), kayit (bagimlilik), muhakeme (insan)
const TEHDIT = [
  ["katalog sorgusunun birlestirilerek kurulmasi", "desen", 1],
  ["koda gomulu imza anahtari", "desen", 1],
  ["dizgiden kod ureten bicimleyici", "desen", 1],
  ["uye kaydinin kimliksiz donmesi", "yanit", 1],
  ["hata yanitinda ic yol bildirimi", "yanit", 1],
  ["odunc isteginin kimliksiz kabulu", "yanit", 1],
  ["girdinin yanitta aynen yankilanmasi", "yanit", 1],
  ["kabul edilen istegin kayda donusmesi", "yanit", 2],
  ["kuyruk istemcisinin bildirilmis kusuru", "kayit", 1],
  ["desen derleyicisinin bildirilmis kusuru", "kayit", 1],
  ["raporun toplamindan tek uyenin cikarilabilmesi", "muhakeme", 1],
  ["cezanin ortaklik uzerinden devredilmesi", "muhakeme", 4],
  ["uyeligi bitmis okuyucunun odunc sirasini korumasi", "muhakeme", 3],
  ["uzatmanin ceza esigini sifirlamasi", "muhakeme", 2],
];

const OTOMATIK = ["desen", "yanit", "kayit"];
const say = (f) => TEHDIT.filter(f).length;
const oto = (t) => OTOMATIK.includes(t[1]);

console.log(`${TEHDIT.length} tehdit girdisi`);
for (const k of [...OTOMATIK, "muhakeme"]) {
  const n = say((t) => t[1] === k);
  console.log(`  ${k.padEnd(10)}${String(n).padStart(3)}  %${((100 * n) / TEHDIT.length).toFixed(0)}`);
}
console.log(`otomatik kapsanan: ${say(oto)}/${TEHDIT.length} = %${((100 * say(oto)) / TEHDIT.length).toFixed(0)}`);

for (const [ad, f] of [["tek adimli", (t) => t[2] === 1], ["cok adimli", (t) => t[2] > 1]]) {
  const grup = TEHDIT.filter(f);
  const k = grup.filter(oto).length;
  console.log(`${ad}: ${grup.length} tehdit, ${k} otomatik = %${((100 * k) / grup.length).toFixed(0)}`);
}

const el = TEHDIT.filter((t) => !oto(t));
console.log(`\ninsan muhakemesi gerektiren ${el.length} girdi, toplam ${el.reduce((s, t) => s + t[2], 0)} adim:`);
for (const [ad, , n] of el) console.log(`  ${String(n)} adim  ${ad}`);
```

```
14 tehdit girdisi
  desen       3  %21
  yanit       5  %36
  kayit       2  %14
  muhakeme    4  %29
otomatik kapsanan: 10/14 = %71
tek adimli: 10 tehdit, 9 otomatik = %90
cok adimli: 4 tehdit, 1 otomatik = %25

insan muhakemesi gerektiren 4 girdi, toplam 10 adim:
  1 adim  raporun toplamindan tek uyenin cikarilabilmesi
  4 adim  cezanin ortaklik uzerinden devredilmesi
  3 adim  uyeligi bitmis okuyucunun odunc sirasini korumasi
  2 adim  uzatmanin ceza esigini sifirlamasi
```

İki oran arasındaki fark bu dersin ana bulgusudur. Tek adımlı tehditlerin %90'ı otomatik
karara bağlanıyor; çok adımlı tehditlerin yalnız %25'i. Otomasyon **istek başına** karar
verir: bir satır, bir yanıt, bir sürüm numarası. Adımlar arasındaki ilişki hiçbir kuralın
girdisi değildir.

Ayrım adım sayısıyla da bitmiyor. Listedeki tek adımlı muhakeme girdisi, rapor ucunun tek
bir isteğidir; yanıt kusursuz biçimlidir, yetki denetimi geçer, hiçbir iç ayrıntı sızmaz.
Kusur, dönen toplamın belirli bir sorgu ile tek bir üyeye indirgenebilmesidir — bu kararı
verebilmek için verinin **ne anlama geldiğini** bilmek gerekir.

#### Tek Tek Yetkili Adımlar

Muhakeme girdilerinin en pahalısı dört adımlıydı. Ödünç sisteminde beş işlem var ve her
birinin kendi yetki koşulu bulunuyor: ortaklık ekleme yalnız üyelere açık, ceza devri
yalnız ortaklık varken, ortaklıktan çıkma yalnız ortağa. Otomatik denetim her adımda bu
koşulu sınar ve hiçbir adım kural dışı değildir.

```js
// zincir.mjs — tek tek yetkili adimlarin birlesiminden dogan ihlal ve zincir uzunlugu esigi
const BASLANGIC = { uye: true, ortak: false, oduncte: true, ceza: 0, ortakCeza: 0 };

// Her islemin kendi yetki kosulu var; hicbiri tek basina kural disi degil.
const ISLEM = {
  "ortak-ekle": { yetki: (d) => d.uye && !d.ortak, etki: (d) => ({ ...d, ortak: true }) },
  "ortak-cikar": { yetki: (d) => d.ortak, etki: (d) => ({ ...d, ortak: false }) },
  "ceza-devret": { yetki: (d) => d.ortak && d.ceza > 0, etki: (d) => ({ ...d, ceza: 0, ortakCeza: d.ortakCeza + d.ceza }) },
  "iade-et": { yetki: (d) => d.oduncte, etki: (d) => ({ ...d, oduncte: false }) },
  "kayip-bildir": { yetki: (d) => d.oduncte, etki: (d) => ({ ...d, oduncte: false, ceza: d.ceza + 30 }) },
};

// Insan eliyle yazilmis iki degismez. Dogru olan ortakligin kapanmasini sarta baglar.
const DEGISMEZ = {
  dogru: (d) => !(d.ortakCeza > 0 && !d.ortak),
  dar: (d) => !(d.ortakCeza > 0),
};

// Uzunlugu k'ya kadar olan butun yetkili islem dizilerini uretir.
function diziler(k) {
  const cikti = [];
  const yuru = (d, iz) => {
    if (iz.length) cikti.push({ iz, son: d });
    if (iz.length === k) return;
    for (const [ad, i] of Object.entries(ISLEM)) if (i.yetki(d)) yuru(i.etki(d), [...iz, ad]);
  };
  yuru(BASLANGIC, []);
  return cikti;
}

console.log(`${Object.keys(ISLEM).length} islem, her adimda yetki kosulu denetleniyor`);
console.log(`${"k".padStart(2)}${"dizi".padStart(8)}${"adim".padStart(8)}${"yetki gecen".padStart(13)}${"dogru ihlal".padStart(13)}${"dar ihlal".padStart(11)}${"dar yanlis alarm".padStart(18)}`);
for (const k of [1, 2, 3, 4, 5]) {
  const d = diziler(k);
  const adim = d.reduce((t, x) => t + x.iz.length, 0);
  const dogru = d.filter((x) => !DEGISMEZ.dogru(x.son));
  const dar = d.filter((x) => !DEGISMEZ.dar(x.son));
  console.log(`${String(k).padStart(2)}${String(d.length).padStart(8)}${String(adim).padStart(8)}` +
    `${String(adim).padStart(13)}${String(dogru.length).padStart(13)}${String(dar.length).padStart(11)}` +
    `${String(dar.length - dogru.length).padStart(18)}`);
}

const ilk = diziler(5).filter((x) => !DEGISMEZ.dogru(x.son)).sort((a, b) => a.iz.length - b.iz.length)[0];
console.log(`\nen kisa ihlal (${ilk.iz.length} adim): ${ilk.iz.join(" -> ")}`);
console.log(`son durum: ceza ${ilk.son.ceza}, ortak uzerindeki ceza ${ilk.son.ortakCeza}, ortaklik ${ilk.son.ortak}`);
```

```
5 islem, her adimda yetki kosulu denetleniyor
 k    dizi    adim  yetki gecen  dogru ihlal  dar ihlal  dar yanlis alarm
 1       3       3            3            0          0                 0
 2       8      13           13            0          0                 0
 3      17      40           40            0          2                 2
 4      28      84           84            2          4                 2
 5      45     169          169            2         10                 8

en kisa ihlal (4 adim): ortak-ekle -> kayip-bildir -> ceza-devret -> ortak-cikar
son durum: ceza 0, ortak uzerindeki ceza 30, ortaklik false
```

Dördüncü sütun her satırda üçüncüyle aynı: **atılan her adımın yetki koşulu geçti.** Yüz
altmış dokuz adımın hiçbiri kural dışı değil. Beşinci sütun dört adımda ikiye çıkıyor:
ortaklık kurulur, kitap kayıp bildirilir, ceza ortağa devredilir, ortaklık kapatılır. Sonuç,
üstünde ceza duran ama artık ortak olmayan bir üyedir.

Bu ihlali bulan şey bir kural değil, **elle yazılmış bir değişmezdir**. Ödünç sisteminin
hiçbir yerinde "ortaklık kapandığında devredilmiş ceza da geri döner" cümlesi yazılı
değildi; onu yazmak, alanı bilen bir insanın işidir. Otomasyonun bittiği nokta tam olarak
burasıdır: makine zincirleri üretebilir, ihlalin ne olduğunu söyleyemez.

Altıncı ve yedinci sütun muhakemenin kendi hata payını gösteriyor. Dar değişmez "ceza hiç
devredilemez" diyor ve meşru devirleri de ihlal sayıyor: k=4'te dört bulgunun ikisi yanlış
alarm, k=5'te on bulgunun sekizi. Yanlış yazılmış bir değişmez, yanlış yazılmış bir tarama
kuralından farksızdır — yalnız daha pahalıdır, çünkü bulgularını bir insan eler.

#### Zincir Uzunluğu Eşiği

**IO12 (varsayım):** incelenecek en uzun zincir dört adımdır. Eşiğin kaynağı bir gereksinim
değil, bir bütçedir; tablonun ikinci ve üçüncü sütunu bu bütçenin neye karşılık geldiğini
verir.

**Kaçırma**, eşiğin altında kalan ihlaldir: k=3'te iki gerçek ihlalin ikisi de kaçtı,
k=4'te ikisi de yakalandı. **Yanlış alarm**, dar değişmezin meşru zincirleri kırmızıya
döndürmesidir ve zincir uzadıkça hızla büyür. İki sütun aynı yönde büyüdüğü için eşiği
yükseltmenin bedeli iki katlıdır: hem daha çok dizi incelenir hem de daha çok yanlış alarm
elenir.

Bedelin koşumdan bağımsız ölçüsü adım sayısıdır: k=3'te 40, k=4'te 84, k=5'te 169. Beş
işlemlik bir alanda bile sayı iki katına yakın artıyor; gerçek bir sistemde işlem sayısı
onlarcadır ve zincir uzunluğunu bir artırmak incelenecek diziyi kat kat büyütür. Sızma
testinin bir bütçeyle sınırlanmasının nedeni budur — kapsam, bulunabilecek her şey değil,
**seçilmiş bir uzunluğa kadar** her şeydir.

**Kararın sahibi:** bu bulgular yayımı durdurmaz, çünkü koşum döngüsünün içinde değildir.
Bir sızma testi bulgusu bir kayıt açar, kaydın önceliği tehdit modelindeki varlığın
değerinden gelir ve düzeltme genellikle bir kural değişikliğidir; ardından o kural
otomatikleştirilebilir hâle gelir. Muhakeme girdileri zamanla otomatik girdilere dönüşür,
ama liste hiç boşalmaz.

#### Özet

- Tehdit modeli, kapsamanın sayılabilmesi için gereken listedir; on dört girdinin onu
  otomatik yöntemlerle karara bağlandı, dördü insan muhakemesi gerektirdi.
- Kapsama farkı adım sayısındadır: tek adımlı tehditlerin %90'ı otomatik, çok adımlı
  tehditlerin %25'i. Otomasyon istek başına karar verir, adımlar arasındaki ilişkiye bakmaz.
- Beş işlemlik ödünç alanında yüz altmış dokuz adımın hepsi yetki koşulunu geçti; ihlal
  ancak elle yazılmış bir değişmezle ve dört adımlık bir zincirle görünür oldu.
- Yakalanan sınıf işlem birleşiminden doğan iş mantığı kusurudur; yakalanmayan sınıf,
  değişmezi yazılmamış her kuraldır — makine zincir üretir, ihlalin tanımını üretemez.
- İnsan eliyle yazılan değişmezin de yanlış alarmı vardır: dar yazılmış bir değişmez k=5'te
  on bulgunun sekizini boşa çıkardı.
- Bedel zincir uzunluğuyla büyür: k=3'te 40 adım, k=4'te 84, k=5'te 169. Eşiğin kaynağı bir
  gereksinim değil bütçedir ve kapsam bu bütçeyle tanımlanır.

#### Sonraki Adım

Buraya kadarki dört ders sistemi kötüye kullanan bir istemciyi varsaydı. Aynı sorunun bir
başka yüzü, sistemi **kullanamayan** bir okuyucudur: ekranı görmeyen, fareyi kullanmayan ya
da metni büyüterek okuyan biri için ödünç ekranı çalışıyor mu? Bu sorunun ölçütleri ve
arayüz tarafındaki karşılıkları Frontend Kalitesi kursunda kuruldu; sonraki dersin sorusu
başkadır ve bu kursun sorusudur: o ölçütlerin kaçı bir programa yaptırılabilir, kaçı elle
doğrulanmak zorundadır ve otomatik denetimin kendi yanlış alarm ile kaçırma oranı nedir.

### Erişilebilirlik Testi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/erisilebilirlik-testi

Otomatik denetimin kapsama payının ölçülmesi: ölçütlerin karar kaynağına göre ayrılması, küçük bir öge ağacı üzerinde gerçekten koşan bir denetçinin yanlış alarm ve kaçırma sayıları ve eşiği yükseltmenin hiçbir kazanç getirmediği bir denetimde elle doğrulamanın zorunlu alanı.

Dört ders sistemi kötüye kullanan bir istemciyi varsaydı. Aynı sorunun bir başka yüzü,
sistemi **kullanamayan** bir okuyucudur: ekranı görmeyen, fareyi kullanmayan ya da metni
büyüterek okuyan biri için ödünç ekranı çalışıyor mu?

Bu sorunun ölçütleri, arayüz tarafındaki karşılıkları ve otomatik denetleyicinin kural
sınıfları Frontend Kalitesi kursunda kuruldu; burada tekrarlanmaz. Bu dersin sorusu bir
test sorusudur ve **erişilebilirlik denetiminin** kendisini ölçer: **kapsama payı**.
Ölçüt kümesinin kaçı bir programa yaptırılabilir, kaçı
elle doğrulanmak zorundadır ve otomatik denetimin kendi yanlış alarm ile kaçırma oranı
nedir? Ölçüm, tarayıcı çıktısı üzerinden değil, küçük bir belge modeli üzerinde yapılacak.

#### Belge Modeli ve Bilinen Kusurlar

Ödünç ekranının öge ağacı sekiz kusur taşıyor; kusurların yeri elle doğrulanmıştır.

```js
// belge.mjs — odunc ekraninin oge agaci ve elle dogrulanmis kusur listesi
export const BELGE = {
  tur: "sayfa", baslik: "Odunc Islemleri", dil: null, cocuk: [
    { tur: "bolum", ad: "gezinme", cocuk: [
      { tur: "baglanti", hedef: "/katalog", metin: "Katalog" },
      { tur: "baglanti", hedef: "/odunc", metin: "Odunc" },
    ] },
    { tur: "bolum", ad: "ana", cocuk: [
      { tur: "baslik", duzey: 1, metin: "Odunc Islemleri" },
      { tur: "baslik", duzey: 3, metin: "Rafta Bekleyenler" },
      { tur: "liste", cocuk: [
        { tur: "satir", cocuk: [
          { tur: "metin", metin: "Kum Saati" },
          { tur: "eylem", kimlik: "al-1", metin: "", simge: "sepet", boyut: 40, odaklanabilir: true },
        ] },
        { tur: "satir", cocuk: [
          { tur: "metin", metin: "Rafta Kalan" },
          { tur: "eylem", kimlik: "al-2", metin: "Al", boyut: 40, odaklanabilir: true },
        ] },
      ] },
      { tur: "alan", kimlik: "uye-no", tip: "metin", etiket: null },
      { tur: "metin", metin: "Ceza: 30", renk: "#8a8a8a", zemin: "#ffffff" },
      { tur: "eylem", kimlik: "gecmis", metin: "Gecmis", boyut: 40, odaklanabilir: true,
        edilgen: true, renk: "#9b9b9b", zemin: "#ffffff" },
    ] },
  ],
};

// Elle dogrulanmis kusurlar: olcut kodu, yer ve kisa gerekce.
// Edilgen "gecmis" eylemi karsitlik esiginden muaftir; listede yoktur.
export const GERCEK = [
  ["D1", "al-1", "sepet simgeli eylemin adi bos"],
  ["D2", "uye-no", "uye numarasi alaninin etiketi yok"],
  ["D3", "baslik 3", "birinci duzeyden ucuncu duzeye atlaniyor"],
  ["D5", "sayfa", "belge dil bildirimi tasimiyor"],
  ["D7", "Ceza: 30", "ceza metninin karsitligi esigin altinda"],
  ["D9", "-", "gecikme yalniz renk degisimiyle bildiriliyor"],
  ["D10", "-", "iki ayri islemi yapan eylem ayni adi tasiyor"],
  ["D11", "-", "odak sirasi gorsel sirayla uyusmuyor"],
];

export function* gez(d = BELGE) {
  yield d;
  for (const c of d.cocuk ?? []) yield* gez(c);
}
```

#### Ölçütlerin Karar Kaynağı

Bir ölçütün otomatikleştirilip otomatikleştirilemeyeceğini belirleyen şey ölçütün önemi
değil, **kararın nereden okunduğudur**. Dört kaynak var: ağaçtan doğrudan okunan bir
bildirim, bildirilen değerlerden hesaplanan bir sayı, içeriğin ne anlattığına bağlı bir
niyet ve zaman içindeki bir etkileşim dizisi. İlk ikisi bir programa yaptırılabilir,
son ikisi yaptırılamaz.

```js
// denetci.mjs — olcut kumesi, karar kaynaklari ve agactan karar verilebilen denetimler
import { gez } from "./belge.mjs";

// karar kaynagi: agac | hesap (ikisi otomatik) | niyet | dizi (ikisi elle)
export const OLCUT = [
  ["D1", "eylem adi bos", "agac", 90], ["D2", "alan etiketsiz", "agac", 90],
  ["D3", "baslik duzeyi atlanmis", "agac", 70], ["D4", "kimlik yineleniyor", "agac", 90],
  ["D5", "dil bildirimi yok", "agac", 90], ["D6", "eylem odak almiyor", "agac", 90],
  ["D7", "karsitlik esigin altinda", "hesap", 60], ["D8", "dokunma hedefi kucuk", "hesap", 50],
  ["D9", "bildirim yalniz renkle", "niyet", 0], ["D10", "eylem adi anlamsiz", "niyet", 0],
  ["D11", "odak sirasi gorsel siraya uymuyor", "niyet", 0],
  ["D12", "pencereden klavyeyle cikilamiyor", "dizi", 0],
  ["D13", "degisim duyurusunun zamanlamasi", "dizi", 0],
  ["D14", "buyutulmus metinde icerik kaybi", "dizi", 0],
];

const kanal = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const isik = (h) => {
  const [r, g, b] = [1, 3, 5].map((i) => kanal(parseInt(h.slice(i, i + 2), 16) / 255));
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
export const karsitlik = (on, arka) => {
  const [a, b] = [isik(on), isik(arka)].sort((x, y) => y - x);
  return (a + 0.05) / (b + 0.05);
};

export function denetle(belge) {
  const bulgu = [], kimlikler = [];
  let onceki = 0;
  for (const d of gez(belge)) {
    if (d.tur === "sayfa" && !d.dil) bulgu.push({ olcut: "D5", yer: "sayfa" });
    if (d.kimlik) kimlikler.push(d.kimlik);
    if (d.tur === "baslik") {
      if (onceki && d.duzey > onceki + 1) bulgu.push({ olcut: "D3", yer: `baslik ${d.duzey}` });
      onceki = d.duzey;
    }
    if (d.tur === "eylem") {
      if (!d.metin) bulgu.push({ olcut: "D1", yer: d.kimlik });
      if (!d.odaklanabilir) bulgu.push({ olcut: "D6", yer: d.kimlik });
      if (d.boyut < 24) bulgu.push({ olcut: "D8", yer: d.kimlik });
    }
    if (d.tur === "alan" && !d.etiket) bulgu.push({ olcut: "D2", yer: d.kimlik });
    if (d.renk && d.zemin && karsitlik(d.renk, d.zemin) < 4.5)
      bulgu.push({ olcut: "D7", yer: d.kimlik ?? d.metin, oran: karsitlik(d.renk, d.zemin).toFixed(2) });
  }
  const yineleyen = kimlikler.filter((k, i) => kimlikler.indexOf(k) !== i);
  for (const k of new Set(yineleyen)) bulgu.push({ olcut: "D4", yer: k });
  return bulgu;
}
```

**IO13 (varsayım):** karar kaynağı sınıflandırması ve elle doğrulanmış kusur listesi
doğrudur. Kapsama payı bu iki listeye görelidir; ölçüt kümesi genişletildiğinde pay değişir.

#### Kapsama Payı ve Bulgular

```js
// olcum.mjs — kapsama payi, bulgular ve esik taramasi
import { BELGE, GERCEK } from "./belge.mjs";
import { OLCUT, denetle } from "./denetci.mjs";

const OTOMATIK = ["agac", "hesap"];
const oto = OLCUT.filter(([, , k]) => OTOMATIK.includes(k));
console.log(`${OLCUT.length} olcut; karar kaynagina gore dagilim:`);
for (const k of ["agac", "hesap", "niyet", "dizi"]) {
  const n = OLCUT.filter(([, , x]) => x === k).length;
  console.log(`  ${k.padEnd(7)}${String(n).padStart(3)}  %${((100 * n) / OLCUT.length).toFixed(0)}`);
}
console.log(`kapsama payi: ${oto.length}/${OLCUT.length} = %${((100 * oto.length) / OLCUT.length).toFixed(0)}`);

const puan = Object.fromEntries(OLCUT.map(([k, , , p]) => [k, p]));
const ad = Object.fromEntries(OLCUT.map(([k, a]) => [k, a]));
const gercekKume = new Set(GERCEK.map(([k, y]) => `${k}|${y}`));
const anahtar = (b) => `${b.olcut}|${b.yer}`;
const bulgu = denetle(BELGE);
const dogru = bulgu.filter((b) => gercekKume.has(anahtar(b))).length;
console.log(`\n${bulgu.length} bulgu, ${GERCEK.length} elle dogrulanmis kusur, otomatik bulunan ${dogru}` +
  ` = %${((100 * dogru) / GERCEK.length).toFixed(0)}`);
for (const b of bulgu) {
  console.log(`  ${b.olcut}  ${ad[b.olcut].padEnd(26)}${String(b.yer).padEnd(11)}` +
    `${String(puan[b.olcut]).padStart(3)}  gercek: ${gercekKume.has(anahtar(b)) ? "evet" : "hayir"}` +
    `${b.oran ? `  (oran ${b.oran})` : ""}`);
}

console.log(`\n${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=5".padStart(12)}`);
for (const e of [50, 60, 70, 90]) {
  const k = bulgu.filter((b) => puan[b.olcut] >= e);
  const ya = k.filter((b) => !gercekKume.has(anahtar(b))).length;
  const kac = GERCEK.length - k.filter((b) => gercekKume.has(anahtar(b))).length;
  console.log(`${String(e).padStart(4)}${String(k.length).padStart(7)}${String(ya).padStart(14)}${String(kac).padStart(9)}${String(ya + 5 * kac).padStart(12)}`);
}
const otomatikKod = oto.map(([k]) => k);
const elle = GERCEK.filter(([k]) => !otomatikKod.includes(k));
console.log(`\nhicbir esikte gorunmeyen ${elle.length} kusur (elle dogrulama alani):`);
for (const [k, , g] of elle) console.log(`  ${k}  ${g}`);
```

```
14 olcut; karar kaynagina gore dagilim:
  agac     6  %43
  hesap    2  %14
  niyet    3  %21
  dizi     3  %21
kapsama payi: 8/14 = %57

6 bulgu, 8 elle dogrulanmis kusur, otomatik bulunan 5 = %63
  D5  dil bildirimi yok         sayfa       90  gercek: evet
  D3  baslik duzeyi atlanmis    baslik 3    70  gercek: evet
  D1  eylem adi bos             al-1        90  gercek: evet
  D2  alan etiketsiz            uye-no      90  gercek: evet
  D7  karsitlik esigin altinda  Ceza: 30    60  gercek: evet  (oran 3.45)
  D7  karsitlik esigin altinda  gecmis      60  gercek: hayir  (oran 2.78)

esik  kalan  yanlis alarm  kacirma maliyet w=5
  50      6             1        3          16
  60      6             1        3          16
  70      4             0        4          20
  90      3             0        5          25

hicbir esikte gorunmeyen 3 kusur (elle dogrulama alani):
  D9  gecikme yalniz renk degisimiyle bildiriliyor
  D10  iki ayri islemi yapan eylem ayni adi tasiyor
  D11  odak sirasi gorsel sirayla uyusmuyor
```

İki oran ayrı şeyleri söylüyor. **Kapsama payı %57**, ölçüt kümesinin bir programa
yaptırılabilen bölümüdür ve belgeden bağımsızdır. **Bulunan kusur oranı %63**, bu belgedeki
sekiz kusurun beşinin otomatik denetimle yakalandığını söyler ve belgeye bağlıdır: kusurlar
başka bir dağılıma düşseydi bu oran değişirdi. İkinci sayı birinciden türetilemez.

Tek yanlış alarm karşıtlık hesabından geldi. Edilgen bir eylemin metin karşıtlığı eşiğin
altında, ama edilgen denetimler bu eşikten muaftır ve denetçi bunu bilmiyor. Hesaplanan
ölçütlerin ortak sorunu budur: sayı doğru, kural doğru, **bağlam eksik**.

#### Eşiği Yükseltmek Neden İşe Yaramıyor

Eşik taraması önceki üç dersten farklı bir sonuç veriyor. Orada eşiği yükseltmek yanlış
alarmları düşürürken bazı gerçek kusurları kaçırıyordu ve arada bir en uygun nokta vardı.
Burada eşiği yükseltmek tek bir yanlış alarmı düşürüyor, karşılığında iki gerçek kusuru
kaçırıyor; maliyet monoton olarak artıyor. En düşük maliyet, taramanın **en alt** ucundadır.

Tablonun yanlış alarm sütunu **yanlış kalma**, kaçırma sütunu **yanlış geçme** sayısıdır;
burada yanlış geçme hiçbir eşikte üçün altına inmiyor.
Nedeni tablonun son üç satırındadır: kaçırılan üç kusur hiçbir eşikte görünmüyor, çünkü
onlara karşılık gelen ölçütlerin puanı yok — programa yaptırılamıyorlar. **Kaçırma burada
eşik seçiminin sonucu değil, yöntemin yapısal sınırıdır.** Bir gecikmenin yalnız renk
değişimiyle bildirilmesi, aynı adı taşıyan iki eylemin ayrı işler yapması ve odak sırasının
görsel sırayla uyuşmaması: üçü de ağaçta hiçbir bildirim eksikliği bırakmaz.

Bu yüzden bu denetimin eşiği bir puan değildir. **Eşik ve kaynağı** şöyle yazılır: yayın,
otomatik denetimden sıfır bulgu **ve** elle doğrulama listesindeki altı ölçütün
işaretlenmiş olması koşuluna bağlanır. Eşiğin kaynağı bu ölçümdür — sekiz kusurdan üçünün
otomatik denetimde hiç görünmemesi, yalnız otomatik denetime dayanan bir kapının üç gerçek
kusurla yeşil döneceğini gösterir.

#### Denetimin Bedeli

Koşumdan bağımsız bedel üç yerdedir. Otomatik yan ucuzdur: bir ağaç dolaşımı, on dört
öge, sıfır süreç, sıfır istek; her değişiklikte koşturulabilir.

Elle doğrulama yanı ucuz değildir ve bedeli **ekran başına ölçüt sayısıyla** ölçülür: altı
ölçüt, her ekran için ayrı ayrı. Ödünç sisteminin ekran sayısı arttıkça bu sayı doğrusal
büyür ve otomatik denetimin aksine sürüm başına yeniden ödenir.

Üçüncü bedel sınıflandırmanın bakımıdır. Ölçütün karar kaynağı değişebilir: bugün niyet
gerektiren bir kontrol, belge modeline yeni bir bildirim eklendiğinde ağaçtan okunabilir
hâle gelir. Sınıflandırma güncellenmezse kapsama payı olduğundan düşük görünür ve elle
doğrulama listesi gereğinden uzun kalır.

**Kararın sahibi:** otomatik bulgu yayımı durdurur; elle doğrulama listesi bir kontrol
kaydıdır ve işaretlenmemiş bir ölçüt yayını durdurmaz, yalnız yayının hangi ölçütler
sınanmadan çıktığını kayda geçirir.

#### Özet

- Bir ölçütün otomatikleştirilebilirliğini belirleyen şey kararın kaynağıdır: ağaçtan
  okunan bildirim ve hesaplanan değer otomatiktir, niyet ve etkileşim dizisi değildir.
- On dört ölçütün sekizi otomatik denetlenebilir (kapsama payı %57); bu belgedeki sekiz
  kusurun beşi otomatik bulundu (%63). İki oran ayrı şeyleri ölçer ve biri diğerinden
  türetilemez.
- Tek yanlış alarm hesaplanan bir ölçütten geldi: edilgen bir denetimin karşıtlığı eşiğin
  altındaydı ama o denetim eşikten muaftı — sayı doğru, bağlam eksikti.
- Kaçan üç kusur hiçbir eşikte görünmedi; kaçırma burada eşik seçiminin değil yöntemin
  sonucudur ve eşiği yükseltmek maliyeti tek yönde artırır.
- Eşik bir puan değil bir koşuldur: sıfır otomatik bulgu **ve** altı elle doğrulama
  ölçütünün işaretlenmesi. Kaynağı, yalnız otomatik denetime dayanan bir kapının üç gerçek
  kusurla yeşil döneceği ölçümüdür.
- Bedel: otomatik yan bir ağaç dolaşımı kadar ucuz, elle yan ekran başına altı ölçüt kadar
  pahalı ve her sürümde yeniden ödeniyor.

#### Sonraki Adım

Bu denetim tek bir belge modeli üzerinde koştu ve tek bir sonuç verdi. Oysa aynı ödünç
ekranı her okuyucuda aynı ağacı üretmez: farklı oluşturma motorları aynı bildirimi farklı
yorumlar, farklı ekran genişlikleri farklı düzen kurar, farklı giriş biçimleri farklı
etkileşim sırası dayatır. Buraya kadarki bütün ölçümler tek bir ortamda yapıldı ve o
ortamın temsil ettiği kullanıcı payı hiç sorulmadı. Sonraki ders ortam eksenlerini bir
matrise açar, matrisin hangi alt kümesinin koşulacağını iki ayrı ölçütle seçer ve seçimin
kapsadığı payı sayar.

### Uyumluluk Testi

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/uyumluluk-testi

Oluşturma motoru, platform ve ekran eksenlerinin çarpımıyla büyüyen matrisin ölçülmesi: özellik destek profillerinin ayrık sınıflara indirgenmesi, paya göre ve profile göre seçimlerin yakaladığı kusur farkı ve seçim ölçütünün eşikten daha belirleyici olması.

Önceki denetim tek bir belge modeli üzerinde koştu ve tek bir sonuç verdi. Oysa aynı ödünç
ekranı her okuyucuda aynı ağacı üretmez: farklı oluşturma motorları aynı bildirimi farklı
destekler, farklı ekran genişlikleri farklı düzen kurar. Buraya kadarki bütün ölçümler tek
bir ortamda yapıldı ve o ortamın temsil ettiği kullanıcı payı hiç sorulmadı.

**Uyumluluk testi** bu eksenlerin çarpımını bir matrise açar ve tek bir soruyu sorar: bu
matrisin hangi alt kümesi koşulacak? Tümleşim, Sözleşme ve Uçtan Uca Test kursundaki
aygıt–sürüm matrisi aynı aritmetiği aygıt ve işletim sürümü üzerinden kurmuştu; buradaki
matrisin ekseni aygıt değil **özellik destek profilidir** ve seçim ölçütü kullanıcı payından
değil profillerin ayrıklığından türer.

#### Matris ve Destek Profilleri

Üç oluşturma motoru, iki platform ve iki ekran sınıfı on iki ortam veriyor. Altı özelliğin
her ortamdaki desteği bir vektördür; aynı vektörü taşıyan ortamlar özellik açısından
**ayırt edilemez**.

**IO14 (varsayım):** ortam payları ölçülmüştür ve matris eksiksizdir. Listede olmayan bir
ortam yoktur sayılır; bütün kapsama oranları bu on iki satıra görelidir.

```js
// matris.mjs — uyumluluk matrisi, ozellik destek profilleri ve bilinen kusurlar
export const OZELLIK = ["kalici depo", "gecis animasyonu", "izgara duzeni",
  "tarih bicimleyici", "gozlemci", "girdi onerisi"];

const MOTOR = { a: [1, 1, 1, 1, 1, 1], b: [1, 0, 0, 1, 1, 0], c: [1, 1, 1, 1, 0, 1] };

// Mobil platformda tarih bicimleyici (3. ozellik) hicbir motorda yok.
export const profil = (o) =>
  MOTOR[o.motor].map((v, i) => (i === 3 && o.platform === "mobil" ? 0 : v)).join("");

// Kullanici payi olculmus on iki ortam.
export const ORTAM = [
  { motor: "a", platform: "masaustu", ekran: "genis", pay: 42 },
  { motor: "a", platform: "mobil", ekran: "dar", pay: 26 },
  { motor: "b", platform: "masaustu", ekran: "genis", pay: 14 },
  { motor: "c", platform: "mobil", ekran: "dar", pay: 9 },
  { motor: "b", platform: "mobil", ekran: "dar", pay: 3 },
  { motor: "c", platform: "masaustu", ekran: "genis", pay: 2 },
  { motor: "a", platform: "masaustu", ekran: "dar", pay: 1.5 },
  { motor: "a", platform: "mobil", ekran: "genis", pay: 1 },
  { motor: "b", platform: "masaustu", ekran: "dar", pay: 0.7 },
  { motor: "c", platform: "masaustu", ekran: "dar", pay: 0.4 },
  { motor: "b", platform: "mobil", ekran: "genis", pay: 0.3 },
  { motor: "c", platform: "mobil", ekran: "genis", pay: 0.1 },
].map((o) => ({ ...o, ad: `${o.motor}-${o.platform}-${o.ekran}` }));

// Elle dogrulanmis kusurlar ve gorunduklari ortam kosulu.
export const KUSUR = [
  ["K1 izgara duzeni cizilmiyor", (o) => o.motor === "b"],
  ["K2 tarih bicimi bos donuyor", (o) => o.platform === "mobil"],
  ["K3 gozlemci olmadan liste yenilenmiyor", (o) => o.motor === "c"],
  ["K4 masaustu duzeni dar ekranda tasiyor", (o) => o.platform === "masaustu" && o.ekran === "dar"],
  ["K5 kuyruk istemcisi mobilde baglanti kuramiyor", (o) => o.motor === "b" && o.platform === "mobil"],
  ["K6 mobil duzen genis ekranda dagiliyor", (o) => o.platform === "mobil" && o.ekran === "genis"],
];

// Saglam davranislar; kati dogrulama bunlari da kirmiziya cevirir.
export const SAGLAM = [
  ["Y1 gecis animasyonu atlaniyor", (o) => o.motor === "b"],
  ["Y2 menu dar ekranda katlaniyor", (o) => o.ekran === "dar"],
];

export const bulunan = (secim, liste) => liste.filter(([, k]) => secim.some(k)).length;
export const kirmizi = (secim, liste) => secim.filter((o) => liste.some(([, k]) => k(o))).length;
```

#### Üç Seçim Ölçütü

```js
// secim.mjs — matrisin buyuklugu, ayirt edici profiller ve uc secim olcutu
import { ORTAM, OZELLIK, KUSUR, SAGLAM, profil, bulunan, kirmizi } from "./matris.mjs";

const profiller = [...new Set(ORTAM.map(profil))];
console.log(`${ORTAM.length} ortam, ${OZELLIK.length} ozellik, ${profiller.length} ayirt edici destek profili`);
for (const p of profiller) {
  const uyan = ORTAM.filter((o) => profil(o) === p);
  console.log(`  ${p}  ${String(uyan.reduce((t, o) => t + o.pay, 0).toFixed(1)).padStart(5)}%  ${uyan.map((o) => o.ad).join(", ")}`);
}

// (a) Paya gore: paydan buyukten kucuge, birikimli pay esige ulasana kadar.
const payaGore = (esik) => {
  const s = [];
  for (const o of ORTAM) { if (s.reduce((t, x) => t + x.pay, 0) >= esik) break; s.push(o); }
  return s;
};
// (b) Profile gore: her destek profilinden payi en buyuk ortam.
const profileGore = profiller.map((p) => ORTAM.filter((o) => profil(o) === p)[0]);
// (c) Profil ve duzen: (b) uzerine eksik kalan platform-ekran ciftleri eklenir.
const cift = (o) => `${o.platform}-${o.ekran}`;
const profilVeDuzen = [...profileGore];
for (const o of ORTAM) if (!profilVeDuzen.some((x) => cift(x) === cift(o))) profilVeDuzen.push(o);

console.log(`\n${"secim".padEnd(18)}${"kosum".padStart(6)}${"pay".padStart(7)}${"profil".padStart(8)}${"kusur".padStart(7)}${"kirmizi kosum".padStart(15)}`);
for (const [ad, s] of [["paya gore %90", payaGore(90)], ["profile gore", profileGore],
  ["profil ve duzen", profilVeDuzen], ["tam matris", ORTAM]]) {
  console.log(`${ad.padEnd(18)}${String(s.length).padStart(6)}${`${s.reduce((t, o) => t + o.pay, 0).toFixed(1)}%`.padStart(7)}` +
    `${`${new Set(s.map(profil)).size}/${profiller.length}`.padStart(8)}${`${bulunan(s, KUSUR)}/${KUSUR.length}`.padStart(7)}` +
    `${String(kirmizi(s, SAGLAM)).padStart(15)}`);
}

const kacan = KUSUR.filter(([, k]) => !payaGore(90).some(k));
console.log(`\npaya gore secimin kacirdiklari:`);
for (const [ad] of kacan) console.log(`  ${ad}`);
```

```
12 ortam, 6 ozellik, 6 ayirt edici destek profili
  111111   43.5%  a-masaustu-genis, a-masaustu-dar
  111011   27.0%  a-mobil-dar, a-mobil-genis
  100110   14.7%  b-masaustu-genis, b-masaustu-dar
  111001    9.1%  c-mobil-dar, c-mobil-genis
  100010    3.3%  b-mobil-dar, b-mobil-genis
  111101    2.4%  c-masaustu-genis, c-masaustu-dar

secim              kosum    pay  profil  kusur  kirmizi kosum
paya gore %90          4  91.0%     4/6    3/6              3
profile gore           6  96.0%     6/6    4/6              4
profil ve duzen        8  98.5%     6/6    6/6              5
tam matris            12 100.0%     6/6    6/6              8

paya gore secimin kacirdiklari:
  K4 masaustu duzeni dar ekranda tasiyor
  K5 kuyruk istemcisi mobilde baglanti kuramiyor
  K6 mobil duzen genis ekranda dagiliyor
```

On iki ortam altı profile iniyor: özellik desteği açısından matris yarı yarıya küçük.
Paya göre seçim dört koşumla kullanıcıların %91'ini kapsıyor ama altı profilin dördünü
görüyor ve altı kusurun üçünü kaçırıyor. Kaçırdıklarının ortak özelliği ise ilginçtir:
üçü de **eksenlerin birleşiminden** doğuyor. Masaüstü düzeninin dar ekranda taşması hiçbir
motorun desteğiyle ilgili değil; iki eksenin birlikte aldığı bir değerle ilgili.

Profile göre seçim bu yüzden yeterli değil: altı koşumla bütün destek profillerini kapsıyor
ama düzen kusurlarını hâlâ göremiyor, çünkü destek vektörü ekran sınıfını hiç taşımıyor.
Sekiz koşumluk üçüncü seçim, eksik kalan platform–ekran çiftlerini ekleyerek altı kusurun
altısını da buluyor — tam matrisin üçte ikisi kadar koşumla.

#### Eşik Taraması

Yaygın kural "kullanıcıların şu kadarını kapsa" biçiminde yazılır. Bu eşiğin ne
kazandırdığı taranabilir.

```js
// esik.mjs — kapsanan pay esigi taranir; yanlis gecme ve yanlis kalma sayilir
import { ORTAM, KUSUR, SAGLAM, profil, bulunan, kirmizi } from "./matris.mjs";

const payaGore = (esik) => {
  const s = [];
  for (const o of ORTAM) { if (s.reduce((t, x) => t + x.pay, 0) >= esik) break; s.push(o); }
  return s;
};
const profiller = new Set(ORTAM.map(profil)).size;

console.log(`${"pay esigi".padEnd(10)}${"kosum".padStart(6)}${"kapsanan".padStart(10)}${"profil".padStart(8)}` +
  `${"yanlis gecme".padStart(14)}${"yanlis kalma".padStart(14)}${"maliyet w=5".padStart(12)}`);
for (const e of [80, 90, 95, 97, 98, 99, 100]) {
  const s = payaGore(e);
  const gecme = KUSUR.length - bulunan(s, KUSUR), kalma = kirmizi(s, SAGLAM);
  console.log(`%${String(e).padEnd(9)}${String(s.length).padStart(6)}${`${s.reduce((t, o) => t + o.pay, 0).toFixed(1)}%`.padStart(10)}` +
    `${`${new Set(s.map(profil)).size}/${profiller}`.padStart(8)}${String(gecme).padStart(14)}${String(kalma).padStart(14)}` +
    `${String(kalma + 5 * gecme).padStart(12)}`);
}

// Profil ve duzen olcutuyle secilen kume ayni tabloya konur.
const cift = (o) => `${o.platform}-${o.ekran}`;
const s = [...new Set(ORTAM.map(profil))].map((p) => ORTAM.filter((o) => profil(o) === p)[0]);
for (const o of ORTAM) if (!s.some((x) => cift(x) === cift(o))) s.push(o);
const gecme = KUSUR.length - bulunan(s, KUSUR), kalma = kirmizi(s, SAGLAM);
console.log(`\nprofil ve duzen olcutu: ${s.length} kosum, yanlis gecme ${gecme}, yanlis kalma ${kalma},` +
  ` maliyet ${kalma + 5 * gecme}`);
console.log(`ayni maliyeti paya gore yakalamak icin gereken kosum: ${payaGore(99).length}`);
```

```
pay esigi  kosum  kapsanan  profil  yanlis gecme  yanlis kalma maliyet w=5
%80            3     82.0%     3/6             4             2          22
%90            4     91.0%     4/6             3             3          18
%95            6     96.0%     6/6             2             4          14
%97            7     97.5%     6/6             1             5          10
%98            8     98.5%     6/6             0             5           5
%99            9     99.2%     6/6             0             6           6
%100          12    100.0%     6/6             0             8           8

profil ve duzen olcutu: 8 kosum, yanlis gecme 0, yanlis kalma 5, maliyet 5
ayni maliyeti paya gore yakalamak icin gereken kosum: 9
```

**Yanlış geçme**, seçimin dışında kalan ortamda duran gerçek kusurdur; **yanlış kalma**,
sağlam ama farklı davranan bir ortamda katı doğrulamanın kırmızıya dönmesidir. İkisi ters
yönde hareket ediyor: ortam ekledikçe kaçan kusur azalıyor, boşa kırmızı dönen koşum
artıyor. Tam matris bile en iyi seçenek değil — sekiz kırmızı koşumun ikisi zaten
kaçınılmaz gürültü.

Asıl sonuç son iki satırdadır. Paya göre seçim bütün kusurları ancak dokuz koşumla
bulabiliyor; profil ve düzen ölçütü aynı sonucu sekiz koşumla veriyor ve maliyeti daha
düşük. **Eşiğin kaynağı** burada bir ölçüm değil bir yanılgıdır: kapsanan kullanıcı payı
sezgisel bir sayıdır ama kusurların dağılımıyla ilgisi yoktur. Kusur, kullanıcı payına göre
değil **ortam sınıfına göre** dağılır ve seçim ölçütü eşikten daha belirleyicidir.

**Kararın sahibi:** matris dışında kalan her ortam bilinen bir boşluktur. Yayım kararı
"tüm koşumlar yeşil" cümlesine değil, "şu kadar profil ve şu kadar düzen çifti koşuldu"
cümlesine bağlanır; koşulmayan sınıflar kayda geçer.

#### Testin Bedeli

Koşumdan bağımsız bedel üç sayıdadır. Birincisi koşum sayısı: sekiz ortam, her biri kendi
kurulumunu, kendi verisini ve kendi zamanını ister; bu sayı eksen eklendikçe çarpımsal
büyür — bir eksene bir değer eklemek matrisi bir katına değil, bir çarpanına taşır.

İkincisi matrisin bakımı: destek vektörleri elle yazılmıştır ve bir motor bir özelliği
desteklemeye başladığında profil sayısı değişir. Güncellenmeyen bir destek tablosu, ayırt
edici saydığı iki ortamı boşuna koşturur ya da ayrı saymadığı iki ortamdan birini hiç
koşturmaz.

Üçüncüsü kırmızı koşumların elenmesi: sekiz koşumun beşi kırmızı döndü ve bunların ikisi
gerçek kusur değildi. Uyumluluk testinin kalıcı yükü budur — her ortam farkı kusur değildir
ve farkı kusurdan ayıran şey kural değil karardır.

#### Özet

- Uyumluluk matrisi eksenlerin çarpımıdır; on iki ortam altı özellik üzerinden altı ayırt
  edici destek profiline indi ve matris özellik açısından yarıya küçüldü.
- Paya göre seçim dört koşumla kullanıcıların %91'ini kapsadı ama altı kusurun üçünü
  kaçırdı; kaçanların üçü de tek bir eksenden değil eksenlerin birleşiminden doğuyordu.
- Destek profili ekran sınıfını taşımadığı için profile göre seçim de yetmedi; profil ve
  düzen ölçütü sekiz koşumla altı kusurun altısını buldu — tam matrisin üçte ikisi kadar.
- Eşik taraması iki sütunu ters yönde hareket ettirdi: ortam eklendikçe yanlış geçme
  azaldı, yanlış kalma arttı. Tam matris bile en düşük maliyetli seçenek değildi.
- Kusurlar kullanıcı payına göre değil ortam sınıfına göre dağılır; seçim ölçütü kapsanan
  pay eşiğinden daha belirleyicidir.
- Bedel çarpımsaldır: her yeni eksen değeri matrisi bir çarpanla büyütür, destek tablosu
  elle bakım ister ve kırmızı koşumların bir bölümü her koşumda yeniden elenir.

#### Sonraki Adım

Bu konunun altı dersi aynı sistemi altı ayrı yöntemle sınadı ve altısı da ortak bir
varsayımı paylaştı: sistemin parçaları çalışıyor. Statik tarama kodun metnine baktı, dinamik
test koşan servise istek gönderdi, bağımlılık taraması kurulu sürümleri kayıtlarla
eşleştirdi, sızma testi kuralların birleşimini sorguladı, erişilebilirlik denetimi çizilen
ağacı okudu, uyumluluk testi aynı ekranı farklı ortamlarda koşturdu. Yük de saldırı da
**dışarıdan** gelen bir baskıydı ve her ikisinde de sistemin kendisi ayaktaydı. Oysa ödünç
servisinin bağlı olduğu veritabanı yanıt vermeyi bıraktığında, kuyruk istemcisi mesajı
kaybettiğinde ya da katalog servisi yarım yanıt döndürdüğünde ne olacağı hiçbir derste
sınanmadı. Sonraki konu baskıyı dışarıdan içeriye taşır: arıza sistemin kendi parçalarından
birinde başlatılır ve sistemin bu arızayı nasıl karşıladığı ölçülür.

## Dayanıklılık Testi

Arızanın beklenmeden önce üretilmesi: hata enjeksiyonuyla arıza senaryolarının bilinçli tetiklenmesi, üretim benzeri ortamda kontrollü bozmanın kural ve durdurma ölçütleriyle yürütülmesi, yedek ile devralma tatbikatının kurtarma hedeflerini doğrulaması ve yükün kırılma noktasına kadar artırılarak kapasite sınırının bulunması.

### Hata Enjeksiyonu

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/hata-enjeksiyonu

Arıza senaryolarının bilinçli tetiklenmesi: katalog servisine gecikme, hata ve kayıp enjekte edilerek ödünç akışının kalıbının gerçekten devreye girip girmediğinin ölçülmesi, düşme oranı eşiğinin taranarak yanlış geçme ile yanlış kalmanın sayılması, enjeksiyon katmanının kod yolu ve varsayılan kapalılık denetimi cinsinden bedeli ve enjeksiyonun göremediği arıza sınıfı.

Önceki konu güvenlik testini kapattı ve bir sınır bıraktı. Yük de saldırı da **dışarıdan**
gelen bir baskıydı: birinde çok sayıda geçerli istek, ötekinde bilerek bozulmuş bir istek.
İkisinde de sistemin parçaları çalışıyordu. Sistemin kendi bileşenlerinden biri bozulduğunda
ne olacağı hiçbir derste sınanmadı.

**Hata enjeksiyonu**, bir arıza senaryosunu çalışan sistemde bilerek tetiklemek ve verilen
yanıtı ölçmektir. Ödünç servisi katalog servisine bağlıdır ve o bağlantıya Dayanıklılık ve
Güvenilirlik kursunda kurulan iki kalıp konmuştur: zaman aşımı ve yedek yolla zarif bozulma.
Kalıpların ne yaptığı orada anlatıldı; buradaki soru başkadır — **kalıp gerçekten devreye
giriyor mu ve bunu hangi eşikle söyleyebiliriz.**

#### Enjeksiyon Düzeneği

Düzenek bir **modeldir** ve kendi yazılmıştır; gerçek bir kaos aracı ya da küme kurulmaz.
Katalog servisi yerel bir `node:http` sürecidir ve önüne bir enjeksiyon katmanı konmuştur.
Katman üç arıza türü üretir: **gecikme** (yanıt geciktirilir), **hata** (503 döner), **kayıp**
(yanıt hiç yazılmaz). Hangi isteğin bozulacağı istek sırasından tohumlu bir karma ile türetilir;
aynı tohum aynı istek kümesini bozar.

**IO14 — sağlıklı katalog isteklerinin 0,03'ü 150 ms duraklar.** Varsayımdır: gerçek bir servis
arızasız günde de tam düz değildir ve bu duraklama, eşiğin tabanını belirleyecektir.

```js
// katalog.mjs — katalog servisi ve enjeksiyon katmani. Enjeksiyon plani disaridan verilir,
// varsayilani kapalidir; hangi istegin bozulacagi istek sirasindan deterministik turetilir.
import { createServer } from 'node:http';

export const KAPALI = { tur: 'kapali', oran: 0, sure: 0 };
export const TOHUM = { enjeksiyon: 20260731, gurultu: 19870415 };
export const GURULTU = { oran: 0.03, sure: 150 };      // IO14: arka plan duraklamasi

export function karma(tohum, i) {                      // tohumlu, sira basina deterministik deger
  let x = (tohum ^ Math.imul(i + 1, 0x9e3779b1)) >>> 0;
  x = Math.imul(x ^ (x >>> 16), 0x85ebca6b) >>> 0;
  x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35) >>> 0;
  return ((x ^ (x >>> 16)) >>> 0) / 4294967296;
}

const KUNYE = { 'K-903': { baslik: 'kitap K-903', yazar: 'Sabahattin Ali' } };

export function katalogKur(plan = KAPALI) {
  const olcu = { istek: 0, enjekte: 0, duraklama: 0 };
  const sunucu = createServer((istek, yanit) => {
    const adres = new URL(istek.url, 'http://yerel');
    const sira = Number(adres.searchParams.get('sira') ?? 0);
    const no = adres.searchParams.get('no') ?? 'K-903';
    olcu.istek += 1;
    const enjekte = plan.tur !== 'kapali' && karma(TOHUM.enjeksiyon, sira) < plan.oran;
    if (enjekte) olcu.enjekte += 1;
    const gecikmeli = enjekte && plan.tur === 'gecikme';
    const duraklama = gecikmeli === false && karma(TOHUM.gurultu, sira) < GURULTU.oran;
    if (duraklama) olcu.duraklama += 1;
    if (enjekte && plan.tur === 'kayip') return;             // yanit hic yazilmaz
    const gonder = () => {
      if (enjekte && plan.tur === 'hata') {
        yanit.writeHead(503, { 'content-type': 'application/json' });
        return yanit.end('{"hata":"katalog yanit veremiyor"}');
      }
      yanit.writeHead(200, { 'content-type': 'application/json' });
      yanit.end(JSON.stringify({ no, ...KUNYE[no] }));
    };
    const bekle = gecikmeli ? plan.sure : (duraklama ? GURULTU.sure : 0);
    if (bekle === 0) gonder(); else setTimeout(gonder, bekle);
  });
  sunucu.listen(0, '127.0.0.1');
  return { sunucu, olcu };
}
```

Ödünç akışı üç sonuçtan birini üretir: **tam** yanıt, **zarif bozulmuş** yanıt (künye alanları
boş) ya da **düşen** istek; yedek yol kapatıldığında ikinci sonuç kaybolur.

```js
// odunc.mjs — odunc akisi katalogdan kunye ister. Zaman asimi ve yedek yol Dayaniklilik ve
// Guvenilirlik kursunda kurulan kaliplardir; burada sinanan devreye girip girmedikleridir.
export function oduncAkisi({ taban, zamanAsimi, yedekYol }) {
  const s = { tam: 0, bozulmus: 0, dusen: 0, zamanAsimiSayisi: 0, hataSayisi: 0 };
  return {
    olcu: () => ({ ...s }),
    async oduncVer(sira, no = 'K-903') {
      try {
        const y = await fetch(`${taban}/kunye?no=${no}&sira=${sira}`,
          { signal: AbortSignal.timeout(zamanAsimi) });
        if (y.ok === false) { await y.text(); throw new Error(`durum ${y.status}`); }
        const g = await y.json();
        s.tam += 1;
        return { no, baslik: g.baslik, durum: 'tam' };
      } catch (h) {
        if (h.name === 'TimeoutError' || h.name === 'AbortError') s.zamanAsimiSayisi += 1;
        else s.hataSayisi += 1;
        if (yedekYol === false) { s.dusen += 1; return null; }
        s.bozulmus += 1;                       // zarif bozulma: kunye alanlari olmadan yanit
        return { no, baslik: null, durum: 'bozulmus' };
      }
    },
  };
}
```

#### Arıza Türü Başına Verilen Yanıt

Enjeksiyon üç yapılandırmada koşturulur. **Sağlam** yapılandırma sınanan sistemdir; **gevşek**
ile **yedeksiz** bilerek bozulmuş iki sürümdür ve testin onları ayırt edip edemediğini ölçer.

**IO13 — koşum 200 istek, 8 eş zamanlı istemci.** **IO15 — enjeksiyon oranı 0,20, gecikme 240 ms.**
**IO16 — zaman aşımı sağlamda 100 ms, gevşekte 400 ms.** Üçü de varsayımdır; IO16'nın gerekçesi
şudur: 100 ms hem 150 ms'lik duraklamayı hem 240 ms'lik gecikmeyi keser, 400 ms ikisini de yutar.

```js
// kosum.mjs — uc yapilandirma dort ariza turunde kosturulur ve olcum olcum.json'a yazilir.
// Sayimlar deterministiktir; yalniz duvar suresi kosuma baglidir.
import { writeFileSync } from 'node:fs';
import { katalogKur, KAPALI, GURULTU } from './katalog.mjs';
import { oduncAkisi } from './odunc.mjs';

const N = 200, ESZAMAN = 8;                     // IO13
const PLAN = {
  kapali: KAPALI,
  gecikme: { tur: 'gecikme', oran: 0.20, sure: 240 },   // IO15
  hata: { tur: 'hata', oran: 0.20, sure: 0 },
  kayip: { tur: 'kayip', oran: 0.20, sure: 0 },
};
const YAPI = {
  saglam: { zamanAsimi: 100, yedekYol: true },           // IO16
  gevsek: { zamanAsimi: 400, yedekYol: true },
  yedeksiz: { zamanAsimi: 100, yedekYol: false },
};

async function kosum(yapiAdi, planAdi) {
  const { sunucu, olcu } = katalogKur(PLAN[planAdi]);
  await new Promise((c) => sunucu.once('listening', c));
  const taban = `http://127.0.0.1:${sunucu.address().port}`;
  const akis = oduncAkisi({ taban, ...YAPI[yapiAdi] });
  let sira = 0;
  const isci = async () => { while (sira < N) { const i = sira; sira += 1; await akis.oduncVer(i); } };
  await Promise.all(Array.from({ length: ESZAMAN }, isci));
  sunucu.closeAllConnections();
  await new Promise((c) => sunucu.close(c));
  return { ...akis.olcu(), ...olcu };
}

const sonuc = {};
const p = (x, n) => String(x).padStart(n);
console.log(`${N} istek, ${ESZAMAN} es zamanli; enjeksiyon orani ${PLAN.gecikme.oran}, gecikme ` +
  `${PLAN.gecikme.sure} ms, arka plan duraklamasi ${GURULTU.oran} / ${GURULTU.sure} ms`);
console.log(`\n${'yapilandirma'.padEnd(13)}${'ariza'.padEnd(9)}${'enjekte'.padStart(8)}` +
  `${'tam'.padStart(6)}${'bozulmus'.padStart(10)}${'dusen'.padStart(7)}` +
  `${'zaman asimi'.padStart(13)}${'dusme orani'.padStart(13)}`);
for (const yapi of Object.keys(YAPI)) {
  for (const plan of Object.keys(PLAN)) {
    const r = await kosum(yapi, plan);
    sonuc[`${yapi}/${plan}`] = r;
    console.log(`${yapi.padEnd(13)}${plan.padEnd(9)}${p(r.enjekte, 8)}${p(r.tam, 6)}` +
      `${p(r.bozulmus, 10)}${p(r.dusen, 7)}${p(r.zamanAsimiSayisi, 13)}` +
      `${p('%' + ((100 * r.dusen) / N).toFixed(2), 13)}`);
  }
}
writeFileSync('olcum.json', `${JSON.stringify({ N, sonuc }, null, 1)}\n`);
console.log(`\narizasiz kosumda enjeksiyon denetimi ${sonuc['saglam/kapali'].istek} istekte calisti, ` +
  `${sonuc['saglam/kapali'].enjekte} tanesinde enjeksiyon yapti`);
```

```
200 istek, 8 es zamanli; enjeksiyon orani 0.2, gecikme 240 ms, arka plan duraklamasi 0.03 / 150 ms

yapilandirma ariza     enjekte   tam  bozulmus  dusen  zaman asimi  dusme orani
saglam       kapali          0   190        10      0           10        %0.00
saglam       gecikme        45   148        52      0           52        %0.00
saglam       hata           45   148        52      0           10        %0.00
saglam       kayip          45   148        52      0           52        %0.00
gevsek       kapali          0   200         0      0            0        %0.00
gevsek       gecikme        45   200         0      0            0        %0.00
gevsek       hata           45   155        45      0            0        %0.00
gevsek       kayip          45   155        45      0           45        %0.00
yedeksiz     kapali          0   190         0     10           10        %5.00
yedeksiz     gecikme        45   148         0     52           52       %26.00
yedeksiz     hata           45   148         0     52           10       %26.00
yedeksiz     kayip          45   148         0     52           52       %26.00

arizasiz kosumda enjeksiyon denetimi 200 istekte calisti, 0 tanesinde enjeksiyon yapti
```

Sağlam yapılandırmanın üç satırı aynı sonucu veriyor: 45 enjekte edilen istek, artı duraklamış 10
istek, zarif bozulmuş yanıta dönüyor — toplam 52 — ve düşen istek sıfır. Ama kalıbı devreye sokan
yol aynı değil: gecikme ve kayıpta 52 zaman aşımı sayılıyor, hatada yalnız 10. Hata türü zaman
aşımını hiç çalıştırmaz; kalıbın öteki yarısı olan durum kodu denetimi devreye girer. **Aynı
düşme oranı farklı kod yollarından gelebilir**, bu yüzden ölçüm hangi yolun çalıştığını da sayar.

Gevşek yapılandırmanın ikinci satırı dersin en önemli sayısıdır: enjekte edilen 45 isteğin hepsi
**tam** yanıta dönüyor, bozulmuş yanıt sıfır, zaman aşımı sıfır. Kalıp hiç devreye girmedi ve
sistem bunu kusursuz bir sonuç gibi gösterdi — 200 tam yanıt, %0,00 düşme oranı. Bozuk
yapılandırma, sağlam yapılandırmadan **daha temiz** görünüyor.

#### Eşik ve Eşiğin Kaynağı

Bir arıza altında "başarılı" demek bir eşik seçmektir; en doğal eşik düşme oranıdır. Aşağıdaki
tarama iki kuralı beş eşikte karşılaştırıyor: her eşikte kaç kusurlu yapılandırma geçiyor
(**yanlış geçme**), kaç sağlam yapılandırma kalıyor (**yanlış kalma**).

```js
// esik.mjs — kosum.mjs'in yazdigi olcum.json uzerinde esik taramasi: her esikte kac kusurlu
// yapilandirma geciyor, kac saglam yapilandirma kaliyor.
import { readFileSync } from 'node:fs';

const { N, sonuc } = JSON.parse(readFileSync('olcum.json', 'utf8'));
const GERCEK = { saglam: 'saglam', gevsek: 'kusurlu', yedeksiz: 'kusurlu' };
const ARIZA = ['gecikme', 'hata', 'kayip'];

function kuralA(yapi, e) {                       // yalniz dusme orani esigi
  const k = sonuc[`${yapi}/kapali`];
  if ((k.bozulmus + k.dusen) / N > e) return false;
  return ARIZA.every((a) => sonuc[`${yapi}/${a}`].dusen / N <= e);
}
function kuralB(yapi, e) {                       // dusme orani + kalibin devreye girme sayisi
  if (kuralA(yapi, e) === false) return false;
  return ARIZA.every((a) => sonuc[`${yapi}/${a}`].bozulmus >= sonuc[`${yapi}/${a}`].enjekte);
}

const say = (kural, e) => {
  let gecme = 0, kalma = 0;
  for (const [yapi, g] of Object.entries(GERCEK)) {
    const gecti = kural(yapi, e);
    if (gecti && g === 'kusurlu') gecme += 1;
    if (gecti === false && g === 'saglam') kalma += 1;
  }
  return { gecme, kalma };
};

const p = (x, n) => String(x).padStart(n);
console.log(`${'esik e'.padEnd(9)}${'kural'.padEnd(9)}${'gecen yapilandirma'.padStart(24)}` +
  `${'yanlis gecme'.padStart(14)}${'yanlis kalma'.padStart(14)}`);
for (const e of [0, 0.02, 0.05, 0.10, 0.30])
  for (const [ad, kural] of [['A', kuralA], ['B', kuralB]]) {
    const r = say(kural, e);
    const gecen = Object.keys(GERCEK).filter((y) => kural(y, e));
    console.log(`${('%' + (100 * e).toFixed(0)).padEnd(9)}${ad.padEnd(9)}` +
      `${(gecen.join(",") || "-").padStart(24)}${p(r.gecme, 14)}${p(r.kalma, 14)}`);
  }
console.log(`\nA: arizasiz kosumda yedek yola giden pay <= e, her arizada dusme orani <= e`);
console.log(`B: A ve her arizada zarif bozulmus yanit >= enjekte edilen istek ` +
  `(${sonuc['saglam/gecikme'].enjekte}; kaynak: duzenegin sayaci)`);
```

```
esik e   kural          gecen yapilandirma  yanlis gecme  yanlis kalma
%0       A                          gevsek             1             1
%0       B                               -             0             1
%2       A                          gevsek             1             1
%2       B                               -             0             1
%5       A                   saglam,gevsek             1             0
%5       B                          saglam             0             0
%10      A                   saglam,gevsek             1             0
%10      B                          saglam             0             0
%30      A          saglam,gevsek,yedeksiz             2             0
%30      B                          saglam             0             0

A: arizasiz kosumda yedek yola giden pay <= e, her arizada dusme orani <= e
B: A ve her arizada zarif bozulmus yanit >= enjekte edilen istek (45; kaynak: duzenegin sayaci)
```

Kural A hiçbir eşikte iki hatayı birden sıfıra indiremiyor. Eşik %2'nin altındayken sağlam
yapılandırma kalıyor — arka plan duraklaması arızasız koşumda bile 10 isteği yedek yola
gönderiyor; bu bir **yanlış kalmadır** ve kaynağı sistemin kusuru değil, ortamın gürültüsüdür.
Eşik %30'da yedeksiz yapılandırma da geçiyor: iki **yanlış geçme**. Arada kalan %5–%10
aralığında yanlış kalma sıfırdır ama gevşek yapılandırma her eşikte geçer; kusurun etkisi düşme
değil, gecikmeye dönüşmedir.

Kural B ikinci bir eşik ekliyor ve o eşiğin kaynağı enjeksiyonun kendisidir: düzenek 45 isteği
bozduğunu **sayıyor**, dolayısıyla kalıptan tam 45 zarif bozulmuş yanıt istenebilir —
**enjeksiyon, eşiğin kaynağını da üretir**. Seçilen eşik budur: e = %5 (kaynağı ölçümdür,
arızasız koşumda yedek yola giden 10/200 payı) ve enjekte başına bir zarif bozulmuş yanıt
(kaynağı düzeneğin sayacıdır). Bu ikilide yanlış geçme ve yanlış kalma sıfırdır.

Kural B'nin sınırı da ölçüldü: gevşek yapılandırmayı yalnız **gecikme** arızası ele veriyor;
hata ve kayıp satırlarında bozulmuş yanıt 45'tir ve kural geçer. Kusuru görünür kılan şey eşik
değil, enjekte edilen arıza türüdür. Sonuç bir yayım kararıdır: kural B kırıldığında dayanıklılık
yapılandırması değişmiş demektir ve yayım durur.

#### Enjeksiyonun Kendi Bedeli

Enjeksiyon katmanı üretim kodunun içinde durur ve arızasız günde de her istekte çalışır.

```js
// bedel.mjs — enjeksiyon duzeneginin kendi bedeli: kod yolu, varsayilan denetimi ve plani
// acabilen yol sayisi. olcum.json'u kosum.mjs uretir.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { katalogKur } from './katalog.mjs';

const kod = readFileSync('katalog.mjs', 'utf8').split('\n')
  .filter((s) => s.trim() !== '' && s.trim().startsWith('//') === false);
const enj = /plan|enjekte|gecikmeli|GURULTU|duraklama|karma|TOHUM/;
const dal = kod.filter((s) => /if |\? /.test(s) && enj.test(s));

const { sunucu, olcu } = katalogKur();               // plan verilmedi: varsayilan kapali
await new Promise((c) => sunucu.once('listening', c));
const taban = `http://127.0.0.1:${sunucu.address().port}`;
for (let i = 0; i < 50; i += 1) await (await fetch(`${taban}/kunye?no=K-903&sira=${i}`)).json();
sunucu.closeAllConnections();
await new Promise((c) => sunucu.close(c));
assert.equal(olcu.enjekte, 0, 'varsayilan plan enjeksiyon yapmamali');

const arizasiz = JSON.parse(readFileSync('olcum.json', 'utf8')).sonuc['saglam/kapali'];
const es = kod.filter((s) => enj.test(s)).length;
console.log(`katalog.mjs: ${kod.length} satir kod, ${es} tanesi enjeksiyon katmani ` +
  `(%${((100 * es) / kod.length).toFixed(1)}); istek yolunda ${dal.length} karar noktasi`);
console.log(`arizasiz kosumda enjeksiyon denetimi ${arizasiz.istek}/${arizasiz.istek} istekte ` +
  `calisti, ${arizasiz.enjekte} enjeksiyon yapti`);
console.log(`plani acabilen yol: 1 (katalogKur'un plan parametresi); varsayilan denetimi ` +
  `50 istekte ${olcu.enjekte} enjeksiyon - gecti`);
```

```
katalog.mjs: 38 satir kod, 13 tanesi enjeksiyon katmani (%34.2); istek yolunda 5 karar noktasi
arizasiz kosumda enjeksiyon denetimi 200/200 istekte calisti, 0 enjeksiyon yapti
plani acabilen yol: 1 (katalogKur'un plan parametresi); varsayilan denetimi 50 istekte 0 enjeksiyon - gecti
```

Bedel koşumdan bağımsız niceliklerle yazılıdır: 38 satırlık katalog kodunun 13'ü (%34,2)
enjeksiyon katmanıdır, istek yolunda 5 karar noktası ekler ve bu denetimler arızasız koşumda
200 isteğin **hepsinde** çalışır. Asıl bedel bu değildir: düzeneğin üretimde yanlışlıkla açık
kalması, ödünç akışının kendi kendini bozması demektir. Bunun karşılığı **planı açabilen yol
sayısıdır**; burada 1'dir ve varsayılanın kapalı olduğu bir iddiayla sınanır. Plan bir çevre
değişkeninden, bir başlıktan ve bir yapılandırma dosyasından da okunsaydı sayı 4 olur, dördü de
ayrı denetlenmek zorunda kalırdı.

#### Enjeksiyonun Göremediği Arıza Sınıfı

Enjeksiyon arızanın **belirtisini** üretir, nedenini değil. Bu ayrım üç sınıfı düzeneğin dışında
bırakır. Birincisi **çok bileşenli arızadır**: katman tek bir sınıra konur, oysa katalog ile
bildirim kanalı aynı anda bozulabilir. İkincisi **belirtinin altındaki katmandır**: disk dolması
aynı gecikmeyi verir ama yanında günlük kaydının kesilmesi gibi ikincil etkiler getirir. Üçüncüsü
**katmanın konmadığı çağrı yerleridir**: bir bağımlılığa katman eklenmemişse o yol hiç sınanmaz
ve tablo yine yeşil çıkar. Düzeneğin kapsamı, kapsadığı sınır sayısı kadardır.

#### Özet

- Hata enjeksiyonu bir arıza senaryosunu bilerek tetikleyip verilen yanıtı ölçer; üç tür enjekte
  edildi (gecikme, hata, kayıp), düzenek kendi yazıldı ve tohum görünürdür.
- Sağlam yapılandırmada üç arıza türü de aynı sonucu verdi (52 zarif bozulmuş yanıt, 0 düşen)
  ama aynı kod yolundan geçmedi: gecikme ve kayıpta 52 zaman aşımı, hatada 10.
- Zaman aşımı enjekte edilen gecikmeden uzun olan bozuk yapılandırma 200 tam yanıt ve %0,00 düşme
  oranı üretti; düşme oranı eşiği bu kusuru hiçbir değerde yakalamaz.
- Eşik taramasında kural A %2 altında 1 yanlış kalma, %30'da 2 yanlış geçme verdi; enjekte edilen
  istek sayısını eşik yapan kural B ikisini de sıfırladı.
- Seçilen eşik e = %5 (kaynağı ölçüm: arızasız koşumda yedek yola giden 10/200) ve enjekte başına
  bir zarif bozulmuş yanıt (kaynağı düzeneğin sayacı); kural kırıldığında yayım durur.
- Bedel 38 satırın 13'ü, istek yolunda 5 karar noktası ve 200/200 denetimdir; asıl risk planın
  üretimde açık kalmasıdır ve açma yolu sayısı 1 tutulup varsayılan bir iddiayla sınanır.

#### Sonraki Adım

Bu derste enjeksiyon bir **test düzeneğiydi**: kapsam tek bir sınırdı, ortam yalnız bu ders için
ayağa kalktı ve bozulan şeyin kimseye zararı yoktu. Aynı enjeksiyonun gerçek trafiğin aktığı bir
ortamda yapılması başka bir sorudur ve o soru kalıbın çalışıp çalışmadığından önce gelir:
**bozmaya ne zaman izin verilir ve ne zaman durdurulur.** Sistemin normal davranışı yazılmadan
bir sapma ölçülemez; deneyin kaç isteği etkileyeceği önceden sınırlanmadan bulaşma denetlenemez;
işler beklenenden kötü giderse deneyi hangi sayıya bakarak durduracağı belli olmalıdır. Sonraki
ders bu üç kararı — kararlı durum önermesi, bulaşma kapsamının sınırlanması ve durdurma ölçütü —
kurar ve her birinin geç ya da yanlış kurulmasının bedelini sayar.

### Kaos Deneyleri

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/kaos-deneyleri

Üretim benzeri ortamda kontrollü bozmanın tasarımı: kararlı durum önermesinin kontrol koşumundan ölçülmesi, deney etiketiyle sınırlanan bulaşma kapsamının yığınlama yüzünden dört katına çıkması, pay marjının taranarak yanlış kalma ile geç algılamanın sayılması ve durdurma ölçütünün ardışık pencere sayısına göre biriken hasarın ölçülmesi.

Önceki ders enjeksiyonu bir test düzeneği olarak kullandı: kapsam tek bir sınırdı, ortam yalnız
o ders için ayağa kalktı ve bozulan şeyin kimseye zararı yoktu. Aynı enjeksiyon gerçek trafiğin
aktığı bir ortamda yapıldığında üç karar öne geçer: sistemin **normal davranışı** nedir, deney
kaç isteği etkileyecektir ve deney hangi sayıya bakılarak durdurulacaktır.

**Kaos deneyi**, üretim benzeri bir ortamda kontrollü bir bozma yapıp sistemin normal
davranışından sapıp sapmadığını ölçmektir. Bu ders deneyin **tasarımını** ele alır; enjekte
edilen arızanın karşısındaki kalıplar Dayanıklılık ve Güvenilirlik kursunda kuruldu ve burada
yeniden anlatılmaz. Üç tasarım kararının her biri bir sayıyla verilir ve her birinin
yanlış verilmesinin bir bedeli ölçülür.

#### Kararlı Durum Önermesi

**Kararlı durum önermesi**, sistemin arızasız davranışı hakkında ölçülebilir bir iddiadır ve
deneyden önce yazılır. "Sistem sağlıklı" bir önerme değildir; "her 40 isteklik pencerede tam
yanıt payı şu eşiğin üstündedir" bir önermedir. Eşiğin kaynağı burada bir gereksinim değil, bir
**ölçümdür**: deney açılmadan aynı yük bir kez koşturulur ve pencere ortalaması taban kabul
edilir.

Ölçüm düzeneği bir **modeldir** ve kendi yazılmıştır. Katalog servisi yerel bir `node:http`
sürecidir; enjeksiyon yalnız **deney etiketi** taşıyan isteklere uygulanır. Ödünç servisi künye
aramalarını dörtlü yığınlar hâlinde gönderir, çünkü bir ödünç listesi tek ekranda birden çok
künye ister.

**IO17 — yığın boyu 4, enjekte edilen gecikme 240 ms, ödünç servisinin zaman aşımı 100 ms.**
**IO18 — etiketsiz isteklerin 0,03'ü 150 ms duraklar.** İkisi de varsayımdır; gecikmenin zaman
aşımından büyük seçilmesi enjeksiyonun etkisini deterministik kılar.

```js
// kaos.mjs — deney kapsamina alinmis katalog servisi ve yigin gonderen odunc istemcisi.
// MODELDIR: gercek kaos araci ya da kume kurulmaz; enjeksiyon yalniz deney etiketi tasiyan
// yigina uygulanir ve hangi kullanicinin etiketlendigi tohumlu karmadan gelir.
import { createServer } from 'node:http';

export const YIGIN = 4, GECIKME = 240, ZAMAN_ASIMI = 100;    // IO17
export const GURULTU = { oran: 0.03, sure: 150 };            // IO18

export function karma(tohum, i) {
  let x = (tohum ^ Math.imul(i + 1, 0x9e3779b1)) >>> 0;
  x = Math.imul(x ^ (x >>> 16), 0x85ebca6b) >>> 0;
  x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35) >>> 0;
  return ((x ^ (x >>> 16)) >>> 0) / 4294967296;
}

export function katalogKur() {
  const olcu = { yigin: 0, etiketli: 0, duraklama: 0 };
  const sunucu = createServer((istek, yanit) => {
    const a = new URL(istek.url, 'http://yerel');
    const etiket = a.searchParams.get('deney') === '1';
    const no = Number(a.searchParams.get('yigin'));
    olcu.yigin += 1;
    if (etiket) olcu.etiketli += 1;
    const duraklama = etiket === false && karma(31337, no) < GURULTU.oran;
    if (duraklama) olcu.duraklama += 1;
    const bekle = etiket ? GECIKME : (duraklama ? GURULTU.sure : 0);
    const gonder = () => {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      yanit.end('{"kunye":"tam"}');
    };
    if (bekle === 0) gonder(); else setTimeout(gonder, bekle);
  });
  sunucu.listen(0, '127.0.0.1');
  return { sunucu, olcu };
}

export function yiginGondericisi(taban) {
  return async (no, etiketli) => {
    try {
      await fetch(`${taban}/kunye?yigin=${no}&deney=${etiketli ? 1 : 0}`,
        { signal: AbortSignal.timeout(ZAMAN_ASIMI) });
      return 'tam';
    } catch { return 'bozulmus'; }
  };
}
```

#### Bulaşma Kapsamının Sınırlanması

**Bulaşma kapsamı**, deneyin etkileyebileceği en geniş alandır ve deney başlamadan **sayıyla**
yazılır. Buradaki plan şudur: kullanıcıların 0,05'i etiketlenir ve enjeksiyon yalnız onlara
uygulanır. Plan bir niyettir; ölçülmesi gereken şey etiketin gerçekten o kadar isteği
kapsayıp kapsamadığıdır.

**IO19 — koşum 400 istek, pencere 40 istek, deney 200. istekten sonra açık, etiketli kullanıcı
payı 0,05.** Varsayımdır; deneyin koşumun ortasında açılması, önceki pencerelerin kontrol
görevi görmesini sağlar.

```js
// deney.mjs — kontrol kosumu ve deney kosumu gercek istekle yapilir; her istegin kaydi
// gunluk.json'a yazilir. Sayimlar deterministiktir, yalniz duvar suresi kosuma baglidir.
import { writeFileSync } from 'node:fs';
import { katalogKur, yiginGondericisi, karma, YIGIN } from './kaos.mjs';

const N = 400, PENCERE = 40, DENEY_BASI = 200, PAY = 0.05, ES = 4;   // IO19
const etiketliKullanici = (i) => karma(20260731, i) < PAY;

async function kosum(deneyAcik) {
  const { sunucu, olcu } = katalogKur();
  await new Promise((c) => sunucu.once('listening', c));
  const gonder = yiginGondericisi(`http://127.0.0.1:${sunucu.address().port}`);
  const kayit = [];
  let sira = 0;
  const isci = async () => {
    while (sira < N / YIGIN) {
      const y = sira; sira += 1;
      const uyeler = Array.from({ length: YIGIN }, (_, k) => y * YIGIN + k);
      const etiketli = deneyAcik && uyeler[0] >= DENEY_BASI && uyeler.some(etiketliKullanici);
      const sonuc = await gonder(y, etiketli);
      for (const u of uyeler) kayit.push({ istek: u, etiketli, sonuc });
    }
  };
  await Promise.all(Array.from({ length: ES }, isci));
  sunucu.closeAllConnections();
  await new Promise((c) => sunucu.close(c));
  return { kayit: kayit.sort((a, b) => a.istek - b.istek), olcu };
}

const kontrol = await kosum(false);
const deney = await kosum(true);
writeFileSync('gunluk.json', `${JSON.stringify({ N, PENCERE, DENEY_BASI,
  kontrol: kontrol.kayit, deney: deney.kayit }, null, 0)}\n`);

const pencereIci = deney.kayit.filter((k) => k.istek >= DENEY_BASI);
const planlanan = pencereIci.filter((k) => etiketliKullanici(k.istek)).length;
const etkilenen = pencereIci.filter((k) => k.etiketli).length;
const bozulan = pencereIci.filter((k) => k.sonuc === 'bozulmus').length;
const y = (a, b) => `${a}/${b} (%${((100 * a) / b).toFixed(2)})`;
console.log(`${N} istek, ${YIGIN}'luk yigin, deney ${DENEY_BASI}. istekten sonra acik`);
console.log(`planlanan bulasma kapsami : ${y(planlanan, pencereIci.length)} etiketli kullanici`);
console.log(`olculen bulasma kapsami   : ${y(etkilenen, pencereIci.length)} etiketli yigin uyesi`);
console.log(`kapsam carpani            : ${(etkilenen / planlanan).toFixed(2)} kat`);
console.log(`deney penceresinde bozulan: ${y(bozulan, pencereIci.length)}`);
console.log(`kontrol kosumunda bozulan : ${y(kontrol.kayit.filter((k) => k.sonuc === 'bozulmus').length, N)}`);
```

```
400 istek, 4'luk yigin, deney 200. istekten sonra acik
planlanan bulasma kapsami : 11/200 (%5.50) etiketli kullanici
olculen bulasma kapsami   : 44/200 (%22.00) etiketli yigin uyesi
kapsam carpani            : 4.00 kat
deney penceresinde bozulan: 52/200 (%26.00)
kontrol kosumunda bozulan : 16/400 (%4.00)
```

Plan 11 kullanıcı diyordu, ölçüm 44 istek buldu: **kapsam çarpanı 4,00 kat**. Kaynağı
yığınlamadır. Etiket kullanıcıya konuyor, enjeksiyon ise yığına uygulanıyor; bir yığında tek bir
etiketli üye bulunması yığının tamamını deneyin içine alıyor. Çarpanın üst sınırı yığın boyudur
ve bu koşumda üst sınıra çarpılmıştır, çünkü on bir etiketli kullanıcının hepsi ayrı yığınlara
düşmüştür. **Bulaşma kapsamı etiketin taşındığı kadar dardır**: etiketi taşımayan her toplama,
kuyruklama ve önbellekleme adımı kapsamı sessizce genişletir.

Bu, bir deney tasarımı kusurudur ve yalnız ölçülerek görülür. Kapsamı gerçekten daraltmanın yolu
etiketi yığın oluşturma kuralına da taşımaktır; bunun bedeli, etiketi taşımak zorunda kalan çağrı
yeri sayısıdır ve o sayı deney başlamadan bilinmelidir.

#### Önerme, Yanlışlanma ve Durdurma Ölçütü

Deney koşumu bittiğinde elde bir istek günlüğü vardır. Önermenin eşiği, durdurma ölçütünün
duyarlılığı ve bu ikisinin bedeli aynı günlük üzerinde taranır; deney yeniden koşturulmaz.

```js
// tarama.mjs — gunluk.json uzerinde iki tarama: kararli durum onermesinin pay marji ve
// durdurma olcutunun ardisik pencere sayisi. Deney tekrar kosturulmaz, kaydi okunur.
import { readFileSync } from 'node:fs';

const g = JSON.parse(readFileSync('gunluk.json', 'utf8'));
const pencereler = (kayit) => {
  const p = [];
  for (let b = 0; b < g.N; b += g.PENCERE) {
    const d = kayit.slice(b, b + g.PENCERE);
    p.push({ tamPay: d.filter((k) => k.sonuc === 'tam').length / d.length,
      hasar: d.filter((k) => k.etiketli && k.sonuc === 'bozulmus').length });
  }
  return p;
};
const K = pencereler(g.kontrol), D = pencereler(g.deney);
const TABAN = K.reduce((a, x) => a + x.tamPay, 0) / K.length;   // esigin kaynagi: kontrol kosumu
const yanlisla = (p, m) => p.tamPay < TABAN - m;
const p = (x, n) => String(x).padStart(n);

console.log(`kararli durum onermesi: her pencerede tam yanit payi >= taban - m; taban kontrol ` +
  `kosumunun pencere ortalamasi = %${(100 * TABAN).toFixed(2)} (${K.length} pencere, ` +
  `pencere basina ${g.PENCERE} istek)`);
console.log(`\n${'pay marji m'.padEnd(13)}${'kontrolde yanlislanan'.padStart(22)}` +
  `${'deneyde yanlislanan'.padStart(20)}${'ilk yanlislanan pencere'.padStart(24)}`);
for (const m of [0, 0.05, 0.10, 0.20]) {
  const ilk = D.findIndex((x) => yanlisla(x, m));
  console.log(`${(`%${(100 * m).toFixed(0)}`).padEnd(13)}` +
    `${p(`${K.filter((x) => yanlisla(x, m)).length}/${K.length}`, 22)}` +
    `${p(`${D.filter((x) => yanlisla(x, m)).length}/${D.length}`, 20)}` +
    `${p(ilk < 0 ? 'yok' : ilk + 1, 24)}`);
}

const M = 0.10;                            // secilen pay marji: kontrolde yanlislanma yok
const TOPLAM = D.reduce((a, x) => a + x.hasar, 0);
const sonrasi = (w) => D.slice(w).reduce((a, x) => a + x.hasar, 0);
console.log(`\nsecilen pay marji %${100 * M}; deneyin toplam hasari ${TOPLAM} istek`);
console.log(`\n${'ardisik pencere k'.padEnd(19)}${'durdurma penceresi'.padStart(20)}` +
  `${'durdurmaya kadar hasar'.padStart(24)}${'k=1 uzerine maliyet'.padStart(21)}`);
let temel = null;
for (const k of [1, 2, 3, 4]) {
  let ardisik = 0, dur = -1;
  for (let i = 0; i < D.length; i += 1) {
    ardisik = yanlisla(D[i], M) ? ardisik + 1 : 0;
    if (ardisik >= k) { dur = i + 1; break; }
  }
  const olan = dur < 0 ? TOPLAM : TOPLAM - sonrasi(dur);
  if (temel === null) temel = olan;
  console.log(`${String(k).padEnd(19)}${p(dur < 0 ? 'durmadi' : dur, 20)}` +
    `${p(olan, 24)}${p(`+${olan - temel}`, 21)}`);
}
```

```
kararli durum onermesi: her pencerede tam yanit payi >= taban - m; taban kontrol kosumunun pencere ortalamasi = %96.00 (10 pencere, pencere basina 40 istek)

pay marji m   kontrolde yanlislanan deneyde yanlislanan ilk yanlislanan pencere
%0                             4/10                7/10                       2
%5                             4/10                7/10                       2
%10                            0/10                4/10                       6
%20                            0/10                2/10                       8

secilen pay marji %10; deneyin toplam hasari 44 istek

ardisik pencere k    durdurma penceresi  durdurmaya kadar hasar  k=1 uzerine maliyet
1                                     6                       8                   +0
2                                     9                      28                  +20
3                                    10                      44                  +36
4                               durmadi                      44                  +36
```

İlk tablo pay marjını tarıyor. Marj %5'in altındayken önerme **kontrol koşumunda** on
pencerenin dördünde yanlışlanıyor — deney hiç açılmamışken. Bu bir **yanlış kalmadır** ve
sonucu ağırdır: durdurma ölçütü bu önermeye bağlanırsa deney daha başlamadan, ikinci pencerede
durdurulur. Marj %20'ye çıkarıldığında kontrol temizlenir ama deney yalnız iki pencerede
yanlışlanır ve ilk yanlışlanma 8. pencereye kayar; hasarın çoğu **görülmeden geçer**. Seçilen
eşik ikisinin arasındadır: **m = %10**, kaynağı kontrol koşumunun ölçülen dağılımıdır — kontrolün
en düşük penceresi tabanın 6 puan altındadır, marj o gürültünün üstüne konur.

İkinci tablo durdurma ölçütünü tarıyor. **Durdurma ölçütü**, deneyin kendiliğinden sonlandığı
kuraldır ve burada "önerme k ardışık pencerede yanlışlanırsa dur" biçimindedir. k = 1 deneyi
altıncı pencerede durdurur ve o ana kadar 8 istek hasar görmüştür. k = 2 dokuzuncu pencereye
kadar bekler: 28 istek, yani **20 istek fazladan**. k = 3 ve k = 4 deneyin sonuna kadar hiç
durdurmaz ve hasarın tamamını, 44 isteği, kabul eder. Geç durdurmanın maliyeti koşum süresiyle
değil, hasar gören istek sayısıyla yazılır.

Yedinci pencere ikinci tablonun neden yanıltıcı olabileceğini gösteriyor: deney açıkken bile o
pencerede önerme yanlışlanmadı, çünkü pencere ortalaması eşiğin üstünde kaldı. Deney beş pencere
boyunca sürdü ve önerme dördünde yanlışlandı; **yanlışlanma oranı 4/5'tir, 5/5 değil.** Pencere
ortalaması seyrek hasarı yutar ve k'yı büyütmek bu boşlukları birleştirmeyi zorlaştırır.

Bu testin sonucu bir yayım kararı değildir; deneyin kendisine ait bir karardır. Durdurma ölçütü
kırıldığında deney durdurulur, etiket geri alınır ve yanlışlanan pencere bir kayıt olarak açılır.

#### Deneyin Yakalamadığı Sınıf

Deney yalnız önermenin ölçtüğü şeyi yanlışlayabilir. Önerme tam yanıt payı üzerine kurulduğu için
**gecikmeye dönüşen hasar** görünmez: zaman aşımı 400 ms olsaydı istekler tam yanıt sayılır ve
önerme sessiz kalırdı — önceki dersteki gevşek yapılandırmanın aynısı. İkinci sınıf **pencere
dışına taşan hasardır**: deney durdurulduktan sonra kuyrukta bekleyen iş ya da yeniden denenen
istek günlüğe girmez. Üçüncüsü **kapsamın dışındaki bağımlılıklardır**: bu deney yalnız katalog
sınırını bozdu; bildirim kanalı hiç sınanmadı ve tablo yine yeşil çıkardı.

#### Özet

- Kaos deneyi üç kararla tanımlanır: kararlı durum önermesi, bulaşma kapsamı ve durdurma ölçütü;
  üçü de deney başlamadan sayıyla yazılır.
- Önermenin eşiği ölçümden gelir: kontrol koşumunun pencere ortalaması %96,00 taban kabul edildi
  ve pay marjı bu tabanın altına konan bir paydır.
- Planlanan bulaşma kapsamı 11 kullanıcıydı, ölçülen 44 istek çıktı: kapsam çarpanı 4,00 kat ve
  üst sınırı yığın boyudur; kapsam yalnız etiketin taşındığı kadar dardır.
- Pay marjı %5'in altındayken önerme kontrol koşumunda 4/10 pencerede yanlışlandı (yanlış kalma);
  %20'de deneyde yalnız 2/10 pencere yanlışlandı ve ilk yanlışlanma 8. pencereye kaydı.
- Seçilen marj %10'da durdurma ölçütü k = 1 iken hasar 8 istek, k = 2 iken 28, k = 3 ve
  sonrasında 44'tür; geç durdurmanın maliyeti hasar gören istek sayısıyla ölçülür.
- Deney beş pencere sürdü ve önerme dördünde yanlışlandı; pencere ortalaması seyrek hasarı yutar.

#### Sonraki Adım

Bu deney sistemin **çalışmaya devam eden** bir parçasını bozdu: katalog yavaşladı, ödünç akışı
zarif bozuldu, deney durduruldu ve her şey eski hâline döndü. Geri dönüş kendiliğinden oldu,
çünkü kaybolan hiçbir şey yoktu. Bozulan şey veri olduğunda bu varsayım çöker: bir kopya
kaybolduğunda, bir kayıt bozulduğunda ya da bir birim tümüyle düştüğünde sistemi geri getiren şey
deneyin durdurulması değil, **yedeğin geri yüklenmesidir**. Yedek her gün alınmakta ve alındığı
her gün başarılı sayılmaktadır — ama bu bir iddiadır ve bu konuda hiçbir yerde sınanmadı. Sonraki
ders o iddiayı sınar: yedeğin var olması geri yüklenebilir olduğunu göstermez, planlanan kurtarma
adımları ile gerçekten gereken adımlar aynı değildir ve doğrulama kümesinin neyi denetlediği
kadar neyi denetlemediği de sayılabilir.

### Kurtarma Doğrulaması

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/kurtarma-dogrulamasi

Yedeğin geri yüklenebilir olduğunun sınanması: aynı verinin üç yedek biçiminde gerçekten geri yüklenip beş denetimden geçirilmesi, doğrulama kümesi büyütüldükçe yanlış geçme ile yanlış kalmanın sayılması, yazma yolunu sınamayan doğrulamanın kaybolan kısıtı görememesi ve geri yükleme işi veriyle büyürken doğrulama kümesinin bedelinin sabit kalması.

Önceki deney sistemin çalışmaya devam eden bir parçasını bozdu; deney durdurulduğunda her şey
eski hâline döndü, çünkü kaybolan bir şey yoktu. Bozulan şey veri olduğunda bu varsayım çöker:
bir kopya kaybolduğunda sistemi geri getiren şey deneyin durdurulması değil, **yedeğin geri
yüklenmesidir.**

Yedek her gün alınıyor ve alındığı her gün başarılı sayılıyor. Bu bir iddiadır. **Kurtarma
doğrulaması**, o iddianın yedeği gerçekten geri yükleyerek sınanmasıdır. Sorulacak soru
kurtarmanın ne kadar sürdüğü değildir — kurtarma hedeflerinin aritmetiği ve devralma modeli
Dayanıklılık ve Güvenilirlik kursunda kuruldu ve burada tekrarlanmaz. Buradaki soru şudur:
**bir yedeğin geri yüklenebilir olduğunu hangi denetim kümesi söyler ve o küme neyi göremez.**

#### Yedek Bir Dökümdür, Kapsamı Bir Karardır

Kütüphane veri tabanı üç tablo ve bir dizinden oluşur; dizin ile üçüncü tablo sonradan gelen iki
göçle eklenmiştir. Yedek bir SQL dökümüdür ve dökümün **kapsamı** hangi nesnelerin içine
alındığıyla belirlenir. Üç biçim karşılaştırılacak: şemayı `sqlite_master` üzerinden okuyan
biçim, tablo listesi elle yazıldığı için son göçten habersiz olan biçim ve tabloları alıp
dizinleri atlayan biçim.

**IO20 — kaynak veri 6 kitap, 10 ödünç ve 3 ceza kaydıdır; ölçek katsayısı 10 ile onda bir
oranında büyütülür.** **IO21 — yedek aynı süreç içinde geri yüklenir.** İkisi de varsayımdır;
ikincisi düzeneği bir **model** yapar, çünkü gerçek bir kurtarmada dosya başka bir makineye
taşınır ve taşımanın kendisi ayrı bir arıza kaynağıdır.

```js
// kutuphane.mjs — katalog ve odunc semasi, veri ve uc yedek alma bicimi. Yedek bir SQL
// dokumudur; kapsami hangi nesnelerin dokume girdigiyle belirlenir.
import { DatabaseSync } from 'node:sqlite';

export const SEMA = [
  'create table kitap(no text primary key, baslik text not null)',
  'create table odunc(id integer primary key, kitap text not null, uye text not null, gun integer not null)',
  'create unique index odunc_tek on odunc(kitap, uye)',                // goc 2
  'create table ceza(uye text primary key, tutar integer not null)',   // goc 3
];

export function kur(yol, olcek = 1) {
  const db = new DatabaseSync(yol);
  for (const s of SEMA) db.exec(s);
  const kitap = Array.from({ length: 6 * olcek }, (_, i) => `K-${901 + i}`);
  const k = db.prepare('insert into kitap values (?, ?)');
  for (const no of [...kitap].reverse()) k.run(no, `kitap ${no}`);     // eklenme sirasi ters
  const o = db.prepare('insert into odunc(kitap, uye, gun) values (?, ?, ?)');
  for (let i = 0; i < 10 * olcek; i += 1)
    o.run(kitap[i % kitap.length], `uye-${Math.floor(i / 6) % 7}`, 14 + (i % 14));
  const c = db.prepare('insert into ceza values (?, ?)');
  for (let i = 0; i < 3 * olcek; i += 1) c.run(`uye-${i}`, 5 * (i + 1));
  return db;
}

// 'tam' semayi sqlite_master'dan okur; 'eksik-tablo' elle yazilmis tablo listesini kullanir;
// 'eksik-dizin' tablolari alir ama dizinleri atlar.
export function yedekAl(db, bicim) {
  const nesne = db.prepare(
    'select type, name, sql from sqlite_master where sql is not null order by rowid').all();
  const tablo = bicim === 'eksik-tablo'
    ? ['kitap', 'odunc']                      // elle yazilmis liste: goc 3'ten haberi yok
    : nesne.filter((n) => n.type === 'table').map((n) => n.name);
  const satir = [];
  for (const n of nesne) {
    if (n.type === 'index' && bicim === 'eksik-dizin') continue;
    if (n.type === 'table' && tablo.includes(n.name) === false) continue;
    satir.push(`${n.sql};`);
  }
  for (const t of tablo) {
    const sutun = db.prepare(`select * from pragma_table_info('${t}')`).all().map((s) => s.name);
    const sira = t === 'kitap' ? ' order by no' : '';    // dokum sirasi eklenme sirasi degil
    for (const r of db.prepare(`select * from ${t}${sira}`).all()) {
      const d = sutun.map((s) => (typeof r[s] === 'number' ? r[s] : `'${r[s]}'`));
      satir.push(`insert into ${t}(${sutun.join(',')}) values (${d.join(',')});`);
    }
  }
  return `${satir.join('\n')}\n`;
}
```

#### Üç Yedek, Beş Denetim

Denetimler güçlerine göre sıralanmıştır. **D1** yalnız dosyanın varlığına bakar. **D2** geri
yüklenen kopyadaki tablo adlarını karşılaştırır. **D3** her tablonun satır sayısını kaynakla
karşılaştırır. **D4** yazma yolunu sınar: yinelenen bir ödünç kaydı reddediliyor mu. **D5** ilk
kaydı sıralama vermeden okur ve kaynakla karşılaştırır.

```js
// dogrula.mjs — uc yedek gercekten geri yuklenir ve bes denetimden gecirilir; ardindan
// dogrulama kumesi buyutulerek yanlis gecme ile yanlis kalma sayilir.
import { writeFileSync, statSync, rmSync } from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
import { kur, yedekAl } from './kutuphane.mjs';

const kaynak = kur(':memory:');
const tablolar = (db) => db.prepare(
  "select name from sqlite_master where type='table' order by name").all().map((r) => r.name);
const sayim = (db, t) => db.prepare(`select count(*) c from ${t}`).get().c;
const ILK = 'select baslik from kitap limit 1';
const BEKLENEN = { tablo: tablolar(kaynak), ilk: kaynak.prepare(ILK).get().baslik };
for (const t of BEKLENEN.tablo) BEKLENEN[t] = sayim(kaynak, t);

const DENETIM = {
  D1: (_, dosya) => statSync(dosya).size > 0,
  D2: (db) => tablolar(db).join() === BEKLENEN.tablo.join(),
  D3: (db) => BEKLENEN.tablo.every((t) => tablolar(db).includes(t) && sayim(db, t) === BEKLENEN[t]),
  D4: (db) => {                                   // yazma yolu: yinelenen odunc reddedilmeli
    const r = db.prepare('select kitap, uye from odunc limit 1').get();
    try { db.prepare('insert into odunc(kitap, uye, gun) values (?, ?, 1)').run(r.kitap, r.uye); }
    catch { return true; }
    return false;
  },
  D5: (db) => db.prepare(ILK).get().baslik === BEKLENEN.ilk,
};
const BICIM = { tam: 'saglam', 'eksik-tablo': 'kusurlu', 'eksik-dizin': 'kusurlu' };
const AD = Object.keys(DENETIM);

const sonuc = {}, olcu = {};
for (const bicim of Object.keys(BICIM)) {
  const dosya = `yedek-${bicim}.sql`;
  const dokum = yedekAl(kaynak, bicim);
  writeFileSync(dosya, dokum);
  const db = new DatabaseSync(':memory:');
  db.exec(dokum);
  sonuc[bicim] = Object.fromEntries(AD.map((a) => {
    try { return [a, DENETIM[a](db, dosya)]; } catch { return [a, false]; }
  }));
  olcu[bicim] = { satir: dokum.split('\n').length - 1, bayt: statSync(dosya).size };
  rmSync(dosya);
}

const p = (x, n) => String(x).padStart(n);
console.log(`kaynak: ${BEKLENEN.tablo.length} tablo, ` +
  `${BEKLENEN.tablo.map((t) => `${t}=${BEKLENEN[t]}`).join(', ')} satir`);
console.log(`\n${'yedek bicimi'.padEnd(14)}${'gercek'.padEnd(10)}` +
  `${AD.map((a) => a.padStart(7)).join('')}${'dokum satiri'.padStart(14)}${'bayt'.padStart(7)}`);
for (const b of Object.keys(BICIM))
  console.log(`${b.padEnd(14)}${BICIM[b].padEnd(10)}` +
    `${AD.map((a) => (sonuc[b][a] ? 'gecti' : 'kaldi').padStart(7)).join('')}` +
    `${p(olcu[b].satir, 14)}${p(olcu[b].bayt, 7)}`);
console.log(`D1 dosya var  D2 tablo adlari  D3 satir sayimi  D4 yazma yolu  D5 ilk kayit (siralamasiz)`);

console.log(`\n${'dogrulama kumesi'.padEnd(20)}${'gecen yedek'.padStart(26)}` +
  `${'yanlis gecme'.padStart(14)}${'yanlis kalma'.padStart(14)}`);
for (let n = 1; n <= AD.length; n += 1) {
  const kume = AD.slice(0, n);
  const gecen = Object.keys(BICIM).filter((b) => kume.every((a) => sonuc[b][a]));
  const gecme = gecen.filter((b) => BICIM[b] === 'kusurlu').length;
  const kalma = Object.keys(BICIM).filter((b) => BICIM[b] === 'saglam' && gecen.includes(b) === false).length;
  console.log(`${kume.join('+').padEnd(20)}${(gecen.join(',') || '-').padStart(26)}` +
    `${p(gecme, 14)}${p(kalma, 14)}`);
}
```

```
kaynak: 3 tablo, ceza=3, kitap=6, odunc=10 satir

yedek bicimi  gercek         D1     D2     D3     D4     D5  dokum satiri   bayt
tam           saglam      gecti  gecti  gecti  gecti  kaldi            23   1469
eksik-tablo   kusurlu     gecti  kaldi  kaldi  gecti  kaldi            19   1258
eksik-dizin   kusurlu     gecti  gecti  gecti  kaldi  kaldi            22   1417
D1 dosya var  D2 tablo adlari  D3 satir sayimi  D4 yazma yolu  D5 ilk kayit (siralamasiz)

dogrulama kumesi                   gecen yedek  yanlis gecme  yanlis kalma
D1                  tam,eksik-tablo,eksik-dizin             2             0
D1+D2                          tam,eksik-dizin             1             0
D1+D2+D3                       tam,eksik-dizin             1             0
D1+D2+D3+D4                                tam             0             0
D1+D2+D3+D4+D5                               -             0             1
```

İlk satırın söylediği şey dersin çıkış noktasıdır: **üç yedeğin üçü de vardır ve üçü de boş
değildir.** D1 hiçbir kusuru ayırt etmez; yedek işinin yeşil kalması bu denetimden fazlasını
söylemez. Yedeğin varlığı geri yüklenebilirliğinin kanıtı değildir.

D2 ve D3 birinci kusuru yakalıyor. Elle yazılmış tablo listesi son göçle gelen `ceza` tablosunu
almamıştır; geri yüklenen kopyada iki tablo vardır ve ceza kayıtları yoktur. Kusur sınıfının adı
**yedek kapsamı kaymasıdır**: şema göçlerle büyür, yedeğin kapsamı elle yazılmış bir listede
sabit kalır ve aradaki fark yalnız geri yükleyerek görünür.

İkinci kusur çok daha sessizdir. `eksik-dizin` biçiminde bütün tablolar ve bütün satırlar
yerindedir: D2 geçer, D3 geçer. Kaybolan tek şey ödünç tablosundaki tekillik dizinidir. Geri
yüklenen kopya **okunduğunda kusursuzdur** ve ancak yazıldığında bozulur — aynı üyeye aynı kitap
ikinci kez ödünç verilebilir. D4 bunu yakalayan tek denetimdir. **Okuma yolunu sınayan bir
doğrulama, kaybolan bir kısıtı göremez.**

D5 ters yöndeki hatayı gösteriyor. Sıralama vermeden okunan ilk kayıt, döküm satırları farklı
sırayla yazıldığı için geri yüklenen kopyada başkadır — ama geri yükleme doğrudur. Kusur
denetimdedir: sıralamasız bir sorgunun sonucu bir doğrulama ölçütü olamaz. Bu denetim kümeye
eklendiğinde sağlam yedek de kalır ve **yanlış kalma** 1'e çıkar.

Seçilen eşik tablodan okunur: **doğrulama kümesi D1+D2+D3+D4 ve geçme ölçütü kümenin tamamının
geçmesidir.** Eşiğin kaynağı bir gereksinim değil, bu taramanın kendisidir — küme D4'e kadar
büyütüldüğünde yanlış geçme 2'den 0'a iner ve yanlış kalma 0'da kalır; bir denetim daha
eklendiğinde kazanç yoktur, kayıp vardır. Kararın sahibi yedek işidir: küme kırıldığında yedek
başarısız sayılır ve o günün yedeği kullanılabilir kabul edilmez.

#### Doğrulama Kümesinin Bedeli

Kurtarma planı tek bir "geri yükle" adımı yazar ve o adıma sabit bir süre biçer. Ölçüm iki
büyüklüğü ayırır: geri yüklemenin işi ile doğrulamanın işi aynı biçimde büyümez.

```js
// bedel.mjs — geri yuklemenin ve dogrulama kumesinin bedeli, iki veri boyutunda. Sorgu
// sayilari sayacla olculur; sure ortama bagli oldugu icin basilmaz, is miktari basilir.
import { DatabaseSync } from 'node:sqlite';
import { kur, yedekAl } from './kutuphane.mjs';

const sayacli = (db) => { const s = { n: 0 }; return { s, prepare: (q) => { s.n += 1; return db.prepare(q); } }; };
const tablolar = (db) => db.prepare(
  "select name from sqlite_master where type='table' order by name").all().map((r) => r.name);
const sayim = (db, t) => db.prepare(`select count(*) c from ${t}`).get().c;
const ADIM = {
  'D2 sema': (d) => tablolar(d),
  'D3 sayim': (d) => tablolar(d).map((t) => sayim(d, t)),
  'D4 yazma': (d) => {
    const r = d.prepare('select kitap, uye from odunc limit 1').get();
    try { d.prepare('insert into odunc(kitap, uye, gun) values (?, ?, 1)').run(r.kitap, r.uye); }
    catch { /* kisit yururlukte: beklenen */ }
  },
};

const p = (x, n) => String(x).padStart(n);
console.log(`${'olcek'.padEnd(8)}${'dokum kaydi'.padStart(13)}${'geri yuklenen satir'.padStart(21)}` +
  `${'D2'.padStart(5)}${'D3'.padStart(5)}${'D4'.padStart(5)}${'kume sorgusu'.padStart(14)}`);
const olculen = {};
for (const olcek of [1, 10]) {
  const dokum = yedekAl(kur(':memory:', olcek), 'tam');
  const geri = new DatabaseSync(':memory:');
  geri.exec(dokum);
  const kayit = dokum.split('\n').filter((x) => x.startsWith('insert')).length;
  const yuklenen = tablolar(geri).reduce((a, t) => a + sayim(geri, t), 0);
  const say = [];
  for (const f of Object.values(ADIM)) { const w = sayacli(geri); f(w); say.push(w.s.n); }
  const toplam = say.reduce((a, x) => a + x, 0);
  olculen[olcek] = { kayit, toplam };
  console.log(`${String(olcek).padEnd(8)}${p(kayit, 13)}${p(yuklenen, 21)}` +
    `${say.map((x) => p(x, 5)).join('')}${p(toplam, 14)}`);
}
console.log(`\nveri 10 kat buyudugunde geri yukleme isi ` +
  `${(olculen[10].kayit / olculen[1].kayit).toFixed(2)} kat, dogrulama kumesi ` +
  `${(olculen[10].toplam / olculen[1].toplam).toFixed(2)} kat`);
```

```
olcek     dokum kaydi  geri yuklenen satir   D2   D3   D4  kume sorgusu
1                  19                   19    1    4    2             7
10                190                  190    1    4    2             7

veri 10 kat buyudugunde geri yukleme isi 10.00 kat, dogrulama kumesi 1.00 kat
```

Sayılar koşumdan bağımsızdır: süre yerine iş miktarı ölçülmüştür. Geri yükleme 19 kayıttan 190
kayda, yani 10,00 kat büyüdü; doğrulama kümesi 7 sorguda kaldı, 1,00 kat. **Kurtarmanın bedeli
veriyle büyür, doğrulamanın bedeli büyümez.** Bunun pratik sonucu şudur: doğrulamayı atlamak
kurtarma süresini kayda değer ölçüde kısaltmaz, ama kurtarmanın başarılı sayılmasını bir
iddiaya geri döndürür.

Aynı tablo plandaki eksik adımı da adlandırıyor. Plandaki doğrulama adımı satır sayımıydı — D3.
Tatbikat iki adımın daha gerektiğini gösterdi: şema nesnelerinin sayılması ve yazma yolunun
sınanması. Plan dört adım yazıyordu, ölçülen altı adım oldu ve eklenen iki adımın toplam bedeli
üç sorgudur. **Planın en pahalı eksiği süre tahmini değil, hiç yazılmamış doğrulama adımıdır.**

#### Doğrulamanın Göremediği Sınıf

Bu küme dört şeyi göremez. Birincisi **içerik bozulmasıdır**: satır sayıları tutarken değerlerin
kayması D3'e görünmez. İkincisi **yedeğin alındığı andaki tutarlılıktır**: yarım kalmış bir işlem
sırasında alınmış döküm bütün denetimleri geçebilir. Üçüncüsü **hedef ortamdır**; burada yedek
aynı süreçte geri yüklendi, oysa gerçek kurtarmada dosya taşınır ve taşımanın kendisi ayrı bir
arıza kaynağıdır. Dördüncüsü **yedeğin yaşıdır**: bu denetimlerin hepsi dünkü bir yedekte de
geçer ve aradaki veri kaybını hiçbiri saymaz.

#### Özet

- Yedeğin varlığı geri yüklenebilirliğinin kanıtı değildir: üç yedeğin üçü de D1'i geçti, ikisi
  kusurluydu.
- Elle yazılmış tablo listesi son göçün tablosunu almadı; kusur sınıfının adı yedek kapsamı
  kaymasıdır ve yalnız geri yükleyerek görünür.
- Dizini almayan yedek bütün okuma denetimlerini geçti (D2, D3) ve yalnız yazma yolunu sınayan
  D4'te kaldı; kaybolan kısıt okunarak görülmez.
- Doğrulama kümesi D4'e kadar büyütüldüğünde yanlış geçme 2'den 0'a indi; sıralamasız sorguya
  bakan D5 eklendiğinde sağlam yedek de kaldı ve yanlış kalma 1 oldu.
- Seçilen eşik D1+D2+D3+D4 kümesinin tamamının geçmesidir; kaynağı bu taramadır ve küme
  kırıldığında o günün yedeği kullanılabilir sayılmaz.
- Veri 10 kat büyüdüğünde geri yükleme işi 10,00 kat arttı, doğrulama kümesi 1,00 katta kaldı;
  plandaki dört adım altıya çıktı ve eklenen iki adımın bedeli üç sorgudur.

#### Sonraki Adım

Bu konudaki üç deney aynı biçimde kuruldu: sistemin **bir parçası** seçildi ve bilerek bozuldu.
Katalog yavaşladı, bir yığın etiketlendi, bir tablo yedekten düştü. Her seferinde geri kalan her
şey çalışmaya devam etti ve ölçülen şey, bozulan parçanın karşısındaki kalıbın ya da denetimin
işini yapıp yapmadığıydı. Bu üç deneyin hiçbiri sistemi **kendi sınırına** kadar itmedi.

Sınırın nerede olduğu sorulmadı. Kütüphane ödünç sistemi bir yığını dört istekte topluyor, bir
katalog çağrısını 100 ms'de kesiyor ve bir kurtarmayı 190 ifadede tamamlıyor — ama bu sayıların
hangi yükte anlamını yitirdiği bilinmiyor. Yük artırıldığında sistem bir noktada yavaşlamaktan
vazgeçip düşmeye başlar; o noktanın önce bulunması, sonra bir sayıyla yazılması gerekir. Sonraki
ders yükü kademe kademe artırarak o noktayı arar ve kırılma noktasının hangi ölçütle, hangi
eşikle ilan edildiğini sorar.

### Kapasite Sınırının Bulunması

Source: https://academia.sh/tr/kurslar/islevsel-olmayan-test/kapasite-sinirinin-bulunmasi

Kırılma noktasının aranması: yükün basamak basamak artırılıp her basamakta verim, bekleme turu ve başarılı istek sayısının ayrı okunması, verimin artmayı bıraktığı doyum basamağı ile gecikmenin eşiği aştığı diz basamağının aynı yerde çıkmaması, sınırı verimle ilan eden kuralın üç basamağı yanlış geçirmesi ve kuyruğu uzatmanın verimi büyütürken başarılı istek sayısını değiştirmemesi.

Bu konudaki üç deney aynı biçimde kuruldu: sistemin bir parçası seçildi ve bilerek bozuldu —
katalog yavaşladı, bir yığın etiketlendi, bir tablo yedekten düştü. Hiçbiri sistemi **kendi
sınırına** kadar itmedi.

**Kırılma noktası testi** yükü kademe kademe artırıp sistemin bozulmaya başladığı basamağı
arar. Zorluk ölçmekte değil **tanımaktadır**: verimin artmayı bıraktığı basamak ile gecikmenin
dikleştiği basamak aynı yerde olmayabilir. Sistem Tasarımına Giriş kursundaki **doyum** ve
**doluluk** terimleri bu iki okumanın adıdır; Başarım Karşıt Kalıpları ve İzleme kursundaki
kapasite planlaması ise bu testin **girdisidir**. Soru şudur: **hangisi sınır ilan edilecek ve
bu kararın iki hata biçimi kaç ediyor.**

#### Yük Merdiveni ve Bekleme Turu

Ödünç ucu, sabit hizmet süreli bir hizmet yuvası kümesi ve sınırlı bir kuyruk olarak kurulur.
Süre yerine **tur** sayılır: bir tur bir hizmet süresidir ve bir isteğin bekleme turu, kabul
edildiği andaki kuyruk konumundan çıkar. Aşağıdaki sayılar koşumdan koşuma değişmez.

**IO22 — bir hizmet turu 40 ms, hizmet yuvası 4.** **IO23 — kuyruk uzunluğu 16, basamak
başına 5 dalga.** İkisi de varsayımdır; dalgadaki istekler ilk hizmet bitmeden varır, bu
yüzden kuyruk konumu deterministiktir.

```js
// kapasite.mjs — odunc ucu sabit hizmet suresi, S hizmet yuvasi ve Q uzunlugunda kuyrukla
// calisir. Her yanit, istegin kabul anindaki kuyruk konumundan hesaplanan bekleme turunu
// tasir; kuyruk dolduysa istek reddedilir (yuk bosaltma).
import { createServer } from 'node:http';

export const TUR = 40;                       // IO22: bir hizmet turu 40 ms

export function ucKur({ yuva, kuyruk }) {
  const olcu = { kabul: 0, red: 0 };
  const bekleyen = [];
  let calisan = 0;
  const basla = (bitir) => {
    calisan += 1;
    setTimeout(() => {
      calisan -= 1;
      bitir();
      if (bekleyen.length > 0) basla(bekleyen.shift());
    }, TUR);
  };
  const sunucu = createServer((istek, yanit) => {
    const dolu = calisan + bekleyen.length;
    if (dolu >= yuva + kuyruk) {
      olcu.red += 1;
      yanit.writeHead(503, { 'content-type': 'application/json' });
      return yanit.end('{"durum":"red"}');
    }
    olcu.kabul += 1;
    const tur = Math.floor(dolu / yuva);      // kacinci turda hizmete girecek
    const bitir = () => {
      yanit.writeHead(200, { 'content-type': 'application/json' });
      yanit.end(`{"tur":${tur}}`);
    };
    if (calisan < yuva) basla(bitir); else bekleyen.push(bitir);
  });
  sunucu.listen(0, '127.0.0.1');
  return { sunucu, olcu };
}
```

#### Basamaklar

Kabul edilebilirliğin ölçüsü bu konuda ölçülmüş bir sayıdan gelir: birinci derste ödünç
servisinin katalog çağrısına koyduğu zaman aşımı 100 ms'dir; 40 ms'lik turla bu, isteğin en
çok **bir tur** bekleyebileceği anlamına gelir. Daha uzun bekleyen istek işlenir ama çağırana
ulaşmaz. **IO24 — kabul edilebilir bekleme, zaman aşımının izin verdiği tur
sayısıdır**; varsayım değil, birinci dersin ölçüsünden türetilmiş bir hesaptır.

```js
// merdiven.mjs — yuk basamak basamak artirilir; her basamakta verim, bekleme turu ve
// basarili istek sayilir. Butun sayilar tamsayidir ve kosumdan bagimsizdir; sure basilmaz.
import { writeFileSync } from 'node:fs';
import { ucKur, TUR } from './kapasite.mjs';

const YUVA = 4, KUYRUK = 16, DALGA = 5;                 // IO22, IO23
const BASAMAK = [1, 2, 4, 8, 12, 16, 20, 24, 32];
const ZAMAN_ASIMI = 100;                                // 01. dersin IO16'si: 100 ms
const EN_COK_TUR = Math.ceil(ZAMAN_ASIMI / TUR) - 2;    // IO24: bekleme turu ust siniri

async function basamakKos(taban, c) {
  const tur = [];
  let red = 0;
  for (let d = 0; d < DALGA; d += 1) {
    const s = await Promise.all(Array.from({ length: c }, async () => {
      const y = await fetch(taban);
      const g = await y.json();
      return y.status === 503 ? null : g.tur;
    }));
    for (const x of s) { if (x === null) red += 1; else tur.push(x); }
  }
  const s = [...tur].sort((a, b) => a - b);
  return { c, gonderilen: c * DALGA, islenen: tur.length, red,
    p95: s.length > 0 ? s[Math.ceil(0.95 * s.length) - 1] : 0,
    basarili: tur.filter((x) => x <= EN_COK_TUR).length };
}

async function merdiven(kuyruk, basamaklar) {
  const { sunucu } = ucKur({ yuva: YUVA, kuyruk });
  await new Promise((c) => sunucu.once('listening', c));
  const taban = `http://127.0.0.1:${sunucu.address().port}/odunc`;
  const r = [];
  for (const c of basamaklar) r.push(await basamakKos(taban, c));
  sunucu.closeAllConnections();
  await new Promise((c) => sunucu.close(c));
  return r;
}

const p = (x, n) => String(x).padStart(n);
const sonuc = await merdiven(KUYRUK, BASAMAK);
writeFileSync('merdiven.json', `${JSON.stringify({ EN_COK_TUR, sonuc }, null, 0)}\n`);
console.log(`${YUVA} hizmet yuvasi, ${KUYRUK} kuyruk, tur ${TUR} ms, basamak basina ${DALGA} dalga; ` +
  `zaman asimi ${ZAMAN_ASIMI} ms -> kabul edilebilir bekleme en cok ${EN_COK_TUR} tur`);
console.log(`\n${'es zamanli'.padStart(11)}${'gonderilen'.padStart(12)}${'islenen'.padStart(9)}` +
  `${'reddedilen'.padStart(12)}${'p95 bekleme turu'.padStart(18)}${'basarili'.padStart(10)}`);
for (const r of sonuc)
  console.log(`${p(r.c, 11)}${p(r.gonderilen, 12)}${p(r.islenen, 9)}${p(r.red, 12)}` +
    `${p(r.p95, 18)}${p(r.basarili, 10)}`);

const doyum = sonuc.find((r, i) => i > 0 && r.islenen === sonuc[i - 1].islenen);
const diz = sonuc.find((r) => r.p95 > EN_COK_TUR);
console.log(`\ndoyum basamagi (verim artmayi biraktigi yer): c=${doyum.c}, verim ${doyum.islenen} istek`);
console.log(`diz basamagi (p95 bekleme ${EN_COK_TUR} turu astigi yer): c=${diz.c}`);

console.log(`\nayni basamak (c=32), iki kuyruk uzunlugu:`);
for (const q of [KUYRUK, 64]) {
  const [r] = await merdiven(q, [32]);
  console.log(`kuyruk ${p(q, 2)}: islenen ${p(r.islenen, 3)}, reddedilen ${p(r.red, 2)}, ` +
    `p95 ${r.p95} tur, basarili ${r.basarili}`);
}
```

```
4 hizmet yuvasi, 16 kuyruk, tur 40 ms, basamak basina 5 dalga; zaman asimi 100 ms -> kabul edilebilir bekleme en cok 1 tur

 es zamanli  gonderilen  islenen  reddedilen  p95 bekleme turu  basarili
          1           5        5           0                 0         5
          2          10       10           0                 0        10
          4          20       20           0                 0        20
          8          40       40           0                 1        40
         12          60       60           0                 2        40
         16          80       80           0                 3        40
         20         100      100           0                 4        40
         24         120      100          20                 4        40
         32         160      100          60                 4        40

doyum basamagi (verim artmayi biraktigi yer): c=24, verim 100 istek
diz basamagi (p95 bekleme 1 turu astigi yer): c=12

ayni basamak (c=32), iki kuyruk uzunlugu:
kuyruk 16: islenen 100, reddedilen 60, p95 4 tur, basarili 40
kuyruk 64: islenen 160, reddedilen  0, p95 7 tur, basarili 40
```

#### Doyum mu, Diz mi

Üç sütun üç ayrı hikâye anlatıyor. **İşlenen istek**, yani verim, c = 24'e kadar artıyor ve
orada duruyor: doyum basamağı burasıdır. **p95 bekleme turu** çok daha erken kıpırdıyor ve
c = 12'de eşiği aşıyor: diz basamağı burasıdır. Arada üç kademe var ve o üç kademede sistem
"daha çok istek işliyor" görünürken yanıtlarının çoğu zaman aşımına uğruyor.

Kararı üçüncü sütun veriyor. **Başarılı istek sayısı c = 8'den sonra hiç artmıyor: 40'ta
sabit.** Eklenen her istek yalnız kuyruğa giriyor, bekleyenlerin süresini uzatıyor ve zaman
aşımına uğruyor. Gerçek kapasite 40 başarılı istektir ve verimin düzleştiği yerin çok
gerisinde bulunmuştur.

Son iki satır bunun neden yalnız verimle görülemeyeceğini söylüyor. Aynı basamak (c = 32) iki
kuyruk uzunluğunda koşturuldu: kuyruk 16'da verim 100 ve 60 red, kuyruk 64'te verim 160 ve
hiç red — 160/100, yani **1,60 kat.** Başarılı istek ikisinde de 40, yani **1,00 kat.**
Kuyruğu uzatmak doyum basamağını sağa taşır ve kullanıcıya ulaşan hiçbir şeyi değiştirmez;
**verim ölçütü kırılma noktasını kuyruk ayarıyla saklanabilir kılar.**

#### Sınırı İlan Etme Kuralı

Sınır bir sayı olarak yazılır ve hangi kuralla seçildiği eşiğin kendisidir. Tarama iki kural
ailesini karşılaştırıyor: gecikmeye bakan (p95 bekleme ≤ t tur) ve verime bakan (doyumdan
önceki son basamak).

**IO25 — bir basamak, başarılı istek payı %99'un altına düşmediği sürece kabul edilebilir.**
Varsayımdır ve kuralların dışında tutulur; kurallar bu payı bilmeden karar verir.

```js
// sinir.mjs — merdiven.json uzerinde sinir ilan etme kurallarinin taranmasi. Gercek kabul
// olcutu basarili istek payidir; kurallar bunu bilmeden karar verir.
import { readFileSync } from 'node:fs';

const { sonuc } = JSON.parse(readFileSync('merdiven.json', 'utf8'));
const PAY = 0.99;                                        // IO25
const gercek = (r) => r.basarili / r.gonderilen >= PAY;
const doyum = sonuc.findIndex((r, i) => i > 0 && r.islenen === sonuc[i - 1].islenen);

const say = (kabul) => {
  let gecme = 0, kalma = 0;
  sonuc.forEach((r, i) => {
    if (kabul(r, i) && gercek(r) === false) gecme += 1;
    if (kabul(r, i) === false && gercek(r)) kalma += 1;
  });
  return { gecme, kalma, sinir: sonuc.filter(kabul).at(-1)?.c };
};

const p = (x, n) => String(x).padStart(n);
console.log(`gercek kabul olcutu: basarili istek payi >= %${100 * PAY}; ` +
  `bu olcutu saglayan en yuksek basamak c=${sonuc.filter(gercek).at(-1).c}`);
console.log(`\n${'kural'.padEnd(26)}${'ilan edilen sinir'.padStart(18)}` +
  `${'yanlis gecme'.padStart(14)}${'yanlis kalma'.padStart(14)}`);
for (const t of [0, 1, 2, 3, 4]) {
  const r = say((x) => x.p95 <= t);
  console.log(`${`gecikme: p95 <= ${t} tur`.padEnd(26)}${p(`c=${r.sinir}`, 18)}${p(r.gecme, 14)}${p(r.kalma, 14)}`);
}
const v = say((_, i) => i < doyum);
console.log(`${'verim: doyumdan onceki'.padEnd(26)}${p(`c=${v.sinir}`, 18)}${p(v.gecme, 14)}${p(v.kalma, 14)}`);
```

```
gercek kabul olcutu: basarili istek payi >= %99; bu olcutu saglayan en yuksek basamak c=8

kural                      ilan edilen sinir  yanlis gecme  yanlis kalma
gecikme: p95 <= 0 tur                    c=4             0             1
gecikme: p95 <= 1 tur                    c=8             0             0
gecikme: p95 <= 2 tur                   c=12             1             0
gecikme: p95 <= 3 tur                   c=16             2             0
gecikme: p95 <= 4 tur                   c=32             5             0
verim: doyumdan onceki                  c=20             3             0
```

Son satır dersin sonucudur. **Verime bakan kural sınırı c = 20 ilan ediyor ve üç basamağı
yanlış geçiriyor** — 12, 16 ve 20. Üçünde de sistem daha çok istek işliyor, hiçbirinde daha
çok kullanıcı yanıt almıyor. Gecikmeye bakan kural eşik bir tura konduğunda sınırı c = 8'de
ilan ediyor ve iki hata biçimini de sıfırlıyor.

Taramanın öteki ucu eşiği sıkmanın bedelini veriyor: p95 ≤ 0 tur kuralı hiç bekleme kabul
etmiyor ve c = 8'i de eliyor — bir **yanlış kalma**; o basamakta bekleme zaman aşımının
altındadır ve kapasitenin yarısı boşa yazılır. Seçilen eşik **p95 bekleme ≤ 1 turdur** ve
kaynağı birinci dersin ölçtüğü zaman aşımıdır. Kararın sahibi kapasite planıdır: bu sayı
yayımı durdurmaz, ölçekleme kararına girdi olur.

#### Bedel ve Görülmeyen Sınır

Koşumdan bağımsız bedel: dokuz basamak, 595 gönderilen istek, 515 kabul ve 80 red. Asıl bedel
bu değildir — **sınırı bulmanın tek yolu sınırın ötesine geçmektir.** Son beş basamak bilerek
zarar veriyor: orada işlenen 440 isteğin 240'ı zaman aşımına uğradı. Üretim benzeri bir
ortamda bu, bulaşma kapsamı sınırlanmadan yapılamaz ve önceki dersin durdurma ölçütü gerekir.

Merdivenin göremediği üç sınıf var. Birincisi **iş karışımına bağlı sınırdır**: merdiven tek
bir uca aynı isteği gönderdi. İkincisi **birikmeli bozulmadır**: beş dalgalık bir
basamak, saatlerce süren yükün penceresi değildir. Üçüncüsü **çöküş davranışıdır**: buradaki uç yük boşaltma yaptığı için verim
doyumdan sonra düz kaldı; boşaltması olmayan bir sistemde verim düşer.

#### Özet

- Kırılma noktası testi yükü basamak basamak artırır; dokuz basamakta verim, p95 bekleme turu
  ve başarılı istek ayrı okundu, bütün sayılar tam sayıdır.
- Doyum basamağı c = 24, diz basamağı c = 12; arada üç kademe var.
- Başarılı istek c = 8'den sonra 40'ta sabit kaldı: eklenen her istek kuyruğu uzattı ve zaman
  aşımına uğradı.
- Kuyruk 16'dan 64'e çıkarıldığında verim 1,60 kat büyüdü, başarılı istek 1,00 kat kaldı.
- Seçilen eşik p95 bekleme ≤ 1 turdur, kaynağı birinci dersin 100 ms'lik zaman aşımıdır ve
  0 / 0 verir; verime bakan kural üç basamağı yanlış geçirir.
- Bedel 595 gönderilen istektir ve sınırın ötesine geçmeyi gerektirir: son beş basamakta
  işlenen 400 isteğin 240'ı zaman aşımına uğradı.

#### Kurs Kapanışı

Bu kursun on beş dersi aynı sistemi üç soruyla sınadı — yük, saldırı ve arıza altında ne
olduğu — ve her ders sonucu bir eşiğe bağladı.

| Ders | Sınadığı eşik | Eşiğin kaynağı | Yanlış geçme / yanlış kalma | Bedeli |
|---|---|---|---|---|
| Başarım Testi Türleri | p95 ≤ 20 ms, dört karar penceresinde | IO1 varsayım+hesap: 60 ms bütçenin üçe bölünmesi | 1 / 0; 3 ms'de 0 / 1 | 9 süreç, 55.000 istek, 1,00 milyar satır adımı |
| Test Senaryosu Modelleme | aynı 20 ms, senaryo değişirken | IO1'den devralınır | 1 / 0; 5 ms'de 0 / 1; 100 ms'de 3 / 0 | koşum başına 3.000 istek; tarama adımı 10,7 kat yayılıyor |
| Ölçüt Seçimi | yüzdelik başına en az $1/(1-p)$ örnek | IO5 hesap | 20 örnekte 86/200 / 0; 100 örnekte 0 / 0 | 8.494 gecikme değeri, 1.068.000 çekilen örnek |
| Darboğaz Analizi | katman payı ≥ 0,50 | IO6 varsayım | 1 yanlış atıf / 0 atfedilemez; 0,80'de 0 / 2 | 5 süreç, istek başına 4 saat okuması |
| Karşılaştırmalı Ölçüm Disiplini | aday/taban ±%5, 8 tur | IO9 varsayım; alt sınırı ölçülen yayılım | 0 / 0; %2'de 0 / 141; %20'de 140 / 0 | 30 süreç, 2,4 milyon satır dolum, 28.000 yeniden verilmiş karar |
| Statik Uygulama Güvenlik Testi | bulgu puanı ≥ 40 | IO8 varsayım: kaçan kusur = 5 yanlış alarm | 1 / 2; 50'de 2 / 0 | 75 karşılaştırma, 0 süreç, 7 elle inceleme |
| Dinamik Uygulama Güvenlik Testi | bulgu puanı ≥ 50 | ölçüm: en düşük maliyet 50'de | 1 / 0 | 1 süreç, 8 istek, elle rota tablosu |
| Bağımlılık ve Bileşen Taraması | önem ≥ 80 | ölçüm; ağırlık IO8'den | 1 / 0 | 10 paketin 5'i yükseltildi, her biri gerileme testi |
| Sızma Testinin Yeri | zincir uzunluğu ≤ 4 adım | bütçe (IO12), gereksinim değil | k=3'te 2 / 2; k=4'te 0 / 2; k=5'te 0 / 8 | k=3'te 40, k=4'te 84, k=5'te 169 adım |
| Erişilebilirlik Testi | sıfır otomatik bulgu **ve** 6 elle ölçüt | ölçüm: otomatik denetim üç kusurla yeşil döner | 3 / 1 | 1 ağaç dolaşımı, 14 öge; ekran başına 6 ölçüt |
| Uyumluluk Testi | kapsanan kullanıcı payı ≥ %90 | yanılgı: pay kusur dağılımını temsil etmez | 3 / 3; profil ve düzen ölçütüyle 0 / 5 | 8 ortam koşumu, elle destek tablosu, çarpımsal artış |
| Hata Enjeksiyonu | düşme oranı ≤ %5 **ve** enjekte başına bir bozulmuş yanıt | ölçüm: 10/200; ikinci eşik düzeneğin sayacı | 0 / 0; yalnız düşme oranıyla 1 / 0 | 38 satırın 13'ü, 5 karar noktası, 1 açma yolu |
| Kaos Deneyleri | pencere tam yanıt payı ≥ taban − %10 | ölçüm: kontrol koşumu ortalaması %96,00 | 1 / 0; %5'te 0 / 4; %20'de algılama 8. pencereye kayar | kapsam çarpanı 4,00; k = 3'te 44 hasarlı istek |
| Kurtarma Doğrulaması | D1+D2+D3+D4'ün tamamı geçmeli | ölçüm: küme taraması | 0 / 0; yalnız D1 ile 2 / 0; D5 ile 0 / 1 | geri yükleme 10,00 kat büyür, doğrulama 7 sorguda sabit |
| Kapasite Sınırının Bulunması | p95 bekleme ≤ 1 tur | ölçüm: birinci dersin 100 ms zaman aşımı | 0 / 0; verim ölçütüyle 3 / 0 | 595 istek, 9 basamak, 240 zaman aşımı |

Üçüncü sütun bu kursun kuralıdır: **kaynaksız eşik karar taşımaz.** Dördüncü sütun kuralın
öteki yarısıdır. İşlevsel testte sonuç bir "geçti/kaldı"dır, çünkü beklenen değer bellidir.
İşlevsel olmayan testte sonuç bir dağılımdır ve "başarılı" demek bir eşik seçmektir; o eşik
ancak iki hata biçimiyle savunulabilir — kaç gerçek kusuru geçirdiği ve kaç sağlam koşumu
kırmızıya döndürdüğü. Tabloda ikisini birden sıfırlayan satırların hiçbiri bunu eşiği sıkarak
yapmadı; hepsi ölçtüğü **niceliği** değiştirdi.

Dört kurs boyunca sınamanın kendisi ölçüldü: hangi test hangi kusur sınıfını yakalıyor,
hangisini kaçırıyor, karşılığında ne ödüyor. Ama bir soru hiç sorulmadı — **hangi test ne
zaman koşacak.** Buradaki bazı testler her değişiklikte koşacak kadar ucuz, bazıları sürüm
başına bir kez koşacak kadar pahalı, biri üretim benzeri bir ortam istiyor. Sınırlı zamanın
bu testler arasında nasıl paylaştırılacağı ve kırmızı bir sonucun ekipte nasıl bir karara
dönüşeceği hiçbir derste ele alınmadı. Test Süreci ve Otomasyon Altyapısı kursu bu soruyla
başlar.

