@akredit/acik-mcp 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ import { z } from "zod";
15
15
  import { GENEL_ALAN_ADLARI, genelAyikla, genelAyristir, ingilizceAlanVar, prolizMetniMi } from "./genel-ayristir.js";
16
16
  import { kaynakImzasiDogru } from "./kaynak-dogrula.js";
17
17
  import { girisBilgisiAyikla } from "./kisisel-veri.js";
18
+ import { prolizMetniCoz, prolizSayfaMetniMi } from "./proliz-metin.js";
18
19
  import { AracHatasi, arac, KOD_KAYNAGI, PESIN_ONAY_ACIKLAMASI, pesinOnayVar, TARAMA_SECENEKLERI, } from "./temel.js";
19
20
  // ---------------------------------------------------------------- Alan modeli
20
21
  export const ALAN_KODLARI = [
@@ -955,7 +956,7 @@ const taslakDenetleAraci = arac({
955
956
  sonuc: {
956
957
  bicimdenGecenler: sonuclar.filter((x) => x.bicimDenetimi === "geçti").map((x) => x.etiket),
957
958
  secenekler: sonuclar,
958
- not: "Biçim denetiminden geçti; içerik teyidi: hoca. Sonuç yalnız Akredit'in biçim kurallarına uyulduğunu söyler; içeriğin doğruluğuna ve derse uygunluğuna hoca karar verir.",
959
+ not: taslakNotu(sonuclar),
959
960
  resmiDayanak: RESMI_DAYANAK,
960
961
  },
961
962
  kaynak: [KAYNAK_KURALLAR],
@@ -1047,6 +1048,19 @@ export function ayristirOnAyikla(g) {
1047
1048
  atilan,
1048
1049
  ...ek,
1049
1050
  };
1051
+ // Proliz ders sayfasının düz metni: bölüm bölüm ayrıştırıcı ham metni okur (görev ve kaynak satırları dahil).
1052
+ if (prolizSayfaMetniMi(giris.metin))
1053
+ return {
1054
+ girdi: {
1055
+ ...kalan,
1056
+ sayfaIpucu: { ...sayfaIpucu, prolizMetni: true },
1057
+ metin: giris.metin,
1058
+ ...(onceki.length ? { kaynaklar: onceki } : {}),
1059
+ ...(oncekiY.length ? { yetkililer: oncekiY } : {}),
1060
+ },
1061
+ atilan,
1062
+ ...ek,
1063
+ };
1050
1064
  // Proliz dışı: önce tablo satırları, sonra etiketli bölümler.
1051
1065
  const r = genelAyikla(satir.html);
1052
1066
  return {
@@ -1061,6 +1075,17 @@ export function ayristirOnAyikla(g) {
1061
1075
  ...ek,
1062
1076
  };
1063
1077
  }
1078
+ /** Taslak denetiminin notu sonuca göre: hiçbiri geçmediyse "geçti" denmez. */
1079
+ function taslakNotu(xs) {
1080
+ const gecen = xs.filter((x) => x.bicimDenetimi === "geçti").map((x) => x.etiket);
1081
+ if (!gecen.length)
1082
+ return "Biçim denetiminden geçmeyen alanlar var; düzeltip yeniden denetleyin. İçerik teyidi: hoca.";
1083
+ const kismi = gecen.length < xs.length
1084
+ ? ` Yalnız şu seçenekler geçti: ${gecen.join(", ")}; öbürlerinde geçmeyen alanlar var.`
1085
+ : "";
1086
+ return `Biçim denetiminden geçti; içerik teyidi: hoca.${kismi} Sonuç yalnız Akredit'in biçim kurallarına uyulduğunu söyler; içeriğin doğruluğuna ve derse uygunluğuna hoca karar verir.`;
1087
+ }
1088
+ const TUL_TEYIDI = "Haftalık ders saatleri (teori + uygulama + laboratuvar)";
1064
1089
  /**
1065
1090
  * Ayrıştırmanın hemen ardından denetim: tarama sırasında ders başına iki çağrı yerine bir çağrı yeter.
1066
1091
  * Girdi denetimin şemasından geçmezse (sayfa eksik okunduysa) denetim atlanır, asistan bologna_denetle'yi çağırır.
@@ -1081,6 +1106,8 @@ function gomuluDenetim(ders, b) {
1081
1106
  teyitSorulacak: [
1082
1107
  ...(d.degerlendirme.length ? [] : ["Değerlendirme yöntem ve yüzdeleri"]),
1083
1108
  ...(d.kaynaklar?.length || d.kaynakSayisi ? [] : ["Kullanılan kaynaklar"]),
1109
+ // T+U+L yoksa ya da 0+0+0 ise (kayıt yok) iş yükü hesaplanamaz; haftalık saatler hocaya sorulur.
1110
+ ...((tulSaati(d.tul) ?? 0) > 0 ? [] : [TUL_TEYIDI]),
1084
1111
  ],
1085
1112
  };
1086
1113
  }
@@ -1095,11 +1122,80 @@ const gomuluDenetimAlani = (ders, b) => {
1095
1122
  const d = gomuluDenetim(ders, b);
1096
1123
  return d ? { denetim: d } : {};
1097
1124
  };
1125
+ /** Proliz ders sayfasının düz metninden ayrıştırma sonucu (dolu ya da boş paket). */
1126
+ function prolizMetniSonucu(ham, ek, b) {
1127
+ const p = prolizMetniCoz(ham);
1128
+ const d = p.ders;
1129
+ const kaynaklar = [...ek.kaynaklar, ...p.kaynaklar];
1130
+ const yetkililer = [...ek.yetkililer, ...p.yetkililer];
1131
+ const ad = d.ad ?? ek.dersIpucu?.ad;
1132
+ // 0+0+0 kayıt yok demektir: sayfadaki ya da ipucundaki sıfırdan büyük T+U+L kullanılır, yoksa alan boş kalır.
1133
+ const tul = [d.tul, ek.dersIpucu?.tul].find((t) => (tulSaati(t) ?? 0) > 0);
1134
+ const tulYok = tul === undefined;
1135
+ const ders = {
1136
+ kod: d.kod ?? ek.dersIpucu?.kod ?? "DERS",
1137
+ ...(ad ? { ad } : {}),
1138
+ yariyil: d.yariyil,
1139
+ ...(tul ? { tul } : {}),
1140
+ kredi: d.kredi,
1141
+ akts: d.akts ?? ek.dersIpucu?.akts ?? null,
1142
+ ...(d.guncelleme ? { guncelleme: d.guncelleme } : {}),
1143
+ amac: d.amac,
1144
+ icerik: d.icerik,
1145
+ yontem: d.yontem,
1146
+ notlar: d.notlar,
1147
+ kaynaklar,
1148
+ kaynakSayisi: kaynaklar.length,
1149
+ ...(yetkililer.length ? { yetkililer } : {}),
1150
+ ciktilar: d.ciktilar,
1151
+ degerlendirme: d.degerlendirme,
1152
+ isyuku: d.isyuku,
1153
+ dersYapisi: d.dersYapisi,
1154
+ haftalar: d.haftaKonulari.length,
1155
+ // Denetim şeması metin değer bekler; sayısal matris ayrıca katkiMatrisi'nde.
1156
+ katki: Object.fromEntries(Object.entries(d.katki).map(([o, s]) => [
1157
+ o,
1158
+ Object.fromEntries(p.ders.pKodlari.map((k) => [k, s[k] === undefined ? "" : String(s[k])])),
1159
+ ])),
1160
+ };
1161
+ const bosPaket = p.bulunan.length === 0 || (!d.amac && !d.icerik && !d.ciktilar.length && !d.degerlendirme.length);
1162
+ const tulNotu = tulYok
1163
+ ? " T+U+L yok ya da 0+0+0: haftalık ders saatleri (teori + uygulama + laboratuvar) aynı TEK mesajda sorulur; peşin onayda sorulmaz, iş yükü EKSİK kalır."
1164
+ : "";
1165
+ const sonrakiAdim = `${bosPaket
1166
+ ? "Boş paket: sayfadaki bütün alanlar boş (denetim.doldurulacak). Hepsini öneriyle doldurun (bologna_alan_rehberi, bologna_taslak_denetle) ve 'öneri' etiketiyle son özette gösterin. Değerlendirme yöntem ve yüzdeleri ile kullanılan kaynaklar hocaya TEK mesajda sorulur; peşin onayda sorulmaz, EKSİK bırakılır. Uydurmayın."
1167
+ : d.degerlendirme.length
1168
+ ? "Yanıttaki denetim alanı hazırdır; bologna_denetle'yi ayrıca çağırmayın. Değerlendirme sayfadan okundu: hocaya sormayın; taslakta degerlendirmeDayanagi: 'sayfa' ve degerlendirmeKaniti olarak bu yanıttaki degerlendirmeKaniti metnini aynen verin. Yalnız denetim.teyitSorulacak'taki bilgiler hocaya TEK mesajda sorulur. Sonraki çağrılarda ingilizce değerini aynen verin."
1169
+ : "Yanıttaki denetim alanı hazırdır; bologna_denetle'yi ayrıca çağırmayın. Değerlendirme yöntem ve yüzdeleri sayfada yok: hocaya TEK mesajda sorun (peşin onayda EKSİK bırakın), tahmin etmeyin. Sonraki çağrılarda ingilizce değerini aynen verin."}${tulNotu}`;
1170
+ return {
1171
+ sonuc: {
1172
+ tur: "ders",
1173
+ bicim: "proliz_duz_metin",
1174
+ guven: bosPaket ? "düşük" : p.bulunamayan.length <= 3 ? "yüksek" : "orta",
1175
+ bosPaket,
1176
+ ingilizce: ingilizceAlanVar(ham),
1177
+ bolognaDenetleGirdisi: { ders },
1178
+ ...gomuluDenetimAlani(ders, b),
1179
+ tarayiciEylemPlani: AYRISTIRMA_SONRASI,
1180
+ bilgiler: p.bilgiler,
1181
+ haftaKonulari: d.haftaKonulari,
1182
+ ...(d.degerlendirmeKaniti ? { degerlendirmeDayanagi: "sayfa", degerlendirmeKaniti: d.degerlendirmeKaniti } : {}),
1183
+ katkiMatrisi: d.katki,
1184
+ ...(Object.keys(d.katkiTum).length ? { katkiTum: d.katkiTum } : {}),
1185
+ ...(d.pKodlari.length ? { pKodlari: d.pKodlari } : {}),
1186
+ ...(!d.kod && ek.dersIpucu ? { dersKimligi: "dersIpucu" } : {}),
1187
+ bulunan: p.bulunan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1188
+ bulunamayan: p.bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1189
+ },
1190
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/proliz-metin.ts", "Akredit Proliz düz metin ayrıştırıcısı")],
1191
+ sonrakiAdim,
1192
+ };
1193
+ }
1098
1194
  const ayristirAraci = arac({
1099
1195
  ad: "bologna_ayristir",
1100
1196
  baslik: "Bologna sayfasını ayrıştır",
1101
1197
  aciklama: "Yapıştırılan Bologna ders sayfasını ya da program çıktıları sayfasını çözer. Önce Proliz biçimini dener, olmazsa her OBS için genel ayrıştırıcı çalışır (HTML ya da düz metin, Türkçe ya da İngilizce etiketler). Bulunan ve bulunamayan alanları, güven düzeyini söyler. Adres çekmez.",
1102
- sonrakiAdim: "Yanıttaki denetim alanı varsa bologna_denetle'yi ayrıca çağırmayın: eksikler (denetim.doldurulacak) ve teyit sorulacaklar (denetim.teyitSorulacak) hazırdır. Denetim alanı yoksa ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Sonraki çağrılarda ingilizce değerini aynen verin. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları hocaya TEK mesajda sorun; yüzdeyi tahmin etmeyin. Uygulama/lab T+U+L'den okunur (U+L > 0 ise var), sorulmaz. Amaç ve içerik sayfada yoksa önden sorulmaz; 'öneri' olarak yazılıp son özette teyide sunulur.",
1198
+ sonrakiAdim: "Yanıttaki denetim alanı varsa bologna_denetle'yi ayrıca çağırmayın: eksikler (denetim.doldurulacak) ve teyit sorulacaklar (denetim.teyitSorulacak) hazırdır. Denetim alanı yoksa ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Sonraki çağrılarda ingilizce değerini aynen verin. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları hocaya TEK mesajda sorun; yüzdeyi tahmin etmeyin. Uygulama/lab T+U+L'den okunur (U+L > 0 ise var), sorulmaz; T+U+L yok ya da 0+0+0 ise haftalık ders saatleri (teori + uygulama + laboratuvar) aynı TEK mesajda sorulur (peşin onayda sorulmaz, iş yükü EKSİK kalır). Amaç ve içerik sayfada yoksa önden sorulmaz; 'öneri' olarak yazılıp son özette teyide sunulur.",
1103
1199
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1104
1200
  kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1105
1201
  onAyikla: ayristirOnAyikla,
@@ -1114,12 +1210,26 @@ const ayristirAraci = arac({
1114
1210
  kaynaklar: z.array(z.string().max(600)).max(30).optional().describe("Ayrıca verilen künyeler"),
1115
1211
  yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
1116
1212
  tur: z.enum(["otomatik", "ders", "program_ciktilari"]).default("otomatik"),
1213
+ dersIpucu: z
1214
+ .object({
1215
+ kod: z.string().trim().min(1).max(30),
1216
+ ad: z.string().trim().max(200).optional(),
1217
+ akts: z.number().min(0).max(60).optional(),
1218
+ tul: z
1219
+ .string()
1220
+ .trim()
1221
+ .max(20)
1222
+ .optional()
1223
+ .describe("Ders listesindeki T+U+L, ör. '2+1+0'. 0+0+0 kayıt yok demektir."),
1224
+ })
1225
+ .optional()
1226
+ .describe("Ders listesinden bilinen kod, ad, AKTS ve T+U+L (obs_ders_listesi taramaPlani). Sayfada yoksa bunlar kullanılır; boş pakette ders kodu ve adı sayfada görünmez."),
1117
1227
  sayfaIpucu: z
1118
1228
  .object({ prolizMetni: z.boolean(), ingilizce: z.boolean() })
1119
1229
  .optional()
1120
1230
  .describe("Sunucu doldurur; göndermeyin"),
1121
1231
  }),
1122
- calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }, b) => {
1232
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, dersIpucu, sayfaIpucu }, b) => {
1123
1233
  if (!html && !metin)
1124
1234
  throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
1125
1235
  const kaynakAlani = {
@@ -1188,6 +1298,8 @@ const ayristirAraci = arac({
1188
1298
  };
1189
1299
  }
1190
1300
  const ham = html ?? metin ?? "";
1301
+ if (prolizSayfaMetniMi(ham))
1302
+ return prolizMetniSonucu(ham, { kaynaklar, yetkililer, dersIpucu }, b);
1191
1303
  const g = genelAyristir(ham, kaynaklar.length);
1192
1304
  if (g.bulunan.length < 2)
1193
1305
  throw new AracHatasi("Sayfa tanınmadı: ders alanlarından en çok biri bulundu. Ders bilgi paketi sayfasının tamamını yapıştırın (sayfada Ctrl+A, Ctrl+C, sonra buraya Ctrl+V) ya da alanları tek tek yazın.");
@@ -127,11 +127,13 @@ export function yazarEslesir(sorgu, yazarlar) {
127
127
  });
128
128
  }
129
129
  // ---------------------------------------------------------------- Güvenli istek
130
- async function getir(f, adres) {
130
+ async function getir(f, adres, hata) {
131
131
  if (adres.protocol !== "https:" || !IZINLI_HOSTLAR.includes(adres.hostname))
132
132
  throw new Error("İzinsiz host");
133
133
  if (adres.hostname === "www.googleapis.com" && !adres.pathname.startsWith("/books/"))
134
134
  throw new Error("İzinsiz yol");
135
+ if (adres.hostname === "openlibrary.org" && !OPENLIBRARY_YOLLARI.includes(adres.pathname))
136
+ throw new Error("İzinsiz yol");
135
137
  return sinirli(async () => {
136
138
  let y;
137
139
  try {
@@ -142,10 +144,13 @@ async function getir(f, adres) {
142
144
  });
143
145
  }
144
146
  catch {
147
+ hata?.();
145
148
  return null;
146
149
  }
147
- if (!y.ok)
150
+ if (!y.ok) {
151
+ hata?.();
148
152
  return null;
153
+ }
149
154
  const uzunluk = Number(y.headers.get("content-length") ?? "0");
150
155
  if (uzunluk > EN_BUYUK_YANIT)
151
156
  return null;
@@ -211,6 +216,18 @@ const OpenLibraryKitap = z.object({
211
216
  publish_date: z.string().optional(),
212
217
  });
213
218
  const OpenLibraryYanit = z.record(z.string(), OpenLibraryKitap);
219
+ const OpenLibraryAramaYanit = z.object({
220
+ docs: z
221
+ .array(z.object({
222
+ key: z.string().regex(/^\/works\/OL\d+W$/u),
223
+ title: z.string(),
224
+ author_name: z.array(z.string()).optional(),
225
+ publish_year: z.array(z.number().int()).optional(),
226
+ publisher: z.array(z.string()).optional(),
227
+ isbn: z.array(z.string()).optional(),
228
+ }))
229
+ .optional(),
230
+ });
214
231
  const GoogleCilt = z.object({
215
232
  id: z.string(),
216
233
  volumeInfo: z.object({
@@ -301,6 +318,8 @@ function googleKunye(c) {
301
318
  // ---------------------------------------------------------------- Sorgular
302
319
  const CROSSREF = "https://api.crossref.org/works";
303
320
  const OPENLIBRARY = "https://openlibrary.org/api/books";
321
+ const OPENLIBRARY_ARAMA = "https://openlibrary.org/search.json";
322
+ const OPENLIBRARY_YOLLARI = ["/api/books", "/search.json"];
304
323
  const GOOGLE = "https://www.googleapis.com/books/v1/volumes";
305
324
  const KAYIT_ADI = {
306
325
  kitap: "kitap",
@@ -366,18 +385,12 @@ async function isbnIle(f, isbn) {
366
385
  : undefined;
367
386
  return c ? googleKunye(c) : null;
368
387
  }
388
+ /**
389
+ * Başlık+yazar araması. Sıra: Crossref (makale), Open Library (kitap), Google Books (son çare).
390
+ * Google'a ulaşılamazsa (kota, HTTP hatası) o katalog atlanır ve bu durum bildirilir.
391
+ */
369
392
  async function baslikIle(f, baslik, yazar, s) {
370
393
  const uygun = (k) => baslikEslesir(baslik, k.baslik) && yazarEslesir(yazar, k.yazarlar) && ekDenetim(k, s) === null;
371
- const g = new URL(GOOGLE);
372
- g.searchParams.set("q", `intitle:${baslik} inauthor:${yazar}`);
373
- g.searchParams.set("maxResults", "5");
374
- const gy = GoogleYanit.safeParse(await getir(f, g));
375
- if (gy.success)
376
- for (const x of gy.data.items ?? []) {
377
- const k = googleKunye(x);
378
- if (baslikEslesir(baslik, x.volumeInfo.title) && uygun(k))
379
- return k;
380
- }
381
394
  const cr = new URL(CROSSREF);
382
395
  cr.searchParams.set("query.bibliographic", baslik);
383
396
  cr.searchParams.set("query.author", yazar);
@@ -387,9 +400,47 @@ async function baslikIle(f, baslik, yazar, s) {
387
400
  for (const e of cy.data.message.items) {
388
401
  const k = crossrefKunye(e);
389
402
  if (k && uygun(k))
390
- return k;
403
+ return { k, googleUlasilamadi: false };
391
404
  }
392
- return null;
405
+ if (s.tur === undefined || s.tur === "kitap" || s.tur === "herhangi") {
406
+ const ol = new URL(OPENLIBRARY_ARAMA);
407
+ ol.searchParams.set("title", baslik);
408
+ ol.searchParams.set("author", yazar);
409
+ ol.searchParams.set("limit", "10");
410
+ ol.searchParams.set("fields", "key,title,author_name,publish_year,publisher,isbn");
411
+ const oy = OpenLibraryAramaYanit.safeParse(await getir(f, ol));
412
+ if (oy.success)
413
+ for (const d of oy.data.docs ?? []) {
414
+ // Yıl verildiyse kaydın baskı yılları (publish_year) arasında geçmeli; ilk yayın yılı tek başına yetmez.
415
+ // Yıl verilmediyse künyeye yıl yazılmaz (hangi baskı olduğu bilinmez).
416
+ const yil = s.yil !== undefined && (d.publish_year ?? []).includes(s.yil) ? s.yil : undefined;
417
+ if (s.yil !== undefined && yil === undefined)
418
+ continue;
419
+ const k = kur({
420
+ tur: "kitap",
421
+ yazarlar: d.author_name ?? [],
422
+ baslik: d.title,
423
+ ...(yil ? { yil } : {}),
424
+ ...(d.publisher?.length === 1 && d.publisher[0] ? { yayinevi: d.publisher[0] } : {}),
425
+ }, `https://openlibrary.org${d.key}`, "Open Library");
426
+ if (uygun(k))
427
+ return { k, googleUlasilamadi: false };
428
+ }
429
+ }
430
+ let googleUlasilamadi = false;
431
+ const g = new URL(GOOGLE);
432
+ g.searchParams.set("q", `intitle:${baslik} inauthor:${yazar}`);
433
+ g.searchParams.set("maxResults", "5");
434
+ const gy = GoogleYanit.safeParse(await getir(f, g, () => {
435
+ googleUlasilamadi = true;
436
+ }));
437
+ if (gy.success)
438
+ for (const x of gy.data.items ?? []) {
439
+ const k = googleKunye(x);
440
+ if (baslikEslesir(baslik, x.volumeInfo.title) && uygun(k))
441
+ return { k, googleUlasilamadi: false };
442
+ }
443
+ return { k: null, googleUlasilamadi };
393
444
  }
394
445
  const KaynakSorgusu = z
395
446
  .object({
@@ -421,11 +472,15 @@ export async function kaynakDogrula(f, k) {
421
472
  return bulunamadi("ISBN geçersiz (sağlama basamağı tutmuyor).");
422
473
  return sonla(await isbnIle(f, i), "isbn", { ...ek, tur: k.tur ?? "kitap" }, "Bu ISBN Open Library ve Google Books'ta bulunamadı.");
423
474
  }
424
- return sonla(await baslikIle(f, k.baslik ?? "", k.yazar ?? "", ek), "baslik_yazar", ek, "Bu başlık ve yazarla (tam soyad, verildiyse tür ve yıl) eşleşen eser Google Books ve Crossref kataloglarında bulunamadı. Türkçe kitapların çoğu bu kataloglarda yoktur.");
475
+ const b = await baslikIle(f, k.baslik ?? "", k.yazar ?? "", ek);
476
+ return sonla(b.k, "baslik_yazar", ek, b.googleUlasilamadi
477
+ ? "Bu başlık ve yazarla (tam soyad, verildiyse tür ve yıl) eşleşen eser Crossref ve Open Library kataloglarında bulunamadı; Google Books'a ulaşılamadı (kota ya da bağlantı sorunu), orada aranamadı. Daha sonra yeniden deneyin ya da ISBN ile arayın."
478
+ : "Bu başlık ve yazarla (tam soyad, verildiyse tür ve yıl) eşleşen eser Crossref, Open Library ve Google Books kataloglarında bulunamadı. Türkçe kitapların çoğu bu kataloglarda yoktur.");
425
479
  }
426
480
  const KATALOG_KAYNAKLARI = [
427
481
  { ad: "Crossref REST API", adres: "https://api.crossref.org" },
428
482
  { ad: "Open Library Books API", adres: "https://openlibrary.org/developers/api" },
483
+ { ad: "Open Library Search API", adres: "https://openlibrary.org/dev/docs/api/search" },
429
484
  { ad: "Google Books API", adres: "https://developers.google.com/books" },
430
485
  ];
431
486
  export const kaynakDogrulaAraci = arac({
@@ -0,0 +1,60 @@
1
+ export type ProlizBolum = "ayrinti" | "detay" | "kaynak" | "yapi" | "etkinlik" | "degerlendirme" | "isyuku" | "ciktilar" | "konular" | "katki";
2
+ /** Sayfada yazan ama içi boş değer. */
3
+ export declare const bosDeger: (s: string | undefined) => boolean;
4
+ /** Proliz ders sayfasının düz metni mi? En az üç bölüm başlığı satır olarak geçer (boş pakette de). */
5
+ export declare function prolizSayfaMetniMi(girdi: string): boolean;
6
+ export interface ProlizMetinDersi {
7
+ kod?: string;
8
+ ad?: string;
9
+ yariyil: number | null;
10
+ tul?: string;
11
+ kredi: number | null;
12
+ akts: number | null;
13
+ guncelleme?: string;
14
+ amac: string;
15
+ icerik: string;
16
+ yontem: string;
17
+ notlar: string;
18
+ ciktilar: {
19
+ kod: string;
20
+ metin: string;
21
+ }[];
22
+ degerlendirme: {
23
+ ad: string;
24
+ sayi: number | null;
25
+ katki: number | null;
26
+ }[];
27
+ /** Sayfadaki değerlendirme tablosu satırları, olduğu gibi (degerlendirmeKaniti olarak verilir). */
28
+ degerlendirmeKaniti: string;
29
+ isyuku: {
30
+ ad: string;
31
+ sayi: number | null;
32
+ sure: number | null;
33
+ toplam: number | null;
34
+ }[];
35
+ dersYapisi: {
36
+ alan: string;
37
+ yuzde: number | null;
38
+ }[];
39
+ haftaKonulari: string[];
40
+ /** Ö satırları, P sütunları, sayı 1-5 (boş hücre yok). */
41
+ katki: Record<string, Record<string, number>>;
42
+ /** "Tüm" satırı ayrı. */
43
+ katkiTum: Record<string, number>;
44
+ /** Matris başlığındaki P kodları (boş pakette de okunur). */
45
+ pKodlari: string[];
46
+ }
47
+ export interface ProlizMetinSonucu {
48
+ ders: ProlizMetinDersi;
49
+ bilgiler: Record<string, string>;
50
+ yetkililer: {
51
+ rol: string;
52
+ deger: string;
53
+ }[];
54
+ kaynaklar: string[];
55
+ /** Sayfada değeri olan alanlar. */
56
+ bulunan: string[];
57
+ bulunamayan: string[];
58
+ }
59
+ /** Proliz düz metnini bölüm bölüm okur. */
60
+ export declare function prolizMetniCoz(girdi: string): ProlizMetinSonucu;
@@ -0,0 +1,271 @@
1
+ // Proliz OBS herkese açık Bologna ders sayfasının DÜZ METNİ (tarayıcı aracının okuduğu biçim, sekmeli tablolar).
2
+ // Sayfa bölüm başlıklarıyla (Dersin Ayrıntıları, Dersin Detayları, Değerlendirme Ölçütleri, Ders Konuları...) ayrılır;
3
+ // her bölüm kendi tablo biçimine göre okunur. Bölümler arası karışma olmaz: "Ders Konuları"nın haftaları öğrenme
4
+ // çıktısı, değerlendirme kalemleri ders yapısı sayılmaz. Boş değer ("Veri yok", "Kayıt Yok", ".") boş kalır; sayfada
5
+ // olmayan hiçbir şey doldurulmaz. Ağ ve model yoktur.
6
+ /** Bölüm başlıkları; satırın tamamı (iki nokta ve sonrası hariç) eşleşir. */
7
+ /** Başlıklar küçük harfle (tr) yazılır; satır da küçültülerek eşlenir ("İ" büyük-küçük eşlemesi için). */
8
+ const BASLIKLAR = [
9
+ [/^dersin ayrıntıları$/iu, "ayrinti"],
10
+ [/^dersin detayları$/iu, "detay"],
11
+ [/^ders kaynakları$/iu, "kaynak"],
12
+ [/^ders yapısı$/iu, "yapi"],
13
+ [/^planlanan öğrenme aktiviteleri ve metodları$/iu, "etkinlik"],
14
+ [/^değerlendirme ölçütleri$/iu, "degerlendirme"],
15
+ [/^akts hesaplama içeriği$/iu, "isyuku"],
16
+ [/^dersin öğrenme çıktıları(\s*:.*)?$/iu, "ciktilar"],
17
+ [/^ders konuları$/iu, "konular"],
18
+ [/^dersin program çıktılarına katkısı$/iu, "katki"],
19
+ ];
20
+ /** Anahtar-değer bölümlerindeki bilinen etiketler (sekmesi kırpılmış boş etiket de tanınsın diye). */
21
+ const ETIKETLER = [
22
+ "Dersin Dili",
23
+ "Dersin Düzeyi",
24
+ "Bölümü / Programı",
25
+ "Öğrenim Türü",
26
+ "Dersin Türü",
27
+ "Dersin Öğretim Şekli",
28
+ "Dersin Amacı",
29
+ "Dersin İçeriği",
30
+ "Dersin Yöntem ve Teknikleri",
31
+ "Ön Koşulları",
32
+ "Dersin Koordinatörü",
33
+ "Dersi Verenler",
34
+ "Dersin Yardımcıları",
35
+ "Dersin Staj Durumu",
36
+ "Kaynaklar",
37
+ "Ders Notları",
38
+ "Dökümanlar",
39
+ "Ödevler",
40
+ "Sınavlar",
41
+ ];
42
+ const kucuk = (s) => s.toLocaleLowerCase("tr").replace(/\s+/gu, " ").trim();
43
+ const ETIKET_KUMESI = new Set(ETIKETLER.map(kucuk));
44
+ function baslikMi(satir) {
45
+ const s = kucuk(satir);
46
+ for (const [re, b] of BASLIKLAR)
47
+ if (re.test(s))
48
+ return b;
49
+ return null;
50
+ }
51
+ /** Sayfada yazan ama içi boş değer. */
52
+ export const bosDeger = (s) => {
53
+ const t = (s ?? "").replace(/\s+/gu, " ").trim();
54
+ return !t || /^(veri yok|kayıt yok|yok kayıt)\.*$/iu.test(t) || /^[.\-–]+$/u.test(t);
55
+ };
56
+ /** Proliz ders sayfasının düz metni mi? En az üç bölüm başlığı satır olarak geçer (boş pakette de). */
57
+ export function prolizSayfaMetniMi(girdi) {
58
+ if (/<\/?(table|tr|td)\b/iu.test(girdi))
59
+ return false;
60
+ const bulunan = new Set();
61
+ for (const s of girdi.split(/\r?\n/u)) {
62
+ const b = baslikMi(s);
63
+ if (b)
64
+ bulunan.add(b);
65
+ }
66
+ return bulunan.size >= 3;
67
+ }
68
+ const sayiOku = (s) => {
69
+ const m = (s ?? "").replace(",", ".").match(/-?\d+(?:\.\d+)?/u);
70
+ return m ? Number(m[0]) : null;
71
+ };
72
+ const hucreler = (s) => s.split("\t").map((x) => x.replace(/\s+/gu, " ").trim());
73
+ /** Proliz düz metnini bölüm bölüm okur. */
74
+ export function prolizMetniCoz(girdi) {
75
+ const bolumler = new Map();
76
+ let aktif = null;
77
+ for (const ham of girdi.split(/\r?\n/u)) {
78
+ const b = baslikMi(ham);
79
+ if (b) {
80
+ aktif = bolumler.get(b) ?? [];
81
+ bolumler.set(b, aktif);
82
+ continue;
83
+ }
84
+ if (/^\s*katkı düzeyi\s*:/iu.test(ham)) {
85
+ aktif = null;
86
+ continue;
87
+ }
88
+ if (aktif && ham.trim())
89
+ aktif.push(ham);
90
+ }
91
+ const satirlar = (b) => bolumler.get(b) ?? [];
92
+ // Dersin Ayrıntıları: başlık satırı ve altında değer satırı.
93
+ const ayrinti = {};
94
+ const ay = satirlar("ayrinti");
95
+ const bi = ay.findIndex((s) => /\bkodu\b/iu.test(s) && s.includes("\t"));
96
+ if (bi >= 0 && ay[bi + 1]) {
97
+ const bas = hucreler(ay[bi] ?? "");
98
+ const deg = hucreler(ay[bi + 1] ?? "");
99
+ const ALAN = [
100
+ [/^yarıyıl$|^semester$/iu, "yariyil"],
101
+ [/^kodu$|^code$/iu, "kod"],
102
+ [/^adı$|^name$/iu, "ad"],
103
+ [/^t\+u\+l$/iu, "tul"],
104
+ [/^kredi$|^credit$/iu, "kredi"],
105
+ [/^akts$|^ects$/iu, "akts"],
106
+ [/güncelleme/iu, "guncelleme"],
107
+ ];
108
+ bas.forEach((h, j) => {
109
+ const a = ALAN.find(([re]) => re.test(h))?.[1];
110
+ const v = deg[j] ?? "";
111
+ if (a && !bosDeger(v))
112
+ ayrinti[a] = v;
113
+ });
114
+ }
115
+ // Anahtar-değer bölümleri: "Etiket<TAB>değer", değerin devamı sekmesiz sonraki satırlar.
116
+ const anahtarDeger = (b) => {
117
+ const sonuc = [];
118
+ for (const s of satirlar(b)) {
119
+ const sekmeli = s.includes("\t");
120
+ const [ilk = "", ...kalan] = hucreler(s);
121
+ if (sekmeli && ilk)
122
+ sonuc.push([ilk, [kalan.join(" ").trim()]]);
123
+ else if (ETIKET_KUMESI.has(kucuk(s)))
124
+ sonuc.push([s.trim(), [""]]);
125
+ else if (sonuc.length)
126
+ sonuc.at(-1)?.[1].push(s.trim());
127
+ }
128
+ return sonuc.map(([e, d]) => {
129
+ const metin = d
130
+ .map((x) => x.trim())
131
+ .filter((x) => !bosDeger(x))
132
+ .join("\n")
133
+ .trim();
134
+ return [e, metin];
135
+ });
136
+ };
137
+ const kv = new Map();
138
+ for (const [e, d] of [...anahtarDeger("detay"), ...anahtarDeger("kaynak")])
139
+ kv.set(kucuk(e), d);
140
+ const al = (e) => kv.get(kucuk(e)) ?? "";
141
+ const bilgiler = {};
142
+ const BILGI = [
143
+ ["Dersin Dili", "dil"],
144
+ ["Dersin Düzeyi", "duzey"],
145
+ ["Bölümü / Programı", "program"],
146
+ ["Öğrenim Türü", "ogrenimTuru"],
147
+ ["Dersin Türü", "tur"],
148
+ ["Dersin Öğretim Şekli", "ogretimSekli"],
149
+ ["Dersin Staj Durumu", "stajDurumu"],
150
+ ];
151
+ for (const [e, a] of BILGI)
152
+ if (al(e))
153
+ bilgiler[a] = al(e);
154
+ const onKosul = al("Ön Koşulları")
155
+ .replace(/^\(\s*(.*?)\s*\)$/su, "$1")
156
+ .trim();
157
+ if (onKosul)
158
+ bilgiler.onKosul = onKosul;
159
+ const yetkililer = ["Dersin Koordinatörü", "Dersi Verenler", "Dersin Yardımcıları"]
160
+ .filter((e) => kv.has(kucuk(e)) && al(e))
161
+ .map((rol) => ({ rol, deger: al(rol).replace(/\n/gu, ", ") }));
162
+ const kaynaklar = al("Kaynaklar")
163
+ .split(/\n|;\s+(?=\S)/u)
164
+ .map((x) => x.replace(/^\s*(\[\d+\]|\d{1,2}[.)-]|[-•*–])\s*/u, "").trim())
165
+ .filter((x) => x.length >= 5);
166
+ // Tablo bölümleri: başlık satırı (rakamsız) atlanır, "Toplam" satırı sayılmaz.
167
+ const tabloSatirlari = (b) => satirlar(b)
168
+ .map(hucreler)
169
+ .filter((h) => h.length >= 2 && h[0] && !bosDeger(h[0]) && h.slice(1).some((x) => /\d/u.test(x)));
170
+ const TOPLAM = /^toplam/iu;
171
+ const degerlendirmeSatirlari = satirlar("degerlendirme").filter((s) => !bosDeger(s));
172
+ const degerlendirme = tabloSatirlari("degerlendirme")
173
+ .filter((h) => !TOPLAM.test(h[0] ?? ""))
174
+ .map((h) => ({ ad: h[0] ?? "", sayi: sayiOku(h[1]), katki: sayiOku(h.at(-1)) }));
175
+ const isyuku = tabloSatirlari("isyuku")
176
+ .filter((h) => !TOPLAM.test(h[0] ?? "") && h.length >= 4)
177
+ .map((h) => ({ ad: h[0] ?? "", sayi: sayiOku(h[1]), sure: sayiOku(h[2]), toplam: sayiOku(h[3]) }));
178
+ const dersYapisi = satirlar("yapi")
179
+ .map(hucreler)
180
+ .filter((h) => h.length >= 2 && h[0] && /\d/u.test(h[1] ?? ""))
181
+ .map((h) => ({ alan: h[0] ?? "", yuzde: sayiOku(h[1]) }));
182
+ const numarali = (b) => satirlar(b)
183
+ .map(hucreler)
184
+ .filter((h) => /^\d{1,2}$/u.test(h[0] ?? "") && h.slice(1).join(" ").trim())
185
+ .map((h) => h
186
+ .slice(1)
187
+ .filter((x) => !bosDeger(x))
188
+ .join(" ")
189
+ .trim())
190
+ .filter(Boolean);
191
+ const ciktilar = numarali("ciktilar").map((metin, i) => ({ kod: `Ö${i + 1}`, metin }));
192
+ const haftaKonulari = numarali("konular");
193
+ // Katkı matrisi: başlıkta P kodları; satır başı Ö kodu ya da "Tüm".
194
+ const katki = {};
195
+ const katkiTum = {};
196
+ let pKodlari = [];
197
+ let sutunlar = [];
198
+ for (const s of satirlar("katki")) {
199
+ const h = s.split("\t").map((x) => x.trim());
200
+ const ps = h.map((x, j) => ({ j, m: x.match(/^P\s*(\d{1,2})$/iu) })).filter((x) => x.m);
201
+ if (ps.length >= 2) {
202
+ sutunlar = ps.map(({ j, m }) => ({ j, p: `P${Number(m?.[1])}` }));
203
+ pKodlari = sutunlar.map((x) => x.p);
204
+ continue;
205
+ }
206
+ const m = (h[0] ?? "").match(/^(?:Ö|ÖÇ)\s*(\d{1,2})$|^(Tüm)$/iu);
207
+ if (!m || !sutunlar.length)
208
+ continue;
209
+ const deger = {};
210
+ for (const { j, p } of sutunlar) {
211
+ const v = h[j] ?? "";
212
+ if (/^[1-5]$/u.test(v))
213
+ deger[p] = Number(v);
214
+ }
215
+ if (m[2])
216
+ Object.assign(katkiTum, deger);
217
+ else
218
+ katki[`Ö${Number(m[1])}`] = deger;
219
+ }
220
+ const ders = {
221
+ ...(ayrinti.kod ? { kod: ayrinti.kod } : {}),
222
+ ...(ayrinti.ad ? { ad: ayrinti.ad } : {}),
223
+ yariyil: sayiOku(ayrinti.yariyil),
224
+ ...(ayrinti.tul ? { tul: ayrinti.tul.replace(/\s/gu, "") } : {}),
225
+ kredi: sayiOku(ayrinti.kredi),
226
+ akts: sayiOku(ayrinti.akts),
227
+ ...(ayrinti.guncelleme ? { guncelleme: ayrinti.guncelleme } : {}),
228
+ amac: al("Dersin Amacı"),
229
+ icerik: al("Dersin İçeriği"),
230
+ yontem: al("Dersin Yöntem ve Teknikleri"),
231
+ notlar: al("Ders Notları"),
232
+ ciktilar,
233
+ degerlendirme,
234
+ degerlendirmeKaniti: degerlendirme.length ? degerlendirmeSatirlari.join("\n") : "",
235
+ isyuku,
236
+ dersYapisi,
237
+ haftaKonulari,
238
+ katki,
239
+ katkiTum,
240
+ pKodlari,
241
+ };
242
+ const varMi = {
243
+ kod: Boolean(ders.kod),
244
+ ad: Boolean(ders.ad),
245
+ tul: Boolean(ders.tul),
246
+ kredi: ders.kredi !== null,
247
+ akts: ders.akts !== null,
248
+ dil: Boolean(bilgiler.dil),
249
+ tur: Boolean(bilgiler.tur),
250
+ onKosul: Boolean(bilgiler.onKosul),
251
+ amac: Boolean(ders.amac),
252
+ icerik: Boolean(ders.icerik),
253
+ yontem: Boolean(ders.yontem),
254
+ notlar: Boolean(ders.notlar),
255
+ ciktilar: ciktilar.length > 0,
256
+ haftalar: haftaKonulari.length > 0,
257
+ degerlendirme: degerlendirme.length > 0,
258
+ isyuku: isyuku.length > 0,
259
+ kaynaklar: kaynaklar.length > 0,
260
+ katki: Object.keys(katki).length > 0,
261
+ dersYapisi: dersYapisi.length > 0,
262
+ };
263
+ return {
264
+ ders,
265
+ bilgiler,
266
+ yetkililer,
267
+ kaynaklar,
268
+ bulunan: Object.keys(varMi).filter((k) => varMi[k]),
269
+ bulunamayan: Object.keys(varMi).filter((k) => !varMi[k]),
270
+ };
271
+ }
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type AcikArac, type AracBaglami } from "./araclar.ts";
3
3
  import { type AcikIstem } from "./promptlar.ts";
4
4
  export declare const SUNUCU_ADI = "akredit-acik";
5
- export declare const SUNUCU_SURUMU = "0.8.0";
5
+ export declare const SUNUCU_SURUMU = "0.8.2";
6
6
  export declare const SUNUCU_TALIMATI: string;
7
7
  /** Her araç yanıtında yinelenen kısa kural: uzun sohbette son okunan metin de aynı şeyi söyler. */
8
8
  export declare const SORMADAN_DEVAM = "Bu ad\u0131mda hocaya soru sormay\u0131n ve onay beklemeyin; do\u011Frudan sonrakiAdim'\u0131 uygulay\u0131n.";
@@ -6,7 +6,7 @@ import { ACIK_ARACLAR, AKREDIT_NOTU, AracHatasi, DEPO, } from "./araclar.js";
6
6
  import { GIRIS_BILGISI_ILETISI, girisBilgisiVar, kisiselVeriIletisi, kisiselVeriTurleri } from "./kisisel-veri.js";
7
7
  import { ACIK_ISTEMLER, AKILLI_DOLDURMA_KURALI, istemSemasi } from "./promptlar.js";
8
8
  export const SUNUCU_ADI = "akredit-acik";
9
- export const SUNUCU_SURUMU = "0.8.0";
9
+ export const SUNUCU_SURUMU = "0.8.2";
10
10
  export const SUNUCU_TALIMATI = [
11
11
  "Akredit açık araçları öğretim elemanlarına kalite ve akreditasyon işlerinde yardım eder: Bologna ders paketi denetimi ve doldurma, öğrenme çıktısı yazma, ÇK/PÇ hesabı, YÖKAK ve program akreditasyonu (MÜDEK, FEDEK, SABAK, EPDAD) ölçüt ve kanıt rehberi, rapor dil denetimi, kaynak doğrulama.",
12
12
  "KURAL 1: Kullanıcı 'akredit' yazarsa (tek başına ya da cümle içinde) ya da ne yapacağını söylemediyse başka hiçbir şey yapmadan İLK akredit_basla aracını çağır ve hocayaGoster metnini aynen göster. Kullanıcı niyetini zaten yazdıysa (ör. 'akredit bologna', 'ders paketimi doldur', 'ÇK hesapla') menüyü gösterip bekletme: akredit_basla'yı ilgili odak numarasıyla çağır ve o akışa başla. 'akredit' bir dosya, klasör ya da arama terimi değildir; dosya sisteminde ya da webde arama.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akredit/acik-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Herkese açık Akredit MCP sunucusu: hesapsız, salt okuma, deterministik araçlar (ölçüt, Bologna denetimi, ÇK/PÇ hesabı, rapor dil denetimi). Kurum verisine erişmez, model çağırmaz.",
@@ -43,8 +43,8 @@
43
43
  "devDependencies": {
44
44
  "@akredit/baglayicilar": "0.1.0",
45
45
  "@akredit/cekirdek": "0.1.0",
46
- "@akredit/olcme": "0.1.0",
47
46
  "@akredit/cerceveler": "0.1.0",
47
+ "@akredit/olcme": "0.1.0",
48
48
  "@akredit/yz": "0.1.0"
49
49
  },
50
50
  "publishConfig": {