Compare commits

...

6 Commits

Author SHA1 Message Date
bilalgursen
9e9de84a7e merge: fix/2026-yayin-sonrasi — tadımlık canlı tabandan, liste riskleri yenilenir, 30 program kimliği onarıldı
All checks were successful
Deploy / deploy (push) Successful in 7m11s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 23:15:47 +03:00
bilalgursen
0012e0f726 docs(kararlar): 2026 yayın kaydı, yayın sonrası denetim ve gece oturumu kararları
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 23:15:47 +03:00
bilalgursen
75fc12d583 merge: veri/2026-kimlik-onar — kodu devredilmiş 30 programın kimliği onarıldı 2026-09-21 20:26:45 +03:00
bilalgursen
8a7d9f8185 data: yokatlas.db — kimliği kaymış 30 program onarıldı
2026 ham çekimi (2026-2026-09-21.jsonl.gz) `refresh --ham … --kimlik-onar` ile
oynatıldı → db-index --vacuum (+ wal_checkpoint TRUNCATE) → veri-kalite
(0 ZORUNLU kaldı; kimlik uyumsuz 0/21.493).

- 29 satır: kılavuz kodu başka programa devredilmiş (17'sinde puan türü/önlisans
  da farklıydı). isim/tur/onlisans/fakulte/sure API'den; devir öncesi yıllar
  (26 satırda ≤2024, 3 satırda ≤2025) NULL; eski kimlik + geçmişi
  programs_arsiv'de `<kod>~<yıl>` (29 kopya), eski yılların 26 net satırı oraya
  taşındı. Ör. 209210292: "Diş Hekimliği, SAY" [53966, 430335, 306090] →
  "İlk ve Acil Yardım (İngilizce) (Burslu), TYT önlisans" [–, 430335, 306090].
- 1 satır (405210389 Manas): ad değişimi İslam Bilimleri → İlahiyat; geçmiş korundu.
- Onarılan 30 satır dışındaki 21.463 satırda tüm yıl kolonlarının COUNT/TOTAL
  sağlaması ve kimlik kolonları birebir aynı. programs 21.493; programs_arsiv
  3.706 → 3.735; netler 62.975 (değişmedi). Sorgu planı
  idx_programs_tur_onlisans_efektif. integrity_check ok.
