@akredit/acik-mcp 0.4.0 → 0.6.0

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.
@@ -11,7 +11,8 @@ import { dersSayfasiCoz, programCiktilari, tablolar } from "../../../packages/ba
11
11
  import { bolognaDenetle, taslakDenetle, } from "../../../packages/cekirdek/src/index.js";
12
12
  import { DOGRULANMADI as DOGRULANMADI_ISARETI } from "../../../packages/cerceveler/src/index.js";
13
13
  import { z } from "zod";
14
- import { GENEL_ALAN_ADLARI, genelAyikla, genelAyristir } from "./genel-ayristir.js";
14
+ import { GENEL_ALAN_ADLARI, genelAyikla, genelAyristir, ingilizceAlanVar, prolizMetniMi } from "./genel-ayristir.js";
15
+ import { kaynakImzasiDogru } from "./kaynak-dogrula.js";
15
16
  import { girisBilgisiAyikla } from "./kisisel-veri.js";
16
17
  import { AracHatasi, arac, KOD_KAYNAGI } from "./temel.js";
17
18
  // ---------------------------------------------------------------- Alan modeli
@@ -91,10 +92,11 @@ export const ALANLAR = {
91
92
  sekme: "Öğrenme Çıktıları",
92
93
  kurallar: [
93
94
  "K4: Her çıktı ölçülebilir bir eylem fiiliyle biter (açıklar, uygular, tasarlar). 'Bilgi sahibi olur', 'öğrenir', 'kavrar' olmaz.",
94
- "K5: 8-12 çıktı, Ö1'den kesintisiz. Her çıktının türü Bilgi, Beceri ya da Yetkinlik; çoğu Beceri, en az biri Yetkinlik.",
95
+ "K4: '-ebilme/-abilme' (tasarlayabilme) ve '-ebilir' kalıpları kabul edilir. 'Sahip olur', 'farkında olur', 'haberdar olur', 'vakıf olur' ölçülemez.",
96
+ "K5: 4-10 çıktı geçerli, 5-8 önerilir (Akredit önerisi). Ö1'den kesintisiz. Her çıktının türü Bilgi, Beceri ya da Yetkinlik; çoğu Beceri, en az biri Yetkinlik.",
95
97
  "İngilizcesi tam çeviri olsun.",
96
98
  ],
97
- sinir: { enAzCikti: 8, enCokCikti: 12 },
99
+ sinir: { enAzCikti: 4, enCokCikti: 10, onerilenEnAz: 5, onerilenEnCok: 8 },
98
100
  kaliplar: [
99
101
  "[Konu] kavramlarını örneklerle açıklar. (Bilgi)",
100
102
  "[Yöntem] kullanarak [problem türünü] çözer. (Beceri)",
@@ -109,6 +111,7 @@ export const ALANLAR = {
109
111
  kurallar: [
110
112
  "Katkılar toplamı tam %100.",
111
113
  "Uygulamalı derste (U ya da L sıfırdan büyük) uygulama, proje, ödev ya da laboratuvar kalemi olur.",
114
+ "Değerlendirme yöntemi ve yüzdeleri resmî taahhüttür: sayfadan okunur ya da hocaya sorulur, asla tahmin edilmez. Kaynağını degerlendirmeDayanagi olarak verin ('sayfa' ya da 'hoca'); teyitsiz yüzde OBS'ye yazılmaz.",
112
115
  ],
113
116
  sinir: { toplamYuzde: 100 },
114
117
  kaliplar: ["Ara Sınav 1 %40, Yarıyıl Sonu Sınavı 1 %60", "Ara Sınav 1 %30, Proje 1 %20, Yarıyıl Sonu Sınavı 1 %50"],
@@ -119,12 +122,12 @@ export const ALANLAR = {
119
122
  ad: "AKTS iş yükü",
120
123
  sekme: DERS_BILGILERI,
121
124
  kurallar: [
122
- "K8: Toplam iş yükü (sayı x süre toplamı) = AKTS x bölen (varsayılan 30), tam eşit.",
125
+ "K8: Toplam iş yükü (sayı x süre toplamı) AKTS başına 25-30 saat aralığında olur. Kurum AKTS başına saat değerini (bolen) verdiyse toplam tam AKTS x o değerdir.",
123
126
  "AKTS'yi değiştirmeyin, saat dağılımını değiştirin.",
124
127
  "Ders Süresi satırının haftalık süresi T+U+L toplamına eşit olur.",
125
- "Yayımlanmış sayfa denetiminde AKTS başına 25-30 saat kabul edilir; yeni taslakta tam eşitlik istenir.",
128
+ "Yalnız derste gerçekten olan etkinliklere saat verilir (ödev yoksa ödev satırı yok).",
126
129
  ],
127
- sinir: { varsayilanBolen: 30, sayfaAraligi: "25-30 saat/AKTS" },
130
+ sinir: { aralik: "25-30 saat/AKTS", kurumDegeri: "verilirse tam eşitlik" },
128
131
  kaliplar: [
129
132
  "Ders Süresi 14 x (T+U+L)",
130
133
  "Sınıf Dışı Ders Çalışma Süresi 14 x [saat]",
@@ -185,7 +188,8 @@ export const ALANLAR = {
185
188
  sekme: "Diğer Kaynaklar",
186
189
  kurallar: [
187
190
  "En az 2 gerçek kaynak (yazar, yıl ve eser adı).",
188
- "Kaynak eksikse hocaya kitap ya da makale adını, gerekirse yazarını sorun; künyeyi kaynak_bicimle ile biçimlendirin.",
191
+ "Her kaynak { kunye, dogrulama } olarak verilir: dogrulama ya kaynak_dogrula sonucudur (durum, baglanti, imza) ya da 'hoca_beyani'. Düz metin kaynak doğrulanmamış sayılır ve OBS'ye yazılmaz.",
192
+ "Katalogda bulunmayan (çoğu Türkçe kitap) eser yalnız hoca kullandığını söylerse, hocanın verdiği bilgiyle 'hoca_beyani' olarak yazılır.",
189
193
  "Yazar adları yayımlanmış künyedir; bu alanda serbesttir. E-posta ve telefon yazılmaz.",
190
194
  ],
191
195
  sinir: { enAz: 2 },
@@ -228,8 +232,13 @@ const SECENEK_KURALI = "Sunucu metin üretmez. Seçenekleri asistan üretir: 2-3
228
232
  const KAYNAK_KURALLAR = KOD_KAYNAGI("packages/cekirdek/src/bologna/taslak.ts", "Akredit Bologna taslak kuralları");
229
233
  const KAYNAK_SAYFA = KOD_KAYNAGI("packages/cekirdek/src/bologna/denetim.ts", "Akredit Bologna sayfa denetim kuralları");
230
234
  const KAYNAK_AYRISTIRICI = KOD_KAYNAGI("packages/baglayicilar/src/bologna-proliz/ayristir.ts", "Akredit Bologna (Proliz) sayfa ayrıştırıcısı");
231
- /** Bologna sayfası hocanın kendi yayımlanmış ders bilgisidir: yalnız giriş bilgisi denetlenir (bkz. kisisel-veri.ts). */
232
- const BOLOGNA_KAPSAMI = "yok";
235
+ /**
236
+ * Bologna sayfası hocanın kendi yayımlanmış ders bilgisidir. Giriş bilgisine ek olarak T.C. kimlik no, öğrenci
237
+ * numarası ve IBAN reddedilir (dış inceleme 2026-11-05); yayımlanmış öğretim elemanı adları serbesttir.
238
+ */
239
+ const BOLOGNA_KAPSAMI = "ogrenci";
240
+ /** Künye kuralıyla (kişi adı ve uzun numara serbest) denetlenen alanlar: kaynaklar ve görev satırları. */
241
+ const BOLOGNA_KUNYE_ALANLARI = ["kaynaklar", "yetkililer"];
233
242
  // ---------------------------------------------------------------- Ortak hesap ve eşleme
234
243
  const TUL = z
235
244
  .string()
@@ -295,6 +304,33 @@ const IkiDilli = z.object({
295
304
  tr: z.string().max(6000),
296
305
  en: z.string().max(6000).optional(),
297
306
  });
307
+ const KaynakDogrulamasi = z.union([
308
+ z.literal("hoca_beyani"),
309
+ z.object({
310
+ durum: z.string().max(40),
311
+ baglanti: z.string().max(600),
312
+ imza: z.string().max(80).optional(),
313
+ }),
314
+ ]);
315
+ const KaynakOgesi = z.union([
316
+ z.string().trim().min(5).max(600),
317
+ z.object({ kunye: z.string().trim().min(5).max(600), dogrulama: KaynakDogrulamasi }),
318
+ ]);
319
+ /** Kaynağın doğrulama durumu. Düz metin ya da imzası bu sürece ait olmayan kayıt doğrulanmamıştır. */
320
+ export function kaynakDurumu(k) {
321
+ if (typeof k === "string")
322
+ return "dogrulanmamis";
323
+ if (k.dogrulama === "hoca_beyani")
324
+ return "hoca_beyani";
325
+ const d = k.dogrulama;
326
+ return d.durum === "katalogda_bulundu" &&
327
+ /^https:\/\//u.test(d.baglanti) &&
328
+ d.imza !== undefined &&
329
+ kaynakImzasiDogru(k.kunye.trim(), d.baglanti, d.imza)
330
+ ? "katalogda_bulundu"
331
+ : "dogrulanmamis";
332
+ }
333
+ export const kunyeMetni = (k) => (typeof k === "string" ? k : k.kunye).trim();
298
334
  const TaslakAlanlari = z.object({
299
335
  amac: IkiDilli.optional(),
300
336
  icerik: IkiDilli.optional(),
@@ -337,11 +373,15 @@ const TaslakAlanlari = z.object({
337
373
  .optional()
338
374
  .describe("Ö kodu, P kodu, 1-5 ya da boş"),
339
375
  ska: z.array(z.number().int().min(1).max(17)).max(17).optional(),
376
+ degerlendirmeDayanagi: z
377
+ .enum(["sayfa", "hoca", "oneri"])
378
+ .optional()
379
+ .describe("Değerlendirme yöntem ve yüzdelerinin kaynağı: 'sayfa' (OBS sayfasından okundu), 'hoca' (hoca söyledi), 'oneri' (asistan önerisi, teyitsiz). Teyitsiz yüzde OBS'ye yazılmaz."),
340
380
  kaynaklar: z
341
- .array(z.string().trim().min(5).max(600))
381
+ .array(KaynakOgesi)
342
382
  .max(30)
343
383
  .optional()
344
- .describe("Künyeler, her biri tek satır (ör. kaynak_bicimle çıktısı). Yazar adı künyedir, serbesttir."),
384
+ .describe("Her kaynak { kunye, dogrulama }: dogrulama kaynak_dogrula sonucu ({ durum, baglanti, imza }) ya da 'hoca_beyani'. Düz metin doğrulanmamış sayılır."),
345
385
  kaynakSayisi: z
346
386
  .number()
347
387
  .int()
@@ -356,7 +396,14 @@ const DersTemeli = z.object({
356
396
  ad: z.string().trim().max(200).optional(),
357
397
  akts: z.number().min(0).max(60).optional(),
358
398
  tul: TUL.optional().describe("Teori+Uygulama+Laboratuvar, ör. '2+1+0'"),
359
- bolen: z.number().int().min(20).max(40).default(30).describe("AKTS başına saat; varsayılan 30"),
399
+ bolen: z
400
+ .number()
401
+ .int()
402
+ .min(20)
403
+ .max(40)
404
+ .optional()
405
+ .describe("Kurumun AKTS başına saat değeri; verilmezse 25-30 aralığı kabul edilir"),
406
+ donem: z.string().trim().max(40).optional().describe("Dönem, ör. 2026-2027 Güz; OBS geri okumasında denetlenir"),
360
407
  yetkililer: z
361
408
  .array(YetkiliSemasi)
362
409
  .max(20)
@@ -371,7 +418,6 @@ export function alanBulgulari(ders, alanlar, s) {
371
418
  ...(ders.ad ? { ad: ders.ad } : {}),
372
419
  ...(ders.akts !== undefined ? { akts: ders.akts } : {}),
373
420
  ...(ders.tul ? { tul: ders.tul } : {}),
374
- bolen: ders.bolen,
375
421
  ...(alanlar.amac ? { amac: ikiDil(alanlar.amac, s.ingilizce) } : {}),
376
422
  ...(alanlar.icerik ? { icerik: ikiDil(alanlar.icerik, s.ingilizce) } : {}),
377
423
  ...(alanlar.yontem ? { yontem: ikiDil(alanlar.yontem, s.ingilizce) } : {}),
@@ -408,13 +454,32 @@ export function alanBulgulari(ders, alanlar, s) {
408
454
  const sonuc = new Map();
409
455
  const ekle = (a, b) => sonuc.set(a, [...(sonuc.get(a) ?? []), b]);
410
456
  if (verilen.has("kaynaklar") && alanlar.kaynaklar)
411
- for (const [i, k] of alanlar.kaynaklar.entries())
412
- if (!KUNYE_YILI.test(k))
457
+ for (const [i, k] of alanlar.kaynaklar.entries()) {
458
+ if (kaynakDurumu(k) === "dogrulanmamis")
459
+ ekle("kaynaklar", {
460
+ duzey: "hata",
461
+ kural: "Kaynaklar",
462
+ aciklama: `${i + 1}. kaynak doğrulanmamış; doğrulanmamış kaynak yazılamaz. kaynak_dogrula sonucunu (durum, baglanti, imza) aynen verin ya da hoca bu eseri kullandığını söylediyse dogrulama: 'hoca_beyani' yazın.`,
463
+ kuralTuru: "Akredit önerisi",
464
+ dayanak: "Değişmez ilke 1-2: kanıtsız iddia ve uydurma yok.",
465
+ });
466
+ if (!KUNYE_YILI.test(kunyeMetni(k)))
413
467
  ekle("kaynaklar", {
414
468
  duzey: "uyari",
415
469
  kural: "Kaynaklar",
416
470
  aciklama: `${i + 1}. kaynakta yıl görünmüyor. Yılı hocaya sorun ya da kaynak_bicimle ile biçimlendirin.`,
417
471
  });
472
+ }
473
+ if (verilen.has("degerlendirme") &&
474
+ alanlar.degerlendirmeDayanagi !== "sayfa" &&
475
+ alanlar.degerlendirmeDayanagi !== "hoca")
476
+ ekle("degerlendirme", {
477
+ duzey: "uyari",
478
+ kural: "Değerlendirme",
479
+ aciklama: "Değerlendirme yöntem ve yüzdeleri teyitsiz (sayfadan okunmadı, hoca söylemedi). Hocaya tek mesajda sorun; teyitsiz yüzde OBS'ye yazılmaz.",
480
+ kuralTuru: "Akredit önerisi",
481
+ dayanak: "Değişmez ilke 2-3: değerlendirme oranları resmî taahhüttür, uydurulmaz; beyan hocadadır.",
482
+ });
418
483
  // İş yükü AKTS olmadan denetlenemez; cekirdek "akts boş" diye temel bulgu verir, burada alana bağlanır.
419
484
  if (verilen.has("isyuku") && ders.akts === undefined)
420
485
  ekle("isyuku", {
@@ -422,7 +487,7 @@ export function alanBulgulari(ders, alanlar, s) {
422
487
  kural: "K8",
423
488
  aciklama: "Dersin AKTS değeri verilmedi; iş yükü hedefi hesaplanamaz.",
424
489
  });
425
- for (const b of taslakDenetle(t, { bolen: ders.bolen }).bulgular) {
490
+ for (const b of taslakDenetle(t, { kuralSeti: "guncel", kurumBoleni: ders.bolen }).bulgular) {
426
491
  const a = bulguAlani(b);
427
492
  if (a === "temel" || a === "yetkililer")
428
493
  continue;
@@ -451,7 +516,17 @@ const KUNYE_YILI = /\((?:\d{4}[a-z]?|t\.y\.|n\.d\.|baskıda|in press)\)|\b(1[5-9
451
516
  /** Bütün sayılar burada hesaplanır; asistan toplam yazmaz (ilke 2). */
452
517
  export function bolognaHesabi(ders, alanlar) {
453
518
  const saat = tulSaati(ders.tul);
454
- const hedef = ders.akts !== undefined ? ders.akts * ders.bolen : null;
519
+ const hedefAraligi = ders.akts === undefined
520
+ ? null
521
+ : ders.bolen
522
+ ? [ders.akts * ders.bolen, ders.akts * ders.bolen]
523
+ : [ders.akts * 25, ders.akts * 30];
524
+ const hedef = hedefAraligi ? hedefAraligi[1] : null;
525
+ const hedefMetni = !hedefAraligi
526
+ ? ""
527
+ : ders.bolen
528
+ ? `${ders.akts} x ${ders.bolen} (kurum değeri) = ${hedefAraligi[0]}`
529
+ : `${ders.akts} AKTS için ${hedefAraligi[0]}-${hedefAraligi[1]} (AKTS başına 25-30)`;
455
530
  const isyuku = alanlar.isyuku
456
531
  ? (() => {
457
532
  const satirlar = alanlar.isyuku.map((x) => ({
@@ -462,19 +537,29 @@ export function bolognaHesabi(ders, alanlar) {
462
537
  }));
463
538
  const toplam = yuvarla(satirlar.reduce((t, x) => t + x.toplam, 0), 2);
464
539
  const dersSuresi = alanlar.isyuku.find((x) => /ders süresi/iu.test(x.ad));
540
+ const aralikta = hedefAraligi ? toplam >= hedefAraligi[0] && toplam <= hedefAraligi[1] : null;
541
+ const fark = !hedefAraligi
542
+ ? null
543
+ : toplam < hedefAraligi[0]
544
+ ? yuvarla(hedefAraligi[0] - toplam, 2)
545
+ : toplam > hedefAraligi[1]
546
+ ? yuvarla(hedefAraligi[1] - toplam, 2)
547
+ : 0;
465
548
  return {
466
549
  satirlar,
467
550
  toplam,
468
551
  hedef,
469
- fark: hedef === null ? null : yuvarla(hedef - toplam, 2),
552
+ hedefAraligi,
553
+ aralikta,
554
+ fark,
470
555
  aktsBasinaSaat: ders.akts ? yuvarla(toplam / ders.akts) : null,
471
556
  dersSuresiHaftalikBeklenen: saat,
472
557
  dersSuresiHaftalikVerilen: dersSuresi ? dersSuresi.sure : null,
473
- aciklama: hedef === null
558
+ aciklama: hedefAraligi === null
474
559
  ? "AKTS verilmediği için hedef hesaplanmadı."
475
- : toplam === hedef
476
- ? `Toplam ${toplam} saat, hedef ${ders.akts} x ${ders.bolen} = ${hedef}. Uyumlu.`
477
- : `Toplam ${toplam} saat, hedef ${ders.akts} x ${ders.bolen} = ${hedef}. ${hedef > toplam ? `${yuvarla(hedef - toplam, 2)} saat ekleyin` : `${yuvarla(toplam - hedef, 2)} saat azaltın`}; AKTS'yi değiştirmeyin.`,
560
+ : aralikta
561
+ ? `Toplam ${toplam} saat, hedef ${hedefMetni}. Uyumlu.`
562
+ : `Toplam ${toplam} saat, hedef ${hedefMetni}. ${fark !== null && fark > 0 ? `${fark} saat ekleyin` : `${yuvarla(-(fark ?? 0), 2)} saat azaltın`}; AKTS'yi değiştirmeyin.`,
478
563
  };
479
564
  })()
480
565
  : null;
@@ -547,8 +632,10 @@ function alanDurumlari(d, sayfaBulgulari) {
547
632
  ...(d.kaynakSayisi !== undefined || d.kaynaklar
548
633
  ? { kaynakSayisi: d.kaynaklar?.length ?? d.kaynakSayisi ?? 0 }
549
634
  : {}),
635
+ // Sayfadaki değerlendirme yayımlanmış hâlidir; teyitsiz uyarısı burada anlamsızdır.
636
+ degerlendirmeDayanagi: "sayfa",
550
637
  };
551
- const b = alanBulgulari({ akts: d.akts ?? undefined, bolen: 30 }, alanlar, { ingilizce: false });
638
+ const b = alanBulgulari({ akts: d.akts ?? undefined }, alanlar, { ingilizce: false });
552
639
  // Sayfada çıktı türü görünmez; türle ilgili kurallar burada denetlenmez.
553
640
  const turKurali = /Yetkinlik|Beceri|türü/u;
554
641
  const nedenler = (a) => (b.get(a) ?? []).filter((x) => x.duzey === "hata" && !turKurali.test(x.aciklama)).map((x) => x.aciklama);
@@ -651,9 +738,9 @@ export const bolognaDenetleAraci = arac({
651
738
  ad: "bologna_denetle",
652
739
  baslik: "Bologna ders paketini denetle",
653
740
  aciklama: "Bir dersin Bologna bilgi paketini denetler: hangi alan boş, hangisi zayıf, neden. AKTS ve iş yükü uyumu, değerlendirme toplamı, öğrenme çıktıları, haftalar ve katkı tablosu. Kurallar Akredit'in iç kurallarıdır, karar komisyonundur.",
654
- sonrakiAdim: "Doldurulacak alanları hocaya numaralı gösterin ve sorun: 1) alan alan seçenek göstererek dolduralım 2) yalnız boş olanlar 3) numaraları kendim seçeyim 4) Hepsini akıllıca doldur (önerilen). 4'te her alanı kendiniz doldurup denetleyin, sonunda tek özet gösterin. Her alan için bologna_alan_rehberi'ni çağırın.",
741
+ sonrakiAdim: "Nasıl doldurulacağını hocaya sormayın; doğrudan akıllı doldurun. Sayfada değerlendirme yüzdeleri ya da kaynaklar yoksa yalnız bunları TEK mesajda sorun (yüzdeyi tahmin etmeyin). Gerisini sormadan bologna_alan_rehberi(alan='hepsi'), is_yuku_oner ve bologna_taslak_denetle ile doldurup denetleyin; sonunda bologna_kopya_metni özetini tek seferde gösterin.",
655
742
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
656
- kunyeAlanlari: ["kaynaklar"],
743
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
657
744
  girdi: z.object({
658
745
  ders: BolognaDersGirdisi,
659
746
  programCiktilari: z
@@ -669,8 +756,16 @@ export const bolognaDenetleAraci = arac({
669
756
  .tuple([z.number().min(1), z.number().max(60)])
670
757
  .optional()
671
758
  .describe("AKTS başına saat aralığı; varsayılan [25, 30]"),
759
+ kurumBoleni: z
760
+ .number()
761
+ .int()
762
+ .min(20)
763
+ .max(40)
764
+ .optional()
765
+ .describe("Kurumun AKTS başına saat değeri; verilirse aralık yerine bu değer kullanılır (taslak denetimiyle aynı)"),
672
766
  }),
673
- calistir: ({ ders, programCiktilari, matrisSatiri, aktsSaat }, b) => {
767
+ calistir: ({ ders, programCiktilari, matrisSatiri, aktsSaat: aralik, kurumBoleni }, b) => {
768
+ const aktsSaat = kurumBoleni ? [kurumBoleni, kurumBoleni] : aralik;
674
769
  const { amac: _a, icerik: _i, yontem: _y, notlar: _n, kaynakSayisi: _k, kaynaklar: _kk, ...sayfaAlanlari } = ders;
675
770
  const sayfa = {
676
771
  ...sayfaAlanlari,
@@ -701,7 +796,15 @@ export const bolognaDenetleAraci = arac({
701
796
  tyycIsaret: null,
702
797
  dersler: [{ ...sayfa, zorunlu: ders.zorunlu, bos: false, kaynak: "kullanici-girdisi" }],
703
798
  };
704
- let bulgular = bolognaDenetle(veri, { simdi: b.simdi().getTime(), ...(aktsSaat ? { aktsSaat } : {}) });
799
+ let bulgular = bolognaDenetle(veri, { simdi: b.simdi().getTime(), ...(aktsSaat ? { aktsSaat } : {}) }).map((x) => ({
800
+ ...x,
801
+ kuralTuru: x.kural === "akts-isyuku" && kurumBoleni ? "kurum" : "Akredit önerisi",
802
+ dayanak: x.kural === "akts-isyuku"
803
+ ? kurumBoleni
804
+ ? "Kurumun verdiği AKTS başına saat değeri."
805
+ : "AKTS başına 25-30 saat yaygın uygulamadır; resmî metinle DOĞRULANMADI."
806
+ : "Akredit iç kuralı (packages/cekirdek/src/bologna/denetim.ts); resmî dayanağı DOĞRULANMADI.",
807
+ }));
705
808
  // Program düzeyi kuralları yalnız program çıktıları verildiyse anlamlıdır.
706
809
  if (!programCiktilari)
707
810
  bulgular = bulgular.filter((x) => x.konu !== "-");
@@ -734,15 +837,22 @@ const alanRehberi = arac({
734
837
  ad: "bologna_alan_rehberi",
735
838
  baslik: "Bologna alanı nasıl doldurulur",
736
839
  aciklama: "Bir Bologna alanının (amaç, içerik, çıktılar, iş yükü, haftalar...) doldurma kurallarını, sınırlarını, örnek kalıplarını ve Bloom fiillerini verir. AKTS ve T+U+L verilirse iş yükü hedefini hesaplar. Metin üretmez.",
737
- sonrakiAdim: "Bu kurallara uyan 2-3 kısa seçenek (A, B, C) yazın, bologna_taslak_denetle ile denetleyin, geçenleri hocaya gösterin. Hoca harfle seçer; 'hepsi A' toplu onaydır.",
840
+ sonrakiAdim: "Doldurulacak bütün alanların taslağını bu kurallarla tek seferde yazın ve bologna_taslak_denetle ile denetleyin. Ara adımda hocaya alan alan A/B/C sormayın; en iyi seçeneği kendiniz seçin.",
738
841
  girdi: z.object({
739
842
  alan: z.enum([...ALAN_KODLARI, "hepsi"]).describe("Alan kodu ya da 'hepsi'"),
740
843
  akts: z.number().min(0).max(60).optional(),
741
844
  tul: TUL.optional().describe("ör. '2+1+0'"),
742
845
  hafta: z.number().int().min(10).max(20).default(14).describe("Ders haftası sayısı; varsayılan 14"),
743
- bolen: z.number().int().min(20).max(40).default(30).describe("AKTS başına saat; varsayılan 30"),
846
+ bolen: z
847
+ .number()
848
+ .int()
849
+ .min(20)
850
+ .max(40)
851
+ .optional()
852
+ .describe("Kurumun AKTS başına saati; verilmezse 30 ile hesaplanır, 25-30 aralığı kabul edilir"),
744
853
  }),
745
- calistir: ({ alan, akts, tul, hafta, bolen }) => {
854
+ calistir: ({ alan, akts, tul, hafta, bolen: kurum }) => {
855
+ const bolen = kurum ?? 30;
746
856
  const secilen = alan === "hepsi" ? [...ALAN_KODLARI] : [alan];
747
857
  const saat = tulSaati(tul);
748
858
  const hesap = secilen.includes("isyuku") && akts !== undefined
@@ -752,6 +862,7 @@ const alanRehberi = arac({
752
862
  return {
753
863
  hedefSaat: hedef,
754
864
  formul: `${akts} AKTS x ${bolen} = ${hedef} saat`,
865
+ kabulAraligi: kurum ? [hedef, hedef] : [akts * 25, akts * 30],
755
866
  dersSuresiSatiri: saat === null ? null : { sayi: hafta, sure: saat, toplam: dersSuresi },
756
867
  kalanSaat: dersSuresi === null ? null : hedef - dersSuresi,
757
868
  not: dersSuresi === null
@@ -787,10 +898,10 @@ const alanRehberi = arac({
787
898
  const taslakDenetleAraci = arac({
788
899
  ad: "bologna_taslak_denetle",
789
900
  baslik: "Bologna taslağını denetle",
790
- aciklama: "Asistanın yazdığı 1-3 seçeneği (A, B, C) Akredit kurallarıyla denetler. Puan vermez: her seçenek için geçer ya da eksik nedenlerini döndürür. İş yükü toplamını, AKTS başına saati ve yüzde toplamlarını kendisi hesaplar.",
791
- sonrakiAdim: "Geçen seçenekleri hocaya kısa gösterin (A, B, C; ilki önerilen). Geçmeyeni düzeltip yeniden denetleyin, hocaya göstermeyin. Seçim bitince bologna_kopya_metni ile OBS metnini verin.",
901
+ aciklama: "Asistanın yazdığı 1-3 seçeneği (A, B, C) Akredit kurallarıyla BİÇİM yönünden denetler (sayılar, uzunluklar, fiil, toplamlar, kaynak doğrulaması). İçeriğin doğruluğunu bilemez: içerik teyidi hocadadır. Her seçenek için biçim sonucu ve eksik nedenlerini döndürür; her kuralın türü (resmî, kurum, Akredit önerisi) ve dayanağı vardır. İş yükü toplamını ve yüzde toplamlarını kendisi hesaplar.",
902
+ sonrakiAdim: "Geçmeyen alanı düzeltip yeniden denetleyin; hocaya göstermeyin. Hepsi geçince bologna_durum ve bologna_kopya_metni'ni çağırıp bütün alanları TEK özet tablosunda ('öneri' etiketiyle) gösterin; alan alan soru sormayın.",
792
903
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
793
- kunyeAlanlari: ["kaynaklar"],
904
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
794
905
  girdi: z.object({
795
906
  ders: DersTemeli,
796
907
  secenekler: z
@@ -798,7 +909,10 @@ const taslakDenetleAraci = arac({
798
909
  .min(1)
799
910
  .max(3),
800
911
  programCiktilari: z.array(z.string().max(10)).max(40).optional().describe("Programın P kodları: P1, P2, ..."),
801
- ingilizce: z.boolean().default(true).describe("İngilizce karşılıklar da denetlensin mi"),
912
+ ingilizce: z
913
+ .boolean()
914
+ .default(false)
915
+ .describe("İngilizce karşılıklar da denetlensin mi. bologna_ayristir'in ingilizce değerini verin (sayfada İngilizce alan varsa true)."),
802
916
  }),
803
917
  calistir: ({ ders, secenekler, programCiktilari, ingilizce }) => {
804
918
  const sonuclar = secenekler.map((s) => {
@@ -806,10 +920,17 @@ const taslakDenetleAraci = arac({
806
920
  const hepsi = [...b.entries()].flatMap(([alan, xs]) => xs.map((x) => ({ alan, ...x })));
807
921
  const eksikler = hepsi
808
922
  .filter((x) => x.duzey === "hata")
809
- .map((x) => ({ alan: x.alan, kural: x.kural, neden: x.aciklama }));
923
+ .map((x) => ({
924
+ alan: x.alan,
925
+ kural: x.kural,
926
+ neden: x.aciklama,
927
+ kuralTuru: x.kuralTuru ?? "Akredit önerisi",
928
+ dayanak: x.dayanak ?? "",
929
+ }));
810
930
  return {
811
931
  etiket: s.etiket,
812
- gecer: eksikler.length === 0,
932
+ bicimDenetimi: eksikler.length === 0 ? "geçti" : "geçmedi",
933
+ icerikTeyidi: "hoca",
813
934
  eksikler,
814
935
  uyarilar: hepsi
815
936
  .filter((x) => x.duzey !== "hata")
@@ -819,13 +940,13 @@ const taslakDenetleAraci = arac({
819
940
  });
820
941
  return {
821
942
  sonuc: {
822
- gecenler: sonuclar.filter((x) => x.gecer).map((x) => x.etiket),
943
+ bicimdenGecenler: sonuclar.filter((x) => x.bicimDenetimi === "geçti").map((x) => x.etiket),
823
944
  secenekler: sonuclar,
824
- not: "Geçer, Akredit kurallarına uyuyor demektir; içeriğin doğruluğuna ve derse uygunluğuna hoca karar verir.",
945
+ 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.",
825
946
  resmiDayanak: RESMI_DAYANAK,
826
947
  },
827
948
  kaynak: [KAYNAK_KURALLAR],
828
- ...(sonuclar.some((x) => x.gecer)
949
+ ...(sonuclar.some((x) => x.bicimDenetimi === "geçti")
829
950
  ? {}
830
951
  : {
831
952
  sonrakiAdim: "Hiçbir seçenek geçmedi. Eksik nedenlerine göre düzeltip yeniden denetleyin; hocaya henüz göstermeyin.",
@@ -899,10 +1020,13 @@ export function ayristirOnAyikla(g) {
899
1020
  const { html: _h, metin: _m, ...kalan } = o;
900
1021
  const satir = bolognaSatirAyikla(giris.metin);
901
1022
  const ek = ileti ? { ileti } : {};
1023
+ // Biçim ve dil ipucu ayıklamadan ÖNCE okunur: genel ayıklama etiketleri sadeleştirir.
1024
+ const sayfaIpucu = { prolizMetni: prolizMetniMi(giris.metin), ingilizce: ingilizceAlanVar(giris.metin) };
902
1025
  if (prolizMi(giris.metin))
903
1026
  return {
904
1027
  girdi: {
905
1028
  ...kalan,
1029
+ sayfaIpucu,
906
1030
  html: satir.html,
907
1031
  kaynaklar: [...onceki, ...satir.kaynaklar],
908
1032
  yetkililer: [...oncekiY, ...satir.yetkililer],
@@ -915,6 +1039,7 @@ export function ayristirOnAyikla(g) {
915
1039
  return {
916
1040
  girdi: {
917
1041
  ...kalan,
1042
+ sayfaIpucu,
918
1043
  metin: r.metin,
919
1044
  kaynaklar: [...onceki, ...satir.kaynaklar, ...r.kaynaklar],
920
1045
  yetkililer: [...oncekiY, ...satir.yetkililer, ...r.yetkililer],
@@ -927,9 +1052,9 @@ const ayristirAraci = arac({
927
1052
  ad: "bologna_ayristir",
928
1053
  baslik: "Bologna sayfasını ayrıştır",
929
1054
  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.",
930
- sonrakiAdim: "Çıkan ders verisini bologna_denetle'ye verin (girdi hazır: bolognaDenetleGirdisi). Bulunamayan alanları hocaya tek soruyla sorun ya da OBS düzenleme ekranından okuyun.",
1055
+ sonrakiAdim: "Çıkan 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.",
931
1056
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
932
- kunyeAlanlari: ["kaynaklar"],
1057
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
933
1058
  onAyikla: ayristirOnAyikla,
934
1059
  girdi: z.object({
935
1060
  html: z
@@ -942,8 +1067,12 @@ const ayristirAraci = arac({
942
1067
  kaynaklar: z.array(z.string().max(600)).max(30).optional().describe("Ayrıca verilen künyeler"),
943
1068
  yetkililer: z.array(YetkiliSemasi).max(20).optional().describe("Sayfadaki görev satırları, olduğu gibi"),
944
1069
  tur: z.enum(["otomatik", "ders", "program_ciktilari"]).default("otomatik"),
1070
+ sayfaIpucu: z
1071
+ .object({ prolizMetni: z.boolean(), ingilizce: z.boolean() })
1072
+ .optional()
1073
+ .describe("Sunucu doldurur; göndermeyin"),
945
1074
  }),
946
- calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur }) => {
1075
+ calistir: ({ html, metin, kaynaklar = [], yetkililer = [], tur, sayfaIpucu }) => {
947
1076
  if (!html && !metin)
948
1077
  throw new AracHatasi("Sayfanın HTML'ini ya da metnini yapıştırın.");
949
1078
  const kaynakAlani = {
@@ -997,6 +1126,7 @@ const ayristirAraci = arac({
997
1126
  tur: "ders",
998
1127
  bicim: "proliz",
999
1128
  guven: "yüksek",
1129
+ ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(html),
1000
1130
  bolognaDenetleGirdisi: { ders },
1001
1131
  bulunamayan: bulunamayan.map((k) => GENEL_ALAN_ADLARI[k] ?? k),
1002
1132
  okunamayan: bulunamayan.length
@@ -1008,7 +1138,8 @@ const ayristirAraci = arac({
1008
1138
  kaynak: [KAYNAK_AYRISTIRICI],
1009
1139
  };
1010
1140
  }
1011
- const g = genelAyristir(html ?? metin ?? "", kaynaklar.length);
1141
+ const ham = html ?? metin ?? "";
1142
+ const g = genelAyristir(ham, kaynaklar.length);
1012
1143
  if (g.bulunan.length < 2)
1013
1144
  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.");
1014
1145
  const { haftaKonulari, ...dersAlanlari } = g.ders;
@@ -1020,8 +1151,9 @@ const ayristirAraci = arac({
1020
1151
  return {
1021
1152
  sonuc: {
1022
1153
  tur: "ders",
1023
- bicim: "genel",
1154
+ bicim: (sayfaIpucu?.prolizMetni ?? prolizMetniMi(ham)) ? "proliz_duz_metin" : "genel",
1024
1155
  guven: g.guven,
1156
+ ingilizce: sayfaIpucu?.ingilizce ?? ingilizceAlanVar(ham),
1025
1157
  bolognaDenetleGirdisi: { ders },
1026
1158
  bilgiler: g.bilgiler,
1027
1159
  haftaKonulari,
@@ -1054,7 +1186,7 @@ const durumAraci = arac({
1054
1186
  aciklama: "Hocanın seçtiği alanların bütününe bakar: hangi zorunlu alan tamam, hangisi eksik ya da kurala uymuyor, kaç alan kaldı. Eksik kalmadıysa 'tamamlandı' der. Her seçimden sonra ve OBS'ye yazdıktan sonra çağrılır.",
1055
1187
  sonrakiAdim: "Kalan alan sayısını hocaya tek cümleyle söyleyin ve sıradaki alana geçin. Kalan yoksa OBS'ye yazma adımına geçin.",
1056
1188
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1057
- kunyeAlanlari: ["kaynaklar"],
1189
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1058
1190
  girdi: z.object({
1059
1191
  ders: DersTemeli,
1060
1192
  alanlar: TaslakAlanlari,
@@ -1063,7 +1195,7 @@ const durumAraci = arac({
1063
1195
  .max(40)
1064
1196
  .optional()
1065
1197
  .describe("Verilirse katkı tablosu da zorunlu alan sayılır"),
1066
- ingilizce: z.boolean().default(true),
1198
+ ingilizce: z.boolean().default(false).describe("bologna_ayristir'in ingilizce değeri"),
1067
1199
  }),
1068
1200
  calistir: ({ ders, alanlar, programCiktilari, ingilizce }) => {
1069
1201
  const zorunlu = [...ZORUNLU_ALANLAR, ...(programCiktilari?.length ? ["katki"] : [])];
@@ -1079,12 +1211,19 @@ const durumAraci = arac({
1079
1211
  const genel = (b.get("genel") ?? []).filter((x) => x.duzey === "hata").map((x) => x.aciklama);
1080
1212
  const kalan = satirlar.filter((s) => s.durum !== "tamam");
1081
1213
  const tamamlandi = kalan.length === 0 && genel.length === 0;
1214
+ const teyitBekleyen = [
1215
+ ...(alanlar.degerlendirme && alanlar.degerlendirmeDayanagi !== "sayfa" && alanlar.degerlendirmeDayanagi !== "hoca"
1216
+ ? ["Değerlendirme yöntem ve yüzdeleri"]
1217
+ : []),
1218
+ ];
1082
1219
  return {
1083
1220
  sonuc: {
1084
- tamamlandi,
1221
+ bicimDenetimi: tamamlandi ? "geçti" : "geçmedi",
1222
+ icerikTeyidi: "hoca",
1085
1223
  kalanAlanSayisi: kalan.length,
1224
+ teyitBekleyen,
1086
1225
  ozet: tamamlandi
1087
- ? "Tamamlandı: zorunlu alanların hepsi dolu ve kurallara uyuyor."
1226
+ ? `Biçim denetiminden geçti; içerik teyidi: hoca. Zorunlu alanların hepsi dolu ve biçim kurallarına uyuyor.${teyitBekleyen.length ? ` Teyit bekleyen: ${teyitBekleyen.join(", ")}.` : ""}`
1088
1227
  : `${kalan.length} alan kaldı: ${kalan.map((s) => s.ad).join(", ")}.`,
1089
1228
  alanlar: satirlar,
1090
1229
  genelEksikler: genel,
@@ -1092,113 +1231,295 @@ const durumAraci = arac({
1092
1231
  (ad === "Sürdürülebilir Kalkınma Amaçları" && alanlar.ska) ||
1093
1232
  (ad === "Ön koşul" && alanlar.onKosul !== undefined))),
1094
1233
  hesap: bolognaHesabi(ders, alanlar),
1095
- hatirlatma: "Dersin Yetkilileri alanını hoca OBS'de kendisi doldurur. OBS'de Kaydet düğmesine hoca basar.",
1234
+ hatirlatma: "Dersin Yetkilileri alanına dokunulmaz. OBS'ye yazmadan önce alan alan önceki ve yeni değer özeti gösterilir; hoca bir kez 'kaydet' der. Kayıttan sonra sayfa geri okunur ve obs_karsilastir ile doğrulanır.",
1096
1235
  },
1097
1236
  kaynak: [KAYNAK_KURALLAR],
1098
1237
  ...(tamamlandi
1099
1238
  ? {
1100
- sonrakiAdim: "Hocaya sorun: 1) tarayıcıyla OBS formunu doldurayım, Kaydet'e siz basın (önerilen, tarayıcı aracınız varsa) 2) bologna_kopya_metni ile alan alan kopyalayın 3) bitir.",
1239
+ sonrakiAdim: "Hocaya ara özet gösterip 'devam' beklemeyin; hemen bologna_kopya_metni ile yazma paketini alın (sayfadaki önceki değerleri oncekiDegerler olarak verin). Tek mesajda önceki → yeni tablosunu gösterip sorun: 1) OBS'ye yaz ve kaydet (bu seçim 'kaydet' onayıdır) 2) formu doldur, Kaydet'e ben basayım 3) alan alan kopya metni ver. Değiştirmek istenen alan varsa numarası yazılır.",
1101
1240
  }
1102
1241
  : {}),
1103
1242
  };
1104
1243
  },
1105
1244
  });
1106
1245
  // ---------------------------------------------------------------- is_yuku_oner
1107
- /** İş yükü dağılım biçimleri: kalan saatin (hedef eksi ders süresi) payları. Akredit'in örnek oranlarıdır. */
1246
+ /**
1247
+ * Değerlendirme kalemi → iş yükü satırı. Yalnız sayfadaki (ya da hocanın verdiği) değerlendirme kalemlerinden
1248
+ * satır kurulur; derste olmayan etkinlik (ör. ödev yoksa ödev satırı) eklenmez (dış inceleme 2026-11-05).
1249
+ */
1250
+ const ETKINLIK_ESLEME = [
1251
+ [/kısa ?sınav|quiz/iu, "Kısa Sınav Hazırlık"],
1252
+ [/ara ?sınav|vize|midterm/iu, "Ara Sınav Hazırlık"],
1253
+ [/yarıyıl sonu|final|dönem sonu|genel sınav/iu, "Yarıyıl Sonu Sınavı Hazırlık"],
1254
+ [/ödev|homework|assignment/iu, "Ödev"],
1255
+ [/proje|project/iu, "Proje"],
1256
+ [/laboratuvar|\blab\b|uygulama|practice/iu, "Uygulama / Laboratuvar Hazırlık"],
1257
+ [/sunum|seminer|presentation/iu, "Sunum Hazırlık"],
1258
+ [/rapor|report/iu, "Rapor Hazırlık"],
1259
+ ];
1260
+ /** Değerlendirme kalemlerini iş yükü etkinliklerine çevirir. Eşleşmeyen kalem satır üretmez. */
1261
+ export function etkinliklereCevir(deg) {
1262
+ const harita = new Map();
1263
+ for (const k of deg) {
1264
+ const es = ETKINLIK_ESLEME.find(([re]) => re.test(k.ad));
1265
+ if (!es)
1266
+ continue;
1267
+ const sayi = Math.max(1, Math.round(k.sayi ?? 1));
1268
+ const agirlik = k.katki && k.katki > 0 ? k.katki : 1;
1269
+ const onceki = harita.get(es[1]);
1270
+ if (onceki) {
1271
+ onceki.sayi += sayi;
1272
+ onceki.agirlik += agirlik;
1273
+ onceki.kaynak += `, ${k.ad}`;
1274
+ }
1275
+ else
1276
+ harita.set(es[1], { ad: es[1], sayi, agirlik, kaynak: `Değerlendirme kalemi: ${k.ad}` });
1277
+ }
1278
+ return [...harita.values()];
1279
+ }
1108
1280
  const IS_YUKU_BICIMLERI = [
1109
1281
  {
1110
1282
  ad: "Dengeli",
1111
- aciklama: "Haftalık çalışma ağırlıklı, sınav hazırlığı dengeli.",
1112
- paylar: [
1113
- ["Sınıf Dışı Ders Çalışma Süresi", "haftalik", 0.6],
1114
- ["Ara Sınav Hazırlık", "tek", 0.15],
1115
- ["Yarıyıl Sonu Sınavı Hazırlık", "tek", 0.25],
1116
- ],
1283
+ aciklama: "Kalan saatin %60'ı haftalık çalışmaya, kalanı değerlendirme etkinliklerine.",
1284
+ haftalikPay: 0.6,
1117
1285
  },
1118
1286
  {
1119
- ad: "Ödev ve proje ağırlıklı",
1120
- aciklama: "Haftalık çalışmanın bir kısmı ödev ya da projeye ayrılır.",
1121
- paylar: [
1122
- ["Sınıf Dışı Ders Çalışma Süresi", "haftalik", 0.45],
1123
- ["Ödev / Proje", "tek", 0.25],
1124
- ["Ara Sınav Hazırlık", "tek", 0.1],
1125
- ["Yarıyıl Sonu Sınavı Hazırlık", "tek", 0.2],
1126
- ],
1287
+ ad: "Etkinlik ağırlıklı",
1288
+ aciklama: "Kalan saatin %45'i haftalık çalışmaya, kalanı değerlendirme etkinliklerine.",
1289
+ haftalikPay: 0.45,
1127
1290
  },
1128
1291
  {
1129
- ad: "Sınav ağırlıklı",
1130
- aciklama: "Sınav hazırlığına daha çok saat ayrılır.",
1131
- paylar: [
1132
- ["Sınıf Dışı Ders Çalışma Süresi", "haftalik", 0.5],
1133
- ["Ara Sınav Hazırlık", "tek", 0.2],
1134
- ["Yarıyıl Sonu Sınavı Hazırlık", "tek", 0.3],
1135
- ],
1292
+ ad: "Haftalık çalışma ağırlıklı",
1293
+ aciklama: "Kalan saatin %75'i haftalık çalışmaya, kalanı değerlendirme etkinliklerine.",
1294
+ haftalikPay: 0.75,
1136
1295
  },
1137
1296
  ];
1138
- /** Hedefe tam eşit iş yükü dağılımları (deterministik). Son tek satır artığı alır, böylece toplam tam tutar. */
1139
- export function isYukuDagilimlari(akts, tul, hafta = 14, bolen = 30) {
1297
+ /**
1298
+ * İş yükü dağılım önerileri (deterministik). Satırlar yalnız ders süresi, haftalık çalışma ve verilen değerlendirme
1299
+ * kalemlerinden kurulur. Kurum değeri (bolen) verildiyse toplam tam AKTS × bolen olmaya çalışılır; verilmediyse
1300
+ * AKTS × 30 hedeflenir ve AKTS başına 25-30 saat aralığı kabul edilir.
1301
+ */
1302
+ export function isYukuDagilimlari(akts, tul, hafta = 14, bolen, degerlendirme) {
1140
1303
  const saat = tulSaati(tul);
1141
1304
  if (saat === null)
1142
1305
  throw new AracHatasi("T+U+L '2+1+0' biçiminde olmalı.");
1143
- const hedef = Math.round(akts * bolen);
1306
+ const hedef = Math.round(akts * (bolen ?? 30));
1307
+ const aralik = bolen ? [hedef, hedef] : [Math.round(akts * 25), hedef];
1144
1308
  const dersSuresi = hafta * saat;
1145
1309
  const kalan = hedef - dersSuresi;
1146
1310
  if (kalan < 0)
1147
- throw new AracHatasi(`Ders süresi (${hafta} x ${saat} = ${dersSuresi} saat) hedefi (${akts} AKTS x ${bolen} = ${hedef}) aşıyor. AKTS ya da T+U+L bilgisini bölüm başkanlığıyla denetleyin; AKTS'yi siz değiştirmeyin.`);
1148
- return IS_YUKU_BICIMLERI.map((b, i) => {
1149
- const satirlar = [{ ad: "Ders Süresi", sayi: hafta, sure: saat }];
1150
- let dagitilan = 0;
1151
- b.paylar.forEach(([ad, tur, pay], j) => {
1152
- const son = j === b.paylar.length - 1;
1153
- if (tur === "haftalik") {
1154
- const s = Math.floor((kalan * pay) / hafta);
1155
- if (s > 0)
1156
- satirlar.push({ ad, sayi: hafta, sure: s });
1157
- dagitilan += s * hafta;
1158
- }
1159
- else {
1160
- const s = son ? kalan - dagitilan : Math.round(kalan * pay);
1161
- if (s > 0)
1162
- satirlar.push({ ad, sayi: 1, sure: s });
1163
- dagitilan += s;
1164
- }
1311
+ throw new AracHatasi(`Ders süresi (${hafta} x ${saat} = ${dersSuresi} saat) hedefi (${hedef} saat) aşıyor. AKTS ya da T+U+L bilgisini bölüm başkanlığıyla denetleyin; AKTS'yi siz değiştirmeyin.`);
1312
+ const etkinlikler = etkinliklereCevir(degerlendirme ?? []);
1313
+ const bicimler = etkinlikler.length
1314
+ ? IS_YUKU_BICIMLERI
1315
+ : [
1316
+ {
1317
+ ad: "Yalnız haftalık çalışma",
1318
+ aciklama: "Değerlendirme kalemi verilmediği için etkinlik satırı eklenmedi.",
1319
+ haftalikPay: 1,
1320
+ },
1321
+ ];
1322
+ return bicimler.map((b, i) => {
1323
+ const satirlar = [
1324
+ { ad: "Ders Süresi", sayi: hafta, sure: saat, kaynak: "sayfa (T+U+L)", varsayim: `${hafta} hafta x T+U+L` },
1325
+ ];
1326
+ const haftalik = Math.floor((kalan * b.haftalikPay) / hafta);
1327
+ if (haftalik > 0)
1328
+ satirlar.push({
1329
+ ad: "Sınıf Dışı Ders Çalışma Süresi",
1330
+ sayi: hafta,
1331
+ sure: haftalik,
1332
+ kaynak: "Akredit önerisi",
1333
+ varsayim: `Kalan saatin yaklaşık %${Math.round(b.haftalikPay * 100)}'i haftalık çalışmaya`,
1334
+ });
1335
+ let dagitilan = haftalik * hafta;
1336
+ const toplamAgirlik = etkinlikler.reduce((t, e) => t + e.agirlik, 0);
1337
+ const etkinlikSaati = kalan - dagitilan;
1338
+ etkinlikler.forEach((e, j) => {
1339
+ const son = j === etkinlikler.length - 1;
1340
+ const pay = son ? kalan - dagitilan : Math.round((etkinlikSaati * e.agirlik) / toplamAgirlik);
1341
+ const sure = Math.floor(pay / e.sayi);
1342
+ if (sure <= 0)
1343
+ return;
1344
+ satirlar.push({
1345
+ ad: e.ad,
1346
+ sayi: e.sayi,
1347
+ sure,
1348
+ kaynak: e.kaynak,
1349
+ varsayim: "Saat, değerlendirmedeki katkı yüzdesiyle orantılı (Akredit önerisi)",
1350
+ });
1351
+ dagitilan += sure * e.sayi;
1165
1352
  });
1166
1353
  const toplam = satirlar.reduce((t, x) => t + x.sayi * x.sure, 0);
1167
1354
  return {
1168
1355
  secenek: i + 1,
1169
1356
  ad: b.ad,
1170
1357
  aciklama: b.aciklama,
1358
+ etiket: "öneri",
1171
1359
  isyuku: satirlar,
1172
1360
  toplam,
1173
1361
  hedef,
1362
+ kabulAraligi: aralik,
1174
1363
  tamEsit: toplam === hedef,
1364
+ aralikta: toplam >= aralik[0] && toplam <= aralik[1],
1175
1365
  };
1176
1366
  });
1177
1367
  }
1178
1368
  const isYukuOnerAraci = arac({
1179
1369
  ad: "is_yuku_oner",
1180
1370
  baslik: "AKTS iş yükü önerisi",
1181
- aciklama: "AKTS ve T+U+L'den, toplamı hedefe (AKTS x 30) tam eşit 3 dengeli iş yükü dağılımı önerir: dengeli, ödev ve proje ağırlıklı, sınav ağırlıklı. Hesap deterministiktir; AKTS'yi değiştirmez.",
1182
- sonrakiAdim: "Üç dağılımı hocaya 1, 2, 3 diye kısa gösterin (1 önerilen). Seçileni bologna_taslak_denetle ile denetleyin ve isyuku alanına koyun.",
1371
+ aciklama: "AKTS, T+U+L ve sayfadaki değerlendirme kalemlerinden iş yükü dağılımı ÖNERİR. Yalnız derste olan etkinliklere satır açar (ödev yoksa ödev satırı yok); her satırda kaynak ve varsayım yazar. Sayfadan okunan iş yükü verilirse ayrı gösterir, öneriyle karıştırmaz. Hesap deterministiktir; AKTS'yi değiştirmez.",
1372
+ sonrakiAdim: "Sayfadaki iş yükü AKTS hedef aralığındaysa onu koruyun. Değilse ilk öneriyi ('öneri' etiketiyle) doğrudan taslağa alıp bologna_taslak_denetle ile denetleyin; hocaya ayrı mesajda iş yükü seçeneği sormayın, son özette varsayımıyla gösterin. Değerlendirme kalemleri bilinmiyorsa uydurmayın.",
1373
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1183
1374
  girdi: z.object({
1184
1375
  akts: z.number().min(1).max(60),
1185
1376
  tul: TUL.describe("ör. '2+1+0'"),
1186
1377
  hafta: z.number().int().min(10).max(20).default(14),
1187
- bolen: z.number().int().min(20).max(40).default(30).describe("AKTS başına saat; varsayılan 30"),
1378
+ bolen: z
1379
+ .number()
1380
+ .int()
1381
+ .min(20)
1382
+ .max(40)
1383
+ .optional()
1384
+ .describe("Kurumun AKTS başına saati; verilmezse AKTS x 30 hedeflenir, 25-30 aralığı kabul edilir"),
1385
+ degerlendirme: z
1386
+ .array(z.object({
1387
+ ad: z.string().max(200),
1388
+ sayi: z.number().min(0).max(50).nullable().optional(),
1389
+ katki: z.number().min(0).max(100).nullable().optional(),
1390
+ }))
1391
+ .max(20)
1392
+ .optional()
1393
+ .describe("Sayfadaki (ya da hocanın söylediği) değerlendirme kalemleri; yalnız bunlara etkinlik satırı açılır"),
1394
+ sayfaIsYuku: z
1395
+ .array(z.object({ ad: z.string().max(200), sayi: z.number().min(0).max(100), sure: z.number().min(0).max(200) }))
1396
+ .max(30)
1397
+ .optional()
1398
+ .describe("Sayfadan okunan mevcut iş yükü satırları; öneriden ayrı gösterilir"),
1188
1399
  }),
1189
- calistir: ({ akts, tul, hafta, bolen }) => {
1190
- const secenekler = isYukuDagilimlari(akts, tul, hafta, bolen);
1400
+ calistir: ({ akts, tul, hafta, bolen, degerlendirme, sayfaIsYuku }) => {
1401
+ const secenekler = isYukuDagilimlari(akts, tul, hafta, bolen, degerlendirme);
1402
+ const sayfa = sayfaIsYuku?.length
1403
+ ? {
1404
+ satirlar: sayfaIsYuku.map((x) => ({ ...x, toplam: yuvarla(x.sayi * x.sure, 2), kaynak: "sayfa" })),
1405
+ toplam: yuvarla(sayfaIsYuku.reduce((t, x) => t + x.sayi * x.sure, 0), 2),
1406
+ }
1407
+ : null;
1191
1408
  return {
1192
1409
  sonuc: {
1193
- hedef: `${akts} AKTS x ${bolen} = ${Math.round(akts * bolen)} saat`,
1410
+ hedef: bolen
1411
+ ? `${akts} AKTS x ${bolen} (kurum değeri) = ${Math.round(akts * bolen)} saat`
1412
+ : `${akts} AKTS için ${Math.round(akts * 25)}-${Math.round(akts * 30)} saat (AKTS başına 25-30); öneriler ${Math.round(akts * 30)} saati hedefler`,
1413
+ sayfadanOkunan: sayfa,
1194
1414
  secenekler,
1195
- not: "Oranlar Akredit'in örnek oranlarıdır. Dersinizde gerçekten olmayan bir kalem (ör. proje) varsa o seçeneği seçmeyin.",
1415
+ ...(degerlendirme?.length
1416
+ ? {}
1417
+ : {
1418
+ eksik: "Değerlendirme kalemleri verilmedi: sınav hazırlığı, ödev ya da proje satırı eklenmedi. Sayfadan okuyun ya da hocaya sorun, sonra aracı yeniden çağırın.",
1419
+ }),
1420
+ not: "Önerilen dağılım ile sayfadan okunan değer ayrıdır. Oranlar Akredit önerisidir; hoca teyit eder.",
1196
1421
  resmiDayanak: RESMI_DAYANAK,
1197
1422
  },
1198
1423
  kaynak: [KAYNAK_KURALLAR],
1199
1424
  };
1200
1425
  },
1201
1426
  });
1427
+ // ---------------------------------------------------------------- obs_karsilastir
1428
+ const sadeMetin = (s) => s
1429
+ .replace(/<(script|style)\b[\s\S]*?<\/\1>/giu, " ")
1430
+ .replace(/<[^>]*>/gu, " ")
1431
+ .replace(/&nbsp;/gu, " ")
1432
+ .toLocaleLowerCase("tr")
1433
+ .replace(/[\s ]+/gu, " ")
1434
+ .trim();
1435
+ /** Beklenen değeri satır satır karşılaştırılabilir parçalara böler (sekme boşluk sayılır; tablo satırı tek parça). */
1436
+ const parcalar = (deger) => deger
1437
+ .split(/\n+/u)
1438
+ .map(sadeMetin)
1439
+ .filter((x) => x.length >= 3);
1440
+ /**
1441
+ * OBS'ye yazılan alanları geri okunan sayfa metniyle karşılaştırır. Ders kodu ya da dönem farklıysa durur:
1442
+ * yanlış derse yazılmış olabilir.
1443
+ */
1444
+ export function obsKarsilastir(g) {
1445
+ const sayfa = sadeMetin(g.geriOkunan.metin);
1446
+ const nedenler = [];
1447
+ const kod = sadeMetin(g.dersKodu);
1448
+ if (g.geriOkunan.dersKodu !== undefined && sadeMetin(g.geriOkunan.dersKodu) !== kod)
1449
+ nedenler.push(`Geri okunan sayfanın ders kodu (${g.geriOkunan.dersKodu}) beklenenden (${g.dersKodu}) farklı.`);
1450
+ else if (!sayfa.includes(kod))
1451
+ nedenler.push(`Geri okunan sayfada ders kodu (${g.dersKodu}) görünmüyor.`);
1452
+ if (g.donem) {
1453
+ const d = sadeMetin(g.donem);
1454
+ if (g.geriOkunan.donem !== undefined && sadeMetin(g.geriOkunan.donem) !== d)
1455
+ nedenler.push(`Geri okunan sayfanın dönemi (${g.geriOkunan.donem}) beklenenden (${g.donem}) farklı.`);
1456
+ else if (g.geriOkunan.donem === undefined && !sayfa.includes(d))
1457
+ nedenler.push(`Geri okunan sayfada dönem (${g.donem}) görünmüyor.`);
1458
+ }
1459
+ const alanlar = g.beklenen.map((b) => {
1460
+ const p = parcalar(b.deger);
1461
+ const eksik = p.filter((x) => !sayfa.includes(x));
1462
+ return {
1463
+ alan: b.alan,
1464
+ durum: p.length === 0 ? "bos" : eksik.length ? "farkli" : "eslesti",
1465
+ eksikParcalar: eksik.slice(0, 3).map((x) => (x.length > 120 ? `${x.slice(0, 117)}...` : x)),
1466
+ eksikParcaSayisi: eksik.length,
1467
+ };
1468
+ });
1469
+ return {
1470
+ dur: nedenler.length > 0,
1471
+ nedenler,
1472
+ eslesen: alanlar.filter((a) => a.durum === "eslesti").length,
1473
+ farkli: alanlar.filter((a) => a.durum === "farkli").length,
1474
+ alanlar,
1475
+ };
1476
+ }
1477
+ const obsKarsilastirAraci = arac({
1478
+ ad: "obs_karsilastir",
1479
+ baslik: "OBS'ye yazılanı geri okuyup doğrula",
1480
+ aciklama: "OBS'ye kaydettikten sonra sayfa geri okunur; bu araç beklenen alanları (bologna_kopya_metni'nin obsKarsilastirGirdisi) geri okunan sayfa metniyle karşılaştırır ve farkları döndürür. Ders kodu ya da dönem değişmişse dur der. OBS'ye yazmaz.",
1481
+ sonrakiAdim: "dur true ise yazmayı hemen bırakın ve hocaya nedenini söyleyin. Farklı alan varsa hocaya alan alan gösterin; düzeltmeyi hoca onaylarsa yapın ve yeniden karşılaştırın. Hepsi eşleştiyse 'Kaydedildi ve geri okunarak doğrulandı' deyin.",
1482
+ kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1483
+ kunyeAlanlari: ["beklenen"],
1484
+ onAyikla: (g) => {
1485
+ const o = g;
1486
+ const m = o?.geriOkunan?.metin;
1487
+ if (typeof m !== "string")
1488
+ return { girdi: g, atilan: [] };
1489
+ const giris = girisBilgisiAyikla(m);
1490
+ // Görev satırları (Dersi Verenler, Koordinatör) karşılaştırılmaz; kişi verisi taşınmasın diye atılır.
1491
+ const satir = bolognaSatirAyikla(giris.metin);
1492
+ return {
1493
+ girdi: { ...g, geriOkunan: { ...o?.geriOkunan, metin: satir.html } },
1494
+ atilan: giris.atildi ? ["oturum ve parola alanları"] : [],
1495
+ };
1496
+ },
1497
+ girdi: z.object({
1498
+ dersKodu: z.string().trim().min(1).max(30),
1499
+ donem: z.string().trim().max(40).optional(),
1500
+ beklenen: z
1501
+ .array(z.object({ alan: z.string().max(120), deger: z.string().max(20_000) }))
1502
+ .min(1)
1503
+ .max(40),
1504
+ geriOkunan: z.object({
1505
+ metin: z.string().min(20).max(400_000).describe("Kayıttan sonra sayfadan okunan metin ya da HTML"),
1506
+ dersKodu: z.string().trim().max(30).optional().describe("Sayfanın başlığından okunan ders kodu"),
1507
+ donem: z.string().trim().max(40).optional().describe("Sayfadan okunan dönem"),
1508
+ }),
1509
+ }),
1510
+ calistir: (g) => {
1511
+ const sonuc = obsKarsilastir(g);
1512
+ return {
1513
+ sonuc,
1514
+ kaynak: [KOD_KAYNAGI("apps/acik-mcp/src/bologna.ts", "Akredit OBS geri okuma karşılaştırması")],
1515
+ ...(sonuc.dur
1516
+ ? {
1517
+ sonrakiAdim: "Yazmayı durdurun: ders kodu ya da dönem beklenenden farklı. Hocaya nedenini söyleyin, başka alana yazmayın.",
1518
+ }
1519
+ : {}),
1520
+ };
1521
+ },
1522
+ });
1202
1523
  // ---------------------------------------------------------------- bologna_kopya_metni
1203
1524
  const SEKME_SIRASI = [
1204
1525
  "Ders Bilgileri",
@@ -1214,14 +1535,41 @@ const kopyaMetniAraci = arac({
1214
1535
  aciklama: "Hocanın seçtiği alanları OBS sekmesi ve alan adıyla sıralı, alan alan kopyalanacak düz metin olarak verir. Tablolar sekmeyle ayrılır. Toplamları kendisi hesaplar ve kalan eksikleri yazar. OBS'ye yazmaz; kaydı hoca yapar.",
1215
1536
  sonrakiAdim: "Metni olduğu gibi, kod bloğu içinde gösterin. Hocaya sorun: 1) OBS'ye yapıştırdım, yeniden denetle 2) bir alanı değiştir 3) bitir.",
1216
1537
  kisiselVeriKapsami: BOLOGNA_KAPSAMI,
1217
- kunyeAlanlari: ["kaynaklar"],
1538
+ kunyeAlanlari: BOLOGNA_KUNYE_ALANLARI,
1218
1539
  girdi: z.object({
1219
1540
  ders: DersTemeli,
1220
1541
  alanlar: TaslakAlanlari,
1221
1542
  programCiktilari: z.array(z.string().max(10)).max(40).optional(),
1222
- ingilizce: z.boolean().default(true),
1543
+ ingilizce: z.boolean().default(false).describe("bologna_ayristir'in ingilizce değeri"),
1544
+ oncekiDegerler: z
1545
+ .record(z.string().max(120), z.string().max(20_000))
1546
+ .optional()
1547
+ .describe("OBS'deki şimdiki değerler (alan adı, ör. 'Dersin Amacı (Türkçe)' → değer); önceki/yeni özeti için"),
1223
1548
  }),
1224
- calistir: ({ ders, alanlar, programCiktilari, ingilizce }) => {
1549
+ calistir: ({ ders, alanlar: gelen, programCiktilari, ingilizce, oncekiDegerler }) => {
1550
+ // Yazma paketi kapısı: doğrulanmamış kaynak ve teyitsiz değerlendirme yüzdesi pakete giremez (ilke 1-3).
1551
+ const yazilamayan = [];
1552
+ const kaynaklar = gelen.kaynaklar?.filter((k, i) => {
1553
+ if (kaynakDurumu(k) !== "dogrulanmamis")
1554
+ return true;
1555
+ yazilamayan.push({
1556
+ alan: "Kaynaklar",
1557
+ neden: `${i + 1}. kaynak doğrulanmamış (katalogda bulunmadı ya da hoca beyanı yok); OBS'ye yazılmaz.`,
1558
+ });
1559
+ return false;
1560
+ });
1561
+ const degerlendirmeTeyitli = gelen.degerlendirmeDayanagi === "sayfa" || gelen.degerlendirmeDayanagi === "hoca";
1562
+ if (gelen.degerlendirme && !degerlendirmeTeyitli)
1563
+ yazilamayan.push({
1564
+ alan: "Değerlendirme",
1565
+ neden: "Değerlendirme yöntem ve yüzdeleri teyitsiz (sayfadan okunmadı, hoca söylemedi); OBS'ye yazılmaz. Hocaya sorun.",
1566
+ });
1567
+ const { degerlendirme: _d, kaynaklar: _k, ...kalanAlanlar } = gelen;
1568
+ const alanlar = {
1569
+ ...kalanAlanlar,
1570
+ ...(gelen.degerlendirme && degerlendirmeTeyitli ? { degerlendirme: gelen.degerlendirme } : {}),
1571
+ ...(kaynaklar ? { kaynaklar } : {}),
1572
+ };
1225
1573
  const bolumler = [];
1226
1574
  const ekle = (sekme, alan, deger) => bolumler.push({ sira: bolumler.length + 1, sekme, alan, deger });
1227
1575
  const hesap = bolognaHesabi(ders, alanlar);
@@ -1269,8 +1617,8 @@ const kopyaMetniAraci = arac({
1269
1617
  if (alanlar.ska)
1270
1618
  ekle("Sür. Kalkınma Amaçları", "Seçilecek amaç numaraları", alanlar.ska.join(", "));
1271
1619
  if (alanlar.kaynaklar?.length)
1272
- ekle("Diğer Kaynaklar", "Kaynaklar (her satır bir künye)", alanlar.kaynaklar.map((k) => k.trim()).join("\n"));
1273
- if (!bolumler.length)
1620
+ ekle("Diğer Kaynaklar", "Kaynaklar (her satır bir künye)", alanlar.kaynaklar.map(kunyeMetni).join("\n"));
1621
+ if (!bolumler.length && !yazilamayan.length)
1274
1622
  throw new AracHatasi("Kopyalanacak alan yok. En az bir alan gönderin.");
1275
1623
  const b = alanBulgulari(ders, alanlar, { ingilizce, programCiktilari });
1276
1624
  const kalanEksikler = [...b.entries()].flatMap(([alan, xs]) => xs.filter((x) => x.duzey === "hata").map((x) => ({ alan, neden: x.aciklama })));
@@ -1293,14 +1641,37 @@ const kopyaMetniAraci = arac({
1293
1641
  ...kalanEksikler.map((e) => `- ${ALANLAR[e.alan]?.ad ?? "Genel"}: ${e.neden}`),
1294
1642
  ]
1295
1643
  : []),
1644
+ ...(yazilamayan.length
1645
+ ? ["", "Yazma paketine alınmayanlar:", ...yazilamayan.map((y) => `- ${y.alan}: ${y.neden}`)]
1646
+ : []),
1296
1647
  ].join("\n");
1648
+ const numarali = sirali.map((x, i) => ({ ...x, sira: i + 1 }));
1649
+ const normal = (v) => v.replace(/\s+/gu, " ").trim();
1650
+ const degisiklikOzeti = numarali.map((x) => {
1651
+ const onceki = oncekiDegerler?.[x.alan];
1652
+ return {
1653
+ no: x.sira,
1654
+ sekme: x.sekme,
1655
+ alan: x.alan,
1656
+ onceki: onceki === undefined ? null : onceki,
1657
+ yeni: x.deger,
1658
+ degisim: onceki === undefined ? "bilinmiyor" : normal(onceki) === normal(x.deger) ? "aynı" : "değişecek",
1659
+ };
1660
+ });
1297
1661
  return {
1298
1662
  sonuc: {
1299
1663
  metin,
1300
- bolumler: sirali.map((x, i) => ({ ...x, sira: i + 1 })),
1664
+ bolumler: numarali,
1665
+ degisiklikOzeti,
1666
+ yazilamayan,
1301
1667
  hesap,
1302
1668
  kalanEksikler,
1303
- not: "Alan adları OBS sürümüne göre küçük farklar gösterebilir. Kaydı ve son kontrolü hoca yapar.",
1669
+ obsKarsilastirGirdisi: {
1670
+ ...(ders.kod ? { dersKodu: ders.kod } : {}),
1671
+ ...(ders.donem ? { donem: ders.donem } : {}),
1672
+ beklenen: numarali.map((x) => ({ alan: x.alan, deger: x.deger })),
1673
+ },
1674
+ not: "Biçim denetiminden geçti; içerik teyidi: hoca. Yazmadan önce degisiklikOzeti'ni (önceki/yeni) hocaya alan alan gösterin; hoca bir kez 'kaydet' der. Kayıttan sonra sayfayı geri okuyup obs_karsilastir ile doğrulayın. Alan adları OBS sürümüne göre küçük farklar gösterebilir.",
1304
1675
  },
1305
1676
  kaynak: [KAYNAK_KURALLAR],
1306
1677
  ...(kalanEksikler.length
@@ -1318,4 +1689,5 @@ export const BOLOGNA_ARACLARI = [
1318
1689
  isYukuOnerAraci,
1319
1690
  durumAraci,
1320
1691
  kopyaMetniAraci,
1692
+ obsKarsilastirAraci,
1321
1693
  ];