From b66c720b32beb6363b268f4bc5c8b2da173a71ce Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Wed, 23 Sep 2026 00:24:42 +0300 Subject: [PATCH 1/4] =?UTF-8?q?fix(veri):=20yanl=C4=B1=C5=9F=20DEVLET=20et?= =?UTF-8?q?iketi=20kal=C4=B1c=C4=B1=20onar=C4=B1m=20+=20baz=20ad=20=C3=A7e?= =?UTF-8?q?li=C5=9Fki=20denetimi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 kurum (Kıbrıs Amerikan, Uluslararası Balkan, Uluslararası Saraybosna) kaynak CSV'den `unitur='DEVLET'` geliyordu. `db.ts:79-82` filtreleri yüzünden "devlet üniversitesi istiyorum" diyen öğrencinin aday havuzuna 27 KKTC/yurt dışı programı sızıyor, "vakıf" diyende aynı 27'si eksik kalıyordu. Kullanıcının fark edemeyeceği bir doğruluk hatasıydı. unitur-onar.ts — Aşama D (KURUM_DUZELT): A/B/C yalnız `unitur IS NULL` satırlara dokunuyordu; bu satırlar NULL değil, DOLU ve YANLIŞ. D koşulsuz çalışır ve Aşama A'dan ÖNCE koşar ki yanlış değer başka satırlara kopyalanmasın. İdempotent (2. koşu: 0/0). Her düzeltmenin kanıtı kurumun YÖK Atlas'taki kendi parantezli ikinci yazımı: "(ÜSKÜP-MAKEDONYA)" → YURTDISI KAMU, "(KKTC-LEFKOŞA)" → KKTC. refresh.ts:519 `uniturSec` DB'deki değeri kopyaladığı için düzeltme buraya yazılınca her tazelemede kalıcı olur. veri-kalite.ts — iki yeni ZORUNLU kontrol (dosyanın sonuna): 13. "baz ad başına tek unitur" — kök neden buydu: unitur-onar.ts'in eski çelişki koruması GROUP BY universite (TAM ad) ile çalıştığı için aynı kurumun iki yazımı arasındaki DEVLET↔YURTDISI KAMU çelişkisini hiç görmüyordu. 14. "DEVLET etiketi Türkiye ili ile destekli" — 13'ün kör noktası: Kıbrıs Amerikan'ın İKİ yazımı da yanlış etiketli olduğu için ortada çelişki yok. 13 tek başına 100 satırın yalnız 58'ini koruyordu. Şartnamenin ötesinde, gerekçesi raporun §4'ünde. Doğrulama: düzeltme öncesi DB'de iki kontrol de KALDI (çıkış kodu 1); sonrasında 0 ZORUNLU kaldı, 3 uyarı (üçü de önceden vardı). Tek satır bilerek bozulduğunda ikisi de yakaladı. tsc + eslint temiz. Veri (126 satır) ayrı commit'te. Rapor: docs/gece-vardiyasi/2026-09-22/ 09-p2-unitur.md Co-Authored-By: Claude Opus 5 --- .../gece-vardiyasi/2026-09-22/09-p2-unitur.md | 147 ++++++++++++++++++ scripts/unitur-onar.ts | 69 +++++++- scripts/veri-kalite.ts | 41 +++++ 3 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md diff --git a/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md b/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md new file mode 100644 index 0000000..21353b9 --- /dev/null +++ b/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md @@ -0,0 +1,147 @@ +# 09 · P2 — Sihirbaz havuzundaki sessiz sızıntı kapatıldı + +Dal: `gece/0922-unitur-kacagi` · Şartname: `06-ceo-kararlari.md` §3 P2 · Teşhis: `05-urap.md` §6 +Commit düzeni: (1) kod + bu rapor, (2) `data/yokatlas.db` binary'si. + +## Özet (5 madde) + +1. **Sızıntı kapandı.** "Devlet üniversitesi istiyorum" diyen öğrencinin aday havuzuna giren 27 KKTC/yurt dışı programı artık devlet filtresinde **0**, vakıf filtresinde **27**. Ölçüldü, çıkarım değil. +2. **126 satır düzeldi**, tam şartnamedeki sayı: 100 `unitur` + 26 `il`. Değişen kolon **yalnız bu ikisi**; `programs` tablosunun 45 kolonu tek tek karşılaştırıldı. +3. **2021–2026 puan/sıra/kontenjan/yerleşen kolonlarında değişen satır: 0.** 34 yıl kolonu, 21.493 satır, öncesi/sonrası birebir. `programs_arsiv`, `netler`, `veri_meta` de dokunulmadı. +4. **Kök neden kapandı, tekrarı denetleniyor.** `unitur-onar.ts`'e NULL koşulundan bağımsız **Aşama D** (`KURUM_DUZELT`) eklendi; `veri-kalite.ts`'e iki yeni ZORUNLU kontrol geldi. Kontroller düzeltme öncesi DB'de **KALDI**, sonrasında **GEÇTİ** verdi; ayrıca tek satır bilerek bozulduğunda ikisi de yakaladı. +5. **Bir kapsam genişletmesi var, bilerek:** şartnamedeki "baz ad başına tek `unitur`" kontrolü Kıbrıs Amerikan'ı (100 satırın 42'si) **yakalayamıyor** — o kurumun iki yazımı da yanlış etiketli olduğu için ortada çelişki yok. Bu yüzden ikinci bir 6 satırlık kontrol eklendi (§4). Onaysız bırakılırsa "kök neden kapandı" cümlesi yalan olurdu. + +## 1. Ne değişti + +| Dosya | Değişiklik | +|---|---| +| `scripts/unitur-onar.ts` | `KURUM_DUZELT` haritası + **Aşama D** (Aşama A'dan önce koşar). `KKTC_ILLERI` dışa aktarıldı (tek kaynak). Başlık yorumu "yalnızca NULL satırlara dokunur" → A/B/C NULL, D koşulsuz. | +| `scripts/veri-kalite.ts` | Kontrol 13 (baz ad başına tek `unitur`) + kontrol 14 (DEVLET etiketi Türkiye ili ile destekli). Dosyanın **en sonuna** eklendi; mevcut satırlar yeniden düzenlenmedi (P3 ile çakışmayı küçültmek için). | +| `data/yokatlas.db` | 126 satır (ayrı commit). | + +`KURUM_DUZELT` içeriği ve her satırın kanıtı — kurumun YÖK Atlas'taki **kendi parantezli ikinci yazımı**: + +| Ad (DB'de) | `unitur` önce → sonra | `il` | +|---|---|---| +| KIBRIS AMERİKAN ÜNİVERSİTESİ | DEVLET → **KKTC** (36) | değişmedi | +| KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA) | DEVLET → **KKTC** (6) | değişmedi | +| ULUSLARARASI BALKAN ÜNİVERSİTESİ | DEVLET → **YURTDISI KAMU** (34) | SARAYBOSNA - BOSNA - HERSEK → **ÜSKÜP-MAKEDONYA** (26) | +| ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ | DEVLET → **YURTDISI KAMU** (24) | değişmedi (kurum gerçekten Saraybosna'da) | + +`ÜSKÜP-MAKEDONYA` yazımı uydurma değil: kurumun kendi adındaki parantezli ek, DB'deki diğer yurt dışı illerin biçimiyle (`BİŞKEK-KIRGIZİSTAN`, `TÜRKİSTAN-KAZAKİSTAN`, `BAKÜ-AZERBAYCAN`) aynı. + +`unitur` dağılımı: `DEVLET 12142→12042`, `KKTC 1145→1187`, `YURTDISI KAMU 99→157`. Toplam sabit. + +## 2. Fark raporu (öncesi/sonrası) + +Yöntem: düzeltme öncesi DB `sqlite3 .backup` ile scratchpad'e alındı, sonra `ATTACH` ile kolon kolon karşılaştırıldı. + +``` +programs: önce 21493 → sonra 21493 (satır kaybı/ekleme yok) +programs_arsiv: önce 3735 → sonra 3735 (unitur/il değişen satır: 0) +netler: önce 62975 → sonra 62975 +veri_meta: önce 37 → sonra 37 + +programs, 45 kolon tarandı — DEĞİŞEN: unitur=100, il=26 +yıl kolonları (34 kolon: sıra/puan/kontenjan/yerleşen + 2025 ek kontenjanları, +2021-2026) değişen satır: 0 +``` + +`pnpm veri-kalite --onceki` kendi bağımsız kontrolüyle aynı sonucu verdi: + +``` +GEÇTİ önceki DB: satır kaybı — 0 +GEÇTİ önceki DB: geçmiş yıllar korunmuş — 20 kolonda dolu değeri değişen/silinen satır: 0 + 2025 sağlaması (count | Σsira | Σpuan | Σkontenjan | Σyerlesen) + önceki DB : 21493 | 11099796362 | 5433214.11872 | 756453 | 740586 + şimdi, önceki DB'deki aynı id'ler : 21493 | 11099796362 | 5433214.11872 | 756453 | 740586 +``` + +## 3. Kabul kriterleri — tek tek + +**(1) `pnpm veri-kalite` temiz ve yeni kontrol çelişkiyi gerçekten yakalıyor.** ✅ + +``` +pnpm veri-kalite --yil 2026 --db data/yokatlas.db --onceki <öncesi> +… +GEÇTİ baz ad başına tek unitur — aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor +bilgi baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen) — HOCA AHMET YESEVİ …/YURTDISI KAMU,YURTDISI VAKIF/17 ; KIRGIZİSTAN-TÜRKİYE MANAS …/29 +GEÇTİ DEVLET etiketi Türkiye ili ile destekli — DEVLET etiketli her kurumun en az bir programı Türkiye ilinde +Sonuç: 0 ZORUNLU kontrol kaldı, 3 uyarı. (çıkış kodu 0) +``` + +3 uyarı **düzeltme öncesiyle birebir aynı**, hiçbiri bu işten doğmadı: `il boş` 110 satır (kapsam dışı), `kimlik ATLANDI` (`data/ham` repoda yok), `yetim netler` 2066. + +Yakalama iki yoldan kanıtlandı: + +- *Düzeltme öncesi DB'de* (gerçek kusur): `KALDI baz ad başına tek unitur — … ULUSLARARASI BALKAN ÜNİVERSİTESİ/DEVLET,YURTDISI KAMU/48 ; ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ/DEVLET,YURTDISI KAMU/38` ve `KALDI DEVLET etiketi Türkiye ili ile destekli — KIBRIS AMERİKAN ÜNİVERSİTESİ/LEFKOŞA/36 ; … (KKTC-LEFKOŞA)/KIBRIS/6 ; ULUSLARARASI BALKAN …/34 ; ULUSLARARASI SARAYBOSNA …/24` → `Sonuç: 2 ZORUNLU kontrol kaldı`, çıkış kodu 1. +- *Bilerek bozma* (düzeltilmiş DB'nin atılabilir kopyasında **tek** satır `DEVLET`'e döndürüldü): ikisi de yakaladı → `Sonuç: 2 ZORUNLU kontrol kaldı`. Kopya silindi; gerçek DB'ye dokunulmadı. + +**(2) `db.ts` devlet filtresi 27 programı artık döndürmüyor, vakıf filtresi döndürüyor.** ✅ Ölçüldü. + +`src/lib/db.ts:79-82` filtreleri + `EFEKTIF_SIRA = COALESCE(sira2026, sira2025)` (havuza ancak efektif sırası olan satır girer): + +```sql +-- devlet: AND unitur = 'DEVLET' +-- vakıf : AND unitur IS NOT NULL AND unitur <> 'DEVLET' +SELECT COUNT(*) FROM programs + WHERE universite IN ('KIBRIS AMERİKAN ÜNİVERSİTESİ', + 'KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)', + 'ULUSLARARASI BALKAN ÜNİVERSİTESİ', + 'ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ') + AND AND COALESCE(sira2026, sira2025) IS NOT NULL; +``` + +| | önce | sonra | +|---|---|---| +| devlet havuzu | **27** | **0** | +| vakıf havuzu | **0** | **27** | + +**(3) `/universiteler` hub'ında üç kurum "Devlet" listesinden çıkıyor.** ✅ (kısmen koddan çıkarım — tarayıcı açılmadı) + +`getAllUniversiteler` (`src/lib/katalog.ts:268-271`) `MAX(unitur) GROUP BY universite` okur; `universite-listesi.tsx:44-47` `u.unitur === "DEVLET"` olanı devlet listesine, `VAKIF`/`VAKIF MYO` olanı vakfa, kalanı `digerUniler`'e koyar. Aynı SQL DB'de koşturuldu: + +``` +önce : KIBRIS AMERİKAN …|DEVLET … (KKTC-LEFKOŞA)|DEVLET ULUSLARARASI BALKAN …|DEVLET ULUSLARARASI SARAYBOSNA …|DEVLET +sonra: KIBRIS AMERİKAN …|KKTC … (KKTC-LEFKOŞA)|KKTC ULUSLARARASI BALKAN …|YURTDISI KAMU ULUSLARARASI SARAYBOSNA …|YURTDISI KAMU +Devlet listesindeki kurum sayısı: 170 → 166 +``` + +166 = 170 − **4 yazım** (3 kurum; Kıbrıs Amerikan'ın iki yazımı bugün iki ayrı slug — `kategoriBul` baz ad birleştirmesi kapsam dışı, BACKLOG #10). +Dördü de `KKTC`/`YURTDISI KAMU` olduğu için vakıf listesine **girmiyor**, "Diğer" grubuna düşüyor — hub'da sayı dengesi bozulmuyor. + +**(4) 2021–2026 puan/sıra kolonlarında hiçbir değişiklik yok.** ✅ §2. + +Ayrıca: `npx next typegen && npx tsc --noEmit` temiz, `npx eslint scripts/unitur-onar.ts scripts/veri-kalite.ts` temiz. Betik **idempotent**: ikinci koşu `unitur 0 satır, il 0 satır`. + +## 4. Kapsam genişletmesi ve gerekçesi (şeffaflık) + +Şartname `veri-kalite.ts`'e tek kontrol istiyordu: "baz ad başına tek `unitur`" (~8 satır). Uygulandı — ama düzeltme öncesi DB'de koşturunca **Kıbrıs Amerikan'ı hiç görmüyor**: o kurumun her iki yazımı da `DEVLET` etiketli, yani baz ad içinde çelişki yok. Bu kontrol tek başına 100 satırın yalnız 58'ini koruyor; 42'si (öğrenciye sızan 27 programın 12'si) denetimsiz kalıyordu. + +Bu yüzden **kontrol 14** eklendi (6 satır): *DEVLET etiketli bir kurumun en az bir programı Türkiye ilinde olmalı.* Türkiye illerinde tire geçmiyor, yurt dışı iller tireli, KKTC illeri `KKTC_ILLERI`'nde; hiç ili olmayan kurumlar (MSB/İçişleri) hariç tutuldu. Düzeltme öncesi DB'de yanlış 4 yazımın **dördünü birden**, sıfır yanlış pozitifle yakalıyor. + +Kapsam **dışında bırakılanlara dokunulmadı**: `il` boş 19 satır, burs/dil ekli adlar (B4), `kategoriBul` düzeltmesi. + +## 5. Yeni bulgular (bu geceye alınmadı — backlog önerisi) + +| # | Bulgu | Sorgu / kanıt | Önem | +|---|---|---|---| +| A | **`programs_arsiv`'de aynı yanlış etiket sürüyor**: 31 satır ULUSLARARASI BALKAN + 30 satır ULUSLARARASI SARAYBOSNA `unitur='DEVLET'`, 24 satırda il Saraybosna. CEO §6'nın veri mühendisine sorduğu soru buydu. | `SELECT universite, unitur, il, COUNT(*) FROM programs_arsiv WHERE universite LIKE '%BALKAN%' OR universite LIKE '%SARAYBOSNA%' GROUP BY 1,2,3` | **Düşük — kullanıcıya yansımıyor.** `src/` tarafında `programs_arsiv`'i okuyan hiçbir SQL yok (tablo yalnızca `src/lib/kapanan-sluglar.ts`'te bir yorumda geçer); `db.ts` havuzu yalnız `programs`'tan besleniyor. Yine de bir sonraki denetimde yeniden "bulgu" olarak çıkacak; `KURUM_DUZELT`'i arşive de uygulamak tek satırlık iş. | +| B | **`data/yokatlas.db-shm` ve `-wal` git'te izleniyor** (32 KB + 0 bayt). Bunlar SQLite'ın geçici dosyaları: DB her açılıp kapandığında oluşup siliniyor, dolayısıyla `git status` kendiliğinden kirleniyor. | `git ls-files data/` | **Düşük.** Uygulamanın `better-sqlite3` ile `readonly` açılışı eski `-shm` yanındayken de sorunsuz çalışıyor (ölçtüm); ama `sqlite3 -readonly` CLI'ı bu durumda "unable to open database file (14)" veriyor — denetim sorgusu koşarken yanıltır. Öneri: `git rm --cached` + `.gitignore`. **Bu commit'e dahil etmedim**: `.gitignore`'da Bilal'in commit'lenmemiş değişikliği var. | +| C | Baz ad çelişkisi kalan 2 kurum: Manas ve Hoca Ahmet Yesevi (`YURTDISI KAMU` ↔ `YURTDISI VAKIF`). | kontrol 13'ün `bilgi` satırı | **Yok.** İkisi de `<> 'DEVLET'` olduğu için devlet/vakıf filtresini bölmüyor; kurum gerçekten ortak. Bilerek ZORUNLU yapılmadı, `bilgi` olarak görünür duruyor. | + +## 6. Kullanıcıya yansıyan etki (düzelen) + +| Yüzey | Önce | Sonra | +|---|---|---| +| Sihirbaz → "Devlet üniversitesi" | Havuzda 27 KKTC/Bosna/Makedonya programı | 0 | +| Sihirbaz → "Vakıf" | Aynı 27 program eksik | 27'si havuzda | +| `/universiteler` hub'ı | 3 kurum (4 yazım) "Devlet üniversiteleri" listesinde | "Diğer" grubunda | +| `/universite/uluslararasi-balkan-universitesi` | "Tür: Devlet", şehir Saraybosna (Bosna) | Tür: Yurt dışı kamu, şehir Üsküp-Makedonya | +| Tadımlık satırı (`tadimlik-satiri.tsx:37`) | Aynı semantik, aynı sızıntı | Düzeldi (aynı `unitur` alanını okur) | + +## Bilal'den istenen + +1. **Dalı incele ve merge kararını ver** (`gece/0922-unitur-kacagi`, 2 commit: kod+rapor, veri). Fark raporu §2'de ve veri commit'inin mesajında; geri dönüş için `veri-2025-son` etiketi duruyor. **~10 dk.** +2. **Elindeki ham CSV'de tek `grep`** — `data/ham/` repoda yok: bu üç kurum kaynak dosyada gerçekten `DEVLET` mi yazıyor? Doğrularsa teşhis kapanır, `ingest.ts`'e aynı harita eklenir. **~5 dk.** +3. **Karar:** §5-A (arşiv tablosundaki aynı yanlış etiket) ve §5-B (`-shm`/`-wal` git'ten çıkarılsın mı) backlog'a mı, yoksa bu dala ek commit mi? İkisi de kullanıcıya yansımıyor. **~2 dk.** diff --git a/scripts/unitur-onar.ts b/scripts/unitur-onar.ts index f192d52..11dbfff 100644 --- a/scripts/unitur-onar.ts +++ b/scripts/unitur-onar.ts @@ -3,8 +3,9 @@ * * YÖK Atlas API'si bazı kayıtlarda universiteTuru döndürmüyor; DB'de ~2.081 * satır unitur=NULL kalıyor ve bunlar hem devlet/vakıf filtrelerinden hem - * sihirbaz sayaçlarından sessizce düşüyor. Onarım üç aşamalı: + * sihirbaz sayaçlarından sessizce düşüyor. Onarım dört aşamalı: * + * D) Kaynak veride DOLU ama YANLIŞ olan kurum alanları (KURUM_DUZELT) * A) Aynı üniversitenin dolu satırından kopyala (Nişantaşı, Beykent…) * B) Kalanlarda il KKTC yerleşimiyse unitur='KKTC' * C) Küçük manuel eşleme (Cerrahpaşa, MSB/İçişleri, Alanya, Manas…) @@ -13,7 +14,8 @@ * duplikat adlar normalize edilir; refresh.ts insert yolunda da aynı * normalizasyon uygulanır ki duplikat bir daha oluşmasın. * - * İdempotenttir; yalnızca NULL satırlara dokunur. + * İdempotenttir. A/B/C yalnızca NULL satırlara dokunur; D (KURUM_DUZELT) + * NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama yanlış. * * Kullanım: npx tsx scripts/unitur-onar.ts (dev sunucu KAPALIYKEN — * uygulama DB'yi readonly açar ama WAL checkpoint için tek yazar olmalı) @@ -26,7 +28,8 @@ export function uniAdiNormalize(ad: string): string { return ad.replace(/\s*-\s*/g, "-").replace(/\s{2,}/g, " ").trim(); } -const KKTC_ILLERI = [ +/** KKTC il değerleri — tek kaynak; veri-kalite.ts de bunu okur. */ +export const KKTC_ILLERI = [ "GİRNE", "LEFKOŞA", "GAZİMAĞUSA", @@ -51,6 +54,44 @@ const MANUEL_IL: Record = { "İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "İSTANBUL", }; +/** + * Aşama D — kaynak veride DOLU ama YANLIŞ olan kurum alanları. + * + * MANUEL_UNITUR'dan farkı: burada değer NULL değil, yanlış. Bu yüzden bu + * harita NULL koşulu OLMADAN çalışır. Her satırın kanıtı kurumun YÖK Atlas'ta + * geçen parantezli ikinci yazımıdır (aynı kurum, doğru etiketli): + * + * "ULUSLARARASI BALKAN ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" → YURTDISI KAMU + * "ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ (SARAYBOSNA-…)" → YURTDISI KAMU + * "KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)" → ili KKTC + * + * Yanlış etiket ilk CSV ingest'inden (scripts/ingest.ts) geliyor; refresh.ts + * `uniturSec` DB'deki değeri sadakatle kopyaladığı için her tazelemede + * yeniden üretiliyordu. Düzeltme buraya yazılınca kalıcı olur. + * + * Etki: `src/lib/db.ts` devlet filtresi (`unitur = 'DEVLET'`) bu kurumların + * programlarını sihirbaz havuzuna alıyordu; vakıf filtresi (`unitur <> + * 'DEVLET'`) ise dışarıda bırakıyordu. + * + * `il` düzeltmesi yalnız YANLIŞ yazılmış değeri hedefler (yanlis → dogru); + * boş il'ler kapsam dışı — eksik, eksik olarak görünmeli. + */ +const KURUM_DUZELT: Record< + string, + { unitur?: string; il?: { yanlis: string; dogru: string } } +> = { + "KIBRIS AMERİKAN ÜNİVERSİTESİ": { unitur: "KKTC" }, + "KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)": { unitur: "KKTC" }, + "ULUSLARARASI BALKAN ÜNİVERSİTESİ": { + unitur: "YURTDISI KAMU", + // Kurum Üsküp'te; 26 satırda ili Saraybosna yazıyor. Doğru değerin yazımı + // DB'deki diğer yurt dışı illerin biçimini izler (BİŞKEK-KIRGIZİSTAN…) + // ve kurumun kendi parantezli adından alınmıştır. + il: { yanlis: "SARAYBOSNA - BOSNA - HERSEK", dogru: "ÜSKÜP-MAKEDONYA" }, + }, + "ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ": { unitur: "YURTDISI KAMU" }, +}; + export function uniturOnar(db: Database.Database): void { // 0) Üniversite adı normalizasyonu (Cerrahpaşa duplikatını birleştirir) const adlar = db @@ -69,6 +110,28 @@ export function uniturOnar(db: Database.Database): void { } if (duzeltilen > 0) console.log(`Ad normalizasyonu: ${duzeltilen} kurum`); + // D) Dolu ama yanlış kurum alanları. A'dan ÖNCE koşar: yanlış değer başka + // satırlara kopyalanmadan düzelsin. NULL koşulu yok; değer zaten dolu. + const dUnitur = db.prepare( + "UPDATE programs SET unitur = ? WHERE universite = ? AND unitur IS NOT ?" + ); + const dIl = db.prepare( + "UPDATE programs SET il = ? WHERE universite = ? AND il = ?" + ); + let dUniturToplam = 0; + let dIlToplam = 0; + for (const [ad, duzelt] of Object.entries(KURUM_DUZELT)) { + if (duzelt.unitur) { + dUniturToplam += dUnitur.run(duzelt.unitur, ad, duzelt.unitur).changes; + } + if (duzelt.il) { + dIlToplam += dIl.run(duzelt.il.dogru, ad, duzelt.il.yanlis).changes; + } + } + console.log( + `Aşama D (yanlış kurum alanı): unitur ${dUniturToplam} satır, il ${dIlToplam} satır` + ); + // A) Aynı üniversitenin dolu satırından kopyala. Birden fazla FARKLI dolu // değer varsa sessizce seçim yapma — kaynak veride tutarsızlık demektir. const celiskiler = db diff --git a/scripts/veri-kalite.ts b/scripts/veri-kalite.ts index 862e848..e011e4a 100644 --- a/scripts/veri-kalite.ts +++ b/scripts/veri-kalite.ts @@ -21,6 +21,7 @@ import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari"; import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index"; import { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik"; import { hamOku, sonHamDosya, TUR_MAP } from "./refresh"; +import { KKTC_ILLERI } from "./unitur-onar"; import { argOku, bayrakVar, @@ -291,6 +292,46 @@ async function main() { console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`); } + // 13. Aynı kurumun iki yazımı arasında unitur çelişkisi — BAZ AD üzerinden. + // unitur-onar.ts'in eski çelişki koruması GROUP BY universite (TAM ad) ile + // çalıştığı için "X ÜNİVERSİTESİ" ve "X ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" iki + // ayrı kurum sayılıyor, aralarındaki DEVLET↔YURTDISI KAMU çelişkisi hiç + // görünmüyordu. Baz ad = sondaki parantezli ek atılmış hâl. + const BAZ_AD = + "TRIM(CASE WHEN instr(universite, '(') > 0 THEN substr(universite, 1, instr(universite, '(') - 1) ELSE universite END)"; + const bazAdCeliskileri = `(SELECT ${BAZ_AD} AS bazAd, GROUP_CONCAT(DISTINCT unitur) AS turler, COUNT(*) AS adet, + MAX(unitur = 'DEVLET') <> MIN(unitur = 'DEVLET') AS devletBolunmus + FROM programs WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`; + const bolunmus = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE devletBolunmus`); + zorunlu( + "baz ad başına tek unitur", + bolunmus === "", + bolunmus === "" + ? "aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor" + : `devlet filtresine giren, vakıf filtresinden düşen kurum: ${bolunmus} → scripts/unitur-onar.ts KURUM_DUZELT` + ); + const esdegerFark = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE NOT devletBolunmus`); + bilgi("baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen)", false, esdegerFark || "yok"); + + // 14. DEVLET etiketli ama hiçbir programı Türkiye ilinde olmayan kurum. + // 13'ün kör noktası: kurumun İKİ yazımı da yanlış etiketliyse çelişki yoktur. + // Türkiye illerinde tire geçmez; yurt dışı iller tireli (BİŞKEK-KIRGIZİSTAN…), + // KKTC illeri KKTC_ILLERI'nde. İli hiç olmayan kurumlar (MSB/İçişleri) hariç. + const yurtDisiDevlet = ornek( + `SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM programs + WHERE unitur = 'DEVLET' GROUP BY universite + HAVING SUM(il IS NOT NULL) > 0 + AND SUM(il NOT LIKE '%-%' AND il NOT IN (${KKTC_ILLERI.map(() => "?").join(",")})) = 0`, + ...KKTC_ILLERI + ); + zorunlu( + "DEVLET etiketi Türkiye ili ile destekli", + yurtDisiDevlet === "", + yurtDisiDevlet === "" + ? "DEVLET etiketli her kurumun en az bir programı Türkiye ilinde" + : `tüm programları KKTC/yurt dışında olduğu hâlde DEVLET: ${yurtDisiDevlet}` + ); + db.close(); console.log(`\nSonuç: ${basarisiz} ZORUNLU kontrol kaldı, ${uyari} uyarı.`); if (basarisiz > 0) process.exit(1); From 1df86cfa15336dfe8c417d50efe1a064ca303c0d Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Wed, 23 Sep 2026 00:24:58 +0300 Subject: [PATCH 2/4] =?UTF-8?q?fix(veri):=20yokatlas.db=20=E2=80=94=20126?= =?UTF-8?q?=20sat=C4=B1r=20unitur/il=20d=C3=BCzeltmesi=20(100=20+=2026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx tsx scripts/unitur-onar.ts` çıktısı: Aşama D (yanlış kurum alanı): unitur 100 satır, il 26 satır Aşama A/B/C: 0 satır · Kalan NULL unitur: 0 KIBRIS AMERİKAN ÜNİVERSİTESİ DEVLET → KKTC 36 KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA) DEVLET → KKTC 6 ULUSLARARASI BALKAN ÜNİVERSİTESİ DEVLET → YURTDISI KAMU 34 ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ DEVLET → YURTDISI KAMU 24 ULUSLARARASI BALKAN ÜNİVERSİTESİ il: SARAYBOSNA - BOSNA - HERSEK → ÜSKÜP-MAKEDONYA 26 unitur dağılımı: DEVLET 12142→12042, KKTC 1145→1187, YURTDISI KAMU 99→157. Toplam satır sabit. FARK RAPORU (öncesi `.backup` kopyasıyla ATTACH + kolon kolon kıyas): programs önce 21493 → sonra 21493 programs_arsiv önce 3735 → sonra 3735 (unitur/il değişen: 0) netler önce 62975 → sonra 62975 veri_meta önce 37 → sonra 37 programs, 45 kolon tarandı — DEĞİŞEN KOLON: unitur=100, il=26 2021-2026 yıl kolonları (34 kolon: sıra/puan/kontenjan/yerleşen + 2025 ek kontenjanları) değişen satır: 0 `pnpm veri-kalite --yil 2026 --db data/yokatlas.db --onceki <öncesi>` bağımsız olarak doğruladı: GEÇTİ önceki DB: satır kaybı — 0 GEÇTİ önceki DB: geçmiş yıllar korunmuş — 20 kolonda değişen: 0 Sonuç: 0 ZORUNLU kontrol kaldı, 3 uyarı (üçü de düzeltme öncesinde de vardı: il boş 110, kimlik ATLANDI, yetim netler 2066) Havuz etkisi (db.ts:79-82 + efektif sırası dolu satırlar): devlet filtresi 27 → 0 · vakıf filtresi 0 → 27 /universiteler hub'ında Devlet listesi 170 → 166 kurum. Geri alma: bu commit'i revert etmek yeter; veri-2025-son etiketi duruyor. Co-Authored-By: Claude Opus 5 --- data/yokatlas.db | Bin 17526784 -> 17534976 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/yokatlas.db b/data/yokatlas.db index 302d2af61020bb13d6cfbc962b0ef653b543f4ff..6bc7b0e0a538ddc1116b067bd7629f1c9ca7fedc 100644 GIT binary patch delta 7893 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 Date: Wed, 23 Sep 2026 07:26:56 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(veri):=20KURUM=5FDUZELT=20ar=C5=9Fiv=20?= =?UTF-8?q?tablosunu=20da=20tarar=20(BACKLOG=20#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dün Aşama D yalnız `programs`'a uygulanmıştı; `programs_arsiv`'de aynı yanlış etiket duruyordu (31 ULUSLARARASI BALKAN + 30 ULUSLARARASI SARAYBOSNA `unitur='DEVLET'`, Balkan'ın 24 satırında il Saraybosna). Kapanan programlar arşive `programs`'tan olduğu gibi taşındığı için etiket oraya da yazılmış. - `unitur-onar.ts` Aşama D iki tabloyu da tarar (`programs_arsiv` varsa). Dokunulan kolon yalnız `unitur` ve `il`; A/B/C aşamaları `programs`'ta kalır. - `veri-kalite.ts` kontrol 13 ve 14 arşivi de tarar. Genişletme olmadan iki tablo ayrışabilir ve denetim bunu görmez. - Yanlış etiket duran DB'de iki kontrol de KALDI veriyor (çıkış kodu 1): `baz ad başına tek unitur — … BALKAN/YURTDISI KAMU,DEVLET/81 ; … SARAYBOSNA/68` `DEVLET etiketi Türkiye ili ile destekli — … /SARAYBOSNA - BOSNA - HERSEK/31 ; /30` Kullanıcıya bugün yansımıyor: `src/` tarafında `programs_arsiv`'i okuyan SQL yok. Düzeltme tablo ileride okunduğunda sızıntının geri gelmemesi içindir. Dünkü veri commit'inin (1df86cf) gövdesindeki rakam düzeltmesi: "/universiteler hub'ında Devlet listesi 170 → 166" YANLIŞTIR. 170/166 ham `programs.universite` ad sayısıdır, hub satırı değil; `uniAdiNormalize` parantezli eki attığı için Kıbrıs Amerikan'ın iki yazımı tek slug. Hub'daki doğru değişim 132 → 129 kurum (3 kurum). Kaynak: `12-kod-denetimi.md` O1, dalın kendi build HTML'i. Co-Authored-By: Claude Opus 5 --- scripts/unitur-onar.ts | 74 +++++++++++++++++++++++++++--------------- scripts/veri-kalite.ts | 12 +++++-- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/scripts/unitur-onar.ts b/scripts/unitur-onar.ts index 11dbfff..f4bdb02 100644 --- a/scripts/unitur-onar.ts +++ b/scripts/unitur-onar.ts @@ -5,7 +5,8 @@ * satır unitur=NULL kalıyor ve bunlar hem devlet/vakıf filtrelerinden hem * sihirbaz sayaçlarından sessizce düşüyor. Onarım dört aşamalı: * - * D) Kaynak veride DOLU ama YANLIŞ olan kurum alanları (KURUM_DUZELT) + * D) Kaynak veride DOLU ama YANLIŞ olan kurum alanları (KURUM_DUZELT) — + * `programs` VE `programs_arsiv` üzerinde * A) Aynı üniversitenin dolu satırından kopyala (Nişantaşı, Beykent…) * B) Kalanlarda il KKTC yerleşimiyse unitur='KKTC' * C) Küçük manuel eşleme (Cerrahpaşa, MSB/İçişleri, Alanya, Manas…) @@ -14,14 +15,16 @@ * duplikat adlar normalize edilir; refresh.ts insert yolunda da aynı * normalizasyon uygulanır ki duplikat bir daha oluşmasın. * - * İdempotenttir. A/B/C yalnızca NULL satırlara dokunur; D (KURUM_DUZELT) - * NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama yanlış. + * İdempotenttir. A/B/C yalnızca `programs` tablosunun NULL satırlarına dokunur; + * D (KURUM_DUZELT) NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama + * yanlış — ve `programs_arsiv`'i de tarar (varsa). * * Kullanım: npx tsx scripts/unitur-onar.ts (dev sunucu KAPALIYKEN — * uygulama DB'yi readonly açar ama WAL checkpoint için tek yazar olmalı) */ import path from "node:path"; import Database from "better-sqlite3"; +import { tabloVar } from "./yokatlas-goc"; /** Tire etrafı boşluk + çift boşluk temizliği (üniversite adı için). */ export function uniAdiNormalize(ad: string): string { @@ -75,6 +78,11 @@ const MANUEL_IL: Record = { * * `il` düzeltmesi yalnız YANLIŞ yazılmış değeri hedefler (yanlis → dogru); * boş il'ler kapsam dışı — eksik, eksik olarak görünmeli. + * + * Aynı yanlış etiket `programs_arsiv`'e de yazılmıştı (kapanan programlar oraya + * `programs`'tan olduğu gibi taşınıyor). Bugün `src/` tarafında arşivi okuyan + * SQL yok, ama tablo ileride okunursa sızıntı geri gelirdi; bu yüzden Aşama D + * iki tabloyu da tarar. Dokunulan kolon yalnız `unitur` ve `il`. */ const KURUM_DUZELT: Record< string, @@ -112,25 +120,32 @@ export function uniturOnar(db: Database.Database): void { // D) Dolu ama yanlış kurum alanları. A'dan ÖNCE koşar: yanlış değer başka // satırlara kopyalanmadan düzelsin. NULL koşulu yok; değer zaten dolu. - const dUnitur = db.prepare( - "UPDATE programs SET unitur = ? WHERE universite = ? AND unitur IS NOT ?" - ); - const dIl = db.prepare( - "UPDATE programs SET il = ? WHERE universite = ? AND il = ?" - ); - let dUniturToplam = 0; - let dIlToplam = 0; - for (const [ad, duzelt] of Object.entries(KURUM_DUZELT)) { - if (duzelt.unitur) { - dUniturToplam += dUnitur.run(duzelt.unitur, ad, duzelt.unitur).changes; - } - if (duzelt.il) { - dIlToplam += dIl.run(duzelt.il.dogru, ad, duzelt.il.yanlis).changes; + // `programs` ve (varsa) `programs_arsiv` aynı haritadan geçer — iki tablo + // ayrışırsa arşiv ileride okunduğunda aynı sızıntı geri gelir. + const dTablolar = tabloVar(db, "programs_arsiv") + ? ["programs", "programs_arsiv"] + : ["programs"]; + for (const tablo of dTablolar) { + const dUnitur = db.prepare( + `UPDATE ${tablo} SET unitur = ? WHERE universite = ? AND unitur IS NOT ?` + ); + const dIl = db.prepare( + `UPDATE ${tablo} SET il = ? WHERE universite = ? AND il = ?` + ); + let dUniturToplam = 0; + let dIlToplam = 0; + for (const [ad, duzelt] of Object.entries(KURUM_DUZELT)) { + if (duzelt.unitur) { + dUniturToplam += dUnitur.run(duzelt.unitur, ad, duzelt.unitur).changes; + } + if (duzelt.il) { + dIlToplam += dIl.run(duzelt.il.dogru, ad, duzelt.il.yanlis).changes; + } } + console.log( + `Aşama D (yanlış kurum alanı) ${tablo}: unitur ${dUniturToplam} satır, il ${dIlToplam} satır` + ); } - console.log( - `Aşama D (yanlış kurum alanı): unitur ${dUniturToplam} satır, il ${dIlToplam} satır` - ); // A) Aynı üniversitenin dolu satırından kopyala. Birden fazla FARKLI dolu // değer varsa sessizce seçim yapma — kaynak veride tutarsızlık demektir. @@ -211,11 +226,18 @@ if (process.argv[1]?.endsWith("unitur-onar.ts")) { db.pragma("journal_mode = WAL"); db.transaction(() => uniturOnar(db))(); db.pragma("wal_checkpoint(TRUNCATE)"); - const dagilim = db - .prepare( - "SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM programs GROUP BY unitur ORDER BY n DESC" - ) - .all() as { t: string; n: number }[]; - console.log("Dağılım:", dagilim.map((d) => `${d.t}=${d.n}`).join(" ")); + for (const tablo of tabloVar(db, "programs_arsiv") + ? ["programs", "programs_arsiv"] + : ["programs"]) { + const dagilim = db + .prepare( + `SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM ${tablo} GROUP BY unitur ORDER BY n DESC` + ) + .all() as { t: string; n: number }[]; + console.log( + `Dağılım (${tablo}):`, + dagilim.map((d) => `${d.t}=${d.n}`).join(" ") + ); + } db.close(); } diff --git a/scripts/veri-kalite.ts b/scripts/veri-kalite.ts index e011e4a..be5bfab 100644 --- a/scripts/veri-kalite.ts +++ b/scripts/veri-kalite.ts @@ -299,9 +299,17 @@ async function main() { // görünmüyordu. Baz ad = sondaki parantezli ek atılmış hâl. const BAZ_AD = "TRIM(CASE WHEN instr(universite, '(') > 0 THEN substr(universite, 1, instr(universite, '(') - 1) ELSE universite END)"; + // 13 ve 14 arşivi de tarar: kapanan programlar `programs_arsiv`'e olduğu gibi + // taşındığı için yanlış etiket orada da duruyordu (BACKLOG #21). Bugün `src/` + // tarafında arşivi okuyan SQL yok, ama iki tablo ayrışırsa denetim bunu + // görmez ve tablo ileride okunduğunda sızıntı geri gelir. + const KURUMLAR = tabloVar(db, "programs_arsiv") + ? `(SELECT universite, unitur, il FROM programs + UNION ALL SELECT universite, unitur, il FROM programs_arsiv)` + : "programs"; const bazAdCeliskileri = `(SELECT ${BAZ_AD} AS bazAd, GROUP_CONCAT(DISTINCT unitur) AS turler, COUNT(*) AS adet, MAX(unitur = 'DEVLET') <> MIN(unitur = 'DEVLET') AS devletBolunmus - FROM programs WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`; + FROM ${KURUMLAR} WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`; const bolunmus = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE devletBolunmus`); zorunlu( "baz ad başına tek unitur", @@ -318,7 +326,7 @@ async function main() { // Türkiye illerinde tire geçmez; yurt dışı iller tireli (BİŞKEK-KIRGIZİSTAN…), // KKTC illeri KKTC_ILLERI'nde. İli hiç olmayan kurumlar (MSB/İçişleri) hariç. const yurtDisiDevlet = ornek( - `SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM programs + `SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM ${KURUMLAR} WHERE unitur = 'DEVLET' GROUP BY universite HAVING SUM(il IS NOT NULL) > 0 AND SUM(il NOT LIKE '%-%' AND il NOT IN (${KKTC_ILLERI.map(() => "?").join(",")})) = 0`, From c5fd2cb10f3b184a344e723dd434030819efcf83 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Wed, 23 Sep 2026 07:28:20 +0300 Subject: [PATCH 4/4] =?UTF-8?q?fix(veri):=20yokatlas.db=20=E2=80=94=20prog?= =?UTF-8?q?rams=5Farsiv=2085=20sat=C4=B1r=20unitur/il=20d=C3=BCzeltmesi=20?= =?UTF-8?q?(61=20+=2024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npx tsx scripts/unitur-onar.ts` çıktısı: Aşama D (yanlış kurum alanı) programs: unitur 0 satır, il 0 satır Aşama D (yanlış kurum alanı) programs_arsiv: unitur 61 satır, il 24 satır Aşama A/B/C: 0 / 0 / 0 satır · Kalan NULL unitur: 0 unitur: ULUSLARARASI BALKAN 31 + ULUSLARARASI SARAYBOSNA 30 = 61 DEVLET → YURTDISI KAMU il : ULUSLARARASI BALKAN 24 satır SARAYBOSNA - BOSNA - HERSEK → ÜSKÜP-MAKEDONYA (Saraybosna Ü.'nin ili değişmedi — kurum gerçekten Saraybosna'da. Kıbrıs Amerikan'ın arşivde hiç satırı yok.) Fark raporu — düzeltme öncesi DB `sqlite3 .backup` ile alındı, ATTACH ile her tablonun HER kolonu id üzerinden karşılaştırıldı: programs: eski 21493 → yeni 21493 | 45 kolon | kayıp/yeni id 0 | DEĞİŞEN: yok programs_arsiv: eski 3735 → yeni 3735 | 45 kolon | kayıp/yeni id 0 | DEĞİŞEN: unitur=61, il=24 netler: eski 62975 → yeni 62975 veri_meta: eski 37 → yeni 37 her iki tabloda 34 yıl/ölçü kolonu (sıra/puan/kontenjan/yerleşen + 2025 ek kontenjanları, 2021-2026) Σ sağlaması BİREBİR AYNI ✓ Yani puan/sıra/kontenjan/yerleşen kolonlarında değişen satır: 0. Doğrulama: - `pnpm veri-kalite --yil 2026 --db data/yokatlas.db` → 0 ZORUNLU kontrol kaldı, 3 uyarı. Üç uyarı düzeltme öncesiyle birebir aynı, hiçbiri bu işten doğmadı (il boş 110, kimlik ATLANDI, yetim netler 2066). - `programs_arsiv` ile `programs` arasında `unitur` farkı olan kurum: 0 (önce 2). - Betik ikinci kez koşuldu: her aşamada 0 satır (idempotent). - Dosya 4 KB büyüdü (bir sayfa, WAL checkpoint); satır sayıları sabit. Co-Authored-By: Claude Opus 5 --- data/yokatlas.db | Bin 17534976 -> 17539072 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/yokatlas.db b/data/yokatlas.db index 6bc7b0e0a538ddc1116b067bd7629f1c9ca7fedc..3926e7280a87474ff6186e8f4bc1c439b282b4ff 100644 GIT binary patch delta 4498 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 delta 4008 zcmZ|S30zdi9S888z25BGU3Ou)R)hzNiV_wUk^5L&@Zk19Gzfym1C13MZ>_Q#(Ii!| zYTmR_M^j_kv?|sl8q>I#*f!QyF(%cR+F*0kG{&f@tu=|}_ZF2E)X(QTpAYl?|M%w2 z%zxh6)V7y4?bu6OB6M1n%KFB}=scp`U$?7V(`IZwAXF3+`D9Q`rrKU5Up(5U*QS;U zwu5iR+&*F0)LhLY*0SYDTEk|dZDwQ8u4cJtpJwr^Toc#0CK6DC2DG39JqSR-03sN{ z1ZD^T3s@l#g1`pB5CWkP2H{|bUJwDjArhiMf@p|=KF}9pp&#^z0T2fRVGzW_V~_w2 zNQ5MCLNW}76c_@jkOt|H0hy2m*^mRdkOwZvhoRtx0w{!GFdRm}NGO70D1lNagL0^V zQ7{_Dz*wk+aZm;0VFEl3)u7CG4b;LUsDpZ#3{&6tO@5 zz()8PY=X_O1)c{P82lW50WZK-*aq8y!w%R9FTyWj7rX?!;br(0{2E??-@tES5A20~ z&da{sMo6 zlW+>!;WWGlXW)JK0R9GN;T(Jj=iwvxJ6wQ&z{k)57vZ1q3498l!RPQVxCCFozu`-` z3}3+&_!_RlHMkDnz<=Nd{1^TQH{t(q3%-T#;Cr|Ycb-G-ZvdrHD~(2Jv`V8>8a?si z6d*^>3{q$#+aDTR{NegSgFb$158j^0o@ zj}7PyvJUAPl+JIUH;B$+{X1X5lKRM3cpsPItezFDpYI*qW9)=#1fK5|NFTm$G$neT zl19l`zj5$ZbX4KBV;z!&aaz(Tnc{a{8D~0w550+W3hV2TA`xWBpk#87pcClrYh9^h zeH>CTCZ{G|dAeMG<<7l_uT7$)s7GG16Eo<&f^3^U9xH28%}`mg1FFr7O?!-&jNxKG zLlIpfysW>eAF11*-K{yL{z@H6hRYRg>%(PNhb~t1RI^-%lu>DS^Q)DV%<_MR*IJOq zTgTz$Z=Cb4drY|=Zo+%{9Wr8EBh9Z6kz%cB`n1uB`V7t zg}5*c{*x{uqQ{e{4*xv)EQw#n%9hkt8B6nEK&YmC*c_ZA)(xi z#k6tPY)Z8J05(;M-`#y%AuZO7frO}PRTg_dy}8-+wn=X+5?33J8FX~A@RlIzGj)sh z#a2+F;a3}!x-WM~qcG%A_#=!*&&0{h56z;aM83MfCh!Z0Og4#_oJ>~M6`91Fo_rw2 zDOxb4rKs_4_bS9-E4tq+mz8uyCh~@QN=9|XIElr34ry^}!6UH|s2O>Gzp@ay@$wKI zfBw3b1bR24&Xb7f$zj7BQoORsc7B-<#_=Ga2(U@5+pM|Rrt7?^rbWR z!!tjBbY?)XlIdbY@7Cws+K1Gq)i#)Z=__2+!iGY@=)rE^-V)GtMzRlRUzMbi;xg*ol8ksXVB=Sx$$P->_)H} zhm=)mmv`PQwJG~h&)czVq5KS@bGvq~NzRQL8EnM#MzV=m?uTR~PgXX{{n|3g+p`mF zz5=lx66Lsh9dYiik#$ym5*ybQnaJl&dmu)s`r)k7A(hwIw@;^J zX;-?FA4Z3jX0Wlo%f169iK}NQXX5?WciJq4}0Ta2XAJ00y+ zqxa4-oIq;tGuw@1Az9x<$&n@lAv+|bnS>~nB{yJ;`K-B@X_~Rs7$;U3R@1}6H$v1F zXU^FzuAFxtMRv8t?ej(r5Xn}e^$gz@!jGV++l(bzd>t^C!?y+ddLrctA>YiG4-$#U c*Y_8(Il6ws`h9+hxS<3w-jYE^^6Jzd06OaNzW@LL