- Katalog: /bolum slug'ı 637 → 634 (taki-tasarimi, islam-bilimleri,
  endustri-yonetimi-muhendisligi yalnız yanlış etiketli satırlardan oluşuyordu);
  /universite 229 (değişmedi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 20:25:22 +03:00
bilalgursen
2ff8fe584b fix(veri): kimlik kilidi — kodu başka programa devredilmiş satırlar yakalanır ve onarılır
Kılavuz kodu başka programa devredildiğinde (ör. 209210292: Diş Hekimliği SAY
lisans → İlk ve Acil Yardım TYT önlisans) refresh yalnız koda bakıp yıl
kolonlarını yazıyordu; satır eski adıyla yeni programın sırasını gösteriyordu.

- scripts/kimlik.ts (yeni): kimlik = puan türü + önlisans + baz ad slug'ı.
  Devir yılı kanıt zinciriyle bulunur (API …1/…2/…3 sıra/puanı DB ile eşleşen
  ardışık yıllar + netler.puanTuru); API geçmişi kod bazlı ve kirli olduğu
  için (gk2, bazı basariSirasi3) zincirsiz güvenilmez. Yalnız adı değişen ve
  API penceresinin tamamında sürekliliği olan satır "ad değişimi"dir, geçmişi
  korunur.
- refresh: uyumsuz satır varsa listeler ve HİÇBİR ŞEY yazmadan durur;
  --kimlik-onar ile isim/tur/onlisans/fakulte/sure API'den yazılır, devir
  öncesi tüm yıl kolonları NULL'lanır, eski kimlik + geçmişi programs_arsiv'e
  <kod>~<yıl> id'siyle kopyalanır, eski yılların netleri oraya taşınır,
  veri_meta'ya kimlik_onar:<kod> kaydı düşer. Önceki-yıl sağlaması onarılanlar
  dışındaki satırlar için aynen geçerli. --isim-esitle (varsayılan kapalı):
  kimliği aynı, yalnız burs/dil eki farklı satırlarda isim API'den.
- gecmis-doldur: onarılmış satırda devir öncesi yılları hiçbir kaynaktan
  doldurmaz (yoksa API'nin kod bazlı …N alanları eski programı geri yazar).
- veri-kalite: --ham ile ZORUNLU kimlik kontrolü (0 uyumsuz beklenir);
  --onceki karşılaştırmasında onarılanlar ayrı doğrulanır.
- docs/veri/yillik-guncelleme.md: kimlik kilidi adımı.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 20:24:29 +03:00
bilalgursen
3cf8ec94a0 fix(veri): tadımlık örneği canlı tabanı gösterir; sunucudan gelen liste riskleri yeniden hesaplanır
Yayın sonrası denetim (2026-09-21):
- tadımlık havuzu snapshot'ı 2025 tabanını taşıyordu: örnek satır 'Tıp ~5.118,
  az riskli' derken aynı sayfadaki tablo 4.481 gösteriyordu. Taban ve sıra
  geçmişi artık okuma anında canlı DB'den; güncel kılavuzdan çıkmış program
  örnek gösterilmez (sıradaki kategoriye düşülür)
- sunucuylaBirlestir riskleri her zaman yeniler: sunucu kaydı taze taban +
  başka cihazın bayat riskini taşıyabiliyordu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 20:06:35 +03:00
10 changed files with 605 additions and 18 deletions

Binary file not shown.

View File

@@ -101,3 +101,21 @@ Kayıt biçimi:
- Yetki: [BİLAL] (Bilal onayladı) - Yetki: [BİLAL] (Bilal onayladı)
- Gerekçe: Öğrenci kılavuzda olmayan programı tercih edemez; P-A kalite raporu (2025 ve öncesi değerler 0 fark, sira2026/sira2025 oranı %99,12 makul aralıkta). - Gerekçe: Öğrenci kılavuzda olmayan programı tercih edemez; P-A kalite raporu (2025 ve öncesi değerler 0 fark, sira2026/sira2025 oranı %99,12 makul aralıkta).
- Yerine geçtiği karar: 2026-09-21 — "Son 6 yıl (2021–2026)"; kılavuzda olmayan eski program kayıtları… (sayılar güncellendi) - Yerine geçtiği karar: 2026-09-21 — "Son 6 yıl (2021–2026)"; kılavuzda olmayan eski program kayıtları… (sayılar güncellendi)
## 2026-09-21 — Yayın kaydı: 2026 yerleştirme verisi canlıda (d275e4d, 49683fa)
- Karar: `veri/2026-entegrasyon` 21 Eylül 2026 18:51'de `main`'e alındı (d275e4d), ardından 49683fa (kayıtlı manuel liste tabanlarının yeni veri yılına tazelenmesi + 4 rehber yazısında yıl metinleri) ile birlikte push edildi. Sitedeki taban sıraları, risk hesabı ve liste önerileri bu tarihten itibaren 2026 verisiyle çalışır; 3.706 eski program satırı arşivde. Geri dönüş noktası: `024a2a4` (2025 verili son sürüm). **Not:** merge ve push, asıl oturum kullanım sınırındayken başka bir oturumdan yapıldı; o oturumun denetimi 49683fa'daki bulguyu üretti. Asıl oturumdaki bağımsız `kod-denetcisi` turu yarıda kesildiği için YAYIN SONRASI denetim olarak `origin/main @ 49683fa` üzerinde sürdürülüyor.
- Yetki: [BİLAL] (Bilal onayladı)
- Gerekçe: git geçmişi (d275e4d, 49683fa); Bilal'in aynı günkü yayın onayı.
- Yerine geçtiği karar: —
## 2026-09-21 — Yayın sonrası denetim: canlıda kalabilir; düzeltme paketi hazır (75fc12d, push bekliyor)
- Karar: `kod-denetcisi` `origin/main @ 49683fa` için "canlıda kalabilir, geri alma gerekmez" dedi (kritik 0, yüksek 2, orta 2, düşük 2). Hazırlanan paket `fix/2026-yayin-sonrasi @ 75fc12d` (typegen+tsc+lint+build temiz): (B1) tadımlık örneği tabanı/geçmişi canlı DB'den okur, arşivlenmiş program örnek gösterilmez; (B3) `sunucuylaBirlestir` riskleri her zaman yeniler; (B2) kılavuz kodu başka programa devredilmiş 30 satırın kimliği onarıldı (29 devir + 1 ad değişimi; eski kimlikler `programs_arsiv`'e kopyalandı, `refresh` artık kimlik uyuşmazlığında yazmadan duruyor, `veri-kalite`'de kimlik kontrolü). 3 `/bolum` sayfası daha kapanır (taki-tasarimi, islam-bilimleri, endustri-yonetimi-muhendisligi — yalnız yanlış etiketli satırlardan oluşuyordu). B4 (8.631 satırda burs/dil ekli ad) UYGULANMADI: `kategoriBul` ham isme regex uyguladığı için ek metni sihirbaz kategorisine sızıyor; önce `kategoriBul` baz ada geçirilmeli, sonra `refresh --isim-esitle`.
- Yetki: [BİLAL] (push onayı bekliyor) · ayrıca bekleyen: kapanan 27+3 sayfa için yönlendirme kararı; B4 için tek satırlık kategori düzeltmesi; prod'da `pnpm tadimlik --force`
- Gerekçe: yayın sonrası `kod-denetcisi` raporu (B1–B6) ve `veri-muhendisi` kimlik onarımı raporu, 21 Eylül 2026.
- Yerine geçtiği karar: —
## 2026-09-21 — Gece oturumu: tadımlık canlıya yüklendi; ödeme formu 17:00'den beri çalışıyor; beş karar
- Karar: (1) Canlı `tadimlik_havuzu` 0 satırdı; dış modele gidilmeden elle yazılmış 522 satır (375 tekil gerekçe; risk notu ve trend koddan) konteynere node yükleyiciyle yüklendi — deploy tohumu yalnız ilk kurulumda kopyaladığı için tadımlık deploy'la taşınmaz. Prod'da `pnpm tadimlik --force` maddesi böylece kapandı; tadımlık için LLM koşusu varsayılan yol değildir. (2) Prod `orders`: 7 Ağu–12 Eyl arası ~100 siparişin hiçbiri iyzico token'ı alamamış (form hiç açılmamış); 24 Ağu ve 12 Eyl'deki iki gerçek kullanıcı bu yüzden kayboldu. 21 Eyl 17:00 diliminde 7/7 sipariş token aldı → `fix(odeme)` (8f4669e, 024a2a4) tuttu; uçtan uca tamamlanmış ödeme henüz yok, Bilal tarayıcıdan bir kez deneyecek. (3) `fix/2026-yayin-sonrasi @ 75fc12d` push edilir. (4) Kapanan 27+3 katalog sayfası benzer sayfaya kalıcı yönlendirilir (soft 404 yerine). (5) Ana sayfa karşılaştırma tablosu yeniden kurulur (ücretsiz robotların artık yapay zekâ sunduğu kabul edilir). (6) `/paket` girişsiz CTA "ilk 3 tercihini ücretsiz gör" olur. Açık: vakıf üniversitelerinin adıyla doluluk oranı yayımı (yazı sırası gelince).
- Yetki: [BİLAL] (Bilal onayladı)
- Gerekçe: `docs/ekip-raporlari/2026-09-21/` 01 (QA), 03 (analitik §4), 04 (marka §7 #1–#2), 05 (SEO T1); prod `orders` ve `tadimlik_havuzu` sorguları (21 Eyl 2026 gece, Bilal sunucuda koştu).
- Yerine geçtiği karar: 2026-09-21 — Yayın sonrası denetim… (bekleyen push onayı, yönlendirme kararı ve `tadimlik --force` maddeleri)

View File

@@ -77,6 +77,19 @@ Güncellenen: 19…, eklenen: 1…, atlanan: 0
- Bu betik mevcut programların **eski yıl kolonlarına dokunmaz**; dokunursa kendi - Bu betik mevcut programların **eski yıl kolonlarına dokunmaz**; dokunursa kendi
sağlaması bunu yakalar ve her şeyi geri alır. sağlaması bunu yakalar ve her şeyi geri alır.
- **Kimlik kilidi.** ÖSYM bazen bir kılavuz kodunu BAŞKA programa verir (2025'te
`209210292` "Diş Hekimliği"nden "İlk ve Acil Yardım" önlisansına geçti). Betik her
mevcut satırın puan türü / önlisans / bölüm adını servistekiyle kıyaslar; uyuşmayan
varsa listeyi basıp **hiçbir şey yazmadan durur** (`KİMLİĞİ KAYMIŞ N satır`). Listeye
göz at (yılda birkaç–birkaç on satır beklenir; yüzlerce ise servis biçim değiştirmiştir,
veri mühendisine göster), sonra aynı komutu `--kimlik-onar` ekleyerek, ham dosyadan
tekrarla: `pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz --kimlik-onar`.
Onarım: ad/tür/önlisans/fakülte/süre servisten yazılır; devirden **önceki** yılların
tüm değerleri o satırda boşaltılır (başka programa aitti), eski kimlik + geçmişi
`programs_arsiv`'e `<kod>~<yıl>` id'siyle kopyalanır, eski yılların netleri oraya
taşınır. Çıktıdaki `KİMLİĞİ ONARILAN` tablosunu commit mesajına/rapora koy.
"AD DEĞİŞİMİ" yazan satırlarda yalnız ad güncellenir, geçmiş korunur.
Aynı işlemi servise gitmeden tekrarlamak için (ör. 8. adımda): Aynı işlemi servise gitmeden tekrarlamak için (ör. 8. adımda):
`pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz` `pnpm refresh --yil 2027 --db <db> --ham data/ham/2027-<tarih>.jsonl.gz`
@@ -129,7 +142,7 @@ zaten hata verir: sitedeki sıralama aralığı sorguları yavaşlar.
## 7. Kalite kapısı (1 dk) ## 7. Kalite kapısı (1 dk)
```bash ```bash
pnpm veri-kalite --yil 2027 --db /tmp/veri-2027/yeni.db --onceki /tmp/veri-2027/once.db --arsivli pnpm veri-kalite --yil 2027 --db /tmp/veri-2027/yeni.db --onceki /tmp/veri-2027/once.db --arsivli --ham data/ham/2027-<tarih>.jsonl.gz
echo "çıkış kodu: $?" # 0 olmalı echo "çıkış kodu: $?" # 0 olmalı
``` ```
@@ -137,7 +150,9 @@ echo "çıkış kodu: $?" # 0 olmalı
- Her satır `GEÇTİ`, `KALDI`, `UYARI` ya da `bilgi` ile başlar. **Tek bir `KALDI` varsa - Her satır `GEÇTİ`, `KALDI`, `UYARI` ya da `bilgi` ile başlar. **Tek bir `KALDI` varsa
yayına çıkılmaz**; çıkış kodu 1 olur. yayına çıkılmaz**; çıkış kodu 1 olur.
- En önemli iki satır: - En önemli üç satır:
- `kimlik (tur/önlisans/baz ad = API) — … uyumsuz: 0` — her kod hâlâ aynı programı
gösteriyor (uyumsuzsa satır eski adıyla başka programın sırasını gösterir).
- `önceki DB: geçmiş yıllar korunmuş — … değişen/silinen satır: 0` - `önceki DB: geçmiş yıllar korunmuş — … değişen/silinen satır: 0`
- `sira2027/sira2026 ∈ [0,5; 2] — … (%9x)` — programların büyük çoğunluğunun sırası - `sira2027/sira2026 ∈ [0,5; 2] — … (%9x)` — programların büyük çoğunluğunun sırası
bir yılda yarıya inmez, iki katına çıkmaz. Oran düşükse yıllar kaymıştır. bir yılda yarıya inmez, iki katına çıkmaz. Oran düşükse yıllar kaymıştır.
@@ -211,6 +226,7 @@ Push öncesi `AGENTS.md`'deki "Push öncesi build zorunlu" kuralı geçerli. Yay
| `Benzersiz kod sayısı … ≠ API toplamı` | Çekim sırasında servis sayfaları kaydırdı | Birkaç dakika sonra 3. adımı tekrarla (DB'ye bir şey yazılmadı) | | `Benzersiz kod sayısı … ≠ API toplamı` | Çekim sırasında servis sayfaları kaydırdı | Birkaç dakika sonra 3. adımı tekrarla (DB'ye bir şey yazılmadı) |
| `Aynı üniversitede birden fazla unitur` | Bir kurumun türü iki farklı yazılmış | Listelenen kurumları veri mühendisine göster; `scripts/unitur-onar.ts` | | `Aynı üniversitede birden fazla unitur` | Bir kurumun türü iki farklı yazılmış | Listelenen kurumları veri mühendisine göster; `scripts/unitur-onar.ts` |
| `Güncel satır sayısı … refresh kaydıyla uyuşmuyor` (arşivle) | refresh yarım kalmış | 3. adımı tekrarla; arşivleme bilerek çalışmıyor | | `Güncel satır sayısı … refresh kaydıyla uyuşmuyor` (arşivle) | refresh yarım kalmış | 3. adımı tekrarla; arşivleme bilerek çalışmıyor |
| `N satırın kimliği API ile uyuşmuyor` (refresh) | Kılavuz kodu başka programa devredilmiş (ya da program ad değiştirmiş) | Listeye bak; 3. adımdaki gibi `--ham … --kimlik-onar` ile tekrarla (DB'ye bir şey yazılmadı) |
| `veri-kalite` içinde `KALDI` | O kontrol tutmadı | Yayına çıkma; satırdaki sayıyı ve örnek kodları veri mühendisine ilet | | `veri-kalite` içinde `KALDI` | O kontrol tutmadı | Yayına çıkma; satırdaki sayıyı ve örnek kodları veri mühendisine ilet |
## Betikler ve dokundukları ## Betikler ve dokundukları
@@ -218,8 +234,8 @@ Push öncesi `AGENTS.md`'deki "Push öncesi build zorunlu" kuralı geçerli. Yay
| Betik | Yazar mı? | Ne yazar | | Betik | Yazar mı? | Ne yazar |
|---|---|---| |---|---|---|
| `scripts/yokatlas-goc.ts` (`pnpm yokatlas:goc`) | evet | Yalnız şema: yıl kolonları, `son_kilavuz_yili`, `eski_kod`, `programs_arsiv`, `veri_meta`. refresh bunu kendisi çağırır. | | `scripts/yokatlas-goc.ts` (`pnpm yokatlas:goc`) | evet | Yalnız şema: yıl kolonları, `son_kilavuz_yili`, `eski_kod`, `programs_arsiv`, `veri_meta`. refresh bunu kendisi çağırır. |
| `scripts/refresh.ts` | evet | `sira/puan/kontenjan/yerlesen<yıl>`, `son_kilavuz_yili`, `eski_kod`; yeni programlar; ham dosya | | `scripts/refresh.ts` | evet | `sira/puan/kontenjan/yerlesen<yıl>`, `son_kilavuz_yili`, `eski_kod`; yeni programlar; ham dosya. `--kimlik-onar` ile: kimliği kaymış satırın ad/tür/önlisans/fakülte/süresi, devir öncesi yıl kolonları (NULL), `programs_arsiv` kopyası, `netler.program_id`, `veri_meta` `kimlik_onar:<kod>` |
| `scripts/gecmis-doldur.ts` | evet | Yalnız **NULL** geçmiş yıl alanları + `eski_kod` | | `scripts/gecmis-doldur.ts` | evet | Yalnız **NULL** geçmiş yıl alanları + `eski_kod`; kimliği onarılmış satırda devir öncesi yılları doldurmaz |
| `scripts/eski-arsivle.ts` | `--kuru` ile hayır | Satır taşıma `programs` → `programs_arsiv`; `netler.program_id` yeni koda | | `scripts/eski-arsivle.ts` | `--kuru` ile hayır | Satır taşıma `programs` → `programs_arsiv`; `netler.program_id` yeni koda |
| `scripts/db-index.ts` | evet | İndeks, `--vacuum`, WAL checkpoint | | `scripts/db-index.ts` | evet | İndeks, `--vacuum`, WAL checkpoint |
| `scripts/veri-kalite.ts` | **hayır** | Salt okunur; çıkış kodu 0/1 | | `scripts/veri-kalite.ts` | **hayır** | Salt okunur; çıkış kodu 0/1 |

View File

@@ -19,12 +19,17 @@
* en az bir ortak değer AYNIYSA (kanıt) → eski satırdan doldurulur, güncel * en az bir ortak değer AYNIYSA (kanıt) → eski satırdan doldurulur, güncel
* satıra `eski_kod` yazılır. Kanıtsız ya da çelişkili eşleşme birleştirilmez. * satıra `eski_kod` yazılır. Kanıtsız ya da çelişkili eşleşme birleştirilmez.
* *
* Kimliği onarılmış satırlar (refresh --kimlik-onar; bkz. kimlik.ts): devir
* yılından ÖNCEKİ yıllar hiçbir kaynaktan doldurulmaz — o yıllarda bu kod başka
* programa aitti ve API'nin kod bazlı `…N` alanları o programın değerini verir.
*
* Kullanım: * Kullanım:
* pnpm gecmis-doldur --yil 2026 --db /tmp/deneme.db [--ham data/ham/2026-….jsonl.gz] [--kuru] * pnpm gecmis-doldur --yil 2026 --db /tmp/deneme.db [--ham data/ham/2026-….jsonl.gz] [--kuru]
*/ */
import path from "node:path"; import path from "node:path";
import type Database from "better-sqlite3"; import type Database from "better-sqlite3";
import { ILK_YIL } from "../src/lib/veri-yillari"; import { ILK_YIL } from "../src/lib/veri-yillari";
import { onarimKayitlari } from "./kimlik";
import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh"; import { gecmisAlanlari, hamOku, sonHamDosya } from "./refresh";
import { import {
argOku, argOku,
@@ -95,6 +100,16 @@ async function main() {
const arsivGetir = tabloVar(db, "programs_arsiv") const arsivGetir = tabloVar(db, "programs_arsiv")
? db.prepare("SELECT * FROM programs_arsiv WHERE id = ?") ? db.prepare("SELECT * FROM programs_arsiv WHERE id = ?")
: null; : null;
// id → devir yılı: bu yıldan önceki kolonlar o satırda BİLEREK boş.
const devirYillari = new Map<string, number>();
for (const [id, k] of onarimKayitlari(db)) devirYillari.set(id, k.devirYili);
let devirOncesiAtlanan = 0;
const devirOncesi = (id: string, y: number): boolean => {
const devir = devirYillari.get(id);
if (devir == null || y >= devir) return false;
devirOncesiAtlanan++;
return true;
};
const setCache = new Map<string, Database.Statement<unknown[]>>(); const setCache = new Map<string, Database.Statement<unknown[]>>();
const alanYaz = (id: string, kolon: string, deger: number) => { const alanYaz = (id: string, kolon: string, deger: number) => {
let st = setCache.get(kolon); let st = setCache.get(kolon);
@@ -109,6 +124,7 @@ async function main() {
/** Hedefin NULL alanlarını kaynaktan doldurur; dolu+farklıyı sayar. */ /** Hedefin NULL alanlarını kaynaktan doldurur; dolu+farklıyı sayar. */
const satirdanDoldur = (kaynakAdi: string, hedef: Satir, kaynak: Satir) => { const satirdanDoldur = (kaynakAdi: string, hedef: Satir, kaynak: Satir) => {
for (const y of gecmisYillar) { for (const y of gecmisYillar) {
if (devirOncesi(hedef.id, y)) continue;
for (const olcu of olculer) { for (const olcu of olculer) {
const kolon = `${olcu}${y}`; const kolon = `${olcu}${y}`;
const k = kaynak[kolon] as number | null; const k = kaynak[kolon] as number | null;
@@ -152,6 +168,7 @@ async function main() {
for (const g of gecmisAlanlari(r)) { for (const g of gecmisAlanlari(r)) {
const y = yil - g.fark; const y = yil - g.fark;
if (!gecmisYillar.includes(y)) continue; if (!gecmisYillar.includes(y)) continue;
if (devirOncesi(hedef.id, y)) continue;
for (const olcu of ["sira", "puan", "kontenjan"] as const) { for (const olcu of ["sira", "puan", "kontenjan"] as const) {
const k = g[olcu]; const k = g[olcu];
if (k == null) continue; if (k == null) continue;
@@ -279,6 +296,13 @@ async function main() {
if (v.length) console.log(` örnek ${k}: ${v.join(" ; ")}`); if (v.length) console.log(` örnek ${k}: ${v.join(" ; ")}`);
} }
if (devirYillari.size) {
console.log(
`Kimliği onarılmış ${devirYillari.size} satırda devir öncesi yıl BİLEREK boş bırakıldı ` +
`(atlanan yıl denemesi: ${devirOncesiAtlanan}).`
);
}
console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`); console.log(`\nDoldurulan NULL alanlar${kuru ? " (kuru — yazılmadı)" : ""}:`);
const toplamlar = new Map<string, number>(); const toplamlar = new Map<string, number>();
for (const [k, n] of [...doldurulan].sort()) { for (const [k, n] of [...doldurulan].sort()) {

281
scripts/kimlik.ts Normal file
View File

@@ -0,0 +1,281 @@
/**
* Program KİMLİĞİ tutarlılığı: kılavuz kodu (programs.id) yıllar içinde BAŞKA
* bir programa devredilebiliyor (ör. 209210292: 2024'te "Diş Hekimliği, SAY
* lisans" → 2025'ten beri "İlk ve Acil Yardım, TYT önlisans"). refresh yalnız
* koda bakıp yıl kolonlarını yazarsa satır ESKİ adıyla YENİ programın sırasını
* gösterir. Bu modül refresh / veri-kalite / gecmis-doldur'un ortak kurallarıdır.
*
* Kimlik = puan türü + önlisans bayrağı + baz ad slug'ı
* (`turkishSlugify(bolumBazAdi(isim))`; burs/dil ekleri kimlik DEĞİLDİR —
* DB'deki eski satırlar eksiz, API ekli yazıyor).
*
* DEVİR YILI (kanıt zinciri; tahmin yok): SON yıl kesin olarak yeni programındır
* (API kaydı o kılavuzdan). Geriye doğru her yıl için API'nin `…1/…2/…3` sıra/
* puanı DB'deki değerle EŞLEŞİYORSA o yıl da yeni programındır; eşleşmeyen ya
* da API'nin sıra/puanla sahiplenmediği İLK yılda zincir kopar — o yıl ve
* öncesi eski programa aittir. Zincir ŞART, çünkü API geçmişi kod bazlıdır ve
* kirlidir: `gk2` hep eski programın kontenjanıdır, `basariSirasi3` bazı
* kayıtlarda eski programın sırasıdır (ör. 205590251: TYT Mekatronik'e 2023 için
* DİL sırası 108.099). Sıra eşitliği yerine puan eşitliği de kabul edilir:
* 2024 sırası iki kaynakta farklı tanımlı (bkz. gecmis-doldur), puan aynıdır.
* İkinci bağımsız kaynak `netler.puanTuru`'dur (YÖK Atlas net API'si, yıl bazlı).
*/
import { ILK_YIL } from "../src/lib/veri-yillari";
import { bolumBazAdi, turkishSlugify } from "../src/lib/slug";
import { kolonAdlari, metaTablosunuGarantiEt, tabloVar, type Db } from "./yokatlas-goc";
/** refresh.ts ile döngüsel import olmasın diye kimliğin gerektirdiği alanlar. */
export type KimlikKaydi = {
kilavuzKodu: number;
birimAdi: string;
fymkAdi?: string | null;
puanTuru: string;
ogrenimSuresi?: number | null;
birimTuruId: number;
};
export type GecmisAlani = { fark: number; sira: number | null; puan: number | null };
export type DbKimlik = {
id: string;
isim: string;
tur: string;
onlisans: number;
fakulte: string | null;
sure: number | null;
};
export type KimlikFarki = "tur" | "onlisans" | "ad";
const PUAN_TOLERANS = 0.01;
const META_ONEK = "kimlik_onar:";
export const bazAdSlug = (isim: string): string => turkishSlugify(bolumBazAdi(isim));
/** ÖSYM birim türü 47 = önlisans (refresh INSERT dalıyla aynı kural). */
export const apiOnlisans = (r: KimlikKaydi): number => (r.birimTuruId === 47 ? 1 : 0);
/**
* DB satırı ile API kaydının kimlik farkları. `apiTur` çözülemediyse (kaynak
* puan türünü vermiyor) tür KIYASLANMAZ — bilinmeyen, fark sayılmaz.
*/
export function kimlikFarklari(
satir: DbKimlik,
r: KimlikKaydi,
apiTur: string | undefined
): KimlikFarki[] {
const farklar: KimlikFarki[] = [];
if (apiTur && apiTur !== satir.tur) farklar.push("tur");
if (apiOnlisans(r) !== satir.onlisans) farklar.push("onlisans");
if (bazAdSlug(satir.isim) !== bazAdSlug(r.birimAdi)) farklar.push("ad");
return farklar;
}
export type DevirKaniti = {
/** API `…1/…2/…3` alanları. */
gecmis: GecmisAlani[];
/** `netler.puanTuru` (yıl → DB tür sözlüğüne çevrilmiş); bağımsız ikinci kaynak. */
netTurleri?: Map<number, string>;
eskiTur: string;
yeniTur: string;
farklar: KimlikFarki[];
};
/**
* Yeni programa ait olduğu KANITLANAN ilk yıl. `yil` (son yıl) her zaman
* yenidir; geriye doğru zincir ilk kanıtsız yılda kopar (bkz. dosya başı).
*
* Bir yılın kanıtı: API sıra/puanı DB ile eşleşir. DB'de o yılın sıra/puanı HİÇ
* yoksa (dolmayan program; API'nin doğrulayacağı/sahiplenmeyeceği taban yok) ve
* tür değiştiyse, o yılın `netler` kaydının YENİ türde olması da yeter — böylece
* yalnız kontenjan/yerleşen/net korunur, yanlış etiketlenebilecek sıra kalmaz.
* DB'de sıra VARKEN API o yılı boş veriyorsa net türü yetmez: aynı türde başka
* bir program olabilir (dolu programda API'nin 2024'ü boş verme oranı uyumlu
* satırlarda %0,3; kimliği kaymışlarda 22/23 — API o yılı bilerek sahiplenmiyor).
* Karşı kanıt: tür değiştiyse ve o yılın neti ESKİ türdeyse yıl kesin eskidir.
*
* AD DEĞİŞİMİ: fark yalnız addaysa ve zincir API penceresinin TAMAMINDA (3 yıl)
* sağlamsa, API eski adın geçerli olduğu yılları da bu programa sayıyor demektir
* → program aynı, adı değişmiş; geçmiş korunur (ILK_YIL döner).
*/
export function devirYiliBul(
satir: Record<string, unknown>,
kanit: DevirKaniti,
yil: number
): number {
const turDegisti = kanit.eskiTur !== kanit.yeniTur;
const gecmis = [...kanit.gecmis].sort((a, b) => a.fark - b.fark);
let devir = yil;
for (const g of gecmis) {
const y = yil - g.fark;
if (y < ILK_YIL || y !== devir - 1) break;
const netTur = kanit.netTurleri?.get(y);
if (turDegisti && netTur === kanit.eskiTur) break;
const dbSira = satir[`sira${y}`] as number | null;
const dbPuan = satir[`puan${y}`] as number | null;
const siraAyni = g.sira != null && dbSira != null && g.sira === dbSira;
const puanAyni =
g.puan != null && dbPuan != null && Math.abs(g.puan - dbPuan) <= PUAN_TOLERANS;
const netYeni =
dbSira == null && dbPuan == null && turDegisti && netTur === kanit.yeniTur;
if (!siraAyni && !puanAyni && !netYeni) break;
devir = y;
}
const pencereSonu = Math.max(ILK_YIL, yil - gecmis.length);
const yalnizAd = kanit.farklar.length === 1 && kanit.farklar[0] === "ad";
if (yalnizAd && gecmis.length > 0 && devir === pencereSonu) return ILK_YIL;
return devir;
}
/** `netler.puanTuru` yıl haritası (tablo yoksa boş). Değerler API kısa kodudur (SAY, TYT…). */
export function netTurleriGetir(db: Db, id: string, turSozlugu: Record<string, string>): Map<number, string> {
const sonuc = new Map<number, string>();
if (!tabloVar(db, "netler")) return sonuc;
const satirlar = db
.prepare("SELECT yil, puanTuru FROM netler WHERE program_id = ? AND puanTuru IS NOT NULL")
.all(id) as { yil: number; puanTuru: string }[];
for (const n of satirlar) {
const tur = turSozlugu[n.puanTuru.trim()];
if (tur) sonuc.set(n.yil, tur);
}
return sonuc;
}
/** programs'ın yıla bağlı TÜM kolonları (sira2024, gk2025, obkY2025…) → yıl. */
export function yilKolonlari(db: Db, tablo = "programs"): { kolon: string; yil: number }[] {
const sonuc: { kolon: string; yil: number }[] = [];
for (const kolon of kolonAdlari(db, tablo)) {
const m = /^[A-Za-z0-9]*?[A-Za-z](\d{4})$/.exec(kolon);
if (!m) continue;
const y = Number(m[1]);
if (y >= ILK_YIL && y <= 2100) sonuc.push({ kolon, yil: y });
}
return sonuc;
}
export type OnarimKaydi = {
devirYili: number;
yil: number;
eski: { isim: string; tur: string; onlisans: number; fakulte: string | null; sure: number | null };
yeni: { isim: string; tur: string; onlisans: number; fakulte: string | null; sure: number | null };
silinenYillar: number[];
arsivId: string | null;
tasinanNetYillari: number[];
};
export type OnarimGirdisi = {
id: string;
yil: number;
devirYili: number;
yeni: OnarimKaydi["yeni"];
};
/**
* Tek satırın kimliğini onarır (çağıran transaction içinde olmalı):
* 1. Devirden önce verisi/neti varsa eski kimliğiyle `programs_arsiv`'e kopya:
* id `<kod>~<son veri yılı>` (kod güncel satırda yaşamaya devam ettiği için
* arşivde aynı id kullanılamaz; ileride eski-arsivle ile PK çakışırdı).
* Kopyada devir yılı ve sonrası NULL (o yıllar yeni programın).
* 2. Devirden önceki `netler` satırları arşiv id'sine taşınır (yeni programın
* sayfasında eski programın netleri görünmesin; veri silinmez).
* 3. Güncel satırda devirden önceki TÜM yıl kolonları NULL; isim/tur/onlisans/
* fakulte/sure API'den yazılır.
* 4. `veri_meta`'ya `kimlik_onar:<id>` kaydı — gecmis-doldur bu yılların
* API'nin kirli `…N` alanlarından yeniden dolmasını bununla engeller.
*/
export function kimlikOnar(db: Db, g: OnarimGirdisi): OnarimKaydi {
const satir = db.prepare("SELECT * FROM programs WHERE id = ?").get(g.id) as
| Record<string, string | number | null>
| undefined;
if (!satir) throw new Error(`kimlikOnar: satır yok ${g.id}`);
const kolonlar = yilKolonlari(db);
const onceki = kolonlar.filter((k) => k.yil < g.devirYili);
const sonraki = kolonlar.filter((k) => k.yil >= g.devirYili);
const silinenYillar = [
...new Set(onceki.filter((k) => satir[k.kolon] != null).map((k) => k.yil)),
].sort();
const netYillari = tabloVar(db, "netler")
? (
db
.prepare("SELECT yil FROM netler WHERE program_id = ? AND yil < ? ORDER BY yil")
.all(g.id, g.devirYili) as { yil: number }[]
).map((n) => n.yil)
: [];
let arsivId: string | null = null;
const sonVeriYili = Math.max(-1, ...silinenYillar, ...netYillari);
if (sonVeriYili > 0 && tabloVar(db, "programs_arsiv")) {
arsivId = `${g.id}~${sonVeriYili}`;
const arsivKolonlari = new Set(kolonAdlari(db, "programs_arsiv"));
const tasinan = Object.keys(satir).filter((k) => arsivKolonlari.has(k));
const degerler = tasinan.map((k) => {
if (k === "id") return arsivId;
// Eski programın bu kodla verisinin bulunduğu son yıl (olgu, tahmin değil)
if (k === "son_kilavuz_yili") return sonVeriYili;
if (sonraki.some((s) => s.kolon === k)) return null;
return satir[k];
});
db.prepare(
`INSERT INTO programs_arsiv (${tasinan.join(", ")}) VALUES (${tasinan.map(() => "?").join(", ")})`
).run(...degerler);
if (netYillari.length) {
db.prepare("UPDATE netler SET program_id = ? WHERE program_id = ? AND yil < ?").run(
arsivId,
g.id,
g.devirYili
);
}
}
const atamalar = [
...onceki.map((k) => `${k.kolon} = NULL`),
"isim = ?",
"tur = ?",
"onlisans = ?",
"fakulte = ?",
"sure = ?",
];
db.prepare(`UPDATE programs SET ${atamalar.join(", ")} WHERE id = ?`).run(
g.yeni.isim,
g.yeni.tur,
g.yeni.onlisans,
g.yeni.fakulte,
g.yeni.sure,
g.id
);
const kayit: OnarimKaydi = {
devirYili: g.devirYili,
yil: g.yil,
eski: {
isim: String(satir.isim),
tur: String(satir.tur),
onlisans: Number(satir.onlisans),
fakulte: satir.fakulte as string | null,
sure: satir.sure as number | null,
},
yeni: g.yeni,
silinenYillar,
arsivId,
tasinanNetYillari: arsivId ? netYillari : [],
};
metaTablosunuGarantiEt(db);
db.prepare(
"INSERT INTO veri_meta (anahtar, deger) VALUES (?, ?) " +
"ON CONFLICT(anahtar) DO UPDATE SET deger = excluded.deger"
).run(`${META_ONEK}${g.id}`, JSON.stringify(kayit));
return kayit;
}
/** Daha önce onarılmış satırlar: id → kayıt (gecmis-doldur ve veri-kalite okur). */
export function onarimKayitlari(db: Db): Map<string, OnarimKaydi> {
const sonuc = new Map<string, OnarimKaydi>();
if (!tabloVar(db, "veri_meta")) return sonuc;
const satirlar = db
.prepare("SELECT anahtar, deger FROM veri_meta WHERE anahtar LIKE ?")
.all(`${META_ONEK}%`) as { anahtar: string; deger: string }[];
for (const s of satirlar) {
sonuc.set(s.anahtar.slice(META_ONEK.length), JSON.parse(s.deger) as OnarimKaydi);
}
return sonuc;
}

View File

@@ -21,11 +21,23 @@
* 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction * 4. DB yazımı TEK transaction; önceki yılların kolon sağlaması transaction
* içinde karşılaştırılır, fark varsa geri alınır. * içinde karşılaştırılır, fark varsa geri alınır.
* *
* 5. KİMLİK KİLİDİ: mevcut satırın puan türü / önlisans bayrağı / baz ad
* slug'ı API kaydıyla uyuşmuyorsa kılavuz kodu BAŞKA programa devredilmiştir
* (bkz. kimlik.ts). Liste basılır ve HİÇBİR ŞEY yazılmadan durulur;
* liste gözle incelendikten sonra `--kimlik-onar` ile yeniden koşulur.
*
* Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod. * Yazdıkları: sira/puan/kontenjan/yerlesen<yil>, son_kilavuz_yili, eski_kod.
* Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise * Mevcut satırların ÖNCEKİ yıl kolonlarına dokunmaz (onları yalnız NULL ise
* gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından * gecmis-doldur.ts doldurur). Yeni program eklerken `…1/…2/…3` alanlarından
* önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL. * önceki üç yılı da yazar. API "veri yok" için 0 döndürür → NULL.
* *
* `--kimlik-onar`: kimliği kaymış satırlarda isim/tur/onlisans/fakulte/sure
* API'den yazılır; devirden ÖNCEKİ yılların tüm kolonları NULL'lanır (o geçmiş
* başka programa ait), eski kimlik + geçmişi `programs_arsiv`'e kopyalanır.
* `--isim-esitle`: kimliği AYNI olup yalnız adı farklı satırlarda (API burs/dil
* eki veriyor: "Tıp (İngilizce) (Burslu)") isim API'den yazılır. Varsayılan
* KAPALI: isim, sihirbaz kategorilerini ve bölüm sayfası başlığını besler.
*
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı. * Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
* `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham * `--ham <dosya>` verilirse API'ye HİÇ gidilmez; daha önce kaydedilmiş ham
* dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama). * dosyadan aynı işlem tekrarlanır (kopyada deneme → gerçek DB'ye uygulama).
@@ -40,9 +52,20 @@ import readline from "node:readline";
import zlib from "node:zlib"; import zlib from "node:zlib";
import { ILK_YIL } from "../src/lib/veri-yillari"; import { ILK_YIL } from "../src/lib/veri-yillari";
import { efektifIndeksiKur } from "./db-index"; import { efektifIndeksiKur } from "./db-index";
import {
apiOnlisans,
devirYiliBul,
kimlikFarklari,
kimlikOnar,
netTurleriGetir,
type DbKimlik,
type KimlikFarki,
type OnarimKaydi,
} from "./kimlik";
import { uniAdiNormalize, uniturOnar } from "./unitur-onar"; import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
import { import {
argOku, argOku,
bayrakVar,
calistir, calistir,
dbAc, dbAc,
dbArgumani, dbArgumani,
@@ -371,7 +394,13 @@ async function apidenCek(
return { toplam: first.totalElements, kayitlar, hamDosya }; return { toplam: first.totalElements, kayitlar, hamDosya };
} }
function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string { /** Önceki yıl kolonlarının sağlaması; `haricIdler` (kimliği onarılanlar) dışarıda. */
function oncekiYilSaglamasi(
db: Db,
yil: number,
azamiRowid: number,
haricIdler: string[]
): string {
const kolonlar: string[] = []; const kolonlar: string[] = [];
for (let y = ILK_YIL; y < yil; y++) { for (let y = ILK_YIL; y < yil; y++) {
for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) { for (const olcu of ["sira", "puan", "kontenjan", "yerlesen"]) {
@@ -379,9 +408,12 @@ function oncekiYilSaglamasi(db: Db, yil: number, azamiRowid: number): string {
} }
} }
const r = db const r = db
.prepare(`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs WHERE rowid <= ?`) .prepare(
`SELECT COUNT(*), ${kolonlar.join(", ")} FROM programs
WHERE rowid <= ? AND id NOT IN (SELECT value FROM json_each(?))`
)
.raw() .raw()
.get(azamiRowid) as number[]; .get(azamiRowid, JSON.stringify(haricIdler)) as number[];
return r.join("|"); return r.join("|");
} }
@@ -497,15 +529,124 @@ async function main() {
return mevcut || api; return mevcut || api;
}; };
// ── Kimlik kilidi: kod aynı, program aynı mı? ──────────────────────────
const kimlikOnarIste = bayrakVar("kimlik-onar");
const isimEsitleIste = bayrakVar("isim-esitle");
const kimlikGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
type Uyumsuz = {
id: string;
r: KilavuzKaydi;
satir: DbKimlik & Record<string, unknown>;
farklar: KimlikFarki[];
apiTur: string | undefined;
devirYili: number;
};
const uyumsuzlar: Uyumsuz[] = [];
const ekFarkliIdler: string[] = [];
for (const [id, r] of benzersiz) {
const satir = kimlikGetir.get(id) as Uyumsuz["satir"] | undefined;
if (!satir) continue;
const apiTur = TUR_MAP[r.puanTuru?.trim() ?? ""];
const farklar = kimlikFarklari(satir, r, apiTur);
if (farklar.length) {
uyumsuzlar.push({
id,
r,
satir,
farklar,
apiTur,
devirYili: devirYiliBul(
satir,
{
gecmis: gecmisAlanlari(r),
netTurleri: netTurleriGetir(db, id, TUR_MAP),
eskiTur: satir.tur,
yeniTur: apiTur ?? satir.tur,
farklar,
},
yil
),
});
} else if (satir.isim !== r.birimAdi) {
ekFarkliIdler.push(id);
}
}
if (uyumsuzlar.length) {
console.log(`\nKİMLİĞİ KAYMIŞ ${uyumsuzlar.length} satır (kod aynı, program farklı):`);
for (const u of uyumsuzlar) {
const gecmis = [];
for (let y = ILK_YIL; y <= yil; y++) gecmis.push(u.satir[`sira${y}`] ?? "-");
const netOzet = [...netTurleriGetir(db, u.id, TUR_MAP)]
.sort((a, b) => a[0] - b[0])
.map(([y, t]) => `${y}=${t}`)
.join(" ");
console.log(
` ${u.id} [${u.farklar.join("+")}] ${u.satir.universite}\n` +
` DB : ${u.satir.isim} | ${u.satir.tur} | onlisans=${u.satir.onlisans} | ${u.satir.fakulte}\n` +
` API: ${u.r.birimAdi} | ${u.apiTur ?? `?(${u.r.puanTuru})`} | onlisans=${apiOnlisans(u.r)} | ${u.r.fymkAdi}\n` +
` sira ${ILK_YIL}…${yil}: ${gecmis.join(", ")} | net türleri: ${netOzet || "-"} → ` +
(u.devirYili === ILK_YIL
? "AD DEĞİŞİMİ (API penceresinin tamamı aynı program; geçmiş korunur)"
: `devir yılı ${u.devirYili} (öncesi silinir)`)
);
}
const tursuz = uyumsuzlar.filter((u) => !u.apiTur);
if (!kimlikOnarIste) {
dur(
`${uyumsuzlar.length} satırın kimliği API ile uyuşmuyor; DB'ye HİÇBİR ŞEY yazılmadı. ` +
"Listeyi incele; devirse aynı komutu --kimlik-onar ile yeniden koş " +
"(isim/tur/onlisans/fakulte/sure API'den yazılır, devir öncesi yıllar NULL'lanır, eski kimlik arşive kopyalanır)."
);
}
if (tursuz.length) {
dur(
`${tursuz.length} uyumsuz satırda API puan türü vermiyor (${tursuz.map((u) => u.id).join(", ")}); ` +
"tür UYDURULMAZ, onarım yapılamaz."
);
}
}
console.log(
`Kimlik: ${uyumsuzlar.length} uyumsuz; yalnız adı (burs/dil eki) farklı ${ekFarkliIdler.length} satır` +
(isimEsitleIste ? " → isim API'den yazılacak (--isim-esitle)" : " → isim yazılmıyor (--isim-esitle verilmedi)")
);
let updated = 0; let updated = 0;
let inserted = 0; let inserted = 0;
const atlananlar: string[] = []; const atlananlar: string[] = [];
const onarimlar: { id: string; universite: string; kayit: OnarimKaydi }[] = [];
let isimEsitlenen = 0;
db.transaction(() => { db.transaction(() => {
const azamiRowid = ( const azamiRowid = (
db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number } db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM programs").get() as { m: number }
).m; ).m;
const once = oncekiYilSaglamasi(db, yil, azamiRowid); const onarilanIdler = uyumsuzlar.map((u) => u.id);
const once = oncekiYilSaglamasi(db, yil, azamiRowid, onarilanIdler);
// Kimlik onarımı yıl yazımından ÖNCE: arşiv kopyası eski hâliyle alınır.
for (const u of uyumsuzlar) {
const universite = String(u.satir.universite);
const kayit = kimlikOnar(db, {
id: u.id,
yil,
devirYili: u.devirYili,
yeni: {
isim: u.r.birimAdi,
tur: u.apiTur as string,
onlisans: apiOnlisans(u.r),
fakulte: fakulteSec(universite, u.r.fymkAdi ?? null),
sure: u.r.ogrenimSuresi ?? null,
},
});
onarimlar.push({ id: u.id, universite, kayit });
}
if (isimEsitleIste) {
const isimYaz = db.prepare("UPDATE programs SET isim = ? WHERE id = ?");
for (const id of ekFarkliIdler) {
isimYaz.run((benzersiz.get(id) as KilavuzKaydi).birimAdi, id);
isimEsitlenen++;
}
}
for (const [id, r] of benzersiz) { for (const [id, r] of benzersiz) {
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""]; const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
@@ -549,10 +690,10 @@ async function main() {
} }
} }
const sonra = oncekiYilSaglamasi(db, yil, azamiRowid); const sonra = oncekiYilSaglamasi(db, yil, azamiRowid, onarilanIdler);
if (once !== sonra) { if (once !== sonra) {
dur( dur(
"Mevcut satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" + "Kimliği onarılanlar DIŞINDAKİ satırların önceki yıl kolonları değişti — bu betik onlara dokunmamalı. Geri alındı.\n" +
`önce : ${once}\nsonra: ${sonra}` `önce : ${once}\nsonra: ${sonra}`
); );
} }
@@ -577,6 +718,20 @@ async function main() {
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}` `Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${kodsuz + atlananlar.length}`
); );
for (const a of atlananlar) console.log(` atlandı: ${a}`); for (const a of atlananlar) console.log(` atlandı: ${a}`);
if (isimEsitlenen) console.log(`İsmi API'den eşitlenen (yalnız ek farkı): ${isimEsitlenen}`);
if (onarimlar.length) {
console.log(`\nKİMLİĞİ ONARILAN ${onarimlar.length} satır:`);
console.log(" id | üniversite | eski ad [tür/önlisans] → yeni ad [tür/önlisans] | devir | NULL'lanan yıllar | arşiv | taşınan net yılları");
for (const o of onarimlar) {
const k = o.kayit;
console.log(
` ${o.id} | ${o.universite} | ${k.eski.isim} [${k.eski.tur}/${k.eski.onlisans}] → ` +
`${k.yeni.isim} [${k.yeni.tur}/${k.yeni.onlisans}] | ${k.devirYili} | ` +
`${k.silinenYillar.join(",") || "-"} | ${k.arsivId ?? "-"} | ${k.tasinanNetYillari.join(",") || "-"}`
);
}
metaYaz(db, `kimlik_onar_${yil}_satir`, onarimlar.length);
}
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden // API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent). // düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).

View File

@@ -4,8 +4,13 @@
* UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır. * UYARI'lar çıkış kodunu etkilemez; insan gözüyle bakılır.
* *
* Kullanım: * Kullanım:
* pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli] * pnpm veri-kalite --yil 2026 --db /tmp/deneme.db [--onceki data/yokatlas.db] [--arsivli] [--ham <dosya>]
* *
* --ham <dosya> Ham API çekimi (yoksa data/ham altındaki en yenisi). Verilirse
* KİMLİK kontrolü koşar: her DB satırının puan türü / önlisans /
* baz ad slug'ı aynı koddaki API kaydıyla uyuşmalı (0 beklenir;
* uyuşmayan = kılavuz kodu başka programa devredilmiş, satır eski
* adıyla yeni programın sırasını gösteriyor → refresh --kimlik-onar).
* --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her * --onceki <db> Güncelleme ÖNCESİ DB. Verilirse: iki DB'de de bulunan her
* programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen) * programın önceki yıl kolonları (sira/puan/kontenjan/yerlesen)
* birebir aynı mı (dolu değer değişmiş/silinmiş olamaz). * birebir aynı mı (dolu değer değişmiş/silinmiş olamaz).
@@ -14,6 +19,8 @@
import path from "node:path"; import path from "node:path";
import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari"; import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index"; import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
import { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik";
import { hamOku, sonHamDosya, TUR_MAP } from "./refresh";
import { import {
argOku, argOku,
bayrakVar, bayrakVar,
@@ -164,6 +171,52 @@ async function main() {
); );
bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`); bilgi("eski_kod paylaşımı (bölünme)", eskiKodCakisma > 0, `birden çok güncel programın işaret ettiği eski kod: ${eskiKodCakisma}`);
// 9b. Kimlik: kod aynı, program aynı mı? (ham çekim gerekir)
const hamArg = argOku("ham");
const hamDosya = hamArg
? path.resolve(process.cwd(), hamArg)
: sonHamDosya(yil, path.resolve(process.cwd(), "data", "ham"));
const onarimlar = onarimKayitlari(db);
if (hamDosya) {
const { kayitlar } = await hamOku(hamDosya, yil);
const getir = db.prepare("SELECT id, isim, tur, onlisans, fakulte, sure, universite FROM programs WHERE id = ?");
const uyumsuz: string[] = [];
let kiyaslanan = 0;
let ekFarki = 0;
for (const r of kayitlar) {
if (!r.kilavuzKodu) continue;
const satir = getir.get(String(r.kilavuzKodu)) as (DbKimlik & { universite: string }) | undefined;
if (!satir) continue;
kiyaslanan++;
const farklar = kimlikFarklari(satir, r, TUR_MAP[r.puanTuru?.trim() ?? ""]);
if (farklar.length) {
uyumsuz.push(`${satir.id}[${farklar.join("+")}] DB="${satir.isim}"/${satir.tur}/${satir.onlisans} API="${r.birimAdi}"/${r.puanTuru}/${r.birimTuruId === 47 ? 1 : 0} (${satir.universite})`);
} else if (satir.isim !== r.birimAdi) {
ekFarki++;
}
}
zorunlu(
"kimlik (tur/önlisans/baz ad = API)",
uyumsuz.length === 0,
`${kiyaslanan} satır kıyaslandı, uyumsuz: ${uyumsuz.length}${uyumsuz.length ? ` ör. ${uyumsuz.slice(0, 3).join(" ; ")}` : ""} [${path.basename(hamDosya)}]`
);
bilgi("isim eki (burs/dil) API'den farklı", false, `${ekFarki} satır — kimlik aynı, DB adı eksiz/farklı yazımlı (aynı üniversitede adaş varyantlar ayırt edilemez)`);
} else {
bilgi("kimlik", true, "ATLANDI: --ham verilmedi ve data/ham altında çekim yok; kod devri denetlenemedi");
}
if (onarimlar.size) {
// Onarılan satırda devir öncesi hiçbir yıl kolonu dolu kalmış olamaz.
const yk = yilKolonlari(db);
let sizinti = 0;
const satirGetir = db.prepare("SELECT * FROM programs WHERE id = ?");
for (const [id, k] of onarimlar) {
const s = satirGetir.get(id) as Record<string, unknown> | undefined;
if (!s) continue;
if (yk.some((c) => c.yil < k.devirYili && s[c.kolon] != null)) sizinti++;
}
zorunlu("kimliği onarılanlar: devir öncesi boş", sizinti === 0, `${onarimlar.size} onarılmış satırın ${sizinti} tanesinde devir öncesi yıl yeniden dolmuş`);
}
// 10. netler // 10. netler
if (tabloVar(db, "netler")) { if (tabloVar(db, "netler")) {
const arsivVar = tabloVar(db, "programs_arsiv"); const arsivVar = tabloVar(db, "programs_arsiv");
@@ -202,11 +255,29 @@ async function main() {
const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`); const kaybolan = sayi(`SELECT COUNT(*) AS n FROM eski.programs e WHERE e.id NOT IN (SELECT id FROM ${simdi})`);
zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`); zorunlu("önceki DB: satır kaybı", kaybolan === 0, `önceki DB'de olup şimdi ne programs ne arşivde olan: ${kaybolan}`);
// Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest. // Dolu değer değişmiş ya da NULL'a dönmüş olamaz; NULL → dolu (gecmis-doldur) serbest.
// Kimliği onarılan satırlarda devir öncesi yıllar BİLEREK NULL'landı; onlar
// ayrı sayılır: devir yılı ve sonrası yine birebir korunmuş olmalı.
const onarilanJson = JSON.stringify([...onarimlar.keys()]);
const degisen = sayi( const degisen = sayi(
`SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id `SELECT COUNT(*) AS n FROM eski.programs e JOIN ${simdi} s ON s.id = e.id
WHERE ${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")}` WHERE e.id NOT IN (SELECT value FROM json_each(?))
AND (${ortak.map((k) => `(e.${k} IS NOT NULL AND e.${k} IS NOT s.${k})`).join(" OR ")})`,
onarilanJson
); );
zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen}`); zorunlu("önceki DB: geçmiş yıllar korunmuş", degisen === 0, `${ortak.length} kolonda dolu değeri değişen/silinen satır: ${degisen} (kimliği onarılan ${onarimlar.size} satır hariç)`);
if (onarimlar.size) {
let bozulan = 0;
const eskiGetir = db.prepare("SELECT * FROM eski.programs WHERE id = ?");
const yeniGetir = db.prepare("SELECT * FROM main.programs WHERE id = ?");
for (const [id, k] of onarimlar) {
const e = eskiGetir.get(id) as Record<string, unknown> | undefined;
const s = yeniGetir.get(id) as Record<string, unknown> | undefined;
if (!e || !s) continue;
const korunmali = ortak.filter((kolon) => Number(kolon.slice(-4)) >= k.devirYili);
if (korunmali.some((kolon) => e[kolon] != null && e[kolon] !== s[kolon])) bozulan++;
}
zorunlu("önceki DB: onarılanlarda devir sonrası korunmuş", bozulan === 0, `devir yılı ve sonrasında değeri değişen onarılmış satır: ${bozulan}/${onarimlar.size}`);
}
const toplamlar = (tablo: string) => const toplamlar = (tablo: string) =>
(db (db
.prepare( .prepare(

View File

@@ -152,9 +152,11 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX); liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX);
kaydet(); kaydet();
yayinla(); yayinla();
// Başka cihazda profilsiz eklenmiş kayıtlar risksiz gelir; profil lokalde // Sunucudan gelen kayıtların riski başka cihazın o günkü tabanına göredir
// zaten varsa PROFIL_DEGISTI_EVENT yayılmaz, riski burada doldur. // (ya da profilsiz eklendiyse hiç yoktur); sunucu tabanı tazeler ama riski
if (liste.some((t) => t.risk == null)) riskleriYenile(); // yenilemez. Profil lokalde varsa hepsini güncel tabanla yeniden hesapla
// (profil yoksa riskleriYenile zaten işlem yapmaz).
riskleriYenile();
return liste; return liste;
} }

View File

@@ -210,6 +210,14 @@ export function netlerGetir(programId: string): ProgramNetSatiri[] {
.all(programId) as ProgramNetSatiri[]; .all(programId) as ProgramNetSatiri[];
} }
/** Güncel kılavuzdaki tek program; arşivlenmiş ya da bilinmeyen id'de null. */
export function programGetir(id: string): Program | null {
const row = getDb()
.prepare(`SELECT ${SELECT_COLS} FROM programs WHERE id = ?`)
.get(id) as ProgramHamSatiri | undefined;
return row ? programSatiri(row) : null;
}
/** /**
* Kayıtlı manuel liste satırlarının güncel efektif sırası (son yıl, yoksa bir * Kayıtlı manuel liste satırlarının güncel efektif sırası (son yıl, yoksa bir
* önceki). Kayıtlar efektif sırayı ekleme anının snapshot'ı olarak taşır; yeni * önceki). Kayıtlar efektif sırayı ekleme anının snapshot'ı olarak taşır; yeni

View File

@@ -10,7 +10,8 @@ import type { Dilim } from "./rapor-havuzu";
import type { RaporSonuc } from "@/features/rapor/types/rapor"; import type { RaporSonuc } from "@/features/rapor/types/rapor";
import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk";
import { trendOzetiMetni } from "./trend"; import { trendOzetiMetni } from "./trend";
import { VERI_YIL_SAYISI } from "./veri-yillari"; import { ILK_YIL, VERI_YIL_SAYISI } from "./veri-yillari";
import { programGetir } from "./db";
/** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */
export const GENEL_KATEGORI = "genel"; export const GENEL_KATEGORI = "genel";
@@ -123,7 +124,18 @@ export async function tadimlikSec(
), ),
}); });
if (satir && gerekceServisEdilebilir(satir.gerekce)) { if (satir && gerekceServisEdilebilir(satir.gerekce)) {
const program = satir.program as RaporSonuc["programlar"][string]; // Havuz batch'te üretilir ve yeni yerleştirme yılı yüklenince bayatlar:
// snapshot'taki taban/geçmiş yerine CANLI veri servis edilir ki örnek
// satır aynı sayfadaki tabloyla çelişmesin. Güncel kılavuzdan çıkmış
// (arşivlenmiş) program örnek gösterilmez — sıradaki kategoriye düşülür.
const canli = programGetir(satir.programId);
if (!canli) continue;
const program: RaporSonuc["programlar"][string] = {
...(satir.program as RaporSonuc["programlar"][string]),
efektifSira: canli.efektifSira,
siraGecmisi: canli.siraGecmisi,
ilkYil: ILK_YIL,
};
return { return {
programId: satir.programId, programId: satir.programId,
dilim: satir.dilim, dilim: satir.dilim,