feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s
All checks were successful
Deploy / deploy (push) Successful in 7m45s
- Updated the app.db file to reflect recent changes in the application state. - Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates. - Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
This commit is contained in:
@@ -11,16 +11,25 @@
|
||||
import type OpenAI from "openai";
|
||||
import { jsonSchema, tool, type ToolSet } from "ai";
|
||||
import {
|
||||
arsivProgramAra,
|
||||
ayrikListe,
|
||||
dilimAraligi,
|
||||
EFEKTIF_SIRA,
|
||||
getDb,
|
||||
katliEslesenler,
|
||||
KONTENJAN_TURU_ONEK,
|
||||
kontenjanTuruYili,
|
||||
kontenjanTurleriGetir,
|
||||
kontenjanTurleriGetirToplu,
|
||||
netlerGetir,
|
||||
programGecmisiGetir,
|
||||
programGetir,
|
||||
programSatiri,
|
||||
SELECT_COLS,
|
||||
type ProgramHamSatiri,
|
||||
} from "@/lib/db";
|
||||
import {
|
||||
CINSIYET_KOSULU_METNI,
|
||||
kosulMetinleri,
|
||||
ogretimUyesiToplami,
|
||||
ozellikOzeti,
|
||||
@@ -34,12 +43,26 @@ import {
|
||||
getBolumBySlug,
|
||||
getIlBySlug,
|
||||
getUniversiteBySlug,
|
||||
ilgiliBolumler,
|
||||
} from "@/lib/katalog";
|
||||
import { KATEGORILER, kategoriEslesir } from "@/lib/kategoriler";
|
||||
import {
|
||||
KONTENJAN_TURU_ADI,
|
||||
OZEL_DURUMLAR,
|
||||
OZEL_DURUM_KONTENJAN_ADI,
|
||||
OZEL_DURUM_KONTENJAN_ALANI,
|
||||
ozelDurumDogrula,
|
||||
type OzelDurum,
|
||||
} from "@/lib/ozel-durum";
|
||||
import { uyumBaglamiVar, uyumPuanla, type UyumBaglami } from "@/lib/uyum-puani";
|
||||
import { OGRENCI_BASINA_UYE_MEDYAN } from "@/lib/yok-istatistik";
|
||||
import { URAP_SIRA, urapSirasiBul } from "@/lib/urap-siralama";
|
||||
import { getRehber, getRehberAramaKayitlari } from "@/lib/rehber";
|
||||
import { SEZON_TAKVIMI, sezonAsamasi } from "@/lib/sezon";
|
||||
import { kadinYuzdesi, ogrenciBasinaUye, YOK_ISTATISTIK_DONEM } from "@/lib/yok-istatistik";
|
||||
import { URAP_DONEM, URAP_TOPLAM } from "@/lib/urap-siralama";
|
||||
import { bolumSayfaSlug, turkishSlugify } from "@/lib/slug";
|
||||
import type { KontenjanTurleri } from "@/types/yokatlas";
|
||||
import { bolumSayfaSlug, turkishSlugify, uniSayfaSlug } from "@/lib/slug";
|
||||
import {
|
||||
BOLUM_ISTIHDAM,
|
||||
BOLUM_ISTIHDAM_KAYNAK,
|
||||
@@ -49,7 +72,7 @@ import {
|
||||
import { DILIM_ETIKET } from "@/lib/risk";
|
||||
import { trendOzetiMetni } from "@/lib/trend";
|
||||
import { TUR_SAYFALARI } from "@/lib/unitur";
|
||||
import { ONCEKI_YIL, SON_YIL, TERCIH_YILI, YILLAR } from "@/lib/veri-yillari";
|
||||
import { ONCEKI_YIL, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
import { ozelKontenjanSatiriMi } from "@/lib/rapor-havuzu";
|
||||
import { PUAN_TURLERI, type Program, type ProgramNetSatiri, type PuanTuruKey } from "@/types/yokatlas";
|
||||
import {
|
||||
@@ -69,13 +92,32 @@ export type AracBaglami = {
|
||||
tur: PuanTuruKey;
|
||||
/** Sohbet başındaki profil; profil_guncelle sonrası yenilenir */
|
||||
profil: DanismanProfili;
|
||||
/** Sihirbaz öncelik çipleri (uyum puanı / öne çıkan için) */
|
||||
oncelikler?: readonly string[];
|
||||
/** Sihirbaz özel kontenjan durumu (profildekiyle birleşir: adayDurumu) */
|
||||
ozelDurum?: readonly OzelDurum[];
|
||||
/** Sihirbazda il seçildi mi (ev iline uzaklık puanlanmaz) */
|
||||
sihirbazIlVar?: boolean;
|
||||
};
|
||||
|
||||
/** Araç bağlamından uyum puanı bağlamı (profil tazelendikçe yeniden kurulur). */
|
||||
function uyumBaglamiKur(b: AracBaglami): UyumBaglami {
|
||||
return {
|
||||
sira: b.sira,
|
||||
oncelikler: b.oncelikler ?? [],
|
||||
ozelDurum: ozelDurumDogrula([...(b.ozelDurum ?? []), ...(b.profil.ozelDurum ?? [])]),
|
||||
profil: b.profil,
|
||||
sihirbazIlVar: b.sihirbazIlVar ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export type AracTanimi = OpenAI.Chat.Completions.ChatCompletionTool;
|
||||
|
||||
const ARAMA_LIMIT = 8;
|
||||
const ARAMA_LIMIT = 10;
|
||||
const KARSILASTIRMA_UST = 4;
|
||||
const NET_YIL_SAYISI = 2;
|
||||
const SIRALAMA_UST = 30;
|
||||
const FAKULTE_UST = 20;
|
||||
const IL_DAGILIMI_UST = 8;
|
||||
|
||||
export const ARACLAR: AracTanimi[] = [
|
||||
{
|
||||
@@ -83,11 +125,24 @@ export const ARACLAR: AracTanimi[] = [
|
||||
function: {
|
||||
name: "program_ara",
|
||||
description:
|
||||
`Adayın puan türündeki programları veriden arar (YÖK Atlas, ${SON_YIL} yerleştirmesi). Bölüm adı, üniversite, il, devlet/vakıf, öğretim dili ve türü, burs ve yıllık ücret üst sınırına göre süzer; varsayılan olarak adayın sırasına en yakın programları döner. Her sonuçta taban sırası, dilim ve varsa başarı sırası barajı (baraj) gelir; aday barajın dışındaysa dilim yerine "Tercih edilemez (baraj dışı)" yazar. 'X yazabilir miyim', 'tabanı kaç', 'hangi programlar var' sorularında sayı uydurmak yerine bunu çağır.`,
|
||||
`Adayın puan türündeki programları veriden arar (YÖK Atlas, ${SON_YIL} yerleştirmesi). Bölüm adı, alan (kategori), fakülte, üniversite, il, devlet/vakıf, öğretim dili ve türü, burs, yıllık ücret üst sınırı, akreditasyon ve özel kontenjan türüne göre süzer; varsayılan olarak adayın sırasına en yakın ${ARAMA_LIMIT} programı döner, devamı için sayfa=2, 3… Her sonuçta taban sırası, dilim, varsa başarı sırası barajı (baraj; aday dışındaysa dilim yerine "Tercih edilemez (baraj dışı)"), adayın özel durumuna uyan kontenjan türü ve koddan hesaplanan "öne çıkan" veriler gelir. 'X yazabilir miyim', 'tabanı kaç', 'hangi programlar var', 'sağlık alanında neler var' sorularında sayı uydurmak yerine bunu çağır.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
arama: { type: "string", description: "Bölüm adı parçası, ör. 'bilgisayar mühendisliği', 'hukuk'" },
|
||||
kategori: {
|
||||
type: "string",
|
||||
enum: [...KATEGORILER],
|
||||
description: "Alan (sihirbazdaki ilgi alanı); 'sağlık alanında', 'mühendislik dışında ne var' gibi sorularda. Bölüm adı bilinmiyorsa arama yerine bunu kullan.",
|
||||
},
|
||||
fakulte: { type: "string", description: "Fakülte/yüksekokul adı parçası, ör. 'tıp fakültesi', 'meslek yüksekokulu'" },
|
||||
kontenjan_turu: {
|
||||
type: "string",
|
||||
enum: [...OZEL_DURUMLAR],
|
||||
description: "Yalnız bu özel kontenjan türü olan programlar: okul-birincisi, sehit-gazi-yakini, depremzede, kadin-34 (34 yaş üstü kadın). Adayın özel durumu varsa ve 'bana ayrılmış kontenjanı olan' diye sorarsa kullan.",
|
||||
},
|
||||
akreditasyonlu: { type: "boolean", description: "true = yalnız program akreditasyonu olanlar (MÜDEK, TEPDAD, FEDEK …)" },
|
||||
sayfa: { type: "integer", minimum: 1, description: `Sonuç sayfası (her sayfa ${ARAMA_LIMIT} program); 'daha fazla göster' için bir artır` },
|
||||
universite: { type: "string", description: "Üniversite adı parçası ya da yaygın kısaltması, ör. 'Ege', 'ODTÜ', 'İTÜ', 'DEÜ'" },
|
||||
il: { type: "string", description: "İl adı, ör. 'İzmir', 'Urfa'; KKTC için şehir ('Lefkoşa', 'Girne', 'Gazimağusa') ya da tümü için 'KKTC'" },
|
||||
tur: { type: "string", enum: ["devlet", "vakif"], description: "Yalnız devlet ya da yalnız vakıf üniversiteleri" },
|
||||
@@ -115,9 +170,9 @@ export const ARACLAR: AracTanimi[] = [
|
||||
},
|
||||
sirala: {
|
||||
type: "string",
|
||||
enum: ["yakinlik", "taban", "ucret"],
|
||||
enum: ["yakinlik", "taban", "ucret", "kadro"],
|
||||
description:
|
||||
"yakinlik (varsayılan) = adayın sırasına en yakın; taban = en iyi taban sırası önce ('en iyi/en yüksek' sorularında); ucret = en ucuz önce (devlet ve tam burslu 0 sayılır)",
|
||||
"yakinlik (varsayılan) = adayın sırasına en yakın; taban = en iyi taban sırası önce ('en iyi/en yüksek' sorularında); ucret = en ucuz önce (devlet ve tam burslu 0 sayılır); kadro = en çok profesör+doçent önce ('hocası güçlü', 'akademik kadro' sorularında)",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -129,7 +184,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
function: {
|
||||
name: "program_detay",
|
||||
description:
|
||||
"Tek programın ayrıntısı: yıllara göre taban sırası ve kontenjan, ücret/burs, dil, öğretim türü, akreditasyon, kadro, başarı sırası barajı (baraj; aday dışındaysa barajDurumu 'tercih edilemez' der) ve kılavuzdaki yerleşme koşulları (sağlık şartı, mülakat vb.).",
|
||||
"Tek programın ayrıntısı: yıllara göre taban sırası, puan, kontenjan ve yerleşen (gecmis), kontenjan türleri (genel, okul birincisi, şehit-gazi yakını, depremzede, 34 yaş üstü kadın — kontenjan ve yerleşen; bu türlerin ayrı taban sırası elimizde YOK), ücret/burs, dil, öğretim türü, program ve üniversite akreditasyonu, kadro, başarı sırası barajı (baraj; aday dışındaysa barajDurumu 'tercih edilemez' der), yalnız kız/erkek öğrenci koşulu, kılavuzdaki yerleşme koşulları (sağlık şartı, mülakat vb.), bölümün TÜİK mezun istihdamı ve üniversitenin URAP sırası. 'Kontenjan geçen yıl arttı mı', 'kaç kişi yerleşmiş', 'bana ayrılmış kontenjan var mı' sorularında bunu çağır.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -145,7 +200,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
function: {
|
||||
name: "program_netleri",
|
||||
description:
|
||||
"Programa yerleşen SON kişinin ders bazında netleri (TYT/AYT/YDT), taban puanı ve OBP'si; son iki yıl. 'Kaç net lazım' sorularında bunu çağır, net uydurma.",
|
||||
"Programa yerleşen SON kişinin ders bazında netleri (TYT/AYT/YDT), taban puanı, OBP'si ve OBP katsayısı (0,12 standart; 0,18 = meslek lisesi alan içi ek puanla yerleşmiş); verideki tüm yıllar. 'Kaç net lazım' sorularında bunu çağır, net uydurma.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -204,7 +259,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
function: {
|
||||
name: "bolum_ozeti",
|
||||
description:
|
||||
"Bir bölümün Türkiye geneli özeti: kaç program var, kaçı devlet/vakıf, kaç ilde, en iyi taban sırası ve bölüm sayfasının adresi. 'Bu bölüm kaç üniversitede var' gibi genel sorularda bunu çağır; tek tek program için program_ara kullan.",
|
||||
"Bir bölümün Türkiye geneli özeti: kaç program var, kaçı devlet/vakıf, kaç ilde, hangi illerde çok (ilDagilimi), alanları (kategoriler), en iyi taban sırası ve puan aralığı, ilgili bölümler, TÜİK mezun istihdamı ve bölüm sayfasının adresi. 'Bu bölüm kaç üniversitede var', 'en çok hangi şehirde', 'benzer bölümler' sorularında bunu çağır; tek tek program için program_ara kullan.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -261,7 +316,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
function: {
|
||||
name: "universite_profili",
|
||||
description:
|
||||
"Üniversitenin künyesi: il, devlet/vakıf, program sayısı, URAP araştırma sırası, örgün öğrenci sayısı, hoca başına öğrenci ve kadın/erkek öğrenci oranı (üniversite geneli, bölüm bazında veri yok).",
|
||||
"Üniversitenin künyesi: il, devlet/vakıf, program sayısı, fakülteler (program sayılarıyla), kampüs ilçeleri, URAP araştırma sırası, üniversite akreditasyonu (YÖKAK), örgün öğrenci sayısı, öğretim üyesi ve toplam akademisyen sayısı, hoca başına öğrenci (medyanla kıyas) ve kadın/erkek öğrenci oranı (üniversite geneli, bölüm bazında veri yok). 'Hangi fakülteleri var', 'kampüs nerede', 'büyük mü' sorularında bunu çağır.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -272,6 +327,56 @@ export const ARACLAR: AracTanimi[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "universite_siralamasi",
|
||||
description: `URAP Türkiye genel sıralaması (${URAP_DONEM}, ${URAP_TOPLAM} üniversite): sıra, üniversite, il, tür ve sayfa adresi; ile ya da türe göre süzülebilir. 'En iyi üniversiteler', 'İzmir'in en iyi üniversitesi', 'X kaçıncı sırada' sorularında bunu çağır. URAP araştırma performansını ölçer — eğitim kalitesi ya da iş bulma değil; cevabında bunu söyle.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
il: { type: "string", description: "Yalnız bu ildeki üniversiteler, ör. 'İzmir'" },
|
||||
tur: { type: "string", enum: ["devlet", "vakif"], description: "Yalnız devlet ya da yalnız vakıf" },
|
||||
ust: { type: "integer", minimum: 1, maximum: SIRALAMA_UST, description: `Kaç satır (varsayılan 10, en çok ${SIRALAMA_UST})` },
|
||||
sayfa: { type: "integer", minimum: 1, description: "Sonuç sayfası; devamı için bir artır" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "kapanan_program_ara",
|
||||
description: `Güncel kılavuzda OLMAYAN (kapanan, birleşen ya da kodu değişen) programları arşivde arar: son kılavuz yılı, o yılın taban sırası/puanı/kontenjanı. 'X üniversitesinin Y bölümü neden listede/katalogda yok', 'bu bölüm kapandı mı' sorularında önce program_ara ile güncel kılavuza bak, bulamazsan bunu çağır. Arşivdeki program tercih EDİLEMEZ; bunu açıkça söyle.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
arama: { type: "string", description: "Bölüm adı parçası" },
|
||||
universite: { type: "string", description: "Üniversite adı parçası ya da kısaltması" },
|
||||
il: { type: "string", description: "İl adı" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "rehber_oku",
|
||||
description:
|
||||
"Belirli bir rehber yazısının sorulan konuyla ilgili paragraflarını okur (rehber_ara'dan dönen adres/slug ile). Aday 'o yazıda ne diyor', 'ayrıntısı ne', 'oranı kaç' diye sorduğunda ya da rehber_ara'nın özeti yetmediğinde çağır. Kural, oran, tutar ve tarihleri YALNIZ okuduğun paragraflarda yazdığı kadarıyla aktar.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
slug: { type: "string", description: "Yazının adresi ya da slug'ı, ör. '/rehber/obp-nedir-nasil-hesaplanir' ya da 'obp-nedir-nasil-hesaplanir'" },
|
||||
konu: { type: "string", description: "Yazıda aranacak konu, 1-3 anahtar sözcük (verilmezse yazının özü döner)" },
|
||||
},
|
||||
required: ["slug"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -279,7 +384,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
description:
|
||||
"Adayın sohbette söylediği bir tercihi profile kaydeder ya da siler (deger null). YALNIZ şu alanlar: " +
|
||||
PROFIL_ALANLARI.map((a) => `${a} (${PROFIL_ALAN_ACIKLAMA[a]})`).join("; ") +
|
||||
". Sağlık, engel, din, aile, gelir, cinsiyet gibi bilgileri ASLA kaydetme. Kaydettikten sonra cevabında tek cümleyle 'Şunu kaydettim: …' de.",
|
||||
". Sağlık, engel, din, aile, gelir, cinsiyet gibi bilgileri ASLA kaydetme (ozelDurum bir cinsiyet alanı DEĞİLDİR: ÖSYM'nin ayırdığı kontenjan hakkının beyanıdır — 'okul birincisiyim', 'depremzedeyim', 'şehit yakınıyım', '34 yaş üstü kadınım' kaydedilebilir). Kaydettikten sonra cevabında tek cümleyle 'Şunu kaydettim: …' de.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -291,7 +396,8 @@ export const ARACLAR: AracTanimi[] = [
|
||||
`burs: ${Object.keys(PROFIL_SECENEKLERI.burs).join("|")}; ` +
|
||||
`dil: ${Object.keys(PROFIL_SECENEKLERI.dil).join("|")}; ` +
|
||||
`ogretimTuru: ${Object.keys(PROFIL_SECENEKLERI.ogretimTuru).join("|")}; ` +
|
||||
`kampus: ${Object.keys(PROFIL_SECENEKLERI.kampus).join("|")}; null = alanı sil`,
|
||||
`kampus: ${Object.keys(PROFIL_SECENEKLERI.kampus).join("|")}; ` +
|
||||
`ozelDurum: ${OZEL_DURUMLAR.join("|")} (birden çoğu virgülle, tümü yazılır — eklerken eskileri de yaz); null = alanı sil`,
|
||||
},
|
||||
},
|
||||
required: ["alan", "deger"],
|
||||
@@ -319,9 +425,31 @@ function dilimAdi(sira: number, efektif: number | null, baraj?: number | null):
|
||||
return DILIM_ETIKET.garanti;
|
||||
}
|
||||
|
||||
function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
|
||||
/** Adayın özel durumuna uyan kontenjan türleri (yalnız >0 olanlar), öğrenci diliyle. */
|
||||
function uyanKontenjanlar(
|
||||
k: KontenjanTurleri | undefined,
|
||||
durumlar: readonly OzelDurum[],
|
||||
): Record<string, string> | undefined {
|
||||
if (!k || durumlar.length === 0) return undefined;
|
||||
const sonuc: Record<string, string> = {};
|
||||
for (const d of durumlar) {
|
||||
const dilim = k[OZEL_DURUM_KONTENJAN_ALANI[d]];
|
||||
if ((dilim.kontenjan ?? 0) > 0) {
|
||||
sonuc[OZEL_DURUM_KONTENJAN_ADI[d]] = `${k.yil}: ${dilim.kontenjan} kontenjan, ${dilim.yerlesen ?? "—"} yerleşen (ayrı taban sırası elimizde yok)`;
|
||||
}
|
||||
}
|
||||
return Object.keys(sonuc).length ? sonuc : undefined;
|
||||
}
|
||||
|
||||
function programOzeti(
|
||||
p: Program,
|
||||
sira: number,
|
||||
o?: ProgramOzellik,
|
||||
ek?: { kontenjanTurleri?: KontenjanTurleri; ozelDurum?: readonly OzelDurum[]; oneCikan?: string[] },
|
||||
) {
|
||||
// Baraj yalnız veride varsa yazılır; boş = barajsız ya da veride eksik (uydurulmaz)
|
||||
const baraj = o?.minBasariSirasi ?? null;
|
||||
const adayinKontenjani = uyanKontenjanlar(ek?.kontenjanTurleri, ek?.ozelDurum ?? []);
|
||||
return {
|
||||
id: p.id,
|
||||
program: p.isim,
|
||||
@@ -340,9 +468,27 @@ function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
|
||||
: {}),
|
||||
kontenjan: p.kontenjanSon,
|
||||
ozellik: o ? ozellikOzeti(o) : [],
|
||||
...(adayinKontenjani ? { adayinOzelKontenjani: adayinKontenjani } : {}),
|
||||
...(ek?.oneCikan?.length ? { oneCikan: ek.oneCikan } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Sonuç satırlarına uyum puanının "öne çıkan" cümlelerini ekler (ölçüt yoksa boş Map). */
|
||||
function oneCikanlar(b: AracBaglami, programlar: readonly Program[]): Map<string, string[]> {
|
||||
const baglam = uyumBaglamiKur(b);
|
||||
const sonuc = new Map<string, string[]>();
|
||||
if (!uyumBaglamiVar(baglam)) return sonuc;
|
||||
for (const [id, u] of uyumPuanla(programlar, baglam)) {
|
||||
if (u.oneCikan.length) sonuc.set(id, u.oneCikan);
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
/** Adayın etkin özel durumu (sihirbaz ∪ profil). */
|
||||
function adayOzelDurumu(b: AracBaglami): OzelDurum[] {
|
||||
return ozelDurumDogrula([...(b.ozelDurum ?? []), ...(b.profil.ozelDurum ?? [])]);
|
||||
}
|
||||
|
||||
/** Arama metninin katlanmış hâli en az bu kadar harf olmalı; daha kısası her kayıtla eşleşir. */
|
||||
const ARAMA_ALT_HARF = 3;
|
||||
|
||||
@@ -434,28 +580,6 @@ function aramaIliCoz(ham: string): { il: string } | { kktc: true } | null {
|
||||
return adaylar.length === 1 ? { il: adaylar[0].il } : null;
|
||||
}
|
||||
|
||||
const ayrikDegerler = new Map<string, { ham: string; katli: string }[]>();
|
||||
|
||||
/**
|
||||
* Kolonun, katlanmış hâli `katliSorgu`yu içeren değerleri. SQLite LIKE Türkçe
|
||||
* harf katlamayı bilmez ("isletme" İşletme'yi, "bogazici" Boğaziçi'ni bulmaz);
|
||||
* ayrık değerler (birkaç bin) bir kez okunur, eşleşme JS'te yapılır.
|
||||
*/
|
||||
function ayrikListe(kolon: "isim" | "universite"): { ham: string; katli: string }[] {
|
||||
let liste = ayrikDegerler.get(kolon);
|
||||
if (!liste) {
|
||||
liste = (getDb().prepare(`SELECT DISTINCT ${kolon} AS v FROM programs`).all() as { v: string | null }[])
|
||||
.filter((x): x is { v: string } => x.v != null)
|
||||
.map((x) => ({ ham: x.v, katli: turkishSlugify(x.v) }));
|
||||
ayrikDegerler.set(kolon, liste);
|
||||
}
|
||||
return liste;
|
||||
}
|
||||
|
||||
function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): string[] {
|
||||
return ayrikListe(kolon).filter((x) => x.katli.includes(katliSorgu)).map((x) => x.ham);
|
||||
}
|
||||
|
||||
/** Özel kontenjan satırlarının (KKTC Uyruklu, M.T.O.K.) ayrık program adları. */
|
||||
function ozelKontenjanAdlari(): string[] {
|
||||
return ayrikListe("isim").map((x) => x.ham).filter(ozelKontenjanSatiriMi);
|
||||
@@ -515,6 +639,8 @@ function inKosulu(kolon: string, degerler: string[]): string {
|
||||
|
||||
type ProgramAraArg = {
|
||||
arama?: string;
|
||||
kategori?: string;
|
||||
fakulte?: string;
|
||||
universite?: string;
|
||||
il?: string;
|
||||
tur?: "devlet" | "vakif";
|
||||
@@ -524,12 +650,18 @@ type ProgramAraArg = {
|
||||
ucret_ust?: number;
|
||||
dilim?: "hayal" | "dengeli" | "guvenli";
|
||||
ozel_kontenjan?: boolean;
|
||||
sirala?: "yakinlik" | "taban" | "ucret";
|
||||
kontenjan_turu?: OzelDurum;
|
||||
akreditasyonlu?: boolean;
|
||||
sayfa?: number;
|
||||
sirala?: "yakinlik" | "taban" | "ucret" | "kadro";
|
||||
};
|
||||
|
||||
/** Ücret sıralaması: devlet (ücret yok) ve tam burslu 0 sayılır. */
|
||||
const UCRET_SIRA_SQL = `COALESCE((SELECT CASE WHEN o.burs_orani = 'Burslu' THEN 0 ELSE o.ucret END
|
||||
FROM program_ozellik o WHERE o.id = programs.id), 0)`;
|
||||
/** Kadro sıralaması: profesör + doçent (boşlar 0). */
|
||||
const KADRO_SIRA_SQL = `COALESCE((SELECT COALESCE(o.prof,0) + COALESCE(o.doc,0)
|
||||
FROM program_ozellik o WHERE o.id = programs.id), 0)`;
|
||||
|
||||
function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): ProgramAraSonuc {
|
||||
const db = getDb();
|
||||
@@ -547,6 +679,23 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
args.push(...adlar);
|
||||
uygulanan.push(`bölüm ~ "${a.arama}"`);
|
||||
}
|
||||
// Alan ve fakülte DB kolonu değil (kategori isim+fakülteden türer, fakülte
|
||||
// Türkçe katlama ister): JS'te süzülür; o zaman SQL limitsiz koşar.
|
||||
const kategori = a.kategori && (KATEGORILER as readonly string[]).includes(a.kategori) ? a.kategori : undefined;
|
||||
const fakulteKatli = a.fakulte ? turkishSlugify(a.fakulte) : "";
|
||||
if (a.fakulte && fakulteKatli.length < ARAMA_ALT_HARF) {
|
||||
return bos(`Fakülte araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.fakulte}`);
|
||||
}
|
||||
if (kategori) uygulanan.push(`alan = ${kategori}`);
|
||||
if (fakulteKatli) uygulanan.push(`fakülte ~ "${a.fakulte}"`);
|
||||
const jsSuzgec = Boolean(kategori || fakulteKatli);
|
||||
if (a.kontenjan_turu) {
|
||||
const yil = kontenjanTuruYili();
|
||||
if (yil == null) return bos("Kontenjan türü verisi bu veritabanında yok.");
|
||||
const onek = KONTENJAN_TURU_ONEK[OZEL_DURUM_KONTENJAN_ALANI[a.kontenjan_turu]];
|
||||
kosullar.push(`${onek}${yil} > 0`);
|
||||
uygulanan.push(`${OZEL_DURUM_KONTENJAN_ADI[a.kontenjan_turu]} olanlar (${yil} kırılımı)`);
|
||||
}
|
||||
if (a.universite) {
|
||||
const katli = uniKatli(a.universite);
|
||||
if (katli.length < ARAMA_ALT_HARF) return bos(`Üniversite araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.universite}`);
|
||||
@@ -623,6 +772,10 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
ozKosul.push("(ogrenim_turu IS NULL OR ogrenim_turu = 'Örgün Öğretim')");
|
||||
uygulanan.push("profil: yalnız örgün");
|
||||
}
|
||||
if (a.akreditasyonlu) {
|
||||
ozKosul.push("(akreditasyon IS NOT NULL AND akreditasyon <> '')");
|
||||
uygulanan.push("yalnız akreditasyonlu programlar");
|
||||
}
|
||||
if (ozKosul.length) {
|
||||
kosullar.push(`id IN (SELECT id FROM program_ozellik WHERE ${ozKosul.join(" AND ")})`);
|
||||
args.push(...ozArgs);
|
||||
@@ -646,21 +799,44 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
? `${EFEKTIF_SIRA} ASC, ${yakinlik}`
|
||||
: a.sirala === "ucret"
|
||||
? `${UCRET_SIRA_SQL} ASC, ${yakinlik}`
|
||||
: yakinlik;
|
||||
: a.sirala === "kadro"
|
||||
? `${KADRO_SIRA_SQL} DESC, ${yakinlik}`
|
||||
: yakinlik;
|
||||
if (a.sirala === "taban") uygulanan.push("sıralama: en iyi taban sırası önce");
|
||||
if (a.sirala === "ucret") uygulanan.push("sıralama: en ucuz önce");
|
||||
const satirlar = db
|
||||
.prepare(
|
||||
`SELECT ${SELECT_COLS} FROM programs WHERE ${kosullar.join(" AND ")}
|
||||
ORDER BY ${siralama} LIMIT ?`,
|
||||
if (a.sirala === "kadro") uygulanan.push("sıralama: en kalabalık kadro (prof.+doç.) önce");
|
||||
const sayfa = Math.max(1, Math.floor(a.sayfa ?? 1));
|
||||
const offset = (sayfa - 1) * ARAMA_LIMIT;
|
||||
let programlar: Program[];
|
||||
let toplam: number;
|
||||
if (jsSuzgec) {
|
||||
// Limitsiz SQL + JS süzgeci (alan / fakülte), sonra sayfala
|
||||
const tumu = (
|
||||
db
|
||||
.prepare(`SELECT ${SELECT_COLS} FROM programs WHERE ${kosullar.join(" AND ")} ORDER BY ${siralama}`)
|
||||
.all(...args, b.sira) as ProgramHamSatiri[]
|
||||
)
|
||||
.all(...args, b.sira, ARAMA_LIMIT) as ProgramHamSatiri[];
|
||||
const programlar = satirlar.map(programSatiri);
|
||||
const toplam = (
|
||||
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${kosullar.join(" AND ")}`).get(...args) as {
|
||||
n: number;
|
||||
}
|
||||
).n;
|
||||
.map(programSatiri)
|
||||
.filter((p) => !kategori || kategoriEslesir(p.isim, [kategori], p.fakulte))
|
||||
.filter((p) => !fakulteKatli || turkishSlugify(p.fakulte ?? "").includes(fakulteKatli));
|
||||
toplam = tumu.length;
|
||||
programlar = tumu.slice(offset, offset + ARAMA_LIMIT);
|
||||
} else {
|
||||
programlar = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT ${SELECT_COLS} FROM programs WHERE ${kosullar.join(" AND ")}
|
||||
ORDER BY ${siralama} LIMIT ? OFFSET ?`,
|
||||
)
|
||||
.all(...args, b.sira, ARAMA_LIMIT, offset) as ProgramHamSatiri[]
|
||||
).map(programSatiri);
|
||||
toplam = (
|
||||
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${kosullar.join(" AND ")}`).get(...args) as {
|
||||
n: number;
|
||||
}
|
||||
).n;
|
||||
}
|
||||
if (sayfa > 1) uygulanan.push(`sayfa ${sayfa}`);
|
||||
// Profil süzgeçleri sonucu sıfırladıysa profilsiz tekrar dene ve bunu söyle:
|
||||
// model "hiç yok" demek yerine "profilin dışında şunlar var" diyebilsin.
|
||||
if (toplam === 0 && profilUygula && uygulanan.some((u) => u.includes("profil"))) {
|
||||
@@ -670,18 +846,34 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
not: `Profil süzgeçleriyle (${uygulanan.filter((u) => u.includes("profil")).join("; ")}) eşleşen program YOK; aşağıdakiler profil süzgeci olmadan bulundu — adaya bunu açıkça söyle. ${gevsek.not}`,
|
||||
};
|
||||
}
|
||||
const ozellikler = programOzellikleriGetir(programlar.map((p) => p.id));
|
||||
const sonuclar = programlar.map((p) => programOzeti(p, b.sira, ozellikler[p.id]));
|
||||
const ids = programlar.map((p) => p.id);
|
||||
const ozellikler = programOzellikleriGetir(ids);
|
||||
const ozelDurum = adayOzelDurumu(b);
|
||||
const kontenjanlar = ozelDurum.length ? kontenjanTurleriGetirToplu(ids) : {};
|
||||
const oneCikan = oneCikanlar(b, programlar);
|
||||
const sonuclar = programlar.map((p) =>
|
||||
programOzeti(p, b.sira, ozellikler[p.id], {
|
||||
kontenjanTurleri: kontenjanlar[p.id],
|
||||
ozelDurum,
|
||||
oneCikan: oneCikan.get(p.id),
|
||||
}),
|
||||
);
|
||||
const barajDisiSayisi = sonuclar.filter((x) => "barajDurumu" in x).length;
|
||||
const kalan = Math.max(0, toplam - offset - programlar.length);
|
||||
return {
|
||||
toplamEslesen: toplam,
|
||||
gosterilen: programlar.length,
|
||||
sayfa,
|
||||
uygulananFiltreler: uygulanan,
|
||||
sonuclar,
|
||||
not:
|
||||
`tabanSira ${SON_YIL} yerleştirmesinin taban başarı sırasıdır (tabanYili ${ONCEKI_YIL} ise o program ${SON_YIL}'da dolmadı); her yıl değişir.` +
|
||||
(kalan ? ` ${kalan} sonuç daha var: devamı için aynı aramayı sayfa=${sayfa + 1} ile çağır.` : "") +
|
||||
(barajDisiSayisi
|
||||
? ` ${barajDisiSayisi} sonuç BARAJ DIŞI: aday bu programları tercih edemez; öneri olarak sunma, "Hayal olarak yaz" deme, barajı açıkça söyle.`
|
||||
: "") +
|
||||
(ozelDurum.length
|
||||
? " adayinOzelKontenjani alanı: adayın durumuna ayrılmış kontenjan; o kontenjanın taban sırası elimizde YOK, 'kontenjanın var, girersin' deme."
|
||||
: ""),
|
||||
};
|
||||
}
|
||||
@@ -689,6 +881,7 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
type ProgramAraSonuc = {
|
||||
toplamEslesen: number;
|
||||
gosterilen: number;
|
||||
sayfa?: number;
|
||||
uygulananFiltreler: string[];
|
||||
sonuclar: ReturnType<typeof programOzeti>[];
|
||||
not: string;
|
||||
@@ -701,25 +894,52 @@ function programDetay(b: AracBaglami, a: { program_id?: string }) {
|
||||
const o = programOzellikGetir(id);
|
||||
const kosullar = o ? kosulMetinleri(o.kosulKodlari).slice(0, 8) : [];
|
||||
const evKm = b.profil.evIl && p.il ? illerArasiKm(b.profil.evIl, p.il) : null;
|
||||
const kontenjanTurleri = kontenjanTurleriGetir(id);
|
||||
const duzey: IstihdamDuzeyi = b.tur === "tyt" ? "onlisans" : "lisans";
|
||||
const istihdam = BOLUM_ISTIHDAM[duzey][bolumSayfaSlug(p.isim)] ?? null;
|
||||
const uniSlug = uniSayfaSlug(p.universite);
|
||||
const urap = urapSirasiBul(uniSlug);
|
||||
return {
|
||||
...programOzeti(p, b.sira, o ?? undefined),
|
||||
...programOzeti(p, b.sira, o ?? undefined, {
|
||||
kontenjanTurleri: kontenjanTurleri ?? undefined,
|
||||
ozelDurum: adayOzelDurumu(b),
|
||||
oneCikan: oneCikanlar(b, [p]).get(p.id),
|
||||
}),
|
||||
fakulte: p.fakulte,
|
||||
sure: p.sure,
|
||||
tabanSiraYillara: Object.fromEntries(YILLAR.map((y, i) => [y, p.siraGecmisi[i]])),
|
||||
// Yıl → sıra/puan/kontenjan/yerleşen (yeniden eskiye); boş yıl: dolmadı ya da yoktu
|
||||
gecmis: Object.fromEntries(
|
||||
programGecmisiGetir(id).map((g) => [g.yil, { sira: g.sira, puan: g.puan, kontenjan: g.kontenjan, yerlesen: g.yerlesen }]),
|
||||
),
|
||||
trend: trendOzetiMetni(p.siraGecmisi),
|
||||
puanSon: p.puanSon,
|
||||
yerlesenSon: p.yerlesenSon,
|
||||
kontenjanTurleri: kontenjanTurleri
|
||||
? {
|
||||
yil: kontenjanTurleri.yil,
|
||||
...Object.fromEntries(
|
||||
(Object.keys(KONTENJAN_TURU_ADI) as (keyof typeof KONTENJAN_TURU_ADI)[])
|
||||
.filter((t) => (kontenjanTurleri[t].kontenjan ?? 0) > 0)
|
||||
.map((t) => [KONTENJAN_TURU_ADI[t], { kontenjan: kontenjanTurleri[t].kontenjan, yerlesen: kontenjanTurleri[t].yerlesen }]),
|
||||
),
|
||||
not: "Özel kontenjanlar yalnız o durumdaki adaylara açıktır; her birinin ayrı taban sırası elimizde YOK.",
|
||||
}
|
||||
: null,
|
||||
ucretYillik: o?.ucret ?? null,
|
||||
bursOrani: o?.bursOrani ?? null,
|
||||
ogretimDili: o?.ogrenimDili ?? null,
|
||||
ogretimTuru: o?.ogrenimTuru ?? null,
|
||||
akreditasyon: o?.akreditasyon ?? null,
|
||||
uniAkreditasyon: o?.uniAkreditasyon ?? null,
|
||||
kadro: o
|
||||
? { prof: o.prof, docent: o.doc, drOgrUyesi: o.dou, arsGor: o.argor, ogretimUyesiToplam: ogretimUyesiToplami(o) }
|
||||
: null,
|
||||
cinsiyetKosulu: o?.cinsiyetKosulu ? CINSIYET_KOSULU_METNI[o.cinsiyetKosulu] : null,
|
||||
ilce: o?.ilce ?? null,
|
||||
evdenKm: evKm,
|
||||
kosullar: kosullar.map((k) => (k.metin.length > 240 ? `${k.metin.slice(0, 237)}…` : k.metin)),
|
||||
istihdam: istihdam
|
||||
? { kayitliIstihdamYuzde: istihdam.istihdam, ilkIsBulmaAy: istihdam.isBulmaAy, kazancGrubu: istihdam.kazanc, kaynak: BOLUM_ISTIHDAM_KAYNAK, not: "Ülke geneli, bölüm bazında; üniversite kırılımı yok." }
|
||||
: null,
|
||||
urap: urap ? { sira: urap.sira, toplam: URAP_TOPLAM, donem: URAP_DONEM } : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -746,7 +966,7 @@ function programNetleri(_b: AracBaglami, a: { program_id?: string }) {
|
||||
const id = String(a.program_id ?? "").trim();
|
||||
const p = id ? programGetir(id) : null;
|
||||
if (!p) return { hata: "Program bulunamadı; önce program_ara ile id al." };
|
||||
const satirlar = netlerGetir(id).slice(0, NET_YIL_SAYISI);
|
||||
const satirlar = netlerGetir(id);
|
||||
if (!satirlar.length) {
|
||||
return { program: p.isim, universite: p.universite, hata: "Bu program için net verisi yok; uydurma." };
|
||||
}
|
||||
@@ -757,13 +977,14 @@ function programNetleri(_b: AracBaglami, a: { program_id?: string }) {
|
||||
yil: s.yil,
|
||||
tabanPuan: s.tabanPuan,
|
||||
obp: s.obp,
|
||||
obpKatsayisi: s.katsayi,
|
||||
netler: Object.fromEntries(
|
||||
(Object.keys(NET_ETIKET) as (keyof ProgramNetSatiri)[])
|
||||
.filter((k) => s[k] != null)
|
||||
.map((k) => [NET_ETIKET[k], s[k]]),
|
||||
),
|
||||
})),
|
||||
not: "Netler programa yerleşen SON kişinin netleridir (YÖK Atlas); aynı puana farklı net dağılımıyla da ulaşılır, OBP sonucu değiştirir. Hedef değil, yaklaşık düzey olarak aktar.",
|
||||
not: "Netler programa yerleşen SON kişinin netleridir (YÖK Atlas); aynı puana farklı net dağılımıyla da ulaşılır, OBP sonucu değiştirir. Hedef değil, yaklaşık düzey olarak aktar. obpKatsayisi 0,12 standarttır; 0,18 ise o yıl son yerleşen meslek lisesi alan içi ek puanıyla (0,06) yerleşmiştir — düz lise mezunu için netler daha yüksek olmalıydı, bunu söyle.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -832,16 +1053,32 @@ function bolumOzeti(_b: AracBaglami, a: { bolum?: string }) {
|
||||
const bolum = aday ? getBolumBySlug(aday.slug) : null;
|
||||
if (!bolum) return { hata: `Bölüm bulunamadı: ${q}` };
|
||||
const s = bolum.stats;
|
||||
const programlar = [...bolum.lisansProgramlari, ...bolum.onlisansProgramlari];
|
||||
const ilSayac = new Map<string, number>();
|
||||
for (const p of programlar) if (p.il) ilSayac.set(p.il, (ilSayac.get(p.il) ?? 0) + 1);
|
||||
const ilDagilimi = [...ilSayac]
|
||||
.sort((x, y) => y[1] - x[1])
|
||||
.slice(0, IL_DAGILIMI_UST)
|
||||
.map(([il, n]) => `${il}: ${n}`);
|
||||
const duzey: IstihdamDuzeyi = bolum.lisans ? "lisans" : "onlisans";
|
||||
const istihdam = BOLUM_ISTIHDAM[duzey][bolum.slug] ?? null;
|
||||
return {
|
||||
ad: bolum.ad,
|
||||
duzey: [bolum.lisans ? "lisans" : null, bolum.onlisans ? "ön lisans" : null].filter(Boolean),
|
||||
kategoriler: bolum.kategoriler,
|
||||
programSayisi: s.toplam,
|
||||
devletProgram: s.devlet,
|
||||
vakifProgram: s.vakif,
|
||||
ilSayisi: s.ilSayisi,
|
||||
ilDagilimi,
|
||||
enIyiTabanSira: s.minSira,
|
||||
tabanPuanAraligi: s.minPuan != null && s.maxPuan != null ? { enDusuk: s.minPuan, enYuksek: s.maxPuan } : null,
|
||||
ilgiliBolumler: ilgiliBolumler(bolum.slug, 5).map((x) => ({ ad: x.ad, sayfa: `/bolum/${x.slug}` })),
|
||||
istihdam: istihdam
|
||||
? { kayitliIstihdamYuzde: istihdam.istihdam, ilkIsBulmaAy: istihdam.isBulmaAy, kazancGrubu: istihdam.kazanc, kaynak: BOLUM_ISTIHDAM_KAYNAK }
|
||||
: null,
|
||||
sayfa: `/bolum/${bolum.slug}`,
|
||||
not: "Sayılar tüm puan türleri ve Türkiye geneli içindir; adayın sırasına göre program listesi için program_ara kullan.",
|
||||
not: "Sayılar tüm puan türleri ve Türkiye geneli içindir; adayın sırasına göre program listesi için program_ara kullan. ilDagilimi en çok programı olan iller (program sayısı).",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -885,7 +1122,10 @@ const HTML_VARLIK: Record<string, string> = { "&": "&", "<": "<", ">":
|
||||
* "0,75" yazdı). Alıntı kendi rehberimizin metnidir; şema parçalarında şemanın
|
||||
* sözel açıklaması (aria) kullanılır. Okunamazsa boş döner — araç düşmez.
|
||||
*/
|
||||
function rehberAlintisi(slug: string, kelimeler: string[]): string {
|
||||
/** rehber_oku ile doğrudan okunan yazının alıntı üst sınırı (karakter); rehber_ara'dakinden geniş. */
|
||||
const REHBER_OKUMA_UST = 2400;
|
||||
|
||||
function rehberAlintisi(slug: string, kelimeler: string[], ustSinir = REHBER_ALINTI_UST): string {
|
||||
try {
|
||||
const yazi = getRehber(slug);
|
||||
if (!yazi) return "";
|
||||
@@ -906,7 +1146,7 @@ function rehberAlintisi(slug: string, kelimeler: string[]): string {
|
||||
let uzunluk = puanli[0]?.metin.length ?? 0;
|
||||
for (const x of [...puanli].sort((a, b) => b.puan - a.puan || a.i - b.i)) {
|
||||
if (secilen.has(x.i) || x.puan === 0) continue;
|
||||
if (uzunluk + x.metin.length > REHBER_ALINTI_UST) continue;
|
||||
if (uzunluk + x.metin.length > ustSinir) continue;
|
||||
secilen.add(x.i);
|
||||
uzunluk += x.metin.length;
|
||||
}
|
||||
@@ -994,11 +1234,27 @@ function universiteProfili(_b: AracBaglami, a: { universite?: string }) {
|
||||
if (!u) return { hata: `Üniversite bulunamadı: ${q}` };
|
||||
const i = u.istatistik;
|
||||
const kadin = i ? kadinYuzdesi(i) : null;
|
||||
// Üniversite akreditasyonu ve kampüs ilçeleri program_ozellik'ten: en sık değer / ayrık liste
|
||||
const tumProgramlar = u.fakulteler.flatMap((f) => f.programlar);
|
||||
const ozellikler = Object.values(programOzellikleriGetir(tumProgramlar.map((p) => p.id)));
|
||||
const enSik = (degerler: (string | null)[]) => {
|
||||
const sayac = new Map<string, number>();
|
||||
for (const d of degerler) if (d) sayac.set(d, (sayac.get(d) ?? 0) + 1);
|
||||
return [...sayac].sort((x, y) => y[1] - x[1])[0]?.[0] ?? null;
|
||||
};
|
||||
const ilceler = [...new Set(ozellikler.map((o) => o.ilce).filter((x): x is string => Boolean(x)))].sort();
|
||||
return {
|
||||
ad: u.ad,
|
||||
il: u.il,
|
||||
tur: u.unitur,
|
||||
programSayisi: u.programSayisi,
|
||||
fakulteSayisi: u.fakulteler.length,
|
||||
fakulteler: [...u.fakulteler]
|
||||
.sort((x, y) => y.programlar.length - x.programlar.length)
|
||||
.slice(0, FAKULTE_UST)
|
||||
.map((f) => `${f.fakulte} (${f.programlar.length} program)`),
|
||||
kampusIlceleri: ilceler,
|
||||
uniAkreditasyon: enSik(ozellikler.map((o) => o.uniAkreditasyon)),
|
||||
enDusukTabanSira: u.stats.minSira,
|
||||
toplamKontenjan: u.stats.toplamKontenjan,
|
||||
urap: u.urap ? { sira: u.urap.sira, toplam: URAP_TOPLAM, donem: URAP_DONEM, olctugu: "araştırma performansı; eğitim kalitesi ya da iş bulma değil" } : null,
|
||||
@@ -1007,16 +1263,109 @@ function universiteProfili(_b: AracBaglami, a: { universite?: string }) {
|
||||
donem: YOK_ISTATISTIK_DONEM,
|
||||
orgunOgrenci: i.ogrenci,
|
||||
ogretimUyesi: i.ogretimUyesi,
|
||||
akademisyenToplam: i.akademisyen,
|
||||
hocaBasinaOgrenci: ogrenciBasinaUye(i),
|
||||
hocaBasinaOgrenciMedyan: OGRENCI_BASINA_UYE_MEDYAN,
|
||||
kadinYuzdesi: kadin,
|
||||
erkekYuzdesi: kadin == null ? null : 100 - kadin,
|
||||
not: "Kadın/erkek oranı örgün ön lisans + lisans, ÜNİVERSİTE GENELİ; bölüm bazında veri yok. Bilgi olarak söyle, filtre yapma.",
|
||||
not: "Kadın/erkek oranı örgün ön lisans + lisans, ÜNİVERSİTE GENELİ; bölüm bazında veri yok. Bilgi olarak söyle, filtre yapma. hocaBasinaOgrenci medyanın altındaysa hoca başına daha az öğrenci düşüyor demektir.",
|
||||
}
|
||||
: null,
|
||||
sayfa: `/universite/${u.slug}`,
|
||||
};
|
||||
}
|
||||
|
||||
function universiteSiralamasi(_b: AracBaglami, a: { il?: string; tur?: "devlet" | "vakif"; ust?: number; sayfa?: number }) {
|
||||
const ust = Math.min(SIRALAMA_UST, Math.max(1, Math.floor(a.ust ?? 10)));
|
||||
const sayfa = Math.max(1, Math.floor(a.sayfa ?? 1));
|
||||
let ilAdi: string | null = null;
|
||||
if (a.il) {
|
||||
const cozum = aramaIliCoz(a.il);
|
||||
if (!cozum || "kktc" in cozum) return { hata: `İl tanınmadı ya da URAP'ta yok: ${a.il}` };
|
||||
ilAdi = cozum.il;
|
||||
}
|
||||
const uniler = new Map(getAllUniversiteler().map((u) => [u.slug, u]));
|
||||
const liste = Object.entries(URAP_SIRA)
|
||||
.map(([slug, u]) => ({ slug, sira: u.sira, uni: uniler.get(slug) }))
|
||||
.filter((x) => x.uni)
|
||||
.filter((x) => !ilAdi || x.uni!.il === ilAdi)
|
||||
.filter((x) => !a.tur || (TUR_SAYFALARI[a.tur].uniturler as readonly string[]).includes(x.uni!.unitur ?? ""))
|
||||
.sort((x, y) => x.sira - y.sira);
|
||||
const dilim = liste.slice((sayfa - 1) * ust, sayfa * ust);
|
||||
return {
|
||||
donem: URAP_DONEM,
|
||||
toplam: liste.length,
|
||||
gosterilen: dilim.length,
|
||||
sayfa,
|
||||
siralama: dilim.map((x) => ({
|
||||
sira: x.sira,
|
||||
universite: x.uni!.ad,
|
||||
il: x.uni!.il,
|
||||
tur: x.uni!.unitur,
|
||||
sayfa: `/universite/${x.slug}`,
|
||||
})),
|
||||
not: `URAP Türkiye genel sıralaması (${URAP_TOPLAM} üniversite) ARAŞTIRMA performansını ölçer — yayın ve atıf; eğitim kalitesi, iş bulma ya da öğrenci memnuniyeti değildir. Cevabında bunu söyle. "siralama" satırlarını (sıra + üniversite, numaralı liste) cevabına YAZ — "işte liste:" deyip boş bırakma. Listede olmayan üniversite (vakıf/KKTC'nin çoğu) kötü demek değildir, sıralanmamıştır.`,
|
||||
};
|
||||
}
|
||||
|
||||
function kapananProgramAra(_b: AracBaglami, a: { arama?: string; universite?: string; il?: string }) {
|
||||
if (!a.arama && !a.universite && !a.il) return { hata: "En az bir ölçüt gerekli: arama, universite ya da il." };
|
||||
let il: string | undefined;
|
||||
if (a.il) {
|
||||
const cozum = aramaIliCoz(a.il);
|
||||
if (!cozum || "kktc" in cozum) return { hata: `İl tanınmadı: ${a.il}` };
|
||||
il = cozum.il;
|
||||
}
|
||||
const universite = a.universite ? uniKatli(a.universite) : undefined;
|
||||
if (a.arama && turkishSlugify(a.arama).length < ARAMA_ALT_HARF) return { hata: `En az ${ARAMA_ALT_HARF} harf yaz.` };
|
||||
if (universite && universite.length < ARAMA_ALT_HARF) return { hata: `Üniversite için en az ${ARAMA_ALT_HARF} harf yaz.` };
|
||||
// db.arsivProgramAra üniversiteyi katlanmış dizeyle arar; kısaltma burada açıldı
|
||||
const { toplam, programlar } = arsivProgramAra({
|
||||
arama: a.arama,
|
||||
universite: universite ? universite.replace(/-/g, " ") : undefined,
|
||||
il,
|
||||
limit: ARAMA_LIMIT,
|
||||
});
|
||||
if (toplam === 0) return { toplam: 0, programlar: [], not: "Arşivde de böyle bir program yok; güncel kılavuz için program_ara'yı dene. Program adı ya da kod uydurma." };
|
||||
return {
|
||||
toplam,
|
||||
gosterilen: programlar.length,
|
||||
programlar: programlar.map((p) => ({
|
||||
program: p.isim,
|
||||
universite: p.universite,
|
||||
il: p.il,
|
||||
tur: p.unitur,
|
||||
puanTuru: p.tur,
|
||||
sonKilavuzYili: p.sonKilavuzYili,
|
||||
sonYerlestirme: p.sonYil ? { yil: p.sonYil, tabanSira: p.sonSira, tabanPuan: p.sonPuan, kontenjan: p.sonKontenjan, yerlesen: p.sonYerlesen } : null,
|
||||
...(p.eskiKod ? { eskiKod: p.eskiKod } : {}),
|
||||
})),
|
||||
not: `Bu programlar ${TERCIH_YILI} tercih kılavuzunda YOK: kapanmış, başka programla birleşmiş ya da kodu değişmiş olabilir; tercih edilemezler. Aynı bölümün güncel programı varsa program_ara ile bul. Kapanma sebebini uydurma (veride yok).`,
|
||||
};
|
||||
}
|
||||
|
||||
function rehberOku(_b: AracBaglami, a: { slug?: string; konu?: string }) {
|
||||
const slug = String(a.slug ?? "")
|
||||
.trim()
|
||||
.replace(/^https?:\/\/[^/]+/, "")
|
||||
.replace(/^\/?rehber\//, "")
|
||||
.replace(/[/?#].*$/, "");
|
||||
if (!slug) return { hata: "Yazının adresi ya da slug'ı gerekli (rehber_ara sonucundaki sayfa alanı)." };
|
||||
const yazi = getRehber(slug);
|
||||
if (!yazi) return { hata: `Böyle bir rehber yazısı yok: ${slug}. Önce rehber_ara ile adresi bul.` };
|
||||
const kelimeler = turkishSlugify(String(a.konu ?? ""))
|
||||
.split("-")
|
||||
.filter((k) => k.length > 2);
|
||||
const alinti = rehberAlintisi(slug, kelimeler, REHBER_OKUMA_UST);
|
||||
return {
|
||||
baslik: yazi.baslik,
|
||||
ozet: yazi.aciklama,
|
||||
sayfa: `/rehber/${slug}`,
|
||||
alinti: alinti || yazi.aciklama,
|
||||
not: "Kural, oran, katsayı, tutar ve tarihleri YALNIZ alıntıda yazdığı kadarıyla aktar; alıntıda yoksa 'yazının tamamında' de ve adresini ver.",
|
||||
};
|
||||
}
|
||||
|
||||
function sayiParse(v: unknown): number | null {
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v !== "string") return null;
|
||||
@@ -1045,6 +1394,16 @@ async function profilGuncelle(b: AracBaglami, a: { alan?: string; deger?: unknow
|
||||
const il = trIlCoz(String(deger));
|
||||
if (!il) return { hata: `İl tanınmadı: ${String(deger)}` };
|
||||
deger = il;
|
||||
} else if (alan === "ozelDurum") {
|
||||
// "okul-birincisi, depremzede" → allowlist dizisi; bilinmeyen değer hata
|
||||
const parcalar = (Array.isArray(deger) ? deger : String(deger).split(","))
|
||||
.map((x) => turkishSlugify(String(x)).replace(/^sehit-?gazi-?yakini$/, "sehit-gazi-yakini"))
|
||||
.filter(Boolean);
|
||||
const gecerli = ozelDurumDogrula(parcalar);
|
||||
if (gecerli.length !== parcalar.length) {
|
||||
return { hata: `Bilinmeyen özel durum; yalnız şunlar: ${OZEL_DURUMLAR.join(", ")}` };
|
||||
}
|
||||
deger = gecerli;
|
||||
} else {
|
||||
deger = String(deger).trim();
|
||||
}
|
||||
@@ -1181,6 +1540,12 @@ export function aracCalistirici(b: AracBaglami) {
|
||||
return tercihTakvimi();
|
||||
case "universite_profili":
|
||||
return universiteProfili(b, a);
|
||||
case "universite_siralamasi":
|
||||
return universiteSiralamasi(b, a);
|
||||
case "kapanan_program_ara":
|
||||
return kapananProgramAra(b, a);
|
||||
case "rehber_oku":
|
||||
return rehberOku(b, a);
|
||||
case "profil_guncelle":
|
||||
return profilGuncelle(b, a);
|
||||
default:
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { RaporParams } from "@/features/rapor/types/rapor";
|
||||
import { secimOzeti } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { PUAN_TURLERI } from "@/types/yokatlas";
|
||||
import { ACIK_SATIR } from "@/lib/rapor-maske";
|
||||
import { adayDurumu, ozelDurumOzeti } from "@/lib/ozel-durum";
|
||||
import { ONCEKI_YIL, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
|
||||
/**
|
||||
@@ -70,12 +71,19 @@ export function sistemPromptu(opts: {
|
||||
profil?: DanismanProfili;
|
||||
/** Verideki başarı sırası barajları (araclar.barajTablosu); verilmezse baraj bölümü yazılmaz. */
|
||||
barajlar?: { sira: number; bolumler: string }[];
|
||||
/**
|
||||
* Adayın raporunun üst metni (yapay zekânın listeyle birlikte yazdığı genel
|
||||
* değerlendirme ve uyarılar) ve kapsam notu. Kilitli kullanıcıda genel
|
||||
* değerlendirme verilmez (liste gizli); uyarılar herkese.
|
||||
*/
|
||||
rapor?: { genelDegerlendirme?: string; uyarilar?: string[]; kapsam?: { toplam: number; uyarlandi: boolean } };
|
||||
}): string {
|
||||
const { params } = opts;
|
||||
const secimVar =
|
||||
(params.kategoriler?.length ?? 0) > 0 ||
|
||||
(params.iller?.length ?? 0) > 0 ||
|
||||
(params.oncelikler?.length ?? 0) > 0 ||
|
||||
(params.ozelDurum?.length ?? 0) > 0 ||
|
||||
(params.universiteTipi && params.universiteTipi !== "farketmez");
|
||||
|
||||
const parcalar = [
|
||||
@@ -94,6 +102,12 @@ KURALLAR:
|
||||
sıralaması adayın belirgin altında, Az riskli (sarı) = adayın sıralaması
|
||||
civarında, Riskli (kırmızı) = son yerleştirmede adayın sıralamasının
|
||||
üzerinde kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
|
||||
- Satırlarda "neden:" listeyi kuran yapay zekânın o satır için yazdığı
|
||||
gerekçe, "öne çıkan:" ise KODUN hesapladığı gerçek veriler (TÜİK istihdamı,
|
||||
URAP sırası, kadro, özel kontenjan, kampüs, uzaklık). "Bu program neden
|
||||
listemde?" sorusunda ÖNCE bu ikisini aktar; sayı gerekirse program_detay
|
||||
ile destekle. Gerekçe boşsa "liste kurulurken gerekçe yazılmamış" de,
|
||||
uydurma.
|
||||
- VERİ YILI: listedeki ve araçlardaki taban sıraları ile kontenjanlar
|
||||
${SON_YIL} yerleştirmesine aittir (${SON_YIL}'da dolmayan az sayıda programda
|
||||
${ONCEKI_YIL}; araç bunu "tabanYili" alanında söyler). Yıl söylemen
|
||||
@@ -123,13 +137,26 @@ KURALLAR:
|
||||
"Bu durumda listende Güvenli etiketli tercih azalır / kalmaz."
|
||||
(Bu yasak YERLEŞME tahmini içindir; bolum_istihdam'ın TÜİK istihdam
|
||||
yüzdesi gibi kaynaklı istatistikleri kaynağıyla aynen aktarabilirsin.)
|
||||
- Bilmediğin güncel veriyi uydurma: taban sırası, kontenjan, ücret, burs,
|
||||
dil, akreditasyon, koşul, net, mezun istihdamı, bölüm ya da şehir özeti,
|
||||
tercih takvimi, rehber yazısı ya da üniversite künyesi gerekince ARAÇLARI
|
||||
çağır (program_ara, program_detay, program_netleri, program_karsilastir,
|
||||
bolum_istihdam, bolum_ozeti, il_ozeti, rehber_ara, tercih_takvimi,
|
||||
universite_profili); araç veremiyorsa
|
||||
- ÖZEL KONTENJAN (okul birincisi, şehit-gazi yakını, depremzede, 34 yaş üstü
|
||||
kadın): aday böyle bir durum söylerse "yararlanırsın / bu kontenjandan
|
||||
girersin" DEME. ÖSYM bu kontenjanlar için ayrı taban sırası yayımlar ve o
|
||||
sıra elimizde YOK; yararlanma koşulunu ÖSYM kılavuzuna bırak, genel taban
|
||||
sırasıyla kıyasla. Programda böyle bir kontenjan olup olmadığını araç
|
||||
(program_detay, program_ara) söyler; sayı uydurma.
|
||||
- Bilmediğin güncel veriyi uydurma: taban sırası, kontenjan (yıllara göre
|
||||
ve türlere göre), yerleşen, ücret, burs, dil, akreditasyon, koşul, net,
|
||||
mezun istihdamı, bölüm ya da şehir özeti, üniversite sıralaması, kapanan
|
||||
program, tercih takvimi, rehber yazısı ya da üniversite künyesi gerekince
|
||||
ARAÇLARI çağır (program_ara, program_detay, program_netleri,
|
||||
program_karsilastir, bolum_istihdam, bolum_ozeti, il_ozeti, rehber_ara,
|
||||
rehber_oku, tercih_takvimi, universite_profili, universite_siralamasi,
|
||||
kapanan_program_ara); araç veremiyorsa
|
||||
"elimde veri yok" de ve YÖK Atlas'a bakmasını söyle.
|
||||
"X üniversitesinin Y bölümü neden yok / kapandı mı" sorusunda önce
|
||||
program_ara, bulamazsan kapanan_program_ara; arşivdeki program tercih
|
||||
edilemez. "En iyi üniversite" sorusunda universite_siralamasi (URAP
|
||||
araştırma sırası — eğitim kalitesi değil, bunu söyle). Rehber yazısının
|
||||
ayrıntısı sorulunca rehber_oku.
|
||||
SAYI KURALI: bir taban sırası, kontenjan, ücret, net, kira, burs tutarı ya
|
||||
da oran yazacaksan o sayı yukarıdaki listede ya da BU cevapta çağırdığın
|
||||
aracın sonucunda görünüyor olmalı. "Yaklaşık", "civarı", "genelde",
|
||||
@@ -150,6 +177,9 @@ KURALLAR:
|
||||
liste sihirbazla yeniden üretilir. Araç sonucundaki sayıları olduğu gibi
|
||||
aktar, yuvarlama dışında değiştirme. Bir soruda en fazla 2-3 araç çağır
|
||||
(sınır: bir adımda 4, bir cevapta 8 çağrı; fazlası çalıştırılmaz).
|
||||
Araç çağırmadan ÖNCE yazdığın metin ("İşte en iyi 10 üniversite:")
|
||||
listeyi yazmış sayılmaz; araç sonucu geldikten SONRA satırları (sıra +
|
||||
ad, numaralı liste) cevabına mutlaka yaz. "İşte …:" deyip boş bırakma.
|
||||
Listedeki her satırın başında programın kimliği var ("id: …"):
|
||||
program_detay, program_netleri ve program_karsilastir'ı listedeki
|
||||
programlar için DOĞRUDAN bu kimlikle çağır, önce program_ara çağırma.
|
||||
@@ -160,18 +190,27 @@ KURALLAR:
|
||||
olmayan bir soru sorduysa ("hangi üniversitelerde var", "… okutan yerler",
|
||||
"en iyi / en ucuz …") cevabı yalnız listeden verme, program_ara çağır.
|
||||
Kimlikler araç içindir: adaya kimlik numarası, araç adı ya da parametre
|
||||
yazma ("program_ara'ya baktım" değil, "verilere baktım").
|
||||
yazma ("program_ara'ya baktım" değil, "verilere baktım"; "program_detay ile
|
||||
bakabilirim" değil, "ayrıntısına bakabilirim"; "(ID: 103310131)" gibi kimlik
|
||||
yazma). Liste hakkında bir sayım yapacaksan ("kaç satırda özel kontenjan
|
||||
var" gibi) yukarıdaki satırları tek tek say, tahmin etme.
|
||||
- BAĞLANTI: site içi bir sayfaya yönlendirirken markdown bağlantısı yaz —
|
||||
[Yazının başlığı](/rehber/slug), [Üniversite adı](/universite/slug),
|
||||
[Bölüm adı](/bolum/slug); adresi düz metin ya da ters tırnak içinde yazma. Adres ve
|
||||
başlık araç sonucundaki "sayfa" / "baslik" / "ad" alanından gelir; adres
|
||||
uydurma. Ekranda bu bağlantı ikonlu bir hap olarak görünür.
|
||||
- PROFİL: aday bir tercih söylerse (yaşadığı il, evden en fazla kaç km,
|
||||
vakıfta yıllık ücret sınırı, burs beklentisi, öğretim dili, uzaktan/açık
|
||||
kabul mü, kampüs büyüklüğü) ${PROFIL_ONAY_SOR
|
||||
kabul mü, kampüs büyüklüğü, özel kontenjan durumu) ${PROFIL_ONAY_SOR
|
||||
? `HEMEN KAYDETME: cevabının sonunda tek cümleyle, tam bu ibareyle sor:
|
||||
"Bunu profiline kaydedeyim mi? (Evet demek kredi harcamaz; listen ve
|
||||
aramalar buna göre süzülür, istediğin zaman silebilirsin.)" Aday
|
||||
evet/kaydet derse o zaman profil_guncelle ile kaydet ve "Kaydettim: …" de.
|
||||
Aday açıkça "kaydet" diye yazdıysa sormadan kaydet.
|
||||
Bu soruyu YALNIZ aday bu mesajında yukarıdaki yedi tercihten birini
|
||||
söylediyse sor; ilgi alanı, bölüm merakı, duygu, aile durumu ya da genel
|
||||
bir soru için SORMA ve cevabın başına koyma.`
|
||||
Bu soruyu YALNIZ aday bu mesajında yukarıdaki yedi tercihten birini ya da
|
||||
özel kontenjan durumunu (okul birincisi, şehit-gazi yakını, depremzede,
|
||||
34 yaş üstü kadın kontenjanı) söylediyse sor; ilgi alanı, bölüm merakı,
|
||||
duygu, aile durumu ya da genel bir soru için SORMA ve cevabın başına koyma.`
|
||||
: `profil_guncelle ile kaydet ve cevabının sonunda tek cümleyle söyle:
|
||||
"Şunu kaydettim: …". Emin değilsen önce sor, tahminle kaydetme.`}
|
||||
KAYIT DÜRÜSTLÜĞÜ: kayıt YALNIZ profil_guncelle aracıyla olur. "Kaydettim",
|
||||
@@ -232,6 +271,40 @@ KURALLAR:
|
||||
// TYT sırası lisans barajlarıyla kıyaslanmaz; ön lisansta baraj yok
|
||||
const baraj = params.tur === "tyt" ? null : barajBolumu(params.sira, opts.barajlar);
|
||||
if (baraj) parcalar.splice(2, 0, baraj);
|
||||
// Raporun üst metni: adayın listesini kuran yapay zekânın genel
|
||||
// değerlendirmesi ve uyarıları — danışman "listem nasıl kuruldu?" sorusunu
|
||||
// kendi raporundan cevaplar. Kilitli kullanıcıda genel değerlendirme gizli.
|
||||
const raporUst: string[] = [];
|
||||
if (opts.rapor?.genelDegerlendirme && !opts.kilitli) {
|
||||
raporUst.push(`Genel değerlendirme: ${opts.rapor.genelDegerlendirme}`);
|
||||
}
|
||||
if (opts.rapor?.uyarilar?.length) {
|
||||
raporUst.push(`Uyarılar: ${opts.rapor.uyarilar.map((u) => `(${u})`).join(" ")}`);
|
||||
}
|
||||
if (opts.rapor?.kapsam?.uyarlandi) {
|
||||
raporUst.push(
|
||||
`Kapsam: liste ${opts.rapor.kapsam.toplam} tercihtir ve dağılım havuza uyarlandı (ideal 5/13/6 tutmadı); "neden 24 değil / neden güvenli az" sorusunda bunu ve uyarılardaki gevşetme notunu aktar.`,
|
||||
);
|
||||
}
|
||||
if (raporUst.length) {
|
||||
parcalar.push(`LİSTENİN ÜST METNİ (liste kurulurken yazıldı; aday bunu ekranda görüyor):\n${raporUst.join("\n")}`);
|
||||
}
|
||||
const ozelDurum = adayDurumu(params, opts.profil);
|
||||
if (ozelDurum.length) {
|
||||
parcalar.push(
|
||||
`ADAYIN ÖZEL KONTENJAN DURUMU: ${ozelDurumOzeti(ozelDurum)}. ÖSYM bu durumlar için
|
||||
programlarda ayrı (genelde 1-2 kişilik) kontenjan ayırır; araç sonuçlarında
|
||||
"adayinOzelKontenjani" / "kontenjanTurleri" alanı o programda böyle bir
|
||||
kontenjan olup olmadığını söyler. Bu kontenjanların AYRI taban sırası elimizde
|
||||
YOK; genel taban sırasıyla kıyaslama yapılır. Vaat yasağı burada da geçerli:
|
||||
"kontenjanın var, rahat girersin / şansın yüksek" DEME; "bu programda sana
|
||||
ayrılmış N kişilik kontenjan var; taban sırası bu kontenjan için ayrı
|
||||
yayımlanır, elimde yok" de. Cinsiyet ya da sağlık sorma; 34 yaş üstü kadın
|
||||
kontenjanı bir kontenjan hakkıdır, cinsiyet bilgisi olarak kullanma. Bu durum
|
||||
ZATEN kayıtlı (sihirbazdan ya da profilden): yeniden kaydetmeyi teklif etme,
|
||||
bunun için onay sorusu sorma.`,
|
||||
);
|
||||
}
|
||||
if (opts.kilitli) {
|
||||
parcalar.push(
|
||||
`ÖNEMLİ: Adayın paketi yok; listenin yalnızca ilk ${ACIK_SATIR} tercihi açık.
|
||||
@@ -255,6 +328,7 @@ tercih soruları üzerinden yardımcı olmaya devam et.`,
|
||||
iller: params.iller ?? [],
|
||||
universiteTipi: params.universiteTipi ?? "farketmez",
|
||||
oncelikler: params.oncelikler ?? [],
|
||||
ozelDurum: params.ozelDurum ?? [],
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
havuzuKompaktJson,
|
||||
turLabel,
|
||||
dilimHedefiHesapla,
|
||||
idealHedef,
|
||||
IDEAL_HEDEF,
|
||||
type DilimHedef,
|
||||
type Dilim,
|
||||
@@ -110,7 +111,11 @@ KURALLAR (kesin):
|
||||
- SADECE havuzdaki id'leri kullan; havuzda olmayan program ASLA önerme.
|
||||
Aynı id'yi birden fazla kez kullanma.
|
||||
- Seçerken adayın ilgi alanlarını ve illerini, kontenjan/yerleşen hareketini
|
||||
ve bölüm çeşitliliğini gözet.
|
||||
ve bölüm çeşitliliğini gözet. Havuz satırında "one_cikan" varsa bunlar
|
||||
adayın önceliklerine göre KODLA hesaplanmış gerçek verilerdir (TÜİK
|
||||
istihdamı, URAP sırası, kadro, özel kontenjan, kampüs, uzaklık): o
|
||||
programları öne al ve gerekçede bunlardan EN FAZLA BİRİNİ, yazıldığı
|
||||
gibi kullanabilirsin.
|
||||
- gerekce: TEK kısa cümle, en fazla 12 kelime, Türkçe. YALNIZCA o satırdaki
|
||||
programı anlat: alanının ya da ilinin adayın seçimine uyumu, bölümün ya da
|
||||
üniversitenin öne çıkan yanı. Başka bir programın ilini ya da üniversitesini
|
||||
@@ -145,11 +150,11 @@ const MIN_LISTE = 3;
|
||||
|
||||
const DILIMLER: Dilim[] = ["hayal", "dengeli", "garanti"];
|
||||
|
||||
function kapsamUret(hedef: DilimHedef): RaporKapsam {
|
||||
function kapsamUret(hedef: DilimHedef, ideal: DilimHedef = IDEAL_HEDEF): RaporKapsam {
|
||||
return {
|
||||
toplam: hedef.hayal + hedef.dengeli + hedef.garanti,
|
||||
dagilim: { ...hedef },
|
||||
uyarlandi: DILIMLER.some((d) => hedef[d] !== IDEAL_HEDEF[d]),
|
||||
uyarlandi: DILIMLER.some((d) => hedef[d] !== ideal[d]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -359,7 +364,7 @@ function mockRapor(
|
||||
revizyonNotu?: string,
|
||||
): RaporSonuc {
|
||||
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
|
||||
const hedef = dilimHedefiHesapla(havuz);
|
||||
const hedef = dilimHedefiHesapla(havuz, idealHedef(params.oncelikler));
|
||||
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
const efektif = (p: AdayProgram) => p.efektifSira ?? 0;
|
||||
const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
|
||||
@@ -382,6 +387,7 @@ function mockRapor(
|
||||
// Risk ve trend mock'ta da gerçek veriden: canlı davranışla birebir aynı
|
||||
riskNotu: riskNotuUret(p, params.sira),
|
||||
trendOzeti: trendOzetiUret(p),
|
||||
...(p.oneCikan?.length ? { oneCikan: p.oneCikan } : {}),
|
||||
}));
|
||||
|
||||
const programlar: RaporSonuc["programlar"] = {};
|
||||
@@ -401,7 +407,7 @@ function mockRapor(
|
||||
"(Dev mock) Güvenli dilimindeki son tercihleri asla silme.",
|
||||
],
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
kapsam: kapsamUret(hedef, idealHedef(params.oncelikler)),
|
||||
devMock: true,
|
||||
};
|
||||
}
|
||||
@@ -486,14 +492,16 @@ export async function raporUretAyrintili(
|
||||
|
||||
// Dilim hedefleri havuzun gerçek dağılımına uyarlanır: sabit 5/13/6 bazı
|
||||
// sıralamalarda (ör. çok iyi derece → dengeli dilim boş) imkânsızdı ve
|
||||
// üretim her seferinde doğrulamada patlıyordu.
|
||||
const hedef = dilimHedefiHesapla(havuz);
|
||||
// üretim her seferinde doğrulamada patlıyordu. İskelet "Kolay yerleşme"
|
||||
// seçiliyse 4/12/8 (idealHedef).
|
||||
const hedef = dilimHedefiHesapla(havuz, idealHedef(params.oncelikler));
|
||||
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
|
||||
const secimVar =
|
||||
(params.kategoriler?.length ?? 0) > 0 ||
|
||||
(params.iller?.length ?? 0) > 0 ||
|
||||
(params.oncelikler?.length ?? 0) > 0 ||
|
||||
(params.ozelDurum?.length ?? 0) > 0 ||
|
||||
(params.universiteTipi && params.universiteTipi !== "farketmez");
|
||||
|
||||
const gevsetmeNotu: string[] = [];
|
||||
@@ -526,6 +534,7 @@ export async function raporUretAyrintili(
|
||||
iller: params.iller ?? [],
|
||||
universiteTipi: params.universiteTipi ?? "farketmez",
|
||||
oncelikler: params.oncelikler ?? [],
|
||||
ozelDurum: params.ozelDurum ?? [],
|
||||
})}\nSeçili kategori ve illere öncelik ver; havuzun elverdiği ölçüde bu seçimlere sadık kal.`
|
||||
: null,
|
||||
params.notlar
|
||||
@@ -560,7 +569,9 @@ export async function raporUretAyrintili(
|
||||
kategoriEslesir(p.isim, seciliKategoriler, p.fakulte);
|
||||
const ilUyar = (p: AdayProgram) =>
|
||||
seciliIller.length > 0 && p.il != null && seciliIller.includes(p.il);
|
||||
const uyum = (p: AdayProgram) => (alanUyar(p) ? 2 : 0) + (ilUyar(p) ? 1 : 0);
|
||||
// Alan 2, il 1, uyum puanı 0-1: tamamlama ve kırpma önce seçime uyana, eşitte
|
||||
// uyum puanı yüksek olana gider
|
||||
const uyum = (p: AdayProgram) => (alanUyar(p) ? 2 : 0) + (ilUyar(p) ? 1 : 0) + (p.uyum ?? 0.5);
|
||||
|
||||
// Onarım modelin seçiminin en az yarısını koruyabiliyorsa ikinci çağrı
|
||||
// yapılmaz; altındaysa liste çoğunlukla kodla dolardı, model bir kez daha denenir.
|
||||
@@ -671,14 +682,18 @@ export async function raporUretAyrintili(
|
||||
const sablonGerekceMetni = (p: AdayProgram): string => {
|
||||
const alan = alanUyar(p);
|
||||
const il = ilUyar(p);
|
||||
if (alan && il) return "Seçtiğin alan ve ille eşleşiyor.";
|
||||
// Öne çıkan veri varsa şablon da onu söyler (koddan, sayı meşru)
|
||||
const ek = p.oneCikan?.length ? ` Öne çıkan: ${p.oneCikan[0]}.` : "";
|
||||
if (alan && il) return `Seçtiğin alan ve ille eşleşiyor.${ek}`;
|
||||
if (alan) {
|
||||
return seciliIller.length > 0
|
||||
? "Seçtiğin alanla eşleşiyor; seçtiğin illerin dışında."
|
||||
: "Seçtiğin alanla eşleşiyor.";
|
||||
return (
|
||||
(seciliIller.length > 0
|
||||
? "Seçtiğin alanla eşleşiyor; seçtiğin illerin dışında."
|
||||
: "Seçtiğin alanla eşleşiyor.") + ek
|
||||
);
|
||||
}
|
||||
if (il) return "Seçtiğin ilde; seçtiğin alanların dışında, sıralamana yakın bir program.";
|
||||
return "Sıralamana yakın olduğu için listene eklendi.";
|
||||
if (il) return `Seçtiğin ilde; seçtiğin alanların dışında, sıralamana yakın bir program.${ek}`;
|
||||
return `Sıralamana yakın olduğu için listene eklendi.${ek}`;
|
||||
};
|
||||
const sablonGerekce: RaporTani["sablonGerekce"] = [];
|
||||
const gerekceDenetle = (s: SeciliSatir, p: AdayProgram): string => {
|
||||
@@ -708,6 +723,8 @@ export async function raporUretAyrintili(
|
||||
p.yerlesenSon,
|
||||
params.sira,
|
||||
...metindekiSayilar(p.isim, p.universite),
|
||||
// Koddan gelen "öne çıkan" cümlelerdeki sayılar meşrudur (TÜİK %, URAP sırası, km…)
|
||||
...metindekiSayilar(...(p.oneCikan ?? [])),
|
||||
...profilSayilari,
|
||||
].filter((n): n is number => typeof n === "number"),
|
||||
),
|
||||
@@ -804,6 +821,7 @@ export async function raporUretAyrintili(
|
||||
gerekce: gerekceDenetle(satir, p),
|
||||
riskNotu: riskNotuUret(p, params.sira),
|
||||
trendOzeti: trendOzetiUret(p),
|
||||
...(p.oneCikan?.length ? { oneCikan: p.oneCikan } : {}),
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -812,7 +830,7 @@ export async function raporUretAyrintili(
|
||||
genelDegerlendirme: genel,
|
||||
uyarilar,
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
kapsam: kapsamUret(hedef, idealHedef(params.oncelikler)),
|
||||
},
|
||||
tani: {
|
||||
kaynak,
|
||||
@@ -837,7 +855,7 @@ export async function raporUretAyrintili(
|
||||
export function listeOzetiCikar(
|
||||
rapor: RaporSonuc,
|
||||
adaySira?: number,
|
||||
opts: { kimlik?: boolean } = {},
|
||||
opts: { kimlik?: boolean; gerekce?: boolean } = {},
|
||||
): string {
|
||||
return rapor.tercihler
|
||||
.map((t) => {
|
||||
@@ -853,7 +871,16 @@ export function listeOzetiCikar(
|
||||
// sohbet cevaplarına "garanti dilimi" sızıyor. Dilim ve risk aynı kelime
|
||||
// olabildiği için ("Güvenli") alanlar adlandırılır.
|
||||
const kimlik = opts.kimlik ? `id: ${t.programId} | ` : "";
|
||||
return `${t.sira}. [${kimlik}dilim: ${DILIM_ETIKET[t.dilim] ?? t.dilim} | risk: ${RISK_ETIKET[risk]}] ${p?.isim ?? "?"} — ${p?.universite ?? "?"}${taban}`;
|
||||
// Sohbet: satırın gerekçesi ve öne çıkan verileri de girer (danışman
|
||||
// "neden listemde?" sorusuna kendi raporunun gerekçesiyle cevap verir).
|
||||
// Öne çıkanların TAMAMI (≤3, uyum-puani.ts ONE_CIKAN_UST) girer: ikiye
|
||||
// kırpılınca model "24'ün 23'ünde okul birincisi kontenjanı var" diye
|
||||
// eksik saydı (6 Eki 2026 QA).
|
||||
const gerekce =
|
||||
opts.gerekce && t.gerekce ? ` | neden: ${t.gerekce.replace(/\s+/g, " ").trim()}` : "";
|
||||
const oneCikan =
|
||||
opts.gerekce && t.oneCikan?.length ? ` | öne çıkan: ${t.oneCikan.join(" · ")}` : "";
|
||||
return `${t.sira}. [${kimlik}dilim: ${DILIM_ETIKET[t.dilim] ?? t.dilim} | risk: ${RISK_ETIKET[risk]}] ${p?.isim ?? "?"} — ${p?.universite ?? "?"}${taban}${gerekce}${oneCikan}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